13. 按键消抖模块的设计与验证

13.1. 章节导读

按键是最为常见的电子元器件之一,在电子设计中应用广泛。在FPGA的实验工程中,我们可以使用其作为系统复位信号或者控制信号的外部输入;在日常生活中,遥控器、玩具、计算器等等电子产品都使用按键。目前按键种类繁多,常见的有自锁按键、薄膜按键等等。我们开发板上使用的机械按键也是按键的一种,特点是:接触电阻小 ,手感好,按键按下或弹起时有“滴答”清脆声;但由于其构造和原理,在按键闭合及断开的瞬间均伴随有一连串的抖动。

本章节中,我们要根据机械按键的构造与原理,设计并实现按键消抖模块。以开发板上的物理按键作为输入信号,使用设计的按键消抖模块对输入的按键信号进行消抖处理,输出能够正常使用的按键触发信号。

13.2. 理论学习

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

keyfil002

图 19‑1 机械按键外观图

keyfil003

图 19‑2 机械按键抖动原理图

抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。按键抖动会引起一次按键被误读多次。为确保控制器对按键的一次闭合仅作一次处理,必须去除按键的抖动。在按键闭合稳定时读取按键的状态,并且必须判别到按键释放稳定后再作处理 。

消抖是为了避免在按键按下或是抬起时电平剧烈抖动带来的影响。按键的消抖,可用硬件或软件两种方法

13.2.1. 硬件消抖

在按键个数较少时可用硬件方法消除键抖动。如图 19‑3所示的RS触发器为常用的硬件去抖。图中两个与非门构成一个RS触发器。当按键未按下时,输出为0;当键按下时,输出为1。此时即使用按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开B),只要按键不返回原始状态A,双稳态电路的状态不改变,输出保持 为0,不会产生抖动的波形。也就是说,即使B点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。这一点通过分析RS触发器的工作过程很容易得到验证。

keyfil004

图 19‑3 硬件消抖原理

13.2.2. 软件消抖

如果按键个数较多,常用软件方法去抖,即检测出按键闭合后执行一个延时程序,根据抖动的时间为5ms~10ms,我们产生一个20ms的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。

13.3. 实战演练

前面已经分析了按键抖动的机理和消除按键抖动的几种方案,我们知道硬件消抖会使用一些额外的器件占用电路板上的空间,从而在一定程度上增加了PCB布局布线的复杂度,所以我们用软件消抖的方式来实现去抖动的操作,去抖动后的效果是当按键按下后能够准确检测到按键被按下了一次,而不会因机械抖动发生按键重复多次按下的现 象。

13.3.1. 实验目标

利用所学知识,设计并实现一个按键消抖模块,将外部输入的单比特按键信号做消抖处理后输出,输出信号正常可被其他模块调用。

13.3.2. 程序设计

本实验工程只涉及一个模块,就是按键消抖模块。接下来,我们将从模块框图、波形图绘制等各个进行讲解。

13.3.2.1. 模块框图

因为我们要计数过滤掉按键抖动的时间,所以计数器是必不可少的,所以我们设计的模块一定会用到时序电路,所以时钟sys_clk和复位sys_rst_n信号一定先加上,而且是输入信号,另外还有一个输入信号就是按键的输入key_in,我们最终要实现的就是对输入的key_in信号进行去抖动,输出信号为去抖动后的 稳定的按键信号key_flag。根据上面的分析设计出的visio框图如图 19‑4所示。

keyfil005

图 19‑4 按键消抖模块框图

端口列表与功能总结如表格 19‑1所示。

表格 19‑1 端口列表与功能总结

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

工作时钟,频率50MHz

sys_rst_n

1Bit

Input

复位信号,低电平有效

key_in

1Bit

Input

按键的输入

key_flag

1Bit

Output

去抖后按键被按下的标志信号

13.3.2.2. 波形图绘制

首先我们从实际问题出发,分析抖动的本质,再想办法去消除抖动。我们先把波形图的三个输入信号画好,抖动我们就模拟和真实中的情况一样,即当按键被按下和按键被释放时都会有抖动,也就是有前抖动和后抖动,这两种抖动都会对我们的设计产生一定的影响,会让我们的系统误判为按键被多次按下。我们需要做的就是要准确判断出稳 定的按下的那一次状态。

keyfil006

图 19‑5 按键消抖模块波形图(一)

按键的抖动会产生如图 19‑5所示的毛刺,毛刺中会有低电平的情况,但是因为机械抖动的原因很快又回拉高了,如果我们把其中的每次的低电平和高电平都采集到,那么相当于是按键被按下了好多次,而不是我们想要的一次,所以我们一定要把这段抖动给滤除掉,这段抖动的时间我们通过前面的分析是已知的,抖动的时间是小于10 ms的,而当有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时清零来分析,其波形图如图 19‑6所示,按键会因为低电平的时间太久,会存在多个20ms的时间,cnt_20ms计数器计数满清零多次,这样就会有多个计数值为999_999的情况,从而导致key_fla g信号产生多次脉冲,这显然是我们不想要的结果。那我们需要分析是cnt_20ms计数器清零的问题还是key_flag信号拉高时间的问题。经分析key_flag信号即使不是在cnt_20ms计数器计数到999_999时拉高而在其他时间拉高也会出现同样的问题,所以那只能怀疑是cnt_20ms计数器清零的条 件不对了。刚开始的时候cnt_20ms计数器清零已经有一个条件了,那就是当输入信号key_in只要为高电平就将cnt_20ms计数器清零,那这里我们就让cnt_20ms计数器计数满后保持为999_999而不清零,等待输入信号key_in为高电平的时候再清零。

keyfil007

图 19‑6 按键消抖模块波形图(二)

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

keyfil008

图 19‑7 按键消抖模块波形图(三)

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

keyfil009

图 19‑8 按键消抖模块波形图(四)

13.3.2.3. 代码编写

在波形图绘制小节,我们结合相关理论知识,讲解并绘制了按键消抖模块波形图。我们参照波形图,编写模块参考代码。参考代码编写较为简单,且有详细注释,此处不再过多讲解,按键消抖模块参考代码,具体见代码清单 19‑1。

代码清单 19‑1 按键消抖模块参考代码(key_filter.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
module key_filter
#(
parameter CNT_MAX = 20'd999_999 //计数器计数最大值
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire key_in , //按键输入信号

output reg key_flag //key_flag为1时表示消抖后检测到按键被按下
//key_flag为0时表示没有检测到按键被按下
);

////
//\* Parameter and Internal Signal \//
////
//reg define
reg [19:0] cnt_20ms ; //计数器

////
//\* Main Code \//
////

//cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key_in == 1'b1)
cnt_20ms <= 20'b0;
else if(cnt_20ms == CNT_MAX && key_in == 1'b0)
cnt_20ms <= cnt_20ms;
else
cnt_20ms <= cnt_20ms + 1'b1;

//key_flag:当计数满20ms后产生按键有效标志位
//且key_flag在999_999时拉高,维持一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX - 1'b1)
key_flag <= 1'b1;
else
key_flag <= 1'b0;

endmodule

13.3.2.4. 仿真验证

仿真文件编写

按键消模块参考代码编写完毕,为验证代码正确性,对模块参考代码进行仿真验证。编写模块仿真代码,具体见代码清单 19‑2。仿真参考代码中有有详细注释,此处不再过多介绍。

代码清单 19‑2 按键消抖模块仿真参考代码(tb_key_filter.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
\`timescale 1ns/1ns
module tb_key_filter();

////
//\* Parameter and Internal Signal \//
////

//parameter define
//为了缩短仿真时间,我们将参数化的时间值改小
 //但位宽依然定义和参数名的值保持一致
 //也可以将这些参数值改成和参数名的值一致
 parameter CNT_1MS = 20'd19 ,
 CNT_11MS = 21'd69 ,
 CNT_41MS = 22'd149 ,
 CNT_51MS = 22'd199 ,
 CNT_60MS = 22'd249 ;

 //wire define
 wire key_flag ; //消抖后按键信号

 //reg define
 reg sys_clk ; //仿真时钟信号
 reg sys_rst_n ; //仿真复位信号
 reg key_in ; //模拟按键输入
 reg [21:0] tb_cnt ; //模拟按键抖动计数器

 ////
 //\* Main Code \//
 ////

 //初始化输入信号
 initial begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 key_in <= 1'b0;
 #20
 sys_rst_n <= 1'b1;
 end

 //sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
 always #10 sys_clk = ~sys_clk;

 //tb_cnt:按键过程计数器,通过该计数器的计数时间来模拟按键的抖动过程
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 tb_cnt <= 22'b0;
 else if(tb_cnt == CNT_60MS)
 //计数器计数到CNT_60MS完成一次按键从按下到释放的整个过程
 tb_cnt <= 22'b0;
 else
 tb_cnt <= tb_cnt + 1'b1;

 //key_in:产生输入随机数,模拟按键的输入情况
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 key_in <= 1'b1; //按键未按下时的状态为为高电平
 else if((tb_cnt >= CNT_1MS && tb_cnt <= CNT_11MS)
 \|\| (tb_cnt >= CNT_41MS && tb_cnt <= CNT_51MS))
 //在该计数区间内产生非负随机数0、1来模拟10ms的前抖动和10ms的后抖动
 key_in <= {$random} % 2;
 else if(tb_cnt >= CNT_11MS && tb_cnt <= CNT_41MS)
 key_in <= 1'b0;
 //按键经过10ms的前抖动后稳定在低电平,持续时间需大于CNT_MAX
 else
 key_in <= 1'b1;

 ////
 //\* Instantiation \//
 ////

 //------------------------key_filter_inst------------------------
 key_filter
 #(
 .CNT_MAX (20'd24 )
 //修改的CNT_MAX值一定要小于(CNT_41MS - CNT_11MS)
 //否则就会表现为按键一直处于“抖动状态”而没有“稳定状态”
 //无法模拟出按键消抖的效果
 )
 key_filter_inst
 (
 .sys_clk (sys_clk ), //input sys_clk
 .sys_rst_n (sys_rst_n ), //input sys_rst_n
 .key_in (key_in ), //input key_in

 .key_flag (key_flag ) //output key_flag
 );

 endmodule

仿真波形分析

使用ModelSim软件对按键消抖模块进行仿真,仿真方式可选择与Quartus II联合仿真或使用ModelSim单独仿真。

模块仿真波形如下,图 19‑9为按键消抖模块整体仿真波形图;图 19‑10、图 19‑11和图 19‑12为按键消抖模块局部仿真波形图,分别为前抖动部分、稳定部分和后抖动部分的仿真波形。由整体和局部仿真波形可以看出,模块仿真波形和绘制波形图,各信号波形变化一致,模块通过仿真验证。

keyfil010

图 19‑9 按键消抖模块整体仿真波形图

keyfil011

图 19‑10 按键消抖模块前抖动仿真波形图

keyfil012

图 19‑11 按键消抖模块稳定部分仿真波形图

keyfil013

图 19‑12 按键消抖模块后抖动仿真波形图

13.4. 章末总结

通过本例我们可以发现在画波形图时不一定可以保证100%正确,根据分析,我们可以适当的调整,这也是设计之前画波形图的意义所在,而不是一抹黑的、漫无目的的调试代码。

其实在项目的设计过程中并不是一帆风顺的,往往会遇到各种各样的问题,通过经过思考与探索尝试使我们最终得出正确的结果。如果总是站在上帝视角去考虑问题,而抛弃分析真理的过程,这对于学习来说将是巨大的损失。

希望学习者能深入体会设计中遇到的问题并掌握设计分析的方法,养成一个善于思考敢于尝试的习惯,学会通过分析绘制波形图来提前预判潜在的设计问题。

13.5. 拓展训练

本例我们实现的是一个按键的消抖操作,如果我们有很多按键都需要消抖的话,理论上需要实例化多次按键消抖模块,那有什么方法可以更方便的实现这种操作吗?大家可以想一想。