FPGA工程按键实战3:按键消抖模块设计与实现(同步触发 边沿检测)
2026/9/19 23:29:34 网站建设 项目流程

摘要:本文是"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_synckey_sync_dlykey_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_inkey_synckey_sync_dlykey_sync_dly2
初始化1(可能处于亚稳态)111
0周期01(处于亚稳态的几率减少)11
1周期101(电平状态稳定)1
2周期1101

效果: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 end

4.4 第二级:消抖计数(稳定后更新输出)

原理:按键抖动时,电平会在0/1之间快速跳变(5~20ms内)。我们不关心抖动期间的每一个跳变,只需要最终稳定的值。

状态机逻辑:

  1. key_sync_dly2 != key_sync_dly——检测到输入变化,说明可能开始抖动了,立刻重新装载计数器(从头开始等)
  2. debounce_cnt > 0——输入没有变化,正在倒计时,说明电平已经稳定了一段时间
  3. 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 end

4.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; endmodule

4.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_stablekey_presskey_release三种典型用法。

5.1 功能说明

  • LED1:由key1_press(按下边沿)触发翻转,每次按下瞬间翻转一次
  • LED2:由key2_press(按下边沿)触发翻转,每次按下瞬间翻转一次
  • LED3:由key1_stable(稳定电平)直接驱动,按住key1时点亮,松开熄灭

顶层模块实例化两个key_debounce模块,分别处理key1key2的消抖,消抖时间均为 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; endmodule

5.3 代码说明

实例化消抖模块:顶层分别实例化u_debounce_key1u_debounce_key2两个消抖模块,将原始按键key1key2接入key_in,消抖后的稳定电平、按下边沿、释放边沿分别引出到对应的 wire 信号上。

LED1 翻转逻辑:使用key1_press作为触发条件。由于key_press是消抖后的按下边沿,只在按键真正稳定按下瞬间产生一个时钟周期的高脉冲,因此每次按下key1led1_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 电平驱动演示:按住key1led3点亮,松开后熄灭,验证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 endmodule

6.2 仿真结果分析

通过 ModelSim 仿真工具运行 Testbench,观察波形图可以得到以下验证结果:

  1. 消抖功能验证:在按键抖动期间(约 5-20ms),key_stable信号保持稳定,不会随抖动变化。
  2. 边沿检测验证
    • 当按键从释放变为按下时,key_press输出一个时钟周期的高脉冲
    • 当按键从按下变为释放时,key_release输出一个时钟周期的高脉冲
    • 在按键稳定期间,两个边沿信号均为低电平

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询