从一个灵异现象说起
很多新手都遇到过:按键只按了一下,串口却打印了「按下 3 次」;或者做个计数器,数字一次跳了好几。这不是单片机「闹鬼」,而是机械按键的物理天性——触点弹跳(Contact Bounce)。
一、按键为什么会「抖」?
按键内部是两片金属触点。当你按下时,触点并不是「啪」一下严丝合缝地贴上,而是像拍手一样——接触、弹开、再接触、再弹开,来回几次才稳定闭合。整个过程只有 5~10 ms,但对跑得飞快的单片机来说,每次「弹开」都足够它把电平变化扫描一遍。
于是:你以为按了 1 次,单片机眼里是「按了 5 次」。
类比
就像往床上扔一个篮球:球「咚、咚、咚」弹几下才停。触点也一样,只是弹的是自己。
二、看懂动图:抖的是信号,乱的是逻辑
下面的动图里,上排是引脚真实看到的电平(按下瞬间来回横跳),下排是消抖后逻辑认定的电平(干净利落的一次跳变):

红色游标扫过的过程,就是单片机眼中的时间流逝。可以看到:
- 原始信号:在 3.3 V 和 0 V 之间来回弹了 3~4 次,持续约 3 ms(真实按键可达 10 ms);
- 确认信号:等到「持续稳定」之后才承认「真的按下了」——只认一次。
三、软件消抖:最常用的两条路
路线 A:检测到变化,延时 10~20 ms 再确认(入门首选)
if (KEY_DOWN) { // 读到低电平(按下)
HAL_Delay(15); // 跳过抖动窗口
if (KEY_DOWN) { // 还是被按下?
do_action(); // 放心执行
while (KEY_DOWN); // 等待松手(简单阻塞写法)
}
}优点是 5 行搞定;缺点是 HAL_Delay 期间 CPU 干等着,且「等待松手」会卡住主循环。
路线 B:状态机 + 定时器扫描(实际项目标准写法)
思路:每 510 ms 用定时器扫一次按键,**连续 N 次(如 24 次)读到同一状态**才承认状态变化:
松开 ──连续N次读到按下──→ 按下(消抖中) ──确认──→ 触发一次动作
↑ │
└──────连续N次读到松开──────┘
不阻塞、能顺带处理长按/连击,是推荐进阶方向。你的库里已有的《按键滤波程序》就是这一思路的代码实现,可以对照着读。
四、硬件消抖:让电容替你「抹平」
在按键两端并联一颗小电容(0.1 µF),配合上拉电阻,抖动电量会被电容「吸收」,引脚电平就变成平缓的曲线;再靠 MCU 内部或外部的施密特触发器把缓变沿整成干净的方波:

时间常数 τ ≈ R1 × C1 ≈ 10 kΩ × 0.1 µF = 1 ms,远小于抖动的持续时间窗口,按下的动作几乎感觉不到延迟。
硬件消抖的定位
它把「毛刺」挡在芯片外面,软件只需简单读电平。按键数量少、追求可靠的量产板子常用;但多了电容和布线成本,原型的快速验证一般还是软件消抖。
五、两种方案怎么选?
| 场景 | 推荐 |
|---|---|
| 学习阶段、按个位数按键、原型验证 | 软件消抖(路线 A 起步,够用) |
| 正式项目、多按键、需要长按/连击 | 定时器扫描 + 状态机(路线 B) |
| 强干扰环境 / 量产可靠性 | RC 硬件消抖 + 软件兜底,双保险 |
六、小结
- 抖动是机械触点的物理天性,5~10 ms,躲不掉,只能「滤」;
- 软件消抖核心思想:变化后等待并连续确认;
- 硬件消抖核心元件:0.1 µF 电容 + 上拉电阻;
- 实际项目主流:定时器扫描 + 状态机,不阻塞还能扩展长按功能。
顺带一提:按键电路里那颗「上拉电阻」也不是随手画的——它有自己的故事,见《上拉电阻与下拉电阻:让引脚不悬空的小卫士》。