3、软件消抖基础:延时消抖法(20ms延时)、状态机消抖法、计数器消抖法

按键消抖,说白了就是跟机械弹片“斗智斗勇”。

你按下一个按键,你以为只触发了一次,实际上在几毫秒内,它可能弹跳了十几次。如果不处理,你的LED就会像抽风一样乱闪。

我在项目中遇到过最夸张的一次,一个按键按下,计数器直接跳了30多次。从那以后,我对消抖这件事就特别上心。

3.1 延时消抖法(20ms延时)

这是最直观的方法。原理很简单:检测到按键按下后,先等20ms,再读一次电平。如果还是低电平,说明真的按下了。

为什么是20ms?其实这是个经验值。机械按键的弹跳时间一般在5-10ms,留点余量,20ms比较稳妥。

核心思路:检测到边沿 -> 延时等待 -> 再次确认 -> 输出有效信号

// 延时消抖法 - Verilog示例
reg [19:0] cnt_20ms;
reg key_reg0, key_reg1;
wire key_press;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_reg0 <= 1'b1;
        key_reg1 <= 1'b1;
        cnt_20ms <= 20'd0;
    end else begin
        key_reg0 <= key_in;
        key_reg1 <= key_reg0;
        
        if (key_reg1 && !key_reg0)  // 检测到下降沿
            cnt_20ms <= 20'd1_000_000;  // 假设50MHz时钟,20ms计数
        else if (cnt_20ms != 0)
            cnt_20ms <= cnt_20ms - 1'b1;
    end
end

assign key_press = (cnt_20ms == 20'd1) ? 1'b1 : 1'b0;

注意:延时消抖法有个致命问题——它会阻塞你的系统。在延时期间,CPU或状态机啥也干不了。所以它只适合按键数量少、系统不复杂的场景。

我曾经在一个8按键的键盘上用过这种方法,结果发现按下一个键,其他键全被“冻住”了。嗯,从那以后我就很少用纯延时法了。

3.2 状态机消抖法

状态机消抖,说白了就是用有限状态机来管理按键的状态变化。它比延时法灵活得多,也更容易扩展。

我习惯用4状态的状态机:空闲态、抖动态、稳定态、释放态。你想想看,每个状态对应按键的一个阶段,逻辑非常清晰。

状态 描述 触发条件
IDLE 等待按键按下 检测到低电平 -> 进入SHAKE
SHAKE 等待抖动结束 20ms后电平稳定 -> 进入PRESS
PRESS 按键已稳定按下 检测到高电平 -> 进入RELEASE
RELEASE 等待释放完成 20ms后电平稳定 -> 回到IDLE
// 状态机消抖法 - 核心代码
localparam IDLE    = 2'b00;
localparam SHAKE   = 2'b01;
localparam PRESS   = 2'b10;
localparam RELEASE = 2'b11;

reg [1:0] state, next_state;
reg [19:0] timer;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

always @(*) begin
    next_state = state;
    case (state)
        IDLE:    if (!key_in) next_state = SHAKE;
        SHAKE:   if (timer_done) next_state = PRESS;
        PRESS:   if (key_in) next_state = RELEASE;
        RELEASE: if (timer_done) next_state = IDLE;
    endcase
end

我的经验:状态机消抖法特别适合多按键场景。你可以把每个按键的状态机独立开来,互不干扰。我在一个16按键的矩阵键盘上用过,效果非常好。

3.3 计数器消抖法

计数器消抖法,说白了就是“连续采样,多数表决”。

原理很简单:每隔1ms采样一次按键电平,连续采样20次。如果20次中有16次以上是低电平,就认为按键真的按下了。

为什么会这样设计?因为弹跳是随机的,但真实按键信号是持续的。通过多次采样,可以过滤掉那些短暂的弹跳脉冲。

// 计数器消抖法 - 连续采样
reg [4:0] cnt;  // 5位计数器,最大31
reg [3:0] sample_cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 5'd0;
        sample_cnt <= 4'd0;
    end else if (cnt == 5'd19) begin  // 每1ms采样一次
        cnt <= 5'd0;
        if (!key_in)
            sample_cnt <= sample_cnt + 1'b1;
    end else begin
        cnt <= cnt + 1'b1;
    end
end

// 当采样次数达到阈值时,输出有效信号
assign key_press = (sample_cnt >= 4'd12) ? 1'b1 : 1'b0;

关键参数:

  • 采样间隔:1ms(根据时钟频率调整计数器值)
  • 采样次数:20次(覆盖20ms的抖动窗口)
  • 判定阈值:12-16次(根据按键质量调整)

我个人比较喜欢计数器法,因为它不阻塞系统,而且抗干扰能力强。我曾经在一个工业控制板上用过,现场电磁环境很差,但计数器法愣是没出过一次误触发。

避坑指南:计数器法的阈值不要设得太死。比如20次中要求20次全低,那按键稍微有点老化就识别不到了。我一般设成16/20,留点余量。

3.4 三种方法对比

方法 优点 缺点 适用场景
延时消抖法 实现简单,代码量少 阻塞系统,效率低 单按键、低要求系统
状态机消抖法 灵活,可扩展,不阻塞 代码稍复杂 多按键、复杂系统
计数器消抖法 抗干扰强,不阻塞 需要调整阈值 工业环境、高可靠性

嗯,三种方法各有千秋。我个人建议:如果是做产品原型,用状态机法最稳妥;如果是做工业级设备,计数器法更靠谱;至于延时法...除非你只是做个实验,否则尽量别用。

记住一句话:按键消抖不是技术难题,而是工程经验。你做得多了,自然就知道哪种方法最适合你的项目。