在FPGA和嵌入式系统开发中,按键消抖是一个看似简单却至关重要的基础模块。作为一名有十年硬件开发经验的工程师,我见过太多因为按键抖动处理不当导致的系统故障——从简单的菜单跳转异常到关键功能误触发,这些问题往往在项目后期才暴露出来,造成巨大的调试成本。
本文将分享一个经过实际项目验证的按键消抖方案,基于摩尔型状态机设计,采用Verilog实现。不同于教科书上的简单延时法,这个方案具有以下特点:
这个方案已经在我们团队的多个FPGA项目中稳定运行,包括工业控制设备和医疗仪器等对可靠性要求极高的场景。下面我将从原理到实现详细解析这个设计的每个关键环节。
机械按键的抖动问题源于其物理结构。一个典型的轻触按键由以下几个部分组成:
当按键被按下时,金属弹片会发生形变,这个过程中触点会经历"接触-分离-再接触"的多次弹跳,最终稳定接触。释放时则发生相反的过程。这种物理特性导致了一个重要现象:在按键状态变化的瞬间(约5-20ms),信号线上会出现多次快速跳变。
提示:不同厂商、不同材质的按键抖动特性差异很大。工业级按键的抖动时间通常在5ms以内,而廉价的消费级按键可能达到15-20ms。
在FPGA系统中,典型的时钟频率为50MHz(周期20ns)。如果直接将按键信号接入系统,一次物理按键动作可能被误判为数十次甚至上百次按键事件。这种问题在以下场景尤为致命:
我曾经参与调试过一个工业控制器项目,就因为按键消抖处理不当,导致急停按钮偶尔失效,这个教训让我深刻认识到可靠消抖设计的重要性。
原理:利用电阻电容组成低通滤波器,滤除高频抖动信号
优点:实现简单,不占用逻辑资源
缺点:
原理:检测到边沿后固定延时20ms再采样
优点:实现简单
缺点:
原理:通过状态转移判断按键稳定状态
优点:
缺点:实现复杂度稍高
在状态机选型上,我们特别选择了摩尔型而非米利型,主要基于以下考虑:
输出稳定性:摩尔型状态机的输出只与当前状态有关,不受输入变化直接影响,这对按键消抖这种抗干扰需求高的场景特别重要。
设计清晰度:摩尔型的状态转移关系更加明确,便于调试和维护。在实际项目中,当我们需要调整消抖参数时,这种清晰的结构能大大降低修改风险。
时序特性:摩尔型输出会在时钟边沿同步更新,避免了米利型可能出现的毛刺问题。
我们将一次完整的按键过程划分为四个状态:
| 状态 | 编码 | 说明 | 退出条件 |
|---|---|---|---|
| IDLE | 00 | 等待按键按下 | 检测到下降沿 |
| P_FILTER | 01 | 按下消抖,计时判断稳定性 | 稳定20ms或检测到上升沿 |
| WAIT_R | 10 | 按键稳定按下,等待释放 | 检测到上升沿 |
| R_FILTER | 11 | 释放消抖,计时判断稳定性 | 稳定20ms或检测到下降沿 |
状态转移图如下:
code复制 +-----------+
| IDLE |<-----------------+
+-----------+ |
| neg_edge |
v |
+-----------+ |
| P_FILTER |--pos_edge-->+ |
+-----------+ | |
| time_20ms | |
v | |
+-----------+ | |
| WAIT_R | | |
+-----------+ | |
| pos_edge | |
v | |
+-----------+ | |
| R_FILTER |--neg_edge-->+ |
+-----------+ |
| time_20ms |
v |
+-------------------------+
按键信号是典型的异步输入,必须进行同步处理以避免亚稳态问题。我们采用两级寄存器同步链:
verilog复制always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) begin
sync_d0k <= 1'b1;
sync_d1k <= 1'b1;
rk <= 1'b1;
end else begin
sync_d0k <= Key; // 第一级同步
sync_d1k <= sync_d0k; // 第二级同步
rk <= sync_d1k; // 用于边沿检测的寄存
end
end
经验:在实际项目中,我们曾遇到过因为省略第二级同步导致的偶发性系统崩溃。特别是在环境温度变化大的场合,亚稳态问题会更加突出。
边沿检测是状态转移的关键条件,我们通过比较当前信号和上一拍信号来实现:
verilog复制assign neg_edge_k = (rk == 1'b1) && (sync_d1k == 1'b0); // 下降沿
assign pos_edge_k = (rk == 1'b0) && (sync_d1k == 1'b1); // 上升沿
状态机采用经典的三段式写法(状态定义、时序逻辑、组合逻辑),确保代码清晰可维护:
verilog复制// 状态定义
localparam IDLE = 2'b00,
P_FILTER = 2'b01,
WAIT_R = 2'b10,
R_FILTER = 2'b11;
// 状态寄存器
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) curr_state <= IDLE;
else curr_state <= next_state;
end
// 状态转移逻辑
always @(*) begin
next_state = curr_state;
case (curr_state)
IDLE: if (neg_edge_k) next_state = P_FILTER;
P_FILTER: begin
if (time_20ms_reach) next_state = WAIT_R;
else if (pos_edge_k) next_state = IDLE;
end
WAIT_R: if (pos_edge_k) next_state = R_FILTER;
R_FILTER: begin
if (time_20ms_reach) next_state = IDLE;
else if (neg_edge_k) next_state = WAIT_R;
end
endcase
end
20ms计时器是消抖的核心,在50MHz时钟下需要计数1,000,000个周期:
verilog复制reg [19:0] cnt; // 足够计数到1,000,000
assign time_20ms_reach = (cnt >= 20'd1000000);
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) cnt <= 20'd0;
else begin
case (curr_state)
P_FILTER, R_FILTER:
cnt <= time_20ms_reach ? 20'd0 : cnt + 1'b1;
default: cnt <= 20'd0;
endcase
end
end
技巧:在实际应用中,我们通常会将消抖时间做成参数,方便针对不同按键特性调整:
verilog复制parameter DEBOUNCE_TIME = 20'd1000000; // 20ms at 50MHz assign time_debounce_reach = (cnt >= DEBOUNCE_TIME);
我们构建了完整的测试平台,模拟了各种抖动场景:
verilog复制initial begin
// 初始化
Reset_n=0; Key=1;
#201; Reset_n=1;
// 第一次按键测试
Key=1; #100000000; // 空闲100ms
// 按下抖动(模拟实际抖动波形)
Key=0; #18000000; // 低18ms
Key=1; #2000000; // 高2ms
Key=0; #1000000; // 低1ms
Key=1; #200000; // 高0.2ms
Key=0; #20000000; // 低20ms(稳定)
// 按下稳定期
Key=0; #50000000; // 稳定50ms
// 释放抖动
Key=1; #2000000; // 高2ms
Key=0; #1000000; // 低1ms
Key=1; #2000000; // 高20ms(稳定)
// 释放稳定期
Key=1; #50000000; // 稳定50ms
// 第二次按键测试(类似第一次)
// ...
$stop;
end
通过仿真波形我们可以重点验证以下几个方面:
在项目实践中,我们特别关注以下几种边界情况:
在实际系统中通常需要处理多个按键,我们可以通过以下方式扩展:
完善的消抖模块应该支持以下参数配置:
verilog复制module key_filter #(
parameter CLK_FREQ = 50_000_000, // 时钟频率
parameter DEBOUNCE_MS = 20, // 消抖时间(ms)
parameter ACTIVE_LOW = 1 // 按键有效电平
)(
// 端口定义
);
localparam DEBOUNCE_CYCLES = CLK_FREQ * DEBOUNCE_MS / 1000;
// ...
endmodule
在工业环境中,还需要增加以下保护措施:
症状:按键操作到系统响应有明显延迟
可能原因:
症状:偶尔出现未按键时的误触发
可能原因:
症状:快速连续按键时丢失部分按键事件
可能原因:
我们在Xilinx Artix-7 FPGA上实测了这个设计的性能:
资源占用:
时序性能:
功耗影响:
这个设计已经在我们多个量产项目中得到验证,包括:
在严苛的环境测试中(温度-40℃~85℃,湿度20%~90%),按键误触发率<0.001%,完全满足工业级可靠性要求。