【FPGA实战】Verilog状态机实现按键消抖与数码管计数

张开发
2026/5/17 15:45:11 15 分钟阅读
【FPGA实战】Verilog状态机实现按键消抖与数码管计数
1. 按键消抖的必要性与原理机械按键在按下和释放时会产生物理抖动这是电子工程师最头疼的问题之一。我刚开始接触FPGA时曾天真地以为按键信号可以直接使用结果计数器数值疯狂跳变差点让我怀疑人生。实测用示波器观察普通微动开关的抖动时间通常在5-20ms之间会产生多个脉冲边沿。传统解决方案是用电容滤波但在FPGA里我们完全可以用数字逻辑实现更精准的消抖。状态机是最优雅的实现方式它把消抖过程明确划分为四个阶段松开稳定态S1按键未被按下保持高电平按下过渡态S2检测到下降沿后进入消抖计时按下稳定态S3确认按键有效按下释放过渡态S4检测到上升沿后进入释放消抖核心逻辑是通过20ms计时器过滤抖动信号。这里有个细节要注意50MHz时钟下计数1,000,000次正好是20ms1/50MHz × 1,000,000 0.02s。我在Cyclone IV开发板上实测这个参数对各种微动开关都能稳定工作。2. Verilog状态机详细实现2.1 边沿检测技巧状态机运转的关键是准确检测边沿信号。这段代码我优化过三次才稳定reg key_tmp0, key_tmp1; always(posedge clk or negedge rst_n) begin if(!rst_n) begin key_tmp0 1b1; key_tmp1 1b1; end else begin key_tmp0 key_in; // 一级寄存器 key_tmp1 key_tmp0; // 二级寄存器 end end wire pedge key_tmp0 ~key_tmp1; // 上升沿 wire nedge ~key_tmp0 key_tmp1; // 下降沿这种两级寄存器结构能有效消除亚稳态。有个坑要注意必须用非阻塞赋值()否则会出现时序问题。我曾用阻塞赋值导致边沿检测失效排查了整整一天。2.2 四状态转换逻辑状态机的核心是case语句每个状态要处理三种情况always(posedge clk or negedge rst_n) begin case(state) S1: // 松开稳定 if(nedge) begin state S2; en_counter 1b1; // 启动消抖计时 end S2: // 按下消抖 if(cnt_full) begin state S3; key_flag 1b1; // 产生有效按键脉冲 end else if(pedge) state S1; // 判定为抖动 // ...其他状态类似 endcase endkey_flag信号是整个设计的精华它只在按键稳定按下时产生一个时钟周期的高脉冲。实际项目中我常用这个信号驱动计数器、菜单切换等逻辑。3. 数码管驱动设计3.1 十进制计数模块按键消抖稳定后就可以驱动数码管了。这个always块很有意思always(posedge key_flag, negedge rst_n) if(!rst_n) Q 0; else Q (Q 4d9) ? 4b0 : Q 1b1;注意敏感列表里同时有key_flag和rst_n这是标准的异步复位设计。我在早期版本漏了复位信号上电时数码管总是显示乱码。3.2 七段译码器共阴极数码管的译码器就是个查找表always(Q) begin case(Q) 4d0: codeout 7b1111110; // abcdefg 4d1: codeout 7b0110000; // ...其他数字 endcase end不同厂家的数码管段码可能不同有次我用错编码数字7显示成了F。建议在代码里加注释标注段序比如我的板子段码顺序是DP-g-f-e-d-c-b-a。4. 工程实践与调试技巧4.1 Modelsim仿真要点仿真时要注意时间单位设置timescale 1ns/1ps // 定义时间精度 initial begin key_in 1b1; #200 rst_n 1b1; // 复位释放 // 模拟按键抖动 #10 key_in 1b0; #5 key_in 1b1; #8 key_in 1b0; end波形窗口建议设置成ms单位右键Wave窗口选WavePreference这样能直观看到20ms消抖效果。我习惯添加state状态变量到波形用颜色区分不同状态。4.2 实际板级调试引脚分配要特别注意消抖按键不能接在全局时钟引脚上。我的Altera开发板常用配置clk - PIN_90 (50MHz时钟)rst_n - KEY0 (PIN_24)key_in - KEY1 (PIN_31)codeout[6:0] - 数码管段码引脚遇到过最诡异的问题是按键按下后数码管不变化最后发现是约束文件里把按键引脚设为了弱上拉。建议在Quartus的Assignment Editor里确认所有引脚的电平标准。5. 进阶优化方向5.1 参数化设计将消抖时间设为参数更方便移植parameter DEBOUNCE_TIME 20; // 单位ms localparam CNT_MAX 50_000 * DEBOUNCE_TIME; // 50MHz时钟这样换不同频率的板子时只需修改DEBOUNCE_TIME参数即可。我在一个医疗设备项目里需要10ms消抖用这个方案节省了大量调试时间。5.2 多按键扩展实际项目经常需要多个按键module key_debounce #(parameter KEY_NUM 4) ( input [KEY_NUM-1:0] key_in, output [KEY_NUM-1:0] key_flag ); genvar i; generate for(i0; iKEY_NUM; ii1) begin debounce_unit du(.clk(clk), .rst_n(rst_n), .key_in(key_in[i]), .key_flag(key_flag[i])); end endgenerate endmodule这种结构在游戏手柄等应用中非常实用每个按键独立消抖又共享时钟和复位信号。

更多文章