ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA工程按键实战3:按键消抖模块设计与实现(同步触发 边沿检测)

FPGA工程按键实战3:按键消抖模块设计与实现(同步触发 边沿检测) 摘要本文是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系列仿真工具ModelSimQuartus自带或独立版本设计语言Verilog HDL2.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_CNT1)-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(可能处于亚稳态)1110周期01处于亚稳态的几率减少111周期101电平状态稳定12周期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 1b1; key_sync_dly 1b1; key_sync_dly2 1b1; 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 1b1; 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 - 1b1; 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 1b1; 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的消抖消抖时间均为 10ms50MHz 时钟下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, // LED1key1按下翻转 output wire led2, // LED2key2按下翻转 output wire led3 // LED3key1按住时亮电平驱动 ); // ---- 消抖后信号声明 ---- 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 1b0; 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 1b0; 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是消抖后的按下边沿只在按键真正稳定按下瞬间产生一个时钟周期的高脉冲因此每次按下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 电平驱动演示按住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_n1; #100; //按下抖动 key_in0;//按下 #5; key_in1; #5; key_in0;//按下 #5; key_in1; #5; key_in0;//按下 #5; key_in1; #5; key_in0;//稳定 #20000000; //释放抖动 key_in1; #5; key_in0;//按下 #5; key_in1; #5; key_in0;//按下 #5; key_in1; #5; key_in0;//抖动 #5; key_in1;//稳定 #20000000; $finish; end endmodule6.2 仿真结果分析通过 ModelSim 仿真工具运行 Testbench观察波形图可以得到以下验证结果消抖功能验证在按键抖动期间约 5-20mskey_stable信号保持稳定不会随抖动变化。边沿检测验证当按键从释放变为按下时key_press输出一个时钟周期的高脉冲当按键从按下变为释放时key_release输出一个时钟周期的高脉冲在按键稳定期间两个边沿信号均为低电平
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进