这一篇在干嘛?
按键及开关测试那篇我们留下了尾巴:机械按键按下时会抖动,LED 会闪几下。这一篇专门来「治病」——用一种基于双计数器的消抖电路,把抖动滤掉,让每一次按键都产生一次干净、稳定的输出变化。这是按键类设计绕不开的经典课题,方法也远比「等 20ms 再读一次」的软件思路更体现 FPGA 风格。
实验任务与原理
实验任务
掌握消抖动电路的原理。
按键为什么会抖动
机械按键内部是两个金属触点。按下(或松开)的瞬间,触点并不「一次到位」,而是像拍手一样弹跳几下,几毫秒内反复通断。于是理想中干干净净的一个下降沿,实际变成了这样:
理想信号: ──────┐____________
实际信号: ─────┐┌─┐┌──┐┌──___ ← 5~10ms 内反复跳变如果 FPGA 直接对这个信号计数或触发,一次按键就会被误认为按了十几次。这就是「消抖」要解决的问题。
顺便一提:拨码开关几乎不抖(触点滑动式结构),所以本篇的对手只有机械按键。但消抖电路的价值远不止按键——任何「宽真信号 + 窄毛刺」共存的输入,都可以用它来清洗。
双计数器消抖原理
分别用两个计数器去对输入信号的高电平和低电平的持续时间(脉宽)进行计数(在时间上是同时但独立计数)。只有当高电平的计数时间大于某值,则判为遇到正常信号,输出 1;若低电平的计数时间大于某值,则输出 0。此例的仿真波形如图 1 所示。

图 1 消抖动电路仿真波形
这个思路的精髓是「以时间换可靠」:
- 输入为低时,低电平计数器
KL持续累加,高电平计数器KH清零;输入为高时反之,两者同时但独立地工作; - 抖动脉冲很窄(微秒级),计数器刚攒几个数就被反向脉冲清零,永远到不了阈值——于是抖动被自然过滤;
- 只有当某个电平持续存在足够长时间(比如几毫秒以上),对应计数器才超过阈值,输出才翻转。
由波形图可见,其输出信号脉宽比逻辑方式输出的信号要宽得多。此例的输出脉宽与正常信号高电平 KH 的位宽和工作时钟频率共同决定,不单纯由时钟决定。所以优于以上的逻辑方式。此实验给出的设计比前面的电路要容易控制,且效果更好,只是耗用资源比较多。此电路同样能用于消除来自不同情况的干扰、毛刺和电平抖动。其中的工作时钟 CLK 的频率大小要视干扰信号和正常信号的宽度而定。对于类似键抖动产生的干扰信号,频率可以低一些,数万赫即可;若为比较高速的时钟信号,则可利用 FPGA 内的锁相环,使 CLK 能到达 200MHz 以上。此外,KH 和 KL 的计数位宽和计数值都可以根据具体情况调节。

图 2 消抖动 RTL 电路图
参数怎么定?算一笔账
以本例
KH > 4'b1100(计数大于 12)为例:若工作时钟 50kHz,翻转门限就是 12/50kHz ≈ 0.24ms 的持续高电平;若时钟 50MHz,门限只有 0.24μs,就滤不掉抖动了。所以「阈值 × 时钟周期」才是真正的滤波时间窗——时钟越快,需要的计数位宽和阈值越大。原文给出的经验是:对付按键抖动,数万赫的时钟即可;想处理高速信号上的毛刺,就用锁相环把时钟提到 200MHz 以上,同时加大计数器位宽。
实验程序
module ERZP (CLK, KIN,KOUT);
input CLK, KIN; //工作时钟和输入信号
output KOUT; reg KOUT;
reg [3:0] KH,KL; //定义对高电平和低电平脉宽计数之寄存器
always @(posedge CLK) beginif (!KIN) KL<=KL+1; //对键输入的低电平脉宽计数
else KL<=4'b0000; end //若出现高电平,则计数器清零
always @(posedge CLK) begin
if (KIN) KH<=KH+1; //同时对键输入的高电平脉宽计数
else KH<=4'b0000; end //若出现高电平,则计数器清零
always @(posedge CLK) begin
if (KH > 4'b1100) KOUT<=1'B1;//对高电平脉宽计数一旦大于12,则输出1
else if (KL > 4'b0111) KOUT<=1'B0; //对低电平脉宽计数若大于7,则输出0
end
endmodule三个 always 各干一件事,结构非常清爽:
- 第一段:输入为低(
!KIN)时KL加一计数,输入一变高立刻清零——KL记录的是「当前这段低电平已经持续多久」; - 第二段:镜像地统计高电平持续时间
KH; - 第三段:判决。
KH > 12说明高电平已经持续了足够久,输出置 1;KL > 7说明低电平持续够久,输出清 0。两个阈值不同(12 与 7),意味着输出在两个方向上的翻转速度不对称——这是根据实际抖动特性调出来的参数,不是笔误。
注意输出 KOUT 是电平信号而不是脉冲:按键按住多久,消抖后的稳定低电平就有多久。它可以直接去驱动计数器的时钟使能或复位,不会再被抖动干扰。
还有一个工程上的细节值得说:实战中 KIN 在进本电路之前,通常先经过两级寄存器打拍同步(按键及开关测试一篇的老朋友)。消抖电路解决「触点弹跳」,打两拍解决「亚稳态」,两者是互补关系而不是替代关系——先同步再消抖,才是完整的输入链路。
阈值是一把双刃剑
阈值调大,抗抖动更强,但按键响应会变慢(必须按满阈值时间才生效),快速连按也可能丢失;阈值调小,响应快了,抖动又可能漏网。调参时先估算抖动时间(普通按键约 5~10ms),再保证阈值对应的时间窗略大于抖动、远小于人手按键的最短停留(约 100ms),中间留足余量。
引脚锁定
原文引脚约束如下(数码管位选/段选、时钟、按键与开关):
#系统时钟约束
set_pin_assignment { EN[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { EN[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LED7S[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { clk50mhz } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { pclk } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { s } { LOCATION = C5; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }对照「板级地图」:EN 是数码管位选、LED7S 是段选(引脚与前几个实验的 SEL/DIG 完全一致),pclk 接键 1(A2)作为被消抖的按键输入,s 接 SW4(C5)用于上板对比「消抖开/关」的效果。
下载测试
SW4 拨上,按动键 1,数码计数稳定;拨下,按动键 1 计数,数码计数不稳定。

这是一个设计得很巧的对照实验:
- SW4 拨上(接通消抖):按键抖动被双计数器滤掉,每按一次键 1,数码管计数值只加一,干净利落;
- SW4 拨下(旁路消抖):抖动的毛刺直接进入计数逻辑,按一次键可能连跳好几个数——亲自看看「不消抖」有多糟糕。
建议两边各按二十次,对比计数结果与实际按键次数的偏差,你会对消抖的价值有直观认识。这个对比也是面试和竞赛答辩里最常被追问的点:能不能用一句话说清「消抖到底消掉了什么」——答案就是:消掉的是持续时间小于滤波窗口的脉冲,而无论它来自触点弹跳、电源干扰还是线缆毛刺。
看不出抖动 ≠ 没有抖动
有的板子按键质量好,旁路消抖时抖动轻微,计数偶尔才跳两下,容易让人误以为「不消抖也没事」。但抖动是机械件的随机行为,同一块板今天不抖不代表明天不抖,量产时更不能赌运气。消抖电路是必须做的「保险」,而不是可选的「优化」。
现象与总结
本实验的收获可以浓缩为三句话:
- 抖动是时间问题:真信号「长」、毛刺「短」,所以用脉宽计数把两者区分开,而不是简单地打拍或延时;
- 双计数器结构通用性强:高/低电平各配一个计数器与阈值,输出的滤波窗口由「阈值 × 时钟周期」决定,时钟频率、位宽、阈值三者都能按需调节,同样适用于滤除其他来源的毛刺与电平抖动;
- 代价是资源:相比简单的逻辑门或单级同步,它多用了一组计数器,但对按键这种低频场景完全值得。
回头看整个 DEMO 序列的脉络也很清晰:按键及开关测试让我们看见抖动,本实验把抖动治好,而计数器实验里「键 1 复位、键 2 加载」那些看似理所当然的操作,背后其实都应当站着这样一级消抖电路。三篇连起来,就是一条完整的按键输入处理链。
从这里再往前走一步,把判决输出改成单脉冲(按下沿产生一个时钟宽度的脉冲),就是一个完整的「按键事件检测器」,可以直接接到任意计数器的使能端——很多综合实验的按键模块正是这样搭出来的。到那时,这套双计数器消抖电路会跟着你走完全程。
自测
自测
- KH、KL 两个计数器为什么要「同时但独立」计数?只统计低电平、高电平直接输出行不行?
- 若工作时钟为 50kHz,
KH > 4'b1100对应多长的滤波时间窗?把时钟换成 50MHz 后要维持同样的时间窗,4 位计数器还够用吗?- 输入端出现一个 2 个时钟周期宽的窄毛刺,输出会翻转吗?说明判决过程。
- 为什么 KH 的阈值(12)和 KL 的阈值(7)不一样?阈值不对称会带来什么影响?
- 上板对照实验中,SW4 拨下时按一次键计数会连跳多个数,解释毛刺是如何变成多个计数脉冲的。