按键消抖是FPGA学习中的一个必备的基础知识模块,在我的学习过程中,共碰到过两种按键消抖模块,分别是在**《小梅哥FPGA自学笔记》和《FPGA Verilog开发实战指南》**之中,两种方式的实现有着略大的不同,下面分别列举两种方式。
如果赶时间,可以跳过第一种方式,之间看第二种。
按键是最为常见的电子元器件之一,在电子设计中应用广泛,可能大家一听到按键消抖会疑问,按键不就是一个简单的按下置0,松开置1的元器件吗,只需要一句简单的赋值语句应该就可以实现的,为什么要多此一举的写按键消抖模块呢。这是因为我们使用的按键开关为机械弹性快关,当机械触点断开、闭合时,由于机械触点的弹性作用,**一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动。**为了不使抖动影响我们芯片的控制产生误判,就必须要进行按键消抖。
按键消抖具有硬件消抖和软件消抖两种方式,硬件消抖是通过RS触发器进行消抖,可用于按键较少时的消抖,这里我们只对软件消抖进行阐述和分析。
在按键按下和释放的时候都会产生抖动,即极短时间内状态在高电平和低电平之间震荡,当状态在低电平持续至少20ms时我们认为此次变化非抖动,并且按键是按下状态,在释放时和按下时状态相反,超过20ms的高电平状态即已经释放按键。
module key_filter(clk,rst_n,key_in,key_flag,key_state);
input clk;
input rst_n;
input key_in;
output reg key_flag;
output reg key_state;
reg [19:0]cnt;
reg en_cnt;
reg cnt_full;
//50_000_000时钟,周期20ns
//延时20ms,20_000_000ns/20ns=1000_000
//对外部输入信号进行同步处理,消除亚稳态
reg key_in_s0,key_in_s1;
always @(posedge clk or negedge rst_n)
if (!rst_n)begin
key_in_s0<=1'b0;
key_in_s1<=1'b0;
end
else begin
key_in_s0<=key_in;
key_in_s1<=key_in_s0;
end
localparam
IDEL = 4'b0001,
FILTER0 = 4'b0010,
DOWN = 4'b0100,
FILTER1 = 4'b1000;
reg [3:0]state;
reg key_tmp0,key_tmp1;
wire pedge,nedge;
//使用d触发器存储两个相邻时钟上升沿时外部输入信号的电平状态
always @(posedge clk or negedge rst_n)
if (!rst_n)begin
key_tmp0<=1'b0;
key_tmp1<=1'b0;
end
else begin
key_tmp0<=key_in_s1;
key_tmp1<=key_tmp0;
end
//产生跳变沿信号
assign nedge=(!key_tmp0) & key_tmp1;
assign pedge=key_tmp0 & (!key_tmp1);
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
state<=IDEL;
en_cnt<=1'b0;
key_flag<=1'b0;
key_state<=1'b1;
end
else begin
case(state)
IDEL:
begin
key_flag<=1'b0;
if(nedge)begin
state<=FILTER0;
en_cnt<=1'b1;
end
else
state<=IDEL;
end
FILTER0:
if(cnt_full)begin
key_flag<=1'b1;
key_state<=1'b0;
state<=DOWN;
en_cnt<=1'b0;
end
else if(pedge)begin
state<=IDEL;
en_cnt<=1'b0;
end
else
state<=FILTER0;
DOWN:
begin
key_flag<=1'b0;
if(pedge)begin
state<=FILTER1;
en_cnt<=1'b1;
end
else
state<=DOWN;
end
FILTER1:
if(cnt_full)begin
key_state<=1'b1;
key_flag<=1'b1;
state<=IDEL;
en_cnt<=1'b0;
end
else if(nedge)begin
state<=DOWN;
en_cnt<=1'b0;
end
else
state<=FILTER1;
default:
begin
state<=IDEL;
en_cnt<=1'b0;
key_flag<=1'b0;
key_state<=1'b1;
end
endcase
end
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt<=20'd0;
else if(en_cnt)
cnt<=cnt+1'b1;
else
cnt<=20'd0;
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt_full<=1'b0;
else if(cnt==999_999)
cnt_full<=1'b1;
else
cnt_full<=1'b0;
endmodule
`timescale 1ns/1ns
`define clock_period 20
module key_filter_tb;
reg clk;
reg rst_n;
reg key_in;
wire key_flag;
wire key_state;
reg [15:0]myrand;
key_filter key_filter0(
.clk(clk),
.rst_n(rst_n),
.key_in(key_in),
.key_flag(key_flag),
.key_state(key_state)
);
initial clk=1;
always#(`clock_period/2) clk=~clk;
initial begin
rst_n=1'b0;
key_in=1'b1;
#(`clock_period*10) rst_n=1'b1;
#(`clock_period*10+1);
press_key;
#10000;
press_key;
#10000;
press_key;
$stop;
end
task press_key;
begin
repeat(50)begin
myrand={$random}%65536;//加括号是正,产生0-65535
#myrand key_in=~key_in;
end
key_in=1'b0;
#50_000_000;
//按下抖动
repeat(50)begin
myrand={$random}%65536;
#myrand key_in=~key_in;
end
key_in=1'b1;
#50_000_000;
//释放抖动
end
endtask
endmodule
仿真波形:
按下:
释放:
使用状态机实现的按键消抖模块在调用时需要注意标志信号的使用:
例:我们令key_in为需要消抖的按键,key_flag为按键消抖模块中的按下标志信号,key_state为按键消抖模块中的状态信号,在按键按下时key_flag产生一个脉冲信号,key_state由高电平变为低电平。因此,调用时的语句为:
assign key_in=(key_flag) && (~key_state)
这样,只有在模块确定按键按下的一瞬间才会判断按键按下,为低电平,其余时刻key_in均为高电平。
我们认为,按键处于低电平的时刻大于20ms时,按键为按下状态,而按键按下的干扰是短时、多次的从高电平到低电平的跳变。因此,我们可以用一个计数模块,当输入信号为低电平时,开始计时,计时途中,如果跳变为高电平,即计数器清0,等待下一次低电平到来后再次开始计时,如此反复,如果记满20ms,那么,认为按键为一次按下状态,使按键标志产生一个脉冲,代表按键按下。
计时模块:通常FPGA开发板板上晶振产生的系统时钟频率是50MHz,周期为20ns,要记满20ms,即需要计数(20ms/20ns=1000_000-1=999_999)次,我们需要的按键标志只是一个脉冲信号,如果在计数999_999时,将其置1,而按键低电平状态不一定正好等于20ms,一般都为大于20ms,这样的话,会产生一个持续很长时间的标志信号,不是我们想要看到的,因此,在计数器计数到999_998时即产生标志信号,这样就满足了我们的设计需要。
module key_filter
#(parameter CNT_MAX=999_999)
(
input wire clk,
input wire rst_n,
input wire key_in,
output reg key_flag
);
reg [19:0]cnt_20;
always @(posedge clk or negedge rst_n)
if(!rst_n)
cnt_20<=1'b0;
else if(key_in==1'b1)
cnt_20<=1'b0;
else if(cnt_20==CNT_MAX && key_in==1'b0)
cnt_20<=cnt_20;
else
cnt_20<=cnt_20+1'b1;
always @(posedge clk or negedge rst_n)
if(!rst_n)
key_flag<=1'b0;
else if(cnt_20==CNT_MAX-1'b1)
key_flag<=1'b1;
else
key_flag<=1'b0;
endmodule
`timescale 1ns/1ns
`define clk_period 20
module key_filter_tb;
reg clk;
reg rst_n;
reg [3:0]key_in;
wire key_flag;
reg [15:0]myrand;
key_filter
#(
.CNT_MAX(20'd999_999)
)
key_filter(
.clk(clk),
.rst_n(rst_n),
.key_in(key_in),
.key_flag(key_flag)
);
initial clk=1'b1;
always #(`clk_period/2) clk=~clk;
initial begin
rst_n=1'b0;
key_in=1'b1;
#(`clk_period*10) rst_n=1'b1;
#(`clk_period*10+1);
press_key;
#10000;
press_key;
#10000;
press_key;
$stop;
end
task press_key;
begin
repeat(50)begin
myrand={$random}%65536;//加括号是正,产生0-65535
#myrand key_in=~key_in;
end
key_in=1'b0;
#50_000_000;
repeat(50)begin
myrand={$random}%65536;
#myrand key_in=~key_in;
end
key_in=1'b1;
#50_000_000;
end
endtask
endmodule
图一:
图二:
在图二可以看到,标志信号key_flag在计数器计数到到999_998产生一个脉冲信号(寄存器起到延迟一拍的作用,因此看起来是在999_999时上升。)
在调用此按键消抖模块时,只需要看key_flag这个标志信号的变化即可。
至此,两种按键消抖的方式都已经讲解完毕,可以看出,方式二相比于方式一,具有代码更简洁、理解更方便、调用更简便的优点,因此推荐使用第二种方式。