文章目录
- 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 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。