按键消抖
1.1 为什么需要按键消抖
如图所示,我们所使用的按键开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。原理如图所示

图 16-1

图 16-2
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。键抖动会引起一次按键被误读多次。为确保控制器对按键的一次闭合仅作一次处理,必须去除按键的抖动。在按键闭合稳定时读取按键的状态,并且必须判别到按键释放稳定后再作处理。
消抖是为了避免在按键按下或是抬起时电平剧烈抖动带来的影响。按键的消抖,可用硬件或软件两种方法。
硬件消抖:
在按键个数较少时可用硬件方法消除键抖动。如图所示的RS触发器为常用的硬件去抖。图中两个与非门构成一个RS触发器。当按键未按下时,输出为0;当键按下时,输出为1。此时即使用按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开B),只要按键不返回原始状态A,双稳态电路的状态不改变,输出保持为0,不会产生抖动的波形。也就是说,即使B点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。这一点通过分析RS触发器的工作过程很容易得到验证。

图 16-3
软件消抖:
如果按键个数较多,常用软件方法去抖,即检测出按键闭合后执行一个延时程序,根据抖动的时间为5ms~10ms,我们产生一个20ms的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。
1.2 按键去抖动的实现
1.2.1 功能简介
前面已经分析了按键抖动的机理和消除按键抖动的几种方案,我们知道硬件消抖会使用一些额外的器件占用电路板上的空间,从而在一定程度上增加了PCB布局布线的复杂度,所以我们用软件消抖的方式来实现去抖动的操作,去抖动后的效果是当按键按被下后能够准确检测到按键被按下了一次,而不会因机械抖动发生按键重复多次按下的现象。
1.2.2 模块设计
因为我们要计数过滤掉按键抖动的时间,所以计数器是必不可少的,所以我们设计的模块一定会用到时序电路,所以时钟sys_clk和复位sys_rst_n信号一定先加上,而且是输入信号,另外还有一个输入信号就是按键的输入key_in,我们最终要实现的就是对输入的key_in信号进行去抖动,输出信号为去抖动后的稳定的按键信号key_flag。根据上面的分析设计出的Visio框图如图 16-4所示。

图 16-4
端口列表与功能总结如表 16-1所示。
表 16-1
信号 | 位宽 | 类型 | 功能描述 |
sys_clk | 1Bit | Input | 工作时钟,频率50Mhz |
sys_rst_n | 1Bit | Input | 复位信号,低电平有效 |
key_in | 1Bit | Input | 按键的输入 |
key_flag | 1Bit | Output | 去抖后按键被按下的标志信号 |
1.2.3 波形设计
首先我们从实际问题出发,分析抖动的本质,再想办法去消除抖动。我们先把波形图的三个输入信号画好,抖动我们就模拟和真实中的情况一样,即当按键被按下和按键被释放时都会有抖动,也就是有前抖动和后抖动,这两种抖动都会对我们的设计产生一定的影响,会让我们的系统误判为按键被多次按下。我们需要做的就是要准确判断出稳定的按下的那一次状态。

图 16-5
按键的抖动会产生如图 16-5所示的毛刺,毛刺中会有低电平的情况,但是因为机械抖动的原因很快又回拉高了,如果我们把其中的每次的低电平和高电平都采集到,那么相当于是按键被按下了好多次,而不是我们想要的一次,所以我们一定要把这段抖动给滤除掉,这段抖动的时间我们通过前面的分析是已知的,抖动的时间是小于10ms的,而当有20ms的时间内都没有抖动就说明按键已经处于稳定状态了,也就是说我们可以做一个计数器来进行计数,计数20ms的时间,也就是说只要20ms的时间内都没有抖动产生,那结果是什么电平就是什么,我们需要做的是找到最后一次抖动的时间是在什么时候,才能够开启这20ms的计数,否则这20ms内不能够保证都是我们的安全时间。当然有的同学可能会说我在单片机的设计中都是检测第一次按键为低电平了就开始计数,然后延时一段大于30ms的时间后再检测得到的按键电平就是稳定的按键信号,难道这种方式不可以吗?这种方式虽然也是可以的,但不是最好的,因为这会浪费我们的不必要时间,也就是说虽然抖动的时间理论上不会大于10ms,但是具体是多少可能每次按键实验的结果都不相同,如果我们每次都按照最大的抖动时间10ms来计算无疑会“多”考虑了一些时间,所以我们采用一种更“节约”时间的方法,我们添加一个名为cnt_20ms用于计数20ms时间的计数器,每当系统检测到按键输入信号为低电平时cnt_20ms计数器就开始计数,在cnt_20ms计数器计数期间内,如果再次检测到按键为高电平则说明上次检测到的低电平一定是个抖动,那么我们就将这个计数器清零,总结为简单的一句话就是:当系统检测到按键为低电平时cnt_20ms计数器就计数,当检测到按键为高电平时cnt_20ms计数器就清零。讲到这里主要问题我们就已经解决了,然后要考虑cnt_20ms计数器计数个和计数满了后该怎么处理以及滤除抖动后的输出信号key_flag什么时候拉高、拉低的问题。
首先是考虑计数器的问题,根据我们使用的50Mhz的晶振来计算,cnt_20ms计数器计数20ms时间所需要计数的个数为999_999,计数器计数满后我们习惯先清零,如果有问题我们根据分析再进行修改。而key_flag信号则是一个脉冲信号,也就是只有一个时钟周期的高电平,且当cnt_20ms计数器计数到999_999时才拉高,而这个高电平只能存在一个。按照cnt_20ms计数器计数到999_999时清零来分析,其波形图如图 16-6所示,按键会因为低电平的时间太久,会存在多个20ms的时间,cnt_20ms计数器计数满清零多次,这样就会有多个计数值为999_999的情况,从而导致key_flag信号产生多次脉冲,这显然是我们不想要的结果。那我们需要分析是cnt_20ms计数器清零的问题还是key_flag信号拉高时间的问题。经分析key_flag信号即使不是在cnt_20ms计数器计数到999_999时拉高而在其他时间拉高也会出现同样的问题,所以那只能怀疑是cnt_20ms计数器清零的条件不对了。刚开始的时候cnt_20ms计数器清零已经有一个条件了,那就是当输入信号key_in只要为高电平就将cnt_20ms计数器清零,那这里我们就让cnt_20ms计数器计数满后保持为999_999而不清零,等待输入信号key_in为高电平的时候再清零。

图 16-6
修改cnt_20ms计数器清零后的结果如图 16-7所示,我们可以发现key_flag信号确实不会产生多个了,而是出现了新的问题,key_flag信号也不是脉冲了,是一个长长的电平信号,这也不是我们想要的结果,其根本原因是cnt_20ms计数器计数到999_999后保持在999_999的时间太久导致的。

图 16-7
针对上面的探索,我们最终灵机一动,发现cnt_20ms计数器计数到999_998的次数只有一个,而且最接近999_999,在既保证去抖动时间的前提下使key_flag信号只产生一个脉冲信号。最终的波形结果如图 16-8所示。

图 16-8
1.2.4 代码编写
//--------------------------------------------------
01module key_filter
02#(
03 parameter CNT_MAX =20'd999_999
04)
05(
06 input wire sys_clk , //系统时钟50Mhz
07 input wire sys_rst_n , //全局复位
08 input wire key_in , //按键输入信号
09
10 output reg key_flag //key_flag为1时表示消抖后检测到按键被按下,key_flag为0时表示没有检测到按键被按下
11);
12
13reg [19:0] cnt_20ms;
14
15//cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数
16always@(posedge sys_clk ornegedge sys_rst_n)
17 if(sys_rst_n ==1'b0)
18 cnt_20ms <=< span="">20'b0;
19 else if(key_in ==1'b1)
20 cnt_20ms <=< span="">20'b0;
21 else if(cnt_20ms == CNT_MAX && key_in ==1'b0)
22 cnt_20ms <=< span=""> cnt_20ms;
23 else
24 cnt_20ms <=< span=""> cnt_20ms +1'b1;
25
26//key_flag:当计数满20ms后产生按键有效标志位,且key_flag在999_999时拉高,维持一个时钟的高电平
27always@(posedge sys_clk ornegedge sys_rst_n)
28 if(sys_rst_n ==1'b0)
29 key_flag <=< span="">1'b0;
30 else if(cnt_20ms == CNT_MAX -1'b1)
31 key_flag <=< span="">1'b1;
32 else
33 key_flag <=< span="">1'b0;
34
35endmodule
//--------------------------------------------------
1.2.5 仿真文件编写
//--------------------------------------------------
01module tb_key_filter();
02
03reg sys_clk;
04reg sys_rst_n;
05reg key_in;
06
07wire key_flag;
08
09reg [21:0] tb_cnt;
10
11//为了缩短仿真时间,我们将参数化的时间值改小,但位宽依然定义和参数名的值保持一致,也可以将这些参数值改成和参数名的值一致
12parameter CNT_1MS =20'd19;
13parameter CNT_11MS =21'd69;
14parameter CNT_41MS =22'd149;
15parameter CNT_51MS =22'd199;
16parameter CNT_60MS =22'd249;
17
18//初始化系统时钟、全局复位和输入信号
19initial begin
20 sys_clk =1'b1;
21 sys_rst_n <=< span="">1'b0;
22 key_in <=< span="">1'b0;
23 #20
24 sys_rst_n <=< span="">1'b1;
25end
26
27//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
28always#10 sys_clk =~sys_clk;
29
30//tb_cnt:按键过程计数器,通过该计数器的计数时间来模拟按键的抖动过程
31always@(posedge sys_clk ornegedge sys_rst_n)
32 if(sys_rst_n ==1'b0)
33 tb_cnt <=< span="">22'b0;
34 else if(tb_cnt == CNT_60MS) //计数器计数到CNT_60MS完成一次按键从按下到释放的整个过程
35 tb_cnt <=< span="">22'b0;
36 else
37 tb_cnt <=< span=""> tb_cnt +1'b1;
38
39//key_in:产生输入随机数,模拟按键的输入情况
40always@(posedge sys_clk ornegedge sys_rst_n)
41 if(sys_rst_n ==1'b0)
42 key_in <=< span="">1'b1; //按键未按下时的状态为高电平
43 else if((tb_cnt >= CNT_1MS && tb_cnt <=< span=""> CNT_11MS)||(tb_cnt >= CNT_41MS && tb_cnt <=< span=""> CNT_51MS)) //在该计数区间内产生非负随机数0、1来模拟10ms的前抖动和10ms的后抖动
44 key_in <=< span="">{$random}%2;
45 else if(tb_cnt >= CNT_11MS && tb_cnt <=< span=""> CNT_41MS)
46 key_in <=< span="">1'b0; //按键经过10ms的前抖动后稳定在低电平,持续时间需大于CNT_MAX
47 else
48 key_in <=< span="">1'b1;
49
50//------------------------key_filter_inst------------------------
51 key_filter
52#(
53 .CNT_MAX (20'd24 ) //修改的CNT_MAX值一定要小于(CNT_41MS - CNT_11MS),否则就会表现为按键一直处于“抖动状态”而没有“稳定状态”,无法模拟出按键消抖的效果
54)
55 key_filter_inst(
56 .sys_clk (sys_clk ), //input sys_clk
57 .sys_rst_n (sys_rst_n ), //input sys_rst_n
58 .key_in (key_in ), //input key_in
59
60 .key_flag (key_flag ) //output key_flag
61);
62
63endmodule
//--------------------------------------------------
1.2.6 仿真波形分析
Testbench编写完成后,我们启动ModelSim进行功能仿真验证,我们让波形跑了10us,通过图 16-9所示的波形我们可以观察到,我们模拟了完整的两次按键按下与释放抖动的情况,通过cnt_20ms计数器计数来滤除抖动,且是最终的输出的按键脉冲key_flag信号完美的表达出无抖动的按键效果,完全符合我们预期的设计。
图 16-10、图 16-11和图 16-12为按键消抖模块局部仿真波形图,分别为前抖动部分、稳定部分和后抖动部分的仿真波形。由整体和局部仿真波形可以看出,模块仿真波形和绘制波形图,各信号波形变化一致,模块通过仿真验证。

图 16-9

图 16-10

图 16-11

图 16-12
1.2.7 上板验证测试
虽然都说按键需要消抖,用示波器是可以抓到消抖之前和消抖之后的波形变化的,但是如果没有示波器该怎么来看消抖之前和之后变化的效果呢?大家不妨做一个小实验,前面我们也学习了计数器,不过计数的加“1”是通过时钟来实现的,这里为了让按键抖动的效果体现出来我们用按键来实现计数的加“1”操作,做一个二进制的4bit加法器,刚好我们的板子上也有4个led灯,就让我们来验证一下。
1.2.8 本章小结
通过本例我们可以发现在画波形图时不一定可以保证100%正确,根据分析,我们可以适当的调整,这也是设计之前画波形图的意义所在,而不是一抹黑的、漫无目的的调试代码。
其实在项目的设计过程中并不是一番风顺的,往往会遇到各种各样的问题,通过经过思考与探索尝试使我们最终得出正确的结果。如果总是站在上帝视角去考虑问题,而抛弃分析真理的过程,这对于学习来说将是巨大的损失。
希望学习者能深入体会设计中遇到的问题并掌握设计分析的方法,养成一个善于思考敢于尝试的习惯。
1381

被折叠的 条评论
为什么被折叠?



