3. 软件去抖动策略:延时去抖法、状态机去抖法、定时器扫描去抖法
上一章我们聊了抖动是怎么来的,也看了硬件去抖的几种方案。说实话,硬件去抖虽然可靠,但成本摆在那里——多一颗电容、多一个施密特触发器,在小批量产品里就是实打实的成本压力。所以,软件去抖才是大多数嵌入式工程师的日常。
我个人习惯把软件去抖分成三个流派:延时去抖法、状态机去抖法、定时器扫描去抖法。它们各有各的脾气,选对了事半功倍,选错了……嗯,我见过有人用延时去抖处理高速编码器,结果系统直接卡死。
3.1 延时去抖法:简单粗暴,但别滥用
延时去抖的思路很直白:检测到电平变化后,等一段时间再读一次。如果两次结果一致,就认为是有效跳变。
核心逻辑:用时间换确定性。抖动通常在 5~20ms 内结束,延时去抖就是跳过这个窗口。
// 延时去抖示例(STM32 HAL)
uint8_t Debounce_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
uint8_t state1 = HAL_GPIO_ReadPin(GPIOx, GPIO_Pin);
Delay_ms(10); // 等待抖动过去
uint8_t state2 = HAL_GPIO_ReadPin(GPIOx, GPIO_Pin);
return (state1 == state2) ? state1 : state1; // 一致则返回,否则返回第一次的值
}
我曾经踩过的坑:在一个温控器项目里,我用延时去抖处理按键,延时设了 20ms。结果按键响应总是慢半拍,用户按下去要等 20ms 才有反应。更糟的是,如果按键按下时刚好有中断进来,延时函数会阻塞整个系统。说白了,延时去抖只适合非实时、单任务的场景。
| 优点 | 缺点 |
|---|---|
| 代码简单,容易理解 | 阻塞 CPU,浪费算力 |
| 占用资源极少 | 响应延迟固定,不可调 |
| 适合调试阶段快速验证 | 多按键时难以管理 |
3.2 状态机去抖法:优雅且高效
状态机去抖是我在 RTOS 项目里最常用的方法。它不阻塞,不浪费 CPU,而且能同时处理多个按键。你想想看,一个状态机就能搞定所有 GPIO 的去抖,多划算。
状态机通常有四个状态:空闲、按下确认、按下稳定、释放确认。每次采样都驱动状态迁移,只有稳定状态才输出有效电平。
// 状态机去抖示例(伪代码)
typedef enum {
STATE_IDLE,
STATE_PRESS_DEBOUNCE,
STATE_PRESS_STABLE,
STATE_RELEASE_DEBOUNCE
} DebounceState;
uint8_t Debounce_SM(uint8_t raw_level) {
static DebounceState state = STATE_IDLE;
static uint8_t stable_level = 0;
switch(state) {
case STATE_IDLE:
if(raw_level == 0) { // 检测到按下
state = STATE_PRESS_DEBOUNCE;
}
break;
case STATE_PRESS_DEBOUNCE:
if(raw_level == 0) {
state = STATE_PRESS_STABLE;
stable_level = 0;
} else {
state = STATE_IDLE; // 抖动回弹
}
break;
case STATE_PRESS_STABLE:
if(raw_level == 1) { // 检测到释放
state = STATE_RELEASE_DEBOUNCE;
}
break;
case STATE_RELEASE_DEBOUNCE:
if(raw_level == 1) {
state = STATE_IDLE;
stable_level = 1;
} else {
state = STATE_PRESS_STABLE; // 抖动回弹
}
break;
}
return stable_level;
}
我的经验:状态机去抖的采样周期通常设为 5~10ms。太快了浪费 CPU,太慢了响应迟钝。我在一个工业控制器项目里,用 8ms 采样周期处理 12 个按键,CPU 占用率不到 1%。
3.3 定时器扫描去抖法:RTOS 的最佳拍档
如果你在用 RTOS,定时器扫描去抖法几乎是标准答案。它的思路是:用一个定时器(硬件或软件)周期性扫描所有 GPIO,每次扫描都更新状态机。这样,去抖逻辑和主任务完全解耦。
为什么说它适合 RTOS?因为你可以把扫描任务放在一个低优先级的线程里,或者用定时器回调触发。主任务该干嘛干嘛,互不干扰。
// 定时器扫描去抖(FreeRTOS 示例)
void Debounce_TimerCallback(TimerHandle_t xTimer) {
for(int i = 0; i < NUM_KEYS; i++) {
uint8_t raw = HAL_GPIO_ReadPin(key_ports[i], key_pins[i]);
key_states[i] = Debounce_SM(raw); // 调用状态机
}
}
// 创建定时器,周期 10ms
TimerHandle_t debounce_timer = xTimerCreate(
"Debounce",
pdMS_TO_TICKS(10),
pdTRUE,
NULL,
Debounce_TimerCallback
);
关键点:定时器扫描去抖法的核心是采样周期和状态机的组合。采样周期决定了去抖的粒度,状态机决定了去抖的可靠性。两者缺一不可。
| 方法 | CPU 占用 | 响应延迟 | 多按键支持 | RTOS 友好度 |
|---|---|---|---|---|
| 延时去抖法 | 高(阻塞) | 固定(10~20ms) | 差 | 差 |
| 状态机去抖法 | 低(非阻塞) | 可调(1~2 个采样周期) | 好 | 好 |
| 定时器扫描去抖法 | 极低(定时器回调) | 可调(1~2 个采样周期) | 极好 | 极好 |
3.4 三种方法的对比与选型
选哪种方法,说白了要看你的项目需求。我整理了一个简单的选型指南:
- 原型验证、临时测试 → 延时去抖法,快就完了。
- 裸机项目、按键数量少(≤4 个) → 状态机去抖法,性价比最高。
- RTOS 项目、按键数量多(≥8 个) → 定时器扫描去抖法,专业且可靠。
- 对响应时间有严格要求(如机械开关中断) → 定时器扫描 + 硬件去抖混合使用。
避坑指南:我曾经在一个项目里,用延时去抖处理 8 个按键,结果系统响应延迟累积到 160ms。用户按下去,灯要等 0.16 秒才亮。后来换成定时器扫描去抖,延迟降到 10ms 以内。记住:延时去抖是毒药,能不用就别用。
3.5 实战建议:采样周期怎么定?
采样周期是去抖的灵魂。定得太短,抖动还没结束就采样了,等于白做。定得太长,响应延迟大到没法用。
我一般遵循这个原则:采样周期 = 抖动持续时间 × 0.5。比如机械按键抖动通常 10~20ms,我就取 8ms 或 10ms。这样两个采样周期就能覆盖整个抖动窗口,既可靠又快速。
小技巧:用示波器抓一下按键按下时的波形,看看抖动到底持续多久。不同品牌、不同批次的按键,抖动时间可能差一倍。我见过最夸张的,一个国产按键抖动持续了 35ms,差点把我坑惨了。