如何设计一个通用的按键模块?单击、双击、长按,一套代码适配所有项目
摘要每个项目都要写一遍按键扫描每次加个双击功能就要改while(1)这是没有组件化思维的表现。本文将演示如何设计一个状态机驱动的通用按键模块实现“一次编写终身复用”。一、反面教材复制粘贴写法场景检测单击和长按。// main.c void Key_Scan(void) { if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) 0) { HAL_Delay(10); // 阻塞延时系统卡死 if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) 0) { press_time; if (press_time 100) { /* 长按 */ } } } else { if (press_time 5 press_time 100) { /* 单击 */ } press_time 0; } }痛点阻塞HAL_Delay导致系统卡顿。耦合直接操作 GPIO换引脚要改代码。难扩展加个双击逻辑嵌套地狱。二、进阶写法状态机 回调核心思想按键只负责“发生了什么”不负责“干什么”。1. 定义按键事件接口// button.h typedef enum { BTN_EVENT_SINGLE_CLICK 0, BTN_EVENT_DOUBLE_CLICK, BTN_EVENT_LONG_PRESS, } ButtonEvent_t; // 回调函数指针核心 typedef void (*ButtonCallback)(ButtonEvent_t event);2. 按键对象结构体封装// button.h typedef struct { uint8_t pin; // GPIO 引脚 uint8_t state; // 状态机状态 uint16_t timer; // 计时器 uint8_t click_count; // 点击次数 ButtonCallback cb; // 回调函数 } Button_t;3. 实现状态机核心逻辑这是模块的灵魂放在button.c里永不修改。// button.c #include button.h #include bsp_gpio.h // 依赖 BSP 层 void Button_Scan(Button_t *btn) { uint8_t level BSP_GPIO_Read(btn-pin); switch (btn-state) { case 0: // 空闲态 if (level 0) { btn-state 1; btn-timer 0; } break; case 1: // 消抖态 if (level 0) { btn-state 2; // 确认按下 } else { btn-state 0; // 抖动 } break; case 2: // 按下态 if (level 1) { // 松手 btn-click_count; btn-state 3; btn-timer 0; } else if (btn-timer 100) { // 长按判定 btn-cb(BTN_EVENT_LONG_PRESS); btn-state 4; } break; case 3: // 等待双击 if (btn-timer 30) { // 超时 btn-cb(BTN_EVENT_SINGLE_CLICK); btn-state 0; } else if (level 0) { // 再次按下 btn-state 2; } break; case 4: // 长按松开 if (level 1) { btn-state 0; } break; } }4. 业务层使用组件在main.c中像搭积木一样使用。// main.c Button_t user_btn; void User_Button_Handler(ButtonEvent_t event) { switch (event) { case BTN_EVENT_SINGLE_CLICK: BSP_LED_Toggle(); break; case BTN_EVENT_DOUBLE_CLICK: BSP_UART_Send(Hello); break; case BTN_EVENT_LONG_PRESS: System_Reboot(); break; } } void main(void) { // 初始化按键组件 user_btn.pin KEY1_PIN; user_btn.cb User_Button_Handler; // 在 1ms 定时中断或任务中调用 while (1) { Button_Scan(user_btn); vTaskDelay(1); } }三、进阶收益维度传统写法组件化写法移植性复制代码改 GPIO改结构体参数阻塞性有Delay无非阻塞扩展性难嵌套 if易加状态复用性0%100%直接拷文件四、工程级最佳实践非阻塞扫描函数必须在 1ms 内返回绝对不能有Delay。依赖抽象button.c只能调用BSP_GPIO_Read()不能直接调用HAL_GPIO_ReadPin()。事件驱动扫描函数只负责识别事件通过回调函数通知业务层。五、总结 Checklist[ ] 是否使用结构体封装了按键对象[ ] 是否使用状态机代替了if-else延时[ ] 是否通过回调函数通知业务层[ ] 扫描函数是否是非阻塞的六、写在最后关注我少走弯路我是 gqqsherry一个拒绝调包、专注工程落地的嵌入式架构师。写代码的最高境界是把重复的脏活累活变成组件。通用按键模块是每个嵌入式工程师的第一个“轮子”造好了后面都是享受。关注我的专栏《嵌入式系统架构实战》下一篇我们将深入解析《参数存储的磨损均衡如何让 Flash 多活 10 倍寿命》。下一篇预告《参数存储的磨损均衡如何让 Flash 多活 10 倍寿命》原创文章转载请注明出处。