基于按键开源MultiButton框架深入理解代码框架(三)(指针的深入理解与应用)

文章目录

  • 3、分析代码
    • 3.3 按键的插入
    • 3.4 按键的删除
    • 3.5 继续分析状态机核心理解
  • 4、写在最后的总结
  • 5、思想感悟篇
  • 6、慈悲不渡自绝人


3、分析代码

3.3 按键的插入


// Button handle list headstatic Button* head_handle = NULL;/*** @brief  Start the button work, add the handle into work list* @param  handle: target handle struct* @retval 0:succeed, -1:already exist, -2:invalid parameter*/int button_start(Button* handle){if (!handle) return -2;  // invalid parameterButton* target = head_handle;while (target) {if (target == handle) return -1;  // already existtarget = target->next;}handle->next = head_handle;head_handle = handle;return 0;}

主要是最后两行代码:

表示的是头插法,也就是说只要是一个新的按键,这个新的按键结构体里面永远存储的是当前已经存在按键链表的第一个节点,然后我们的头结点就被这个新按键给覆盖掉。为什么head_handle可以被循环替换,这是因为我们前面定义了static关键字,它存在于整个程序生命周期,并且还只能在按键的文件调用。

但是需要注意的是,这个使用的是头指针,不是头结点方式插入。

概念头指针 (head_handle)​头结点
定义指向链表第一个节点的指针变量(存储地址)位于链表首部的辅助节点​(不存储有效数据)
内存占用仅占用一个指针的空间(如 4/8 字节)占用完整节点结构体的空间(含数据域和指针域)
初始化NULL(表示空链表)需动态分配内存(如 malloc
static Button* head_handle = NULL;未定义,代码中未创建头结点
无头结点节省了内存(尤其在小内存嵌入式设备中关键)。

3.4 按键的删除

需要注意的是首节点删除需特殊处理
若删除链表第一个节点(如 btn2),需更新头指针:

void delete_first_node() {Button* temp = head_handle;head_handle = head_handle->next;  // 更新头指针free(temp);
}

相当于是直接把第一个结点内部存的下一个节点的直接赋值给当前的head_handle

在链表头节点删除操作中,使用临时指针 Button* temp = head_handle;是必要且关键的,直接跳过此步骤仅更新头指针(如 head_handle = head_handle->next;)会导致严重内存问题。

void delete_first_node() {head_handle = head_handle->next;  // 直接更新头指针
}
  • 原头节点(head_handle 指向的节点)未被释放,其内存空间无法被系统回收。
  • 在嵌入式系统中,内存资源有限,频繁操作后可能耗尽内存,引发系统崩溃。
  • 若其他代码持有原头节点的指针,该指针会指向已失效的内存区域(称为野指针)。
  • 后续访问野指针(如 head_handle->data)会导致未定义行为(程序崩溃或数据错误)。
  • 原头节点的数据若需清理(如动态分配的子资源),跳过 free(temp) 会遗漏资源释放

相当于是虽然改变了head_handle 指向的节点,但是之前结点的地址还是存着内容,所以我们需要将这个结点的入口地址传递给free(temp);进行释放。

  • 安全释放内存
    temp 临时保存原头节点地址,确保 free() 能准确释放该内存。

  • 避免野指针
    释放后,temp 生命周期结束,不会遗留野指针(而原 head_handle 已更新指向新节点)。

  • 支持资源清理
    若节点包含动态分配的资源(如字符串缓冲区),可在 free(temp) 前先释放其子资源。

  • 当执行 temp = head_handle 时,temp 保存了原头节点的物理内存地址​(如 0x0012FF88)。

  • 此后 head_handle = head_handle->next 修改头指针,但 temp 仍持有原节点的地址,确保能精准定位需释放的内存块。

  • 原节点成为“内存孤岛”,程序失去对其访问权,但内存仍被占用 → ​内存泄漏​(Memory Leak);

  • 在长期运行的嵌入式系统中,此类泄漏会累积耗尽内存,导致系统崩溃。

内存的释放,释放的是这个地址,而这个地址是存储在指针变量里面的,注意我们不是释放指针变量,是释放指针变量存储的地址,这个一定要绕过来,别被绕进去了。这一点很关键。

3.5 继续分析状态机核心理解

static void button_handler(Button* handle)
{uint8_t read_gpio_level = button_read_level(handle);// Increment ticks counter when not in idle stateif (handle->state > BTN_STATE_IDLE) {handle->ticks++;}/*------------Button debounce handling---------------*/if (read_gpio_level != handle->button_level) {// Continue reading same new level for debounceif (++(handle->debounce_cnt) >= DEBOUNCE_TICKS) {handle->button_level = read_gpio_level;handle->debounce_cnt = 0;}} else {// Level not changed, reset counterhandle->debounce_cnt = 0;}/*-----------------State machine-------------------*/switch (handle->state) {case BTN_STATE_IDLE:if (handle->button_level == handle->active_level) {// Button press detectedhandle->event = (uint8_t)BTN_PRESS_DOWN;EVENT_CB(BTN_PRESS_DOWN);handle->ticks = 0;handle->repeat = 1;handle->state = BTN_STATE_PRESS;} else {handle->event = (uint8_t)BTN_NONE_PRESS;}break;case BTN_STATE_PRESS:if (handle->button_level != handle->active_level) {// Button releasedhandle->event = (uint8_t)BTN_PRESS_UP;EVENT_CB(BTN_PRESS_UP);handle->ticks = 0;handle->state = BTN_STATE_RELEASE;} else if (handle->ticks > LONG_TICKS) {// Long press detectedhandle->event = (uint8_t)BTN_LONG_PRESS_START;EVENT_CB(BTN_LONG_PRESS_START);handle->state = BTN_STATE_LONG_HOLD;}break;case BTN_STATE_RELEASE:if (handle->button_level == handle->active_level) {// Button pressed againhandle->event = (uint8_t)BTN_PRESS_DOWN;EVENT_CB(BTN_PRESS_DOWN);if (handle->repeat < PRESS_REPEAT_MAX_NUM) {handle->repeat++;}EVENT_CB(BTN_PRESS_REPEAT);handle->ticks = 0;handle->state = BTN_STATE_REPEAT;} else if (handle->ticks > SHORT_TICKS) {// Timeout reached, determine click typeif (handle->repeat == 1) {handle->event = (uint8_t)BTN_SINGLE_CLICK;EVENT_CB(BTN_SINGLE_CLICK);} else if (handle->repeat == 2) {handle->event = (uint8_t)BTN_DOUBLE_CLICK;EVENT_CB(BTN_DOUBLE_CLICK);}handle->state = BTN_STATE_IDLE;}break;case BTN_STATE_REPEAT:if (handle->button_level != handle->active_level) {// Button releasedhandle->event = (uint8_t)BTN_PRESS_UP;EVENT_CB(BTN_PRESS_UP);if (handle->ticks < SHORT_TICKS) {handle->ticks = 0;handle->state = BTN_STATE_RELEASE;  // Continue waiting for more presses} else {handle->state = BTN_STATE_IDLE;  // End of sequence}} else if (handle->ticks > SHORT_TICKS) {// Held down too long, treat as normal presshandle->state = BTN_STATE_PRESS;}break;case BTN_STATE_LONG_HOLD:if (handle->button_level == handle->active_level) {// Continue holdinghandle->event = (uint8_t)BTN_LONG_PRESS_HOLD;EVENT_CB(BTN_LONG_PRESS_HOLD);} else {// Released from long presshandle->event = (uint8_t)BTN_PRESS_UP;EVENT_CB(BTN_PRESS_UP);handle->state = BTN_STATE_IDLE;}break;default:// Invalid state, reset to idlehandle->state = BTN_STATE_IDLE;break;}
}

在这里插入图片描述

基于以上两个图片进行相关内容解析

1、如果一个按键是按下有效,那么就要避免增加长按功能的。
首先根据这两个图可以看出无论是短按或者长按都会产生一下按键按下事件,因此如果一个按键在设计为长按有效(不论是长按抬起有效还是按下有效)的同时还具有按下有效的功能,都会触发按键按下事件。这样就会造成功能冲突,也就是在想要长按功能的时候必定会带来短按功能的启动,并且在当前状态机时没办法解决的,因此只能在设计的时候避免。

2、该状态机在按键长按模式下,还可以区分按键长按抬起有效和按键长按按下有效。
因为在按键长按的过程中会有划分了三个事件:
第一个是按键长按开始事件(按键按下状态机),这个事件就是按键属性为长按按下有效发生,因为只要满足了长按的时间阈值就会立即触发该事件;
第二个是按键长按保持事件(按键长按保持状态机),这个事件只是单纯的在超过按键长按时间阈值以后会一直触发的事件,保持的时间并没有明确要求,只要是超过按键长按时间阈值以后的时间都属于按键长按保持时间并且会触发按键长安保持事件;
第三个是按键长按抬起事件(按键长按保持状态机),这个事件就是表示在按键长按保存状态的时候释放按键,那么就会产生电平跳变,在消抖以后就会判断出按键抬起,此时就是触发按键长按抬起事件。
长按保持这段时间在当前状态机是没有办法衡量的,因为没有增加相关变量记录时间。

3、该状态机按键短按单击完成的判断时间需要仔细衡量一下。
这是因为在短按按下动作和抬起动作会有消抖因素的存在,因此这一部分其实也是占用时间的,而按键短按单击完成(按键释放状态机)判断的时间累计是从该按键短按抬起以后开始累计的。并且按键单击完成和双击完成都是在按键释放状态机完成相关事件触发的。

4、该状态机Tick时间的更新。
Tick时间的更新条件是(按键状态> 按键释放状态),也就是说存在一种情况再按键长按的时候,Tick是一直增加的,但是在按键释放状态机和按键长按保持状态机都是没有清零的,如果按键长按松开以后状态变为按键空闲状态,此时Tick就不会增加, 按照现有代码清零是在该按键下一次按下的时候清空这个Tick。可以在这里进行改进,将Tick清零放在(按键长按保持状态机)按键长按抬起的时候。

在这里插入图片描述
状态机:按键释放状态。

此时间是两次双击最大间隔时间,如果超过这个间隔,就会触发是按键短按单击事件。

状态机:按键释放状态。
此时处于第二次按键按下状态,首先触发第二次按键按下事件,repeat++自增,接着触发重复按下第二次事件,清空时间tick=0,进入按键重复状态机。
触发重复按下第二次事件:这个地方可以理解为只要出现第二次按键按下的状态,就认为连击完成,仅作为理解。

分析:分析按键重复状态机

需要在这里着重分析里面的状态,因为源码在这里存在一些不合理之处。
1、按键重复状态机在第二次按键按下动作以后并没有释放而是一直按下,此时tick是从零自增,那么时间如果超过SHORT_TICKS状态机跳转至按键按下状态机BTN_STATE_PRESS,并且一直还是按下,那么最终会进入按键长按保持状态机BTN_STATE_LONG_HOLD,这种情况并无什么影响,也符合逻辑。但是如果我在按键长按判断之前释放按键,此时出现按键抬起事件,tick清空并且进入到按键释放状态机,然后在该状态机重新执行handle->ticks > SHORT_TICKS判断,并且由于前面repeat自增,导致repeat=2,那么就会触发按键双击完成事件,这在逻辑是不合理的因为这个过程已经过去了很多个时间,准确来说已经不属于连击的范畴了。

2、按键重复状态机第二次按键抬起, 首先会经过抬起动作消抖,接着会产生一个第二次按键抬起事件,并且tick会自增,并且这个时间是理论上是小于handle->ticks < SHORT_TICKS,这是因为上一个状态是按下,时间会一直自增,接着抬起消抖完成时间不会清空,因为是在SHORT_TICKS时间内完成的抬起,不然就已经跳到了上述分析的情况1,但是由于消抖会有时间,因此也可能存在卡点,那么这种情况就是直接将状态机跳转至按键释放状态,然后产生一个无按键按下事件,这种情况也是无伤大雅,符合逻辑。接着我们继续分析符合理论情况的时间小于SHORT_TICKS,首先将ticks清空,然后状态机跳转至按键释放状态BTN_STATE_RELEASE,最后在延时一个SHORT_TICKS时间,由于此时repeat=2,因此完美的执行按键双击完成事件,

在这里插入图片描述

假如一个按键是按下有效,那么就要避免增加长按功能的,因为这种设计会造成功能冲突,也就是说当这个按键按下的时候,首先会触发按下有效事件,接着会触发长按事件功能。

如果想把一个按键集成短按有用和长按都有不同的功能,那其实就需要将短按设计为抬起有效,这样如果在长按的过程中没有抬起,那就不会触发短按功能,按键按下的时间累计达到长按阈值,就会触发长按功能,这样就完美的避免了长按和短按同时存在,长按会先触发短按功能。

4、写在最后的总结

根据开源按键框架MultiButton-master,在basic_example.c文件里面有一个buttons_init函数,其作用是首先是初始化一个 button_init(&btn1, read_button_gpio, 1, 1);但是这个函数 是在multi_button.c里面,因此为了避免暴露bnt1,传入的是这个结构体的首地址,完成初始化以后,更新链表 button_start(&btn1); button_start(&btn2);同样这两个函数也在multi_button.c里面,而在文件multi_button.c里面static Button* head_handle = NULL;链表用来接收bt1和bt2,并且在调用按键处理函数的时候 simulate_button_press(1, 100);并没有传入btn1和btn2,并且这个函数是实在multi_button.c里面,所以他实际执行 void button_ticks(void) { Button* target; for (target = head_handle; target; target = target->next) { button_handler(target); } }里面的head_handle这个链表已经包含了btn1和btn2,,这样既保证了按键的数据流动,又让处理过程在basic_example.c不可见,并且bt1和btn2对于multi_button.c也是不可见。 这就是模块化编程的意义。

5、思想感悟篇

后续还会再更新一篇文章单独聊一聊这个按键框架的思想,为什么要这么做,试图体会作者的思想,进而触类旁通,举一反三。

加油!!!!!!!

6、慈悲不渡自绝人

未完待续…


文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/91562.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/91562.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ACOUSLIC-AI挑战报告:基于低收入国家盲扫超声数据的胎儿腹围测量|文献速递-医学影像算法文献分享

Title题目ACOUSLIC-AI challenge report: Fetal abdominal circumferencemeasurement on blind-sweep ultrasound data from low-income countriesACOUSLIC-AI挑战报告&#xff1a;基于低收入国家盲扫超声数据的胎儿腹围测量01文献速递介绍胎儿生长受限&#xff08;FGR&#xf…

集群聊天服务器各个类进行详解

1.dh.h类定义概要类名&#xff1a; MySQL功能&#xff1a; 简化MySQL的连接、查询和更新操作&#xff0c;提供接口给上层应用使用。成员变量private:MYSQL *_conn;_conn&#xff1a;指向MYSQL结构体的指针&#xff0c;用于代表数据库连接实例。由mysql_init()初始化&#xff0c…

电缆安全双保险:不止防盗,更能防触电的塔能智慧照明守护方案

城市照明、地下车库以及园区路灯所涉及的电缆安全问题&#xff0c;向来都是运维管理方面颇为棘手的难题。在传统的运维管理模式之下&#xff0c;电缆一旦被盗&#xff0c;那么所造成的影响可不小&#xff0c;一方面会带来直接的经济损失&#xff0c;另一方面还极有可能因为线路…

Leetcode刷题营第二十九,三十题:二叉树的中序以及后序遍历

94.二叉树的中序遍历 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#x…

Rabbitmq Direct Exchange(直连交换机)可以保证消费不被重复消费吗,可以多个消费者,但是需要保证同一个消息,不会被投递给多个消费者

在 RabbitMQ 中&#xff0c;默认情况下&#xff0c;不能保证消息不被重复消费&#xff0c;但可以通过 队列绑定方式 消费者竞争机制 来确保 同一消息只被一个消费者处理。以下是几种可行的方案&#xff1a;方案 1&#xff1a;单队列 竞争消费者模式&#xff08;默认行为&…

常用的OTP语音芯片有哪些?

唯创知音在 OTP 语音芯片有着26年的历史&#xff0c;有着丰富的技术积累与产品迭代历程。1999 年&#xff0c;唯创知音在广州成立&#xff0c;彼时便开始在电子领域积极探索。2000 年&#xff0c;公司敏锐捕捉到语音芯片行业的发展潜力&#xff0c;正式进军该领域。经过数年技术…

分布式光伏发电系统中的“四可”指的是什么?

在分布式光伏电站规模爆发式增长的今天&#xff0c;“看不见、管不住、调不动”的难题却成为行业痛点。如何让散布各处的光伏电站真正成为稳定高效的“绿色能量站”&#xff1f;2025年《分布式光伏发电开发建设管理办法》大型工商业项目&#xff08;≥6MW&#xff09;明确要求具…

健康管理系统新趋势:AI + 物联网如何重塑健康管理

一、传统健康管理的痛点与变革之必然长久以来&#xff0c;我们熟悉的健康管理方式存在明显局限&#xff1a;数据孤岛严重&#xff1a;体检报告在抽屉里沉睡&#xff0c;健身手环数据仅存于手机&#xff0c;不同医疗机构信息互不相通&#xff0c;个人健康信息犹如碎片散落各处。…

git基本操作【GIT-2】

git基本操作初始化一个仓库&#xff08;repository&#xff09;、开始或停止跟踪&#xff08;track&#xff09;文件、暂存&#xff08;stage&#xff09;或提交&#xff08;commit&#xff09;更改如何配置 Git 来忽略指定的文件和文件模式、如何迅速而简单地撤销错误操作、如…

【数据准备】——深度学习.全连接神经网络

目录 1 数据加载器 1.1 构建数据类 1.1.1 Dataset类 1.1.2 TensorDataset类 1.2 数据加载器 2 数据加载案例 2.1 加载csv数据集 2.2 加载图片数据集 2.3 加载官方数据集 2.4 pytorch实现线性回归 1 数据加载器 分数据集和加载器2个步骤~ 1.1 构建数据类 1.1.1 Dat…

健康生活,从细节开始

健康生活&#xff0c;从细节开始在当今快节奏的生活中&#xff0c;健康逐渐成为人们关注的焦点。拥有健康的身体&#xff0c;才能更好地享受生活、追求梦想。那么&#xff0c;如何才能拥有健康呢&#xff1f;这就需要我们从生活中的点滴细节入手&#xff0c;培养良好的生活习惯…

javax.servlet.http.HttpServletResponse;API导入报错解决方案

javax.servlet.http.HttpServletResponse;API导入报错解决方案与Postman上传下载文件验证 1. 主要错误&#xff1a;缺少 Servlet API 依赖 错误信息显示 javax.servlet.http 包不存在。这是因为你的项目缺少 Servlet API 依赖。 解决方案&#xff1a; 如果你使用的是 Maven&…

reids依赖删除,但springboot仍然需要redis参数才能启动

背景&#xff1a;项目需要删除redis。我删除完项目所有配置redis的依赖&#xff0c;启动报错。[2025-07-17 15:08:37:561] [DEBUG] [restartedMain] DEBUG _.s.w.s.H.Mappings - [detectHandlerMethods,295] [] - o.s.b.a.w.s.e.BasicErrorController:{ [/error]}: error(HttpS…

【前端】CSS类命名规范指南

在 CSS 中&#xff0c;合理且规范的 class 命名格式对项目的可维护性和协作效率至关重要。以下是主流的 class 命名规范和方法论&#xff1a;一、核心命名原则语义化命名&#xff1a;描述功能而非样式 ✅ .search-form&#xff08;描述功能&#xff09;❌ .red-text&#xff08…

C++网络编程 4.UDP套接字(socket)编程示例程序

以下是基于UDP协议的完整客户端和服务器代码。UDP与TCP的核心区别在于无连接特性&#xff0c;因此代码结构会更简单&#xff08;无需监听和接受连接&#xff09;。 UDP服务器代码&#xff08;udp_server.cpp&#xff09; #include <iostream> #include <sys/socket.h&…

King’s LIMS:实验室数字化转型的智能高效之选

实验室数字化转型不仅是技术升级&#xff0c;更是管理理念和工作方式的革新。LIMS系统作为这一转型的核心工具&#xff0c;能够将分散的实验数据转化为可分析、可复用的资产&#xff0c;为科研决策提供支持&#xff1b;规范检测流程&#xff0c;减少人为干预&#xff0c;确保结…

【力扣 中等 C】97. 交错字符串

目录 题目 解法一 题目 待添加 解法一 bool isInterleave(char* s1, char* s2, char* s3) {const int len1 strlen(s1);const int len2 strlen(s2);const int len3 strlen(s3);if (len1 len2 ! len3) {return false;}if (len1 < len2) {return isInterleave(s2, s1,…

Class9简洁实现

Class9简洁实现 %matplotlib inline import torch from torch import nn from d2l import torch as d2l# 初始化训练样本、测试样本、样本特征维度和批量大小 n_train,n_test,num_inputs,batch_size 20,100,200,5 # 设置真实权重和偏置 true_w,true_b torch.ones((num_inputs…

ELK日志分析,涉及logstash、elasticsearch、kibana等多方面应用,必看!

目录 ELK日志分析 1、下载lrzsc 2、下载源包 3、解压文件,下载elasticsearch、kibana、 logstash 4、配置elasticsearch 5、配种域名解析 6、配置kibana 7、配置logstash 8、进行测试 ELK日志分析 1、下载lrzsc [rootlocalhost ~]# hostnamectl set-hostname elk ##…

终极剖析HashMap:数据结构、哈希冲突与解决方案全解

文章目录 引言 一、HashMap底层数据结构&#xff1a;三维存储架构 1. 核心存储层&#xff08;硬件优化设计&#xff09; 2. 内存布局对比 二、哈希冲突的本质与数学原理 1. 冲突产生的必然性 2. 冲突概率公式 三、哈希冲突解决方案全景图 1. 链地址法&#xff08;Hash…