一、基础消抖原理:采样定理与消抖、边沿检测法、延时计数法、状态机消抖法

各位同学好,我是你们的FPGA讲师。今天咱们来聊聊按键消抖——这个看似简单、实则暗藏玄机的基础话题。

说实话,我刚开始做FPGA项目时,觉得按键消抖不就是加个延时吗?结果第一次调试时,按键按下去,LED乱跳,波形一抓,全是毛刺。嗯,从那以后我再也不敢小看这个“小问题”了。

1.1 为什么按键需要消抖?

机械按键的本质是什么?说白了,就是两个金属片碰在一起。但金属片不是理想开关,按下时它会弹跳几次才能稳定接触。这个弹跳过程,在数字电路里就表现为一串高低电平的毛刺。

我见过最夸张的一次,一个劣质按键弹跳了20多毫秒才稳定。你想想看,如果FPGA以50MHz采样,20ms能采100万个点,这中间得产生多少误触发?

核心结论:按键消抖的本质,就是滤除机械弹跳产生的毛刺,只输出稳定的电平变化。

1.2 采样定理与消抖的关系

这里我要提一个很多人忽略的点:采样定理。你可能觉得采样定理是ADC的事,跟按键消抖有什么关系?

关系大了。按键消抖本质上就是一个采样+判决的过程。奈奎斯特定理告诉我们:采样频率必须大于信号最高频率的两倍。对于按键来说,弹跳频率一般在几百Hz到几kHz之间。所以,我们的采样频率至少要高于这个范围的两倍。

我个人习惯用1kHz~10kHz的采样频率来做按键消抖。为什么?因为人按按键的速度再快,也快不过100ms。1kHz采样,每个采样点间隔1ms,10个点就是10ms,足够覆盖绝大多数按键的弹跳时间了。

采样频率 采样周期 适用场景
1kHz 1ms 普通按键,弹跳时间<10ms
10kHz 0.1ms 高速按键,弹跳时间<1ms
100kHz 0.01ms 特殊场景,需要极快响应

我的经验:不要一味追求高采样率。采样率越高,消耗的逻辑资源越多,而且容易把噪声也采进来。1kHz~10kHz是黄金区间。

1.3 边沿检测法

边沿检测法是最基础的消抖思路。它的原理很简单:检测按键信号的上升沿或下降沿,然后输出一个脉冲。

但这里有个坑:边沿检测本身不消抖。它只是检测变化,如果信号在弹跳,边沿检测会输出一堆脉冲。所以,边沿检测法必须配合其他消抖手段使用。

我曾经在一个项目中,直接用边沿检测去触发中断,结果按键按一次,中断进了十几次。从那以后,我再也不敢单独用边沿检测了。

// 边沿检测模块(不消抖,仅检测变化)
module edge_detect (
    input  clk,
    input  rst_n,
    input  key_in,
    output pos_edge,
    output neg_edge
);

reg key_in_d1, key_in_d2;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_in_d1 <= 1'b1;
        key_in_d2 <= 1'b1;
    end else begin
        key_in_d1 <= key_in;
        key_in_d2 <= key_in_d1;
    end
end

assign pos_edge = key_in_d1 & ~key_in_d2;  // 上升沿
assign neg_edge = ~key_in_d1 & key_in_d2;  // 下降沿

endmodule

1.4 延时计数法

延时计数法是我最常用的消抖方法。它的思路是:当检测到按键电平变化时,开始计数。如果计数达到某个阈值(比如10ms),就认为按键稳定了,输出有效电平。

为什么用计数而不是直接用延时?因为FPGA是并行执行的,用计数器可以精确控制延时时间,而且不阻塞其他逻辑。

我记得有一次调试,客户要求按键响应时间必须小于5ms。我算了一下,用1kHz采样,计数5次就够了。但实际测试发现,有些按键弹跳时间超过5ms。最后我妥协了,用10ms延时,客户也接受了。毕竟,稳定比快更重要。

// 延时计数法消抖模块
module debounce_counter (
    input  clk,        // 1kHz采样时钟
    input  rst_n,
    input  key_in,
    output reg key_out
);

parameter CNT_MAX = 10;  // 10ms延时

reg [3:0] cnt;
reg key_in_sync;

// 同步打拍,消除亚稳态
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_in_sync <= 1'b1;
    end else begin
        key_in_sync <= key_in;
    end
end

// 延时计数
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 4'd0;
        key_out <= 1'b1;
    end else if (key_in_sync != key_out) begin
        if (cnt == CNT_MAX - 1) begin
            cnt <= 4'd0;
            key_out <= key_in_sync;
        end else begin
            cnt <= cnt + 1'b1;
        end
    end else begin
        cnt <= 4'd0;
    end
end

endmodule

注意:延时计数法的延时时间要大于按键的最大弹跳时间。一般按键弹跳时间在5~20ms,建议取10~20ms。如果延时太短,消抖不彻底;太长,按键响应会变慢。

1.5 状态机消抖法

状态机消抖法比延时计数法更灵活。它把按键的状态分为几个阶段:空闲、按下抖动、按下稳定、释放抖动、释放稳定。每个阶段都有对应的处理逻辑。

我个人觉得,状态机消抖法更适合处理复杂的按键场景,比如长按、双击、连发等。如果你只需要简单的按下/释放检测,延时计数法就够了。

下面是我画的一个状态机消抖流程图,大家可以看看它的工作过程:

状态机消抖法流程图 空闲 key=1 按下抖动 计数中... 按下稳定 key=0 释放抖动 计数中... 释放稳定 key=1 检测到下降沿 计数到阈值 检测到上升沿(弹回) 检测到上升沿 计数到阈值 检测到下降沿(弹回) 自动跳转

状态机消抖法的核心思想是:只有连续采样到N次相同的电平,才认为按键状态稳定了。这个N就是我们的消抖阈值。

// 状态机消抖法模块
module debounce_fsm (
    input  clk,        // 1kHz采样时钟
    input  rst_n,
    input  key_in,
    output reg key_out
);

parameter IDLE      = 2'b00;
parameter PRESS_DEB = 2'b01;
parameter PRESS_STB = 2'b10;
parameter RELEASE_DEB = 2'b11;

parameter CNT_MAX = 10;  // 10ms延时

reg [1:0] state, next_state;
reg [3:0] cnt;

// 状态转移
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: begin
            if (!key_in) next_state = PRESS_DEB;
        end
        PRESS_DEB: begin
            if (key_in) next_state = IDLE;  // 弹回
            else if (cnt == CNT_MAX - 1) next_state = PRESS_STB;
        end
        PRESS_STB: begin
            if (key_in) next_state = RELEASE_DEB;
        end
        RELEASE_DEB: begin
            if (!key_in) next_state = PRESS_STB;  // 弹回
            else if (cnt == CNT_MAX - 1) next_state = IDLE;
        end
    endcase
end

// 计数器逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 4'd0;
    else if (state == PRESS_DEB || state == RELEASE_DEB)
        cnt <= cnt + 1'b1;
    else
        cnt <= 4'd0;
end

// 输出逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        key_out <= 1'b1;
    else if (state == PRESS_STB)
        key_out <= 1'b0;
    else if (state == IDLE)
        key_out <= 1'b1;
end

endmodule

我的建议:如果你刚开始学FPGA,先从延时计数法入手。它简单、直观、够用。等你把基础打牢了,再尝试状态机消抖法。不要一上来就搞复杂的,容易把自己绕晕。

1.6 三种方法对比

方法 资源消耗 灵活性 适用场景
边沿检测法 极低(2个寄存器) 低 必须配合其他消抖使用
延时计数法 低(1个计数器) 中 普通按键,简单场景
状态机消抖法 中(状态寄存器+计数器) 高 复杂按键,长按/双击等

好了,这一章的内容就到这里。三种方法各有优劣,没有的好坏。关键是根据你的项目需求,选择最合适的那一个。

下一章,我会带大家把这些方法应用到实际的高速收发器项目中。到时候,你会看到这些基础原理是如何在真实系统中发挥作用的。

本章小结:

  • 按键消抖的本质是滤除机械弹跳产生的毛刺
  • 采样频率选择1kHz~10kHz比较合适
  • 边沿检测法只检测变化,不消抖
  • 延时计数法简单实用,适合大多数场景
  • 状态机消抖法灵活,适合复杂按键逻辑

可可精品资料库