文章目录
在前面几章节当中,我们讲解了计数器、分频器,那么分频器又讲解了偶数分频和奇数分频。那么在本章节当中,我们来讲一下按键的消抖。
按键消抖的内容分为两个部分:理论学习和实战演练。
那么在理论学习部分,我们会对按键消抖的相关知识做一个讲解;在实战演练部分我们通过实验工程,设计并实现一个具有按键消抖功能的电路。
那么首先是理论学习
1 理论学习
按键
按键是我们最为常见的电子元件之一,在电子设计当中应用广泛,在我们的日常生活中也经常见到,比如说:遥控器、玩具、计算器等等电子产品中都使用按键。
在我们 FPGA 的实验工程当中,我们可以使用其作为系统复位信号或者说控制信号的外部输入。
那么目前按键的种类有很多,常见的有自锁式按键,实物如下图1
还有机械按键,实物如下图2
机械按键又称为机械弹性开关。
在我们征途系列的开发板当中也使用了按键,比如说:我们的电源开关使用的就是自锁式按键
我们的复位按键还有控制信号输入的按键都使用的是机械弹性开关也就是机械按键
按键消抖
那么按键消抖主要针对的是机械弹性开关,就是机械按键。当我们的机械触点断开和闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定的导通,在断开时也不会一下子断开。
因而在闭合及断开的瞬间均伴随有一连串的抖动,如下图3
那么按键没有按下时为高电平,按下时为低电平。那么按下的瞬间与松开的瞬间都有一系列的抖动,按键按下到键稳定就是前抖动,按键从键稳定到按键释放就是后抖动。
那么为了保证系统能够正确的识别按键的开关,就必须对按键的抖动进行处理,那么处理就是按键的消抖。
硬件消抖与软件消抖
那么按键的消抖分为硬件消抖和软件消抖。那么如果按键的个数比较少,可以用硬件方法消除按键抖动,就是硬件消抖;使用 RS 触发器就是常用的硬件去抖方法,就是下面这个电路图所示4
如果按键的个数比较多时,我们常常使用软件消抖的方法。在我们的实战演练当中,就是要使用软件消抖的方式来实现按键的消抖。
2 实战演练
2.1 实验目标
我们的目标是设计并实现一个按键消抖模块,将外部输入的单比特按键信号做消抖处理后进行输出,那么输出的信号可以正常的被其他的模块调用。
我们为什么会选择软件消抖的方式呢?因为硬件消抖会使用一些额外的器件,占用我们电路板上的空间,从而在一定程度上增加了 PCB 布局布线的复杂度,所以说我们选用软件消抖的方式来实现去抖动的操作。去抖动后的效果是:当按键按下后,能够准确的检测到我们的按键被按下了一次,而不会因为抖动发生重复多次按下的一个现象。
2.2 程序设计
那么了解了实验目标之后,我们开始程序的设计。
先来搭建我们的文件体系
然后打开 doc 文件夹,新建一个 Visio 文件,用来绘制我们的模块框图和波形图
2.2.1 模块框图
下面开始模块框图的绘制
如何来实现我们的软件消抖呢?我们打算使用延时程序来去除掉抖动的部分。
既然要通过计数过滤掉按键抖动的时间,所以说计数器是必不可少的,我们的设计当中一定有时序电路,所以说输入信号一定是时钟信号和复位信号;那么除了时钟信号和复位信号之外,还有第三路输入信号,就是我们输入的、没有经过消抖处理的按键信号,把它命名为 key_in
那么这样输入信号就表示完成,下面就是输出信号。输出信号就是经过消抖处理后的稳定的按键信号,我们把它命名为 key_flag
那么这样,模块框图绘制完成,开始波形图的绘制。
2.2.2 波形图绘制
首先是时钟信号的波形;然后是我们复位信号的波形,系统上电后让它保持一段时间的低电平,然后让它一直保持为高电平
下面开始绘制输入信号 key_in 的波形。我们要模拟真实的情况,绘制出带抖动的波形。首先,按键没有被按下时是保持高电平;那么按键按下后会产生抖动,我们来模拟一下抖动,抖动其实就是不规则的高低电平变化,那么这一段就是我们模拟产生的前抖动
那么前抖动完成之后,保持一段时间的稳定的低电平;接下来是后抖动;后部抖动过后就保持原来的高电平
前抖动和后抖动它的保持时间大概是 5ms~10ms 然后中间的稳定时间大概是 20ms
前抖动与后抖动就像我们波形图当中绘制的这些毛刺一样,那么毛刺中会有高电平、低电平的情况,但是因为是机械抖动的原因,低电平会很快地变成高电平,如果我们把其中的每次的低电平和高电平都采集到,那么就相当于我们的按键被按下了好多次,不是我们想要的一次。所以说,我们一定要把这段前抖动和后抖动滤除掉;这段抖动的时间,我们通过前面的分析是已知的,前抖动和后抖动它的时间范围大概是 5~10ms 也就是小于 10ms;中间的稳定状态大于 20ms。如果说:当有 20ms 的时间内都没有产生抖动,那么这个按键就处于一个稳定的状态;换句话说就是:只要在 20ms 的时间内没有抖动的产生,就可以认为按键信号是可以被使用的。在这里我们需要做的就是,找到最后一次抖动的时间,是在什么时候开始的,才能开启这 20ms 的计数;否则在 20ms 之内都不能保证是我们的稳定时间。
那么有的朋友可能会说:如果在单片机的设计当中,我们都是检测第一次按键为低电平时开始计数,然后我们可以进行一段 10ms+20ms 的计数,经过一段大于 30ms 的延时之后,我们再检测按键电平,那么它就是稳定的按键信号。难道这种方式不行吗?这种方式当然是可以的,但不是最好的,因为这样会浪费我们不必要的时间;虽然我们的抖动时间理论上不会大于 10ms 但是它具体的时间是不确定的,因为我们每次按下按键时前抖动的时间都是不确定的;如果说,我们每次都按照最大抖动时间 10ms 来计算,这样就会多考虑一些时间。
我们这儿决定采用一种更节约时间的方法。我们这里添加一个计数器,它计数的时间间隔是 20ms
每当系统检测到按键输入信号为低电平时,它就开始计数;在计数器计数期间,如果再次检测到高电平就说明:上一次检测到的低电平一定是一个抖动,那么我们就将这个计数器进行清零。总结为一句话就是:当系统检测到按键为低电平时候,我们的计数器就进行计数,当检测到按键为高电平时,它就进行清零。
那么讲到这里,主要的问题我们已经解决了。下面就要考虑计数器的计数个数、计数器计满之后该怎么处理,以及要考虑我们的输出信号 key_flag 什么时候拉高、什么时候拉低的问题。
那么首先是我们计数器计数值的问题。我们晶振的时钟频率是 50MHz 换算成为时间单位是 20ns 即时钟周期是 20ns,我们计数时间是 20ms 我们的计数值用 M 来表示,经过计算: M = ( 20 ms / 20 ns ) − 1 = 1 × 1 0 6 − 1 = 999999 \text{M} = \left(20\text{ms} / 20\text{ns}\right) - 1 = 1 \times 10^6 - 1 = 999999 M=(20ms/20ns)−1=1×106−1=999999 那么计数器的计数最大值已经确定了。
下面就要考虑计数器清零的问题。我们前面使用计数器过程中,都是计数器计满之后习惯性地先清零;我们这儿先习惯性地清零,如果有问题,后面根据分析再进行修改。
下面开始绘制计数器的波形
计数器的初值是 0 在时钟信号的上升沿,检测到输入信号 key_in 是低电平就进行计数,检测到高电平清零。
下面是输出信号波形的绘制。我们这儿就要考虑,我们的输出信号 key_flag 什么时候拉高的问题。我们的输出信号 key_flag 是一个脉冲信号,也就是说,只有一个时钟周期是高电平,当我们的计数器计数到最大值 999999 的时候,拉高一个时钟周期。
我们先来绘制一下波形图
首先它初值为低电平;当计数到最大值,保持一个时钟周期的高电平。
那么输出信号的波形绘制完成。
那么大家肯定发现了一个问题:如果说我们的稳定时间足够长,它的低电平时间会远远大于 20ms,这样我们的计数器就会进行多次清零,会出现多个最大值,这样就导致了我们的输出信号 key_flag 产生多次脉冲。这显然不是我们想要的结果,下面就要分析,这种情况是计数器清零的问题导致的,还是我们的 key_flag 信号拉高时间的问题导致的。
那么如果说,我们的 key_flag 信号不在最大值拉高,在其他位置,比如说在 999000 位置拉高,那么同样的也会出现这种问题;那么我们只能怀疑,我们的计数器清零条件是不对的。
在刚刚开始的时候,我们的计数器清零条件已经明确了:就是当输入信号 key_in 为高电平时候,对它进行清零。那么这里我们就让我们的计数器计满之后保持最大值,不清零;等待输入的信号为高电平时,再进行清零。
我们来修改一下我们波形图
什么意思呢?计数到最大值 999999 的时候不再进行清零,保持我们的最大值;只有在输入信号为高电平时,才对计数器进行清零。
那么根据计数器的波形,我们修改一下输出信号的波形。当计数到最大值让它保持高电平,那么这样,它的波形就应该是这样的
那么修改了计数器的清零之后,我们可以发现:我们的 key_flag 信号确实不会产生多个了。但是出现了一个新的问题:我们的 key_flag 信号不是脉冲信号了,而是一个长长的高电平信号了,这也不是我们想要的结果。
那么根本的原因就是:我们的计数器计数到最大值,保持在最大值时间太长导致的。针对这种情况我们想到:如果说,我们的计数器计数到最大值减一也就是 999998 的时候,拉高我们的 key_flag 信号,那么其他时刻让它保持低电平,这样就能保证我们的 key_flag 信号是一个脉冲信号,而且拉高了一次;因为这样,计数器计数到最大值减一只有一次,而且最大值与最大值减一是非常接近的,这样就得到了我们需要的波形图
2.2.3 代码编写
那么波形图绘制完成之后,下面开始代码的编写
我们参照我们的波形图,编写我们的代码。首先是模块开始、模块名称、端口列表,然后是模块结束;那么输入信号有三路:时钟信号、复位信号和未消抖的按键信号;输出信号只有一路:就是经过消抖处理后的 按键脉冲信号
那么端口列表编写完成,下面开始声明变量。首先是我们的计数器变量,它的位宽是多少我们来算一下 999999 需要 20 个位宽也就是 [19:0],因为后面赋值语句是 always 语句,所以说它的变量类型是 reg 型
下面开始对计数器进行赋值,使用异步复位;当我们的复位信号有效时,给我们的计数器赋一个初值为 0;当我们输入的按键信号为高电平时候对它进行清零。
那么这里为了方便代码的编写,可以声明一个参数 CNT_MAX
这儿定义最大值是十进制的 999999 继续我们计数器的赋值。当我们的输入信号为高电平,我们计数器清零;当我们的计数器计数到最大值,然后让它保持这个最大值;如果说复位信号无效,输入的按键信号是低电平,而且没有计数到最大值,让它自加一
那么下面开始输出信号的赋值,我们同样使用 always 语句。当我们的复位信号有效时,给它赋一个初值就是低电平;如果说,当我们的计数器计数到最大值减一,让它保持一个时钟周期的高电平;那么其他时刻是低电平
那么这样代码编写完成,我们保存
key_filter.v
module key_filter
#(
parameter CNT_MAX = 20'd999_999;
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire key_in ,
output reg key_flag
);
reg [19:0] cnt_20ms;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_20ms <= 20'd0;
else if (cnt_20ms == CNT_MAX)
cnt_20ms <= CNT_MAX;
else if (key_in == 1'b1)
cnt_20ms <= 20'd0;
else
cnt_20ms <= cnt_20ms + 20'd1;
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))
key_flag <= 1'b1;
else
key_flag <= 1'b0;
endmodule
2.2.4 代码编译
回到我们的桌面,建立我们的实验工程
然后添加我们编写的代码,进行全编译;出现报错信息,更正代码后
key_filter.v
module key_filter
#(
parameter CNT_MAX = 20'd999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire key_in ,
output reg key_flag
);
reg [19:0] cnt_20ms;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_20ms <= 20'd0;
else if (cnt_20ms == CNT_MAX)
cnt_20ms <= CNT_MAX;
else if (key_in == 1'b1)
cnt_20ms <= 20'd0;
else
cnt_20ms <= cnt_20ms + 20'd1;
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))
key_flag <= 1'b1;
else
key_flag <= 1'b0;
endmodule
再次编译,那么编译通过有 7 个警告,我们点击 OK
那么代码编译通过之后,我们编写我们的仿真代码。
2.2.5 仿真验证
2.2.5.1 仿真文件编写
首先是时间参数,然后是模块开始、模块名称、端口列表(空的)、结束
然后声明我们的变量。然后是我们的按键输入信号,然后使用 wire 型,将消抖后的按键信号引出
首先是赋初值,我们使用 initial 语句
下面是模拟产生我们的按键输入信号,那么按键输入信号怎么产生呢?
首先,它的初值应该是一个高电平,然后模拟一段时间的抖动就是前抖动,然后再有一段时间是低电平就是稳定状态,然后,后面会有一段时间的抖动就是后抖动;那么后抖动完成之后,就保持它的高电平。这样就模拟一次按键按下。
那么高低电平直接赋值就可以,那么抖动怎么产生呢?我们可以使用随机数。那么具体抖动的时间和低电平保持的时间怎么确定呢?我们使用计数器
这儿还需要再声明一个计数器。那么我们使用计数器来进行一个周期的计数,模拟一次按键按下。那么计数的最大值,我们暂定为 250 次;那么它的位宽应该是多少呢?那么它的位宽是 8 位宽。
首先开始给计数器进行赋值,我们使用 always 语句,当我们的复位信号有效时给它一个初值 0,当它的最大值是 249,也就是说它完成了 250 次计数我们给它归零,那么如果上面两个条件都不满足,让它不断加一,就是进行计数
我们可以使用这个计数器来控制我们模拟按键信号的产生,那么怎么产生呢?
我们来这样产生:我们的复位信号有效时,给它一个初值,初值要给它一个高电平;下面就使用我们的计数器对我们的 key_in 进行赋值
这是我们的计数器,它计数的初值是 0 最大值是 249 它完成了 250 次计数。在这一个完整的计数周期内我们分为五段
当我们的计数器计数范围在 0~19 时,让我们的 key_in 保持一个高电平;计数范围在 19~69 之间,我们使用随机数产生一个前抖动;当计数范围在 69~149 之间让它保持一个低电平,也就是我们的稳定状态;当我们的计数范围在 149~169 之间,让它产生后抖动;当计数范围在 169~249 之间,保持一个高电平。这样我们的 key_in 信号就可以模拟一次完整的一个按键过程。
我们来继续编写代码。当计数器大于等于 19 并且计数器小于等于 69 或者它大于等于 149 小于等于 169,那么当它满足这个条件时,我们给我们的 key_in 赋值一个随机数,模拟抖动;当我们的计数器的范围小于 19 大于 169 时,让它保持高电平;那么剩下的情况就是,我们的计数器计数范围处于 69 与 149 之间,我们让它保持一个低电平,模拟我们稳定的一个状态
还需要生成时钟信号 sys_clk
那么这样三路模拟输入信号都已经编写完成。
那么下面就要开始我们的实例化。
那么这儿减小参数,给它一个 24 相当于计数 25
这样仿真文件编写完成,我们保存
tb_key_filter.v
`timescale 1ns/1ns
module tb_key_filter();
reg sys_clk;
reg sys_rst_n;
reg key_in;
reg [7:0] tb_cnt;
wire key_flag;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
tb_cnt <= 8'd0;
else if (tb_cnt == 8'd249)
tb_cnt <= 8'd0;
else
tb_cnt <= tb_cnt + 8'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
key_in <= 1'b0;
else if ((tb_cnt<20'd19) || (tb_cnt>20'd169))
key_in <= 1'b1;
else if ((tb_cnt>=20'd19)&&(tb_cnt<=20'd69)
||(tb_cnt>=20'd149)&&(tb_cnt<=20'd169))
key_in <= {$random} % 2;
else
key_in <= 1'b0;
key_filter
#(
.CNT_MAX (20'd24)
)
key_filter_inst
(
.sys_clk (sys_clk ),
.sys_rst_n(sys_rst_n),
.key_in (key_in ),
.key_flag (key_flag )
);
endmodule
回到我们的实验工程,加载我们的仿真文件;我们的仿真文件添加完成之后,我们先进行一下全编译,查看一下我们的语法错误
编译完成点击 OK。更正一下仿真代码
tb_key_filter.v
`timescale 1ns/1ns
module tb_key_filter();
reg sys_clk;
reg sys_rst_n;
reg key_in;
reg [7:0] tb_cnt;
wire key_flag;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
tb_cnt <= 8'd0;
else if (tb_cnt == 8'd249)
tb_cnt <= 8'd0;
else
tb_cnt <= tb_cnt + 8'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
key_in <= 1'b0;
else if ((tb_cnt<8'd19) || (tb_cnt>8'd169))
key_in <= 1'b1;
else if ((tb_cnt>=8'd19)&&(tb_cnt<=8'd69)
||(tb_cnt>=8'd149)&&(tb_cnt<=8'd169))
key_in <= {$random} % 2;
else
key_in <= 1'b0;
key_filter
#(
.CNT_MAX (20'd24)
)
key_filter_inst
(
.sys_clk (sys_clk ),
.sys_rst_n(sys_rst_n),
.key_in (key_in ),
.key_flag (key_flag )
);
endmodule
进行仿真设置
开始仿真
那么仿真编译完成之后,打开 sim 窗口,添加我们的模块波形,然后回到波形界面,全选、分组、消除路径;然后点击 Restart 运行 10us 查看全局视图
我们来参照着我们的波形图看一下我们的仿真波形。
2.2.5.2 仿真波形分析
首先是模拟的时钟和复位信号
这个应该是没有问题的。
然后看一下我们仿真文件当中的计数器
那么计数器它的初值是 0 每个时钟周期加一,它的最大值是 249,没有问题;计数到最大值清零,重新进行计数。
我们缩小来看一下我们模拟生成的按键信号
当我们的计数值在 0~19 计数范围内,那么这个 key_in 是保持高电平;在 19~69 这个范围内它是一个随机数,模拟的是前部抖动;那么 69~149 是模拟的稳定状态,没有问题;那么 149~169 之间是模拟的后部抖动;那么 169 到最大值,就是保持高电平。这样我们模拟输入的信号,它的波形是正确的。
那么下面看一下我们的模块它的波形,首先是我们的计数器
我们的计数器初值为 0 当按键输入信号是高电平它就一直是 0;当按键信号输入为低电平,它进行计数;高电平又归零;这儿全部是低电平,然后进行计数,计数到最大值 24 就保持;当我们的输入信号为高电平时候,它本应该再次归零,计数器的波形这里出现问题了,回到代码修改
key_filter.v
module key_filter
#(
parameter CNT_MAX = 20'd999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire key_in ,
output reg key_flag
);
reg [19:0] cnt_20ms;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_20ms <= 20'd0;
else if (key_in == 1'b1)
cnt_20ms <= 20'd0;
else if (cnt_20ms == CNT_MAX)
cnt_20ms <= CNT_MAX;
else
cnt_20ms <= cnt_20ms + 20'd1;
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))
key_flag <= 1'b1;
else
key_flag <= 1'b0;
endmodule
这时计数器的波形与我们绘制的波形图是完全吻合的。
下面看一下输出标志信号
当我们的计数器计数到最大值减一的时候,在下一个时钟周期让我们的输出信号保持一个时钟周期的高电平,这儿是没有问题的,我们的输出信号也是正确的。
这样我们的仿真波形与我们绘制的波形图是对应的,仿真验证通过。
按键消抖这个工程不再进行上板验证。
参考资料:
图片参考来源:图 19‑3 硬件消抖原理 ↩︎