按键消抖模块key_filter
1. 原理介绍
如图,按键未按下时keys信号为高电平,按下则为低电平;通过检测keys信号电平,就可以判断按键状态。
但反作用弹簧会导致抖动现象,电平信号出现一段不确定波形
一般情况下,抖动的电平信号为1的持续时间不会超过20ms,只要通过对抖动波形的发生时长计时(计数),当为1的时长超过20ms,则可判定 进入了发生了按键按下/释放动作,并确定按键状态。
也可以采用一些硬件电路进行消抖,如RS触发器,555定时器组成的单稳态触发器等
(原理:《小梅哥-FPGA系统设计与验证实战指南_V24》P164)
2. 模块及接口设计![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5b9a62a76c326269e29d6bd0a679808d.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/750f097c17a05bcea955ab4d6eff4896.png)
3. 状态机设计
由时序图分析可知,存在这样四种状态:
IDLE
未按下时的空闲状态FILTER0
按下抖动滤除状态DOWN
按下稳定状态FILTER1
释放抖动滤除状态
状态转移图
状态转移条件
当前状态 | 下一状态 | 转移条件 | 输出 |
---|---|---|---|
IDLE | FILTER0 | nedge==1(检测到下降沿)(随后开始计数直至20ms) | key_state=1 key_flag=1?? |
FILTER0 | IDLE | pedge==1(在按下消抖状态中,(未计数至20ms)如果检测到上升沿,则说明是一个抖动,仍回到初始态重新等待下降沿到来) | |
FILTER0 | DOWN | Cnt_full(检测到下降沿后即开始计时,判断按键电平是否在计数值满20ms时仍为低电平,若满足则认为确实按下,进入按下状态) | |
DOWN | FILTER1 | pedge==1(按下稳定状态中,如果检测到上升沿,则启动释放消抖)(随后开始计数直至20ms) | |
FILTER1 | DOWN | nedge==1(在释放消抖状态中,如果检测到下降沿且记数值未满,说明是个抖动,回到DOWN状态重新等待上升沿到来) | |
FILTER1 | IDLE | Cnt_full(若最后一次抖动后,处于高电平的时间计数满20ms,则认为确实释放完成,回到IDLE状态) |
4. 模块
(1)单bit异步信号同步化设计(两级DFF消除亚稳态模块)
按键输入信号key_in对于FPGA内部信号而言是一个异步输入的信号,具有随机性,不依赖于系统时钟Clk,如不处理直接作为输入使用,将会引其时序违例,导致亚稳态。因此需要用两级DFF将key_in打慢两拍,依赖于Clk,消除亚稳态。
key_in_b即为打慢两拍同步后的信号。
两级寄存器消除亚稳态具体原理解释
解释1
亚稳态在异步电路中是不可避免的,不管用什么同步电路去同步异步信号,都会出现亚稳态。那同步电路的作用是什么呢?其实是为了不让亚稳态传播。 两级同步电路的思想其实是为了给亚稳态留出足够的时间,好让亚稳态变成稳定态。
一般情况下亚稳态持续的时间很短,所以两级DFF同步之后的信号基本上不会出现亚稳态,可以给后级使用。
这里用两级DFF的目的就是给亚稳态留出一个DFF的工作时钟周期,如果第一级DF的输出出现亚稳态,那么在一个时钟周期内,让亚稳态变成稳定态。
这样第二级DFF采样第一级DFF的输出时,第一级DFF的输出早就稳定了,因此不会出现亚稳态的传播。所以在用两级同步电路对异步信号做了处理后,降低同步电路的工作时钟,就可以给亚稳态留更多的时间让它变成稳定态,因此可以起到使电路更稳定安全的效果。
解释2
假设第一级触发器的输入不满足其建立保持时间,它在第一个脉冲沿到来后输出的数据就为亚稳态,那么在下一个脉冲沿到来之前,其输出的亚稳态数据在一段恢复时间后必须稳定下来,而且稳定的数据必须满足第二级触发器的建立时间,如果都满足了,在下一个脉冲沿到来时,第二级触发器将不会出现亚稳态,因为其输入端的数据满足其建立保持时间。同步器有效的条件:第一级触发器进入亚稳态后的恢复时间 + 第二级触发器的建立时间 < = 时钟周期。
更确切地说,输入脉冲宽度必须大于同步时钟周期与第一级触发器所需的保持时间之和。最保险的脉冲宽度是两倍同步时钟周期。所以,这样的同步电路对于从较慢的时钟域来的异步信号进入较快的时钟域比较有效,对于进入一个较慢的时钟域,可能根本采不到异步信号。
但是一级FF稳定后的out是随机的,到第二级FF后大概率没有亚稳态了,但是得到的数据0/1都有可能,仅适用于对错误不敏感的地方。对于敏感的电路,可以采用双口RAM或FIFO同步。
异步信号同步化代码实现
reg key_in_a,key_in_b;//声明同步寄存器
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
begin
key_in_a <= 1'b0;//第一个寄存器
key_in_sb <= 1'b0;//第二个寄存器
end
else
begin
key_in_a <= key_in;
key_in_b <= key_in_a;
end
在一些高速设计中,也有使用三级触发器进行单bit信号同步的设计,此方法只是为了提高平均故障时间( Mean Time Between Failure,MTBF),其电路为在两级触发器后再接一级触发器,代码为再加一个key_in_c。
(2)边沿检测模块设计
边沿检测电路原理图
边沿检测代码
reg key_tmpa,key_tmpb;
wire pedge,nedge;
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
begin
key_tmpa <= 1'b0;
key_tmpb <= 1'b0;
end
else
begin
key_tmpa <= key_in_sb;
key_tmpb <= key_tmpa;
end
assign nedge = (~key_tmpa) && key_tmpb;
assign pedge = key_tmpa && (~key_tmpb);
在c语言中,!和~均表示取反,这两个的区别在于:
-
! :代表逻辑取反,即:把非0的数值变为0,0变为1;
-
~ :表示按位取反,即在数值的二进制表示方式上,将0变为1,将1变为0;
按位取反“~”:按位取反1变0,0变1
逻辑非“!”:逻辑取反, false变true,true变false,在C中,只要不是0就是真
在这里,对于1bit二进制数来说,!和 ~ 都是一样的
(3)20ms计数器模块
一般推荐一个always块只对一个信号进行操作!!使逻辑清晰易修改
reg [19:0]cnt;
reg en_cnt; //使能计数寄存器
//计数模块
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 == 20'd999_999)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
(4)状态机模块
reg [3:0]state;
localparam
IDEL = 4'b0001,
FILTER0 = 4'b0010,
DOWN = 4'b0100,
FILTER1 = 4'b1000;
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)begin
en_cnt <= 1'b0;
state <= IDEL;
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;
en_cnt <= 1'b0;
state <= DOWN;
end
else if(pedge)begin
state <= IDEL;
en_cnt <= 1'b0;
end
else
state <= FILTER0;
DOWN:
//通过判断 key_flag && !key_state 来确定按键的状态,为 1 发生按下动作
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_flag <= 1'b1;
key_state <= 1'b1;
state <= IDEL;
en_cnt <= 1'b0;
end
else if(nedge)begin
en_cnt <= 1'b0;
state <= DOWN;
end
else
state <= FILTER1;
default:
begin
state <= IDEL;
en_cnt <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
通过判断 (key_flag && !key_state==1) 来确定按键的状态,为 TRUE 则按下动作发生
(5)仿真
仿真方法1 定时发生型
`timescale 1ns/1ns
`define clk_period 20
module key_filter_tb;
reg Clk;
reg Rst_n;
reg key_in;
wire key_flag;
wire key_state;
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#(`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); //与时钟边沿稍微延后1ns,减少门级仿真中亚稳态的产生
//模拟按下抖动 20ms 内
key_in = 1'b0;#1000;
key_in = 1'b1;#2000;
key_in = 1'b0;#1400;
key_in = 1'b1;#2600;
key_in = 1'b0;#1300;
key_in = 1'b1;#200;
key_in = 1'b0;#2000100;
#50000100;
//产生一个低电平大于 20ms,代表按下稳定
//模拟释放抖动 20ms 内
key_in = 1'b1;#2000;
key_in = 1'b0;#1000;
key_in = 1'b1;#2000;
key_in = 1'b0;#1400;
key_in = 1'b1;#2600;
key_in = 1'b0;#1300;
key_in = 1'b1;#2000100;
#50000100;
//产生一个高电平大于 20ms,代表释放稳定
//模拟按下抖动 20ms 内
key_in = 1'b0;#1000;
key_in = 1'b1;#2000;
key_in = 1'b0;#1400;
key_in = 1'b1;#2600;
key_in = 1'b0;#1300;
key_in = 1'b1;#200;
key_in = 1'b0;#2000100;
#50000100;
//产生一个低电平大于 20ms,代表按下稳定
//模拟释放抖动 20ms 内
key_in = 1'b1;#2000;
key_in = 1'b0;#1000;
key_in = 1'b1;#2000;
key_in = 1'b0;#1400;
key_in = 1'b1;#2600;
key_in = 1'b0;#1300;
key_in = 1'b1;#2000100;
#50000100;
//产生一个高电平大于 20ms,代表释放稳定
$stop;
end
endmodule
仿真1波形图
仿真方法2 press_task型
reg [15:0]myrand;//随机数
task press_key;
begin
repeat(50)
begin //五十次随机时间按下抖动
myrand = {$random}%65536;
#myrand key_in = ~key_in;
end
key_in = 0;
#50_000_000; //按下稳定
repeat(50)
begin //50 次随机时间释放抖动
myrand = {$random}%65536;
#myrand key_in = ~key_in;
end
key_in = 1;
#50_000_000; //释放稳定
end
endtask
initial
begin
Rst_n = 1'b0;
key_in = 1'b1;
#(`clk_period*10) Rst_n = 1'b1;
#30000;
press_key; #10000;
press_key; #10000;
press_key; #10000;
$stop;
end
仿真2波形图
仿真方法3 无外部按键型key_model1
按键设置在task press_key内部已设置好
key_filter_top_tb(1)代码
`timescale 1ns/1ns
`define clk_period 20
module key_filter_top_tb;
reg Clk;
reg Rst_n;
wire key_in;
wire key_flag;
wire key_state;
key_filter key_filter0(
.Clk(Clk),
.Rst_n(Rst_n),
.key_in(key_in),
.key_flag(key_flag),
.key_state(key_state)
);
key_model key_model0(.key(key_in));
initial Clk= 1;
always#(`clk_period/2) Clk = ~Clk;
initial
begin
Rst_n = 1'b0;
#(`clk_period*10) Rst_n = 1'b1;
#(`clk_period*10 + 1);
end
endmodule
key_model1代码
`timescale 1ns/1ns
module key_model(key);
output reg key;
reg [15:0]myrand;
initial
begin
key = 1'b1;
press_key;
#10000;
press_key;
#10000;
press_key;
#10000;
$stop;
end
task press_key;
begin
repeat(50)
begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 0;
#25000000;
repeat(50)
begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 1;
#25000000;
end
endtask
endmodule
仿真3波形图
仿真方法4 有外部按键型key_model2
按键设置在task press_key内部未设置好,按键动作为外部人为输入
keu_filter_top代码
module key_filter_top;
input Clk;
input Rst_n;
input press;
reg key_in;
output key_flag;
output key_state;
key_filter key_filter0(
.Clk(Clk),
.Rst_n(Rst_n),
.key_in(key_in),
.key_flag(key_flag),
.key_state(key_state)
);
key_model key_model0(
.press(press),
.key(key_in)
);
endmodule
key_model2代码:按键动作由外部人为键入
`timescale 1ns/1ns
module key_model(press,key);
input press;
output reg key;
reg [15:0]myrand;
initial
begin
key = 1'b1;
end
always@(posedge press)
press_key;
task press_key;
begin
repeat(50)begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 0;
#25000000;
repeat(50)begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 1;
#25000000;
end
endtask
endmodule
5. 边沿D触发器&电平D触发器&锁存器 波形图对比
部人为键入
`timescale 1ns/1ns
module key_model(press,key);
input press;
output reg key;
reg [15:0]myrand;
initial
begin
key = 1'b1;
end
always@(posedge press)
press_key;
task press_key;
begin
repeat(50)begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 0;
#25000000;
repeat(50)begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 1;
#25000000;
end
endtask
endmodule