摘要:本文是"FPGA工程创建实战"系列的第三篇,围绕机械按键的抖动问题,从原理到实现完整讲解了一个基于Verilog HDL的按键消抖模块设计。文章首先分析按键抖动现象及其危害,对比硬件消抖与软件消抖两种方案;随后给出一个三级流水处理的参数化消抖模块,包括两级同步消除亚稳态、消抖计数稳定采样、边沿检测生成按下/释放脉冲三个环节,并详细说明非阻塞赋值与时序关系;最后通过ModelSim仿真和Quartus上板验证,确认模块在抖动期间输出稳定、边沿脉冲准确,可直接用于实际FPGA工程。
1. 引言
在数字电路设计中,机械按键的抖动是一个常见且必须处理的问题。当按键被按下或释放时,由于机械触点的弹性,会在几毫秒到几十毫秒内产生一系列不稳定的电平跳变,这种现象称为“抖动”。如果不对抖动进行处理,FPGA可能会将一次按键动作误判为多次,导致逻辑错误。
本文是"FPGA工程创建实战"系列的第三篇,将带领读者从零开始,创建一个完整的FPGA工程,并实现一个稳定可靠的按键消抖模块。我们将使用Verilog HDL进行设计,并在Quartus开发环境中完成仿真与验证。
2. 工程创建与环境准备
2.1 开发环境
- FPGA开发板:Altera Cyclone IV EP4CE10(如AC620、征途Mini等开发板)
- 开发工具:Quartus Prime 18.0或更高版本(支持Cyclone IV系列)
- 仿真工具:ModelSim(Quartus自带或独立版本)
- 设计语言:Verilog HDL
2.2 创建新工程
具体步骤请阅读之前的文章
3. 按键消抖原理分析
3.1 抖动现象
从波形可以看出,在按键稳定前存在多个脉冲,这些脉冲如果被FPGA采样,就会被误认为是多次按键。
3.2 消抖方法
常用的消抖方法有两种:
- 硬件消抖:使用RC滤波电路或施密特触发器
- 软件消抖:通过FPGA内部的计数器进行延时采样
本文采用软件消抖方法,其核心思想是:当检测到按键状态变化后,等待一段时间(如20ms),如果按键状态保持稳定,则认为抖动结束,输出稳定的按键信号。
4. Verilog模块设计
下面设计一个三级流水处理的按键消抖模块。该模块采用参数化设计,消抖时间可配置,并输出消抖后的稳定电平以及按下、释放边沿脉冲。
4.1 模块接口定义
消抖原理分析:按键是机械结构,按下/释放时触片会弹跳抖动,持续约5~20ms。抖动期间电平会在0和1之间快速跳变,如果直接采样会产生误判。消抖的核心思想是:检测到电平变化后,等待一段时间(本例10ms),如果这段时间内电平保持不变,才认为是稳定的,更新输出。
模块分为三级流水处理:
- 第一级:两级同步——消除跨时钟域的亚稳态(FPGA设计基本要求)
- 第二级:消抖计数——检测输入变化,倒计时等待稳定,稳定后采样输出
- 第三级:边沿检测——比较当前值和上一拍值,提取上升沿/下降沿脉冲
输出信号说明(全部为消抖后信号,可直接用于逻辑判断):
key_stable——消抖后的稳定电平(低有效:0=按下,1=释放),可直接读取当前按键状态key_press——按下边沿(下降沿),持续1个时钟周期的高脉冲,按键从释放变为按下的瞬间拉高,适合触发"按下事件"key_release——释放边沿(上升沿),持续1个时钟周期的高脉冲,按键从按下变为释放的瞬间拉高,适合触发"释放事件"
module key_debounce #( parameter DEBOUNCE_CNT = 500_000 // 消抖计数值,50MHz下约10ms )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位,低有效 input wire key_in, // 原始按键输入(低有效:0=按下,1=释放) // ---- 以下输出均为消抖后信号,可直接用于逻辑判断 ---- output wire key_stable, // 消抖后稳定电平(低有效) output wire key_press, // 消抖后按下边沿(高脉冲1周期) output wire key_release // 消抖后释放边沿(高脉冲1周期) );4.2 内部信号与参数计算
模块内部定义了以下关键信号:
key_sync、key_sync_dly、key_sync_dly2——三级同步寄存器,用于消除亚稳态debounce_cnt——位宽随参数自适应的消抖计数器,防止溢出key_stable_reg——消抖后稳定值寄存器key_stable_dly——稳定值延迟一拍,用于边沿检测
// ---- 内部信号 ---- reg key_sync, key_sync_dly, key_sync_dly2; // 三级同步寄存器 reg [$clog2(DEBOUNCE_CNT+1)-1:0] debounce_cnt; // 位宽随参数自适应,防止溢出 reg key_stable_reg; // 消抖后稳定值寄存器 reg key_stable_dly; // 稳定值延迟一拍,用于边沿检测4.3 第一级:两级同步(消除亚稳态)
原理:key_in是外部信号,与clk不同步,直接采样可能进入亚稳态(输出在0和1之间的不确定状态,会导致后续逻辑出错)。用两个串联的D触发器,第一拍可能仍处于亚稳态,但经过一拍后第二拍的输出就是稳定的了。这是FPGA处理异步输入的标准做法。如何延迟两拍这就用到了我们的
Verilog:<=表示非阻塞赋值。它会并行执行,右边的值先被计算并暂存,等所有赋值操作结束后才统一更新左边的变量。主要用于时序逻辑(如 always 块中触发器的赋值)。
按照时域上来解释,比如0周期,1周期,2周期。
其中key_in是最新状态,代表0周期的状态。当key_in在0周期发生变化后, key_sync <= key_in并不会立马生效,而是等待1周期,将key_in赋值给key_sync,以此来达到延时的功能。同理,key_sync_dly <= key_sync;会在2周期将将key_sync赋值给key_sync_dly。具体如图所示,假设外界在第0个周期key_in由1变为0
| key_in | key_sync | key_sync_dly | key_sync_dly2 | |
| 初始化 | 1(可能处于亚稳态) | 1 | 1 | 1 |
| 0周期 | 0 | 1(处于亚稳态的几率减少) | 1 | 1 |
| 1周期 | 1 | 0 | 1(电平状态稳定) | 1 |
| 2周期 | 1 | 1 | 0 | 1 |
效果:key_sync_dly是同步后的信号,可以安全地在clk域内使用。
// ======================================================== // 第一级:两级同步,消除亚稳态 // ======================================================== // 原理:key_in是外部信号,与clk不同步,直接采样可能进入亚稳态 // (输出在0和1之间的不确定状态,会导致后续逻辑出错)。 // 用两个串联的D触发器,第一拍可能仍处于亚稳态, // 但经过一拍后第二拍的输出就是稳定的了。 // 这是FPGA处理异步输入的标准做法。 // 效果:key_sync_dly是同步后的信号,可以安全地在clk域内使用。 // ======================================================== always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync <= 1'b1; key_sync_dly <= 1'b1; key_sync_dly2 <= 1'b1; end else begin key_sync <= key_in; // 第一拍:可能亚稳态 key_sync_dly <= key_sync; // 第二拍:亚稳态已衰减 key_sync_dly2 <= key_sync_dly; // 再延一拍:用作变化检测基准 end end4.4 第二级:消抖计数(稳定后更新输出)
原理:按键抖动时,电平会在0/1之间快速跳变(5~20ms内)。我们不关心抖动期间的每一个跳变,只需要最终稳定的值。
状态机逻辑:
key_sync_dly2 != key_sync_dly——检测到输入变化,说明可能开始抖动了,立刻重新装载计数器(从头开始等)debounce_cnt > 0——输入没有变化,正在倒计时,说明电平已经稳定了一段时间debounce_cnt == 0——倒计时结束,确认输入已经稳定足够长时间(10ms),将当前值采样到key_stable_reg
关键:抖动期间会反复触发"检测到变化→重新装载",只有最后一次变化后持续稳定了10ms,才会真正更新输出。这样就过滤掉了所有抖动。
// ======================================================== // 第二级:消抖计数,稳定后更新输出 // ======================================================== // 原理:按键抖动时,电平会在0/1之间快速跳变(5~20ms内)。 // 我们不关心抖动期间的每一个跳变,只需要最终稳定的值。 // // 状态机逻辑: // 1. key_sync_dly2 != key_sync_dly → 检测到输入变化,说明可能 // 开始抖动了,立刻重新装载计数器(重头开始等) // 2. debounce_cnt > 0 → 输入没有变化,正在倒计时, // 说明电平已经稳定了一段时间 // 3. debounce_cnt == 0 → 倒计时结束,确认输入已经稳定 // 足够长时间(10ms),将当前值采样到key_stable_reg // // 关键:抖动期间会反复触发"检测到变化→重新装载", // 只有最后一次变化后持续稳定了10ms,才会真正更新输出。 // 这样就过滤掉了所有抖动。 // ======================================================== always @(posedge clk or negedge rst_n) begin if (!rst_n) begin debounce_cnt <= 'd0; key_stable_reg <= 1'b1; end else begin if (key_sync_dly2 != key_sync_dly) begin // 输入发生变化(可能是抖动),重新装载计数器 debounce_cnt <= DEBOUNCE_CNT; end else if (debounce_cnt > 0) begin // 输入稳定,倒计时递减 debounce_cnt <= debounce_cnt - 1'b1; end else begin // 计数归零,确认稳定,采样输出 key_stable_reg <= key_sync_dly; end end end4.5 第三级:边沿检测(生成按下/释放脉冲)
原理:把消抖后的稳定值延迟一拍,得到key_stable_dly。比较相邻两拍的值就能检测边沿:
- 下降沿(按下):上一拍是1,当前是0 →
key_stable_dly & ~key_stable_reg = 1 - 上升沿(释放):上一拍是0,当前是1 →
~key_stable_dly & key_stable_reg = 1
输出是1个时钟周期的高脉冲,可以直接作为触发信号。
输出赋值说明:
key_stable直接输出消抖后的稳定电平(低有效),可直接读取按键状态key_press检测按下边沿(下降沿),条件为上一拍为1(释放)、当前为0(按下),输出1个时钟周期的高脉冲key_release检测释放边沿(上升沿),条件为上一拍为0(按下)、当前为1(释放),输出1个时钟周期的高脉冲
注意:
- 边沿检测中key_press这些边沿信号,不可使用key_sync_dly&~key_sync_dly2来触发,如果使用,将会在机械抖动期间,按键被触发多次
// ======================================================== // 第三级:边沿检测,生成按下/释放脉冲 // ======================================================== // 原理:把消抖后的稳定值延迟一拍,得到key_stable_dly。 // 比较相邻两拍的值就能检测边沿: // - 下降沿(按下):上一拍是1,当前是0 → 1 & ~0 = 1 // - 上升沿(释放):上一拍是0,当前是1 → ~0 & 1 = 1 // 输出是1个时钟周期的高脉冲,可以直接作为触发信号。 // ======================================================== always @(posedge clk or negedge rst_n) begin if (!rst_n) key_stable_dly <= 1'b1; else key_stable_dly <= key_stable_reg; end // ---- 输出赋值 ---- // key_stable:消抖后的稳定电平,低有效,可直接读取按键状态 assign key_stable = key_stable_reg; // key_press:按下边沿(下降沿),持续1个时钟周期的高脉冲 // 条件:上一拍为1(释放),当前为0(按下)→ 检测到按下瞬间 assign key_press = key_stable_dly & ~key_stable_reg; // key_release:释放边沿(上升沿),持续1个时钟周期的高脉冲 // 条件:上一拍为0(按下),当前为1(释放)→ 检测到释放瞬间 assign key_release = ~key_stable_dly & key_stable_reg; endmodule4.6 参数计算与说明
模块中的DEBOUNCE_CNT参数用于控制消抖时间。计算公式为:
消抖计数值 = 系统时钟频率 × 消抖时间
例如,当系统时钟为 50MHz,需要 10ms 消抖时间时:
DEBOUNCE_CNT = 50_000_000 × 0.01 = 500_000用户可以根据实际需求调整此参数:
- 对于抖动较严重的按键,可适当增大计数值(如 20ms)
- 对于响应速度要求高的场景,可适当减小计数值(如 5ms)
- 不同时钟频率下,需要重新计算此参数
5. 顶层示例:按键控制LED翻转
前面完成了key_debounce消抖模块的设计,下面给出一个顶层示例key_control_top,演示如何实例化消抖模块,并利用其三种输出信号实现按键控制LED翻转。本示例包含两个按键和三个LED,覆盖了key_stable、key_press、key_release三种典型用法。
5.1 功能说明
- LED1:由
key1_press(按下边沿)触发翻转,每次按下瞬间翻转一次 - LED2:由
key2_press(按下边沿)触发翻转,每次按下瞬间翻转一次 - LED3:由
key1_stable(稳定电平)直接驱动,按住key1时点亮,松开熄灭
顶层模块实例化两个key_debounce模块,分别处理key1和key2的消抖,消抖时间均为 10ms(50MHz 时钟下DEBOUNCE_CNT = 500_000)。
5.2 顶层模块代码
// ------------------------------------------------------------ // 顶层示例:调用独立消抖模块,实现按键控制LED翻转 // // 本文件演示 key_debounce 模块的三种典型用法: // 1. 读取 key_stable —— 获取当前按键状态 // 2. 用 key_press —— 按下瞬间触发事件(本例用于翻转LED) // 3. 用 key_release —— 释放瞬间触发事件 // ------------------------------------------------------------ module key_control_top ( input wire clk, input wire rst_n, input wire key1, // 按键1(低有效) input wire key2, // 按键2(低有效) output wire led1, // LED1:key1按下翻转 output wire led2, // LED2:key2按下翻转 output wire led3 // LED3:key1按住时亮(电平驱动) ); // ---- 消抖后信号声明 ---- wire key1_stable; // key1 消抖后稳定电平 wire key1_press; // key1 按下边沿脉冲(消抖后) wire key1_release; // key1 释放边沿脉冲(消抖后) wire key2_stable; // key2 消抖后稳定电平 wire key2_press; // key2 按下边沿脉冲(消抖后) wire key2_release; // key2 释放边沿脉冲(消抖后) reg led1_reg, led2_reg; // ======================================================== // 实例化消抖模块 // ======================================================== key_debounce #( .DEBOUNCE_CNT(500_000) // 50MHz时钟下约10ms消抖 ) u_debounce_key1 ( .clk (clk), .rst_n (rst_n), .key_in (key1), // 原始按键输入 // ---- 消抖后输出,可直接用于逻辑判断 ---- .key_stable (key1_stable), // 稳定电平:0=按下,1=释放 .key_press (key1_press), // 按下边沿:按下瞬间高1周期 .key_release (key1_release) // 释放边沿:释放瞬间高1周期 ); key_debounce #( .DEBOUNCE_CNT(500_000) ) u_debounce_key2 ( .clk (clk), .rst_n (rst_n), .key_in (key2), .key_stable (key2_stable), .key_press (key2_press), .key_release (key2_release) ); // ======================================================== // 应用逻辑示例 // ======================================================== // 示例1:用 key_press(按下边沿)触发LED翻转 // 每次按键按下瞬间,LED状态翻转一次 always @(posedge clk or negedge rst_n) begin if (!rst_n) led1_reg <= 1'b0; else if (key1_press) // 消抖后的按下边沿,直接作为触发条件 led1_reg <= ~led1_reg; end // 示例2:用 key_release(释放边沿)触发LED翻转 // 每次按键释放瞬间,LED状态翻转一次 always @(posedge clk or negedge rst_n) begin if (!rst_n) led2_reg <= 1'b0; else if (key2_press) // 这里同样用按下边沿,可改为 key2_release led2_reg <= ~led2_reg; end // 示例3:用 key_stable(稳定电平)直接驱动LED // 按住按键时LED亮,松开就灭 assign led3 = ~key1_stable; // key_stable低有效,取反后高电平点亮 // ---- 输出赋值 ---- assign led1 = led1_reg; assign led2 = led2_reg; endmodule5.3 代码说明
实例化消抖模块:顶层分别实例化u_debounce_key1和u_debounce_key2两个消抖模块,将原始按键key1、key2接入key_in,消抖后的稳定电平、按下边沿、释放边沿分别引出到对应的 wire 信号上。
LED1 翻转逻辑:使用key1_press作为触发条件。由于key_press是消抖后的按下边沿,只在按键真正稳定按下瞬间产生一个时钟周期的高脉冲,因此每次按下key1,led1_reg翻转一次,不会因抖动产生误触发。
LED2 翻转逻辑:使用key2_press作为触发条件,逻辑与 LED1 相同。若希望改为释放瞬间翻转,只需将触发条件替换为key2_release即可。
LED3 电平驱动:直接使用key1_stable稳定电平驱动。由于key_stable是低有效(0=按下,1=释放),取反后~key1_stable在按键按下时为高电平,点亮 LED3;松开后为低电平,LED3 熄灭。
5.4 演示视频
下面给出按键控制LED翻转的上板演示视频,直观展示消抖效果与三种输出信号的典型用法:
视频放置位置:
视频内容建议包含以下三个演示环节:
- LED1 翻转演示:按下
key1,观察led1每次按下翻转一次,验证key_press按下边沿触发逻辑。 - LED2 翻转演示:按下
key2,观察led2每次按下翻转一次,验证第二个消抖通道的独立工作。 - LED3 电平驱动演示:按住
key1时led3点亮,松开后熄灭,验证key_stable稳定电平直接驱动逻辑。
通过视频可以直观看到,即使在快速连续按键的情况下,LED 状态也只会按预期翻转一次,不会因机械抖动产生误触发,说明消抖模块工作正常。
6. 测试验证
6.1 Testbench设计
`timescale 1ns / 1ps module key_control_tb; reg clk; reg rst_n; reg key_in; wire key_stable; wire key_press; wire key_release; key_debounce #( .DEBOUNCE_CNT(500_000) ) u_key_debounce ( .clk(clk), .rst_n(rst_n), .key_in(key_in), .key_stable(key_stable), .key_press(key_press), .key_release(key_release) ); initial begin clk = 0; forever #10 clk = ~clk; end initial begin rst_n = 0; key_in = 1;//初始化 #100; rst_n=1; #100; //按下抖动 key_in=0;//按下 #5; key_in=1; #5; key_in=0;//按下 #5; key_in=1; #5; key_in=0;//按下 #5; key_in=1; #5; key_in=0;//稳定 #20000000; //释放抖动 key_in=1; #5; key_in=0;//按下 #5; key_in=1; #5; key_in=0;//按下 #5; key_in=1; #5; key_in=0;//抖动 #5; key_in=1;//稳定 #20000000; $finish; end endmodule6.2 仿真结果分析
通过 ModelSim 仿真工具运行 Testbench,观察波形图可以得到以下验证结果:
- 消抖功能验证:在按键抖动期间(约 5-20ms),
key_stable信号保持稳定,不会随抖动变化。 - 边沿检测验证:
- 当按键从释放变为按下时,
key_press输出一个时钟周期的高脉冲 - 当按键从按下变为释放时,
key_release输出一个时钟周期的高脉冲 - 在按键稳定期间,两个边沿信号均为低电平
- 当按键从释放变为按下时,