4、按键输入与消抖:机械按键原理、硬件消抖与软件消抖、状态机实现
按键,是嵌入式系统和用户交互最朴素的方式。剃须刀的开关、档位切换、锁定解锁,都离不开它。但机械按键有个“老毛病”——抖动。今天我们就来聊聊怎么治这个毛病。
4.1 机械按键原理:按下那一刻发生了什么?
机械按键,说白了就是两个金属触点。按下时接通,松开时断开。听起来很简单对吧?
但实际不是这样的。你想想看,金属片碰撞的时候,会像乒乓球一样弹跳几下。这个过程叫“抖动”。
抖动的时间一般在 5ms 到 20ms 之间。具体多长,取决于按键的材质和做工。我见过最差的按键,抖了将近 30ms 才稳定下来。
抖动期间,电平会反复跳变。如果你直接读取这个信号,一次按下可能被识别成十几次。剃须刀就会乱切换档位,用户体验极差。
关键点:按键按下和松开的瞬间,都会产生抖动。需要做消抖处理。
4.2 硬件消抖:用电路解决问题
硬件消抖,就是用电阻电容搭一个低通滤波器。把高频的抖动信号滤掉,只留下稳定的电平变化。
最简单的做法:按键串联一个电阻(比如 10kΩ),并联一个电容(比如 0.1μF)。RC 时间常数 τ = R × C,算出来大约是 1ms。这个值可以覆盖大部分按键的抖动时间。
我个人习惯用 10kΩ + 0.1μF 的组合。成本低,效果也够用。但要注意,电容不能太大,否则按键响应会变慢。你按下去要等半天才反应,用户会骂娘的。
小技巧:如果用的是施密特触发器输入引脚,可以省掉电容。施密特触发器自带迟滞特性,能天然抑制抖动。
硬件消抖的优点是:不占 CPU 资源。缺点是:增加了 BOM 成本和 PCB 面积。对于剃须刀这种成本敏感的产品,很多厂商会选择软件消抖。
4.3 软件消抖:用代码解决问题
软件消抖,就是通过延时或采样来判断按键是否真的被按下了。
最原始的方法:检测到电平变化后,延时 20ms,再读一次。如果电平一致,就认为是有效按下。
但这种方法有个问题:延时期间 CPU 被阻塞了。剃须刀的主控还要做电机控制、电池检测、防水检测,哪有空等你 20ms?
所以我推荐用“定时采样法”。
具体做法:
- 每隔 5ms 读一次按键状态
- 连续读到 4 次(即 20ms)相同的状态,才认为按键有效
- 否则认为是抖动,丢弃
这样 CPU 不用傻等,每 5ms 花几微秒读一下引脚就行。效率高得多。
我曾经在一个项目中用过这个方案。按键数量有 8 个,全部用定时采样法处理。主控是 STM32F030,跑 48MHz,一点压力都没有。
4.4 状态机实现:优雅地处理按键
如果你只是做简单的消抖,用上面的方法就够了。但如果你要处理长按、短按、双击、连发,那就得上状态机了。
状态机,说白了就是给按键定义一个“生命周期”。
我常用的按键状态机有 4 个状态:
| 状态 | 含义 | 触发条件 |
|---|---|---|
| IDLE | 空闲,按键未按下 | 初始状态,或按键松开后 |
| PRESS_DETECT | 检测到按下,正在消抖 | 检测到低电平(假设低有效) |
| PRESS_CONFIRM | 确认按下,等待松开 | 消抖完成,电平稳定为低 |
| RELEASE_DETECT | 检测到松开,正在消抖 | 检测到高电平 |
代码实现大概是这样的:
typedef enum {
KEY_IDLE,
KEY_PRESS_DETECT,
KEY_PRESS_CONFIRM,
KEY_RELEASE_DETECT
} KeyState_t;
KeyState_t keyState = KEY_IDLE;
uint8_t debounceCnt = 0;
void Key_Scan(void) {
uint8_t level = GPIO_ReadPin(KEY_PIN);
switch(keyState) {
case KEY_IDLE:
if(level == 0) { // 检测到按下
keyState = KEY_PRESS_DETECT;
debounceCnt = 0;
}
break;
case KEY_PRESS_DETECT:
if(level == 0) {
debounceCnt++;
if(debounceCnt >= 4) { // 连续4次低电平,确认按下
keyState = KEY_PRESS_CONFIRM;
Key_Event(KEY_PRESS); // 触发按下事件
}
} else {
keyState = KEY_IDLE; // 抖动,回到空闲
}
break;
case KEY_PRESS_CONFIRM:
if(level == 1) { // 检测到松开
keyState = KEY_RELEASE_DETECT;
debounceCnt = 0;
}
break;
case KEY_RELEASE_DETECT:
if(level == 1) {
debounceCnt++;
if(debounceCnt >= 4) { // 连续4次高电平,确认松开
keyState = KEY_IDLE;
Key_Event(KEY_RELEASE); // 触发松开事件
}
} else {
keyState = KEY_PRESS_CONFIRM; // 抖动,回到按下状态
}
break;
}
}
注意:这个函数需要在定时器中断或主循环中周期调用。建议调用间隔 5ms,这样消抖总时间就是 20ms。
有了状态机,你可以轻松扩展出长按检测。比如在 PRESS_CONFIRM 状态里加一个计时器,超过 1 秒就触发长按事件。
我曾经在一个剃须刀项目里,用这个状态机实现了:短按开关机、长按 2 秒进入旅行锁模式、双击切换档位。用户反馈说操作很顺手,没有误触。
4.5 避坑指南
做按键处理这么多年,踩过的坑不少。分享几个给你:
- 不要用 delay() 做消抖。 我曾经在一个项目里用了 delay(20),结果按键响应正常了,但电机控制出现了卡顿。因为 delay 阻塞了主循环。
- 注意按键的电气特性。 有些按键在潮湿环境下会漏电。剃须刀是防水产品,按键周围可能有水汽。建议在按键引脚加一个上拉电阻,并开启内部上拉。
- 消抖时间不是越长越好。 50ms 的消抖虽然更稳定,但用户会觉得按键“迟钝”。20ms 是业界公认的黄金值。
- 考虑低功耗场景。 剃须刀电池容量有限。如果按键扫描太频繁,会浪费电量。我一般用 10ms 的扫描间隔,配合休眠唤醒机制。
嗯,按键消抖看起来是个小问题,但处理不好,整个产品的体验都会打折扣。希望今天的分享对你有帮助。