在学习了单按键消抖方法后,按键消抖的关键点就是20ms的延时,这一点和单片机按键消抖的思路是一样的。但是FPGA的延时需要通过寄存器计数,这个是比较消耗内部资源的。如果要检测4个按键时,最简单的方法就是将单按键消抖程序例化4次,传入不同的按键接口就行。这样的话就相当于用了4个不同的寄存器对20ms计数,对 FPGA内部资源浪费比较大,那么能不能用一个20ms的寄存器同时判断四个按键呢。
可以参考单按键检测的思路,首先将按键的当前状态和上一个状态存储起来,然后比较按键状态,如果按键状态发生了变化,说明有按键按下,然后开始20ms计时,如果在20ms内按键状态又发生了变化,说明按键出现了抖动,将计数器清0,重新开始20ms计时,当20ms计时时间到了之后,输出一个按键按下标志,然后存储按键当前值。
思路比较简单,下面直接写代码:
module key_filter(
input sys_clk,
input sys_rst_n,
input [3:0] key_in,
output key_flag, //按键按下标志
output reg [3:0] key_value //按键当前值
);
reg [3:0] key_now;
reg [3:0] key_last;
reg key_state;
reg key_state0,key_state1;
//parameter DELAY = 20'd1_000_000; //延时时间
parameter DELAY = 20'd20; //测试时间
reg [19:0] cnt; //20ms计数
reg en_cnt; //计数使能
首先定义输入输出口和需要用到的相关寄存器,输入信号有3个,时钟、复位、4位按键,输出信号有2个,一个按键按下标志,一个4位按键值。
开始20ms计数
//计数 20ms
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt <= 20'd0;
else if(en_cnt) begin
if(cnt == DELAY - 1'b1)
cnt <= 20'd0;
else if(cnt < DELAY -1'b1)
cnt <= cnt + 1'b1;
end
else
cnt <= 20'd0;
end
en_cnt为计数使能信号,当en_cnt为1时,开始计数,当en_cnt为0时计数清0。
检测按键值是否发生变化方法为:将按键当前值和上一时钟值用寄存器保存下来,然后去判断这两个值是否相等。
//存储按键值
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_now <= 4'b1111;
key_last <= 4'b1111;
end
else begin
key_now <= key_in;
key_last <= key_now;
end
end
key_now为当前时钟沿时按键值,key_last为上一个时钟沿时按键值,这两个值延时了一拍。
下来就可以通过这两个寄存器的值来判断是否有按键按下
//当按键发生改变时,计数器清0,当按键状态稳定20ms后,输出按键值。
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
en_cnt <= 1'b1;
key_state <= 1'b0;
key_value <= 4'b1111;
end
else if(key_last != key_now) begin //按键发生变化计数器清0
en_cnt <= 1'b0;
key_state <= 1'b0;
end
else if(key_last == key_now) begin
en_cnt <= 1'b1;
if(cnt == DELAY - 1'b1) begin //延时时间到,并且按键按下
key_value <= key_last;
if(key_last != 4'b1111)
key_state <= 1'b1;
end
end
end
en_cnt用于使能计数器开始计数,key_state用来表示是否有按键按下,有按键按下时输出高电平,否则输出低电平,这样一旦有按键按下后key_state就变为高电平,直到按键松开。key_value用于存储按键稳定后的按键值。若有按键按下并且稳定了20ms,那么就存储当前按键值。
在复位时就开始使能en_cnt,开始20ms计数。为什么一开始就要计数?而不是等到有按键按下时开始计数。因为这样操作起来比较方便。假如默认情况下en_cnt设置为0,当检测到有按键按下时en_cnt设置为1开始计数,这样的话当按键刚按下就开始计数,如果按键此时出现了抖动需要清0计数器,那么只能把en_cnt先设置为1,在设置为0,让计数器清0一次。这样让计数器清0一次需要3个时钟周期,如果刚好将en_cnt设置为0时,按键不在抖动了,那么计时器就不会计数,按键检测就会失败。所以就只能在按键第一次按下时开始计时,然后中间抖动过程中计数器依然计时,直到20ms计数时间到。这样延时也能实现,但是又违背了设计初衷。希望的是按键有抖动时计数器要清0,20ms的计时是从按键最后一次抖动开始算起,而不是从按键第一次按下算起。
那么就换个思路,复位时就让en_cnt为1,计数器开始计数,如果没有按键按下,那么计数器计数到了20ms时,自动清0继续计数,程序不做任何动作。如果有按键状态发生了变化将en_cnt设置为0,计数器清0。等按键状态不发生变化时,再将en_cnt设置为1,开始计时。这样如果按键按下时发生了抖动,按键状态就会一直变化,这样计数器就会一直被清0,直到按键抖动结束后,按键状态不在发生变化,这时候才开始计时。这样计时到了20ms后,就能检测到真正的按键值。同样按键弹起时发生抖动,计时器也会被清0,直到按键弹起后不在抖动。这样不仅可以检测按键按下抖动,同样可以检测按键弹起抖动。
所以在复位时将en_cnt设置为1,开始20ms计数,复位时默认按键未按下,所以将key_state设置为0,同时将按键值key_value设置为4'b1111,4个按键默认都为高电平,所以按键值都设置为1。
if(!sys_rst_n) begin
en_cnt <= 1'b1;
key_state <= 1'b0;
key_value <= 4'b1111;
end
下面判断按键值是否发生了变化,如果按键值有变化,就将计数器清0。由于此时按键还未稳定,所以key_state仍然设置为0。
else if(key_last != key_now) begin //按键发生变化计数器清0
en_cnt <= 1'b0;
key_state <= 1'b0;
end
如果按键值不在发生变化,那么就等待20ms计数结束。
else if(key_last == key_now) begin
en_cnt <= 1'b1;
if(cnt == DELAY - 1'b1) begin //延时时间到,并且按键按下
key_value <= key_last;
if(key_last != 4'b1111)
key_state <= 1'b1;
end
end
如果按键值没有发生变化,就让en_cnt一直为1。当计时到20ms后,将按键当前值存储到key_value中,如果此时按键值不是4'b1111,说明是按键按下,而不是按键弹起。此时将按键状态key_state拉高,代表已经有按键真正的被按下了。如果按键弹起了,按键值就会发生变化,key_state就会被清0。这样key_state就会在按键被按下的过程中一直为高电平,按键弹起后为低电平。
先看一下输出波形是不是和预想的一样。
通过波形可以看到,当按键值为发生变化时,计数器会一直计数,计时到20ms后不做任何动作。按键按下后抖动一次,计数器就会清0一次,按键按下稳定后,并且计时时间到了20ms,按键状态位就会输出高电平,同时保存按键值,按键弹起后,状态位变为低电平。同时按键值会实时输出按键稳定后的值。这样根据key_state和key_value这两个寄存器值的值就可以知道按键什么时候按下,按下的时间为多长,按键值是多少。
对上面的波形分析后,已经可以成功的滤除按键抖动,并读取按键值。那么是不是程序就完成了?当然不是,还差最后一步。模块还需要向外输出信号,通知其他模块有按键按下。那直接用key_state这个信号输出行不行呢?key_state在没有按键按下时输出电平,在有按键按时时输出高电平。但是这个高电平持续时间比较长,如果外部模块直接用这个信号的话,就会在很多个时钟周期的上升沿检测到key_state为高电平。这样就分不清楚当前高电平是新的按键按下的高电平还是上一个按键按下后持续输出的高电平?如果要让外部其他模块清晰的知道按键是否被按下,只能输出一个时钟周期的高脉冲,这样外部模块每检测到一个高电平就代表按键被按下了一次。就不会造成误判或者漏判的情况。这样就还得输出一个信号,按键按下一次就输出一个时钟周期的脉冲。
通过观察上面的信号就可以想到 key_state 这个信号每次按键按下一次后,就会从0变为1,那么就可以在 key_state 为上升沿的这一瞬间输出一个脉冲,这样就可以实现按下按下一次输出一个脉冲的功能了。
下面就检测 key_state 信号的上升沿。
//读取按键状态
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_state0 <= 1'b0;
key_state1 <= 1'b0;
end
else begin
key_state0 <= key_state;
key_state1 <= key_state0;
end
end
//检测上升沿
assign key_flag = key_state0 & (~key_state1);
同样的方法将key_state当前状态和上一次状态存储起来,如果key_state当前值为1,上一个状态值为0,说明出现了上升沿。
将当前状态和上一个状态取反后再做位与运算,当两个值同时为1时,就说明出现了上升沿,最后将这个脉冲信号输出。
看一下key_flag这个标志的仿真波形
通过波形可以看出key_in、key_state0、key_state1依次延迟一个时钟周期,所以当key_state0第一个高电平时,key_state1刚好是最后一个低电平,key_state1取反然后和key_state0相与的结果在这个时钟周期内是1,在其余时间都为0。通过这个方法就将key_state的上升沿提取出来了。最后将这个上升沿输出给外部其他模块,这个外部模块直接检测到一个高电平就认为有按键按下,然后去读取按键值就行。
这样代码才算真正的写完了,来看看完整代码。
module key_filter(
input sys_clk,
input sys_rst_n,
input [3:0] key_in,
output key_flag, //按键按下标志
output reg [3:0] key_value //按键当前值
);
reg [3:0] key_now;
reg [3:0] key_last;
reg key_state;
reg key_state0,key_state1;
//parameter DELAY = 20'd1_000_000; //延时时间
parameter DELAY = 20'd20; //测试时间
reg [19:0] cnt; //20ms计数
reg en_cnt; //计数使能
//读取按键状态
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_state0 <= 1'b0;
key_state1 <= 1'b0;
end
else begin
key_state0 <= key_state;
key_state1 <= key_state0;
end
end
//检测上升沿
assign key_flag = key_state0 & (~key_state1);
//读取按键值
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_now <= 4'b1111;
key_last <= 4'b1111;
end
else begin
key_now <= key_in;
key_last <= key_now;
end
end
//当按键发生改变时,计数器清0,当按键状态稳定20ms后,输出按键值。
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
en_cnt <= 1'b1;
key_state <= 1'b0;
key_value <= 4'b1111;
end
else if(key_last != key_now) begin //按键发生变化计数器清0
en_cnt <= 1'b0;
key_state <= 1'b0;
end
else if(key_last == key_now) begin
en_cnt <= 1'b1;
if(cnt == DELAY - 1'b1) begin //延时时间到,并且按键按下
key_value <= key_last;
if(key_last != 4'b1111)
key_state <= 1'b1;
end
end
end
//计数 1_000_000次 20ms
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt <= 20'd0;
else if(en_cnt) begin
if(cnt == DELAY - 1'b1)
cnt <= 20'd0;
else if(cnt < DELAY -1'b1)
cnt <= cnt + 1'b1;
end
else
cnt <= 20'd0;
end
endmodule
下面编写测试代码
`timescale 1ns/1ns
module key_filter_tb;
parameter T = 20;
reg sys_clk;
reg sys_rst_n;
reg [3:0] key_in;
wire key_flag;
wire [3:0] key_value;
key_filter key_filter(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.key_in (key_in),
.key_flag (key_flag), //按键按下标志
.key_value (key_value) //按键当前状态
);
initial begin
sys_rst_n <= 1'b0;
key_in = 4'b1111;
#(20*T);
sys_rst_n <= 1'b1;
#(20*T + 3);
//按键默认为1 按下为0
keypress(4'b1110); //key0按下
keypress(4'b1101); //key1按下
keypress(4'b1011); //key2按下
keypress(4'b0111); //key3按下
keypress(4'b1100); //key0 key1 按下
keypress(4'b1000); //key0 key1 key2 按下
keypress(4'b0000); //全部按下
$stop;
end
initial sys_clk <= 1'b0;
always #(T/2) sys_clk = !sys_clk;
//模拟按键抖动
task keypress;
input [3:0] keyValue;
begin
#(T*10+1);
key_in = keyValue;
#(T*3);
key_in = 4'b1111;
#(T*5);
key_in = keyValue;
#(T*3);
key_in = 4'b1111;
#(T*5);
key_in = keyValue;
#(T*100);
#(T*6+1);
key_in = 4'b1111;
#(T*2);
key_in = keyValue;
#(T*8);
key_in = 4'b1111;
#(T*60);
end
endtask
endmodule
在测试代码中将模拟按键过程放在一个任务中,调用任务时传入按键值就行。模拟单个按键按下和多个按键同时按下。
输出整体波形如下
通过波形可以看出,按键按下一次,就会输出一个脉冲。同时输出当前按键值。
源码下载地址 https://download.csdn.net/download/qq_20222919/12687987