第一次接触按键扫描是在大学电子设计课上,当时用while(key==0)这种简单粗暴的方式检测按键,结果整个程序卡死在循环里,数码管显示都变得一卡一卡的。后来改用定时器扫描算是解决了问题,但当我开始做智能家居遥控器项目时,发现传统方法在应对短按/长按/连按这种复杂交互时,代码会变得像意大利面条一样难以维护。
状态机就像给按键行为画了个流程图。想象你家的智能灯泡:短按开灯、长按调亮度、快速连按切换模式。用传统方法可能要写一堆if-else和计时变量,而状态机把这些行为拆解成明确的状态节点和迁移条件。我在开发红外遥控器时就深有体会——当产品经理第5次修改交互逻辑时,只需要调整状态图就能快速重构代码。
在音量调节场景中,我通常定义这5个核心状态:
实际项目中我发现,状态划分粒度直接影响代码复杂度。曾有个智能门锁项目,我把"指纹识别中"也设为独立状态,结果状态爆炸难以维护。后来合并成"生物识别处理"大状态后,用子状态机管理才解决。
迁移条件就是状态切换的扳机,常见的有:
在智能温控器项目里,我遇到过条件冲突的坑:长按调温度时突然收到蓝牙指令,导致状态紊乱。后来加了event_queue队列才解决,这点后面会详细说。
先看我最常用的状态机骨架:
c复制typedef enum {
STATE_IDLE,
STATE_DEBOUNCE,
STATE_PRESSED,
STATE_HOLD,
STATE_REPEAT
} KeyState;
typedef struct {
KeyState current_state;
uint32_t timer;
uint8_t pin_level;
uint8_t pin_last_level;
} KeyFSM;
这个结构体在STM32和ESP32上都验证过,内存占用仅12字节。有个优化技巧:如果资源紧张,可以把timer改用uint16_t,配合定时器分频来节省空间。
以STM32 HAL库为例,分享我的按键扫描核心逻辑:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim == &htim3) { // 10ms定时器
static KeyFSM keys[4];
for(int i=0; i<4; i++) {
keys[i].pin_last_level = keys[i].pin_level;
keys[i].pin_level = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pins[i]);
switch(keys[i].current_state) {
case STATE_IDLE:
if(keys[i].pin_level == 0) {
keys[i].current_state = STATE_DEBOUNCE;
keys[i].timer = 2; // 20ms消抖
}
break;
case STATE_DEBOUNCE:
if(--keys[i].timer == 0) {
keys[i].current_state = (keys[i].pin_level == 0) ?
STATE_PRESSED : STATE_IDLE;
}
break;
// 其他状态处理...
}
}
}
}
关键点:消抖计时我习惯用timer=2配合10ms定时器,而不是直接20ms。这样修改定时周期时只需调整初始值,不用改判断逻辑。
很多开发者卡在连发功能的平滑度上,这是我优化过的方案:
c复制case STATE_HOLD:
if(keys[i].pin_level == 1) {
keys[i].current_state = STATE_IDLE;
} else if(--keys[i].timer == 0) {
keys[i].current_state = STATE_REPEAT;
keys[i].timer = REPEAT_INTERVAL;
trigger_key_event(i, EVENT_REPEAT);
}
break;
case STATE_REPEAT:
if(keys[i].pin_level == 1) {
keys[i].current_state = STATE_IDLE;
} else if(--keys[i].timer == 0) {
keys[i].timer = REPEAT_INTERVAL;
trigger_key_event(i, EVENT_REPEAT);
}
break;
实测发现200ms间隔最符合人体工学,比常见的500ms体验更流畅。在机顶盒项目中,这个参数让音量调节速度提升了150%。
当需要检测组合键时,传统方法会引入复杂的标志位。我的方案是引入分层状态机:
比如"音量+和音量-同时长按"重置设备:
c复制if(keys[VOL_UP].current_state == STATE_HOLD &&
keys[VOL_DOWN].current_state == STATE_HOLD) {
trigger_system_event(EVENT_FACTORY_RESET);
}
在资源受限的MCU上,我常用这些技巧:
在nRF52840项目中,这些优化让功耗降低了37%。具体实现可以参考:
c复制#pragma pack(push, 1)
typedef struct {
uint8_t state:3;
uint8_t timer:5;
} CompactKeyFSM;
#pragma pack(pop)
搭建完整的测试框架很重要,我的方法是用GPIO模拟器+逻辑分析仪:
曾用这个方法发现消抖时间的临界值问题:当按下时间恰好在19-21ms时,部分批次按键会出现误触发。最终将消抖时间统一调整为25ms解决。