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 以内。记住:延时去抖是毒药,能不用就别用。

软件去抖策略选型流程图 开始:需要软件去抖 是否使用 RTOS? 是 否 定时器扫描去抖法 按键数量 < 4? 是 否 状态机去抖法 延时去抖法(临时) 提示:RTOS 项目推荐定时器扫描去抖;裸机项目推荐状态机去抖;延时去抖仅用于快速验证

3.5 实战建议:采样周期怎么定?

采样周期是去抖的灵魂。定得太短,抖动还没结束就采样了,等于白做。定得太长,响应延迟大到没法用。

我一般遵循这个原则:采样周期 = 抖动持续时间 × 0.5。比如机械按键抖动通常 10~20ms,我就取 8ms 或 10ms。这样两个采样周期就能覆盖整个抖动窗口,既可靠又快速。

小技巧:用示波器抓一下按键按下时的波形,看看抖动到底持续多久。不同品牌、不同批次的按键,抖动时间可能差一倍。我见过最夸张的,一个国产按键抖动持续了 35ms,差点把我坑惨了。