FPGA-按键消抖设计与验证
为何要对按键进行消抖?
下图是按键在实际电路中的应用,在无按键按下时keys[2:0]都将其上拉至高电平,首先在按键的硬件结构中存在一个反作用弹簧,当按下或松开时都会产生额外的物理振动,导致按键电平也在同步抖动。
在按键从按下到松开的过程中,电平的理想变化和实际变化如下图所示。
可以看出抖动的次数和间隔都是随机产生的,唯一确定的是在一般情况下,按下抖动和释放抖动的时间会持续5ms~10ms以内。
状态机加计数器实现按键消抖
状态机转移状态
在上图按键信号电平实际变化中,存在四种状态:
1.高电平稳定期,即所谓的空闲态idle;
2.按下抖动,该状态表示由高电平转低电平过程中,在检测到下降沿后,10ms以内又检测到了上升沿,则认定按键还在抖动,返回空闲态idle;命名为filter0;
3.低电平稳定期,检测到下降沿后,10ms以内保持低电平稳定,判定按键按下;命名为down态;
4.释放抖动,该状态表示由低电平转高电平过程中,在检测到上降沿后,10ms以内又检测到了下升沿,则认定按键还在抖动,返回down态;命名为filter1。
异步信号同步处理
为什么要做异步信号同步处理?
首先按键信号对于FPGA内部信号而言是一个异步信号(当一个信号跨越某个时钟域时,对新时钟域的电路来说它就是一个异步信号,接收该信号的电路需要将其进行同步),即按键信号状态不依赖FPGA器件时钟,如果不处理,容易导致出现亚稳态(无法预测信号状态),同步处理可以防止亚稳态在新时钟域传播蔓延。
简单的同步器是由两个寄存器串联而成,中间没有其他组合电路,如上图所示,这种设计可以保证后面的触发器获取前一个触发器输出时,前一个触发器已经退出了亚稳态,并且输出稳定。
对于本实验设计而言,该部分只是对一个按键变化信号做一个同步处理,防止按键对新时钟域其他信号干扰,并不判断是否是上升沿还是下降沿,
边沿检测电路设计
为什么要做边沿检测?
首先异步信号同步处理部分没有判断上升沿和下降沿,而对于状态机状态转移而言这又是一个重要信号。
因此边沿检测电路的作用就是检测输入信号或者内部逻辑信号的跳变,即上升沿或下降沿的检测。
如何去理解边沿检测电路呢?
我们知道在always块的敏感信号列表中可以直接用posedge和negedge来直接提取上升沿和下降沿,但如果要在always程序块的内部检测上升沿和下降沿呢?还是用posedge和negedge吗?显然这是不可以的,这样的语句是不可能综合的,编译时会报错的,实际上posedge和negedge只能用于always块的敏感信号列表或者testbench中,所以边沿检测电路应用就产生了。
上图是应用较广的两级寄存器的边沿检测电路,一般为了防止触发信号的波动,可以多加几级触发器,消除抖动,使得信号更稳定。
边沿检测的原理就是利用寄存器在时钟信号的控制下,输入状态即为下一时刻输出状态这一特性来进行比较判断的。
上述电路是如何检测到上升沿或者下降沿的呢?
上升沿检测
1.上升沿来临之间,trigger_r信号已经保持了一段时间的低电平,trigger_rf和trigger_rs寄存器都保持输出低电平,其中一个信号经过非门与另一个信号进行与(&)操作,pos_edge和neg_edge都输出低电平。
2.当trigger_r信号从0变为1,也就是上升沿,第一个时钟沿到来时,第一个寄存器trigger_rf的输出为1,而第二个寄存器trigger_rs需要在第二个时钟沿到来时才输出1,因此在第一个时钟沿的时候,第二个寄存器trigger_rs的输出0经过非门与第一个寄存器trigger_rf的输出1进行与(&)操作,此时pos_edge输出高电平,neg_edge依旧输出低电平,因此,用pos_edge检测到来上升沿。
3.当clk第二个时钟到来时,此时trigger_r依旧保持高电平,但此时trigger_rf和trigger_rs寄存器都保持输出高电平,导致pos_edge和neg_edge依旧输出低电平,这样就解释了当有上升沿到来时,pos_edge也就产生了一个时钟周期的高电平,没有上升沿或者下降沿变化是pos_edge和neg_edge都保持低电平状态。
下降沿检测
1.上升沿来临之间,trigger_r信号已经保持了一段时间的高电平,trigger_rf和trigger_rs寄存器都保持输出高电平,其中一个信号经过非门与另一个信号进行与(&)操作,pos_edge和neg_edge都输出低电平。
2.当trigger_r信号从1变为0,也就是下降沿,第一个时钟沿到来时,第一个寄存器trigger_rf的输出为0,而第二个寄存器trigger_rs需要在第二个时钟沿到来时才输出0,因此在第一个时钟沿的时候,第一个寄存器trigger_rf的输出0经过非门与第二个寄存器trigger_rs的输出1进行与(&)操作,此时neg_edge输出高电平,pos_edge依旧输出低电平,因此,用neg_edge检测到来下降沿。
3.当clk第二个时钟到来时,此时trigger_r依旧保持低电平,但此时trigger_rf和trigger_rs寄存器都保持输出低电平,导致pos_edge和neg_edge依旧输出低电平,这样就解释了当有下升沿到来时,neg_edge也就产生了一个时钟周期的高电平,没有上升沿或者下降沿变化是pos_edge和neg_edge都保持低电平状态。
首选这里的重要逻辑思想是,无论边沿电路检测到上升沿还是下降沿,它在检测到所需要的边沿后都会产生一个高电平脉冲,上升沿在pos_edge输出一个时钟周期的高电平,下降沿在neg_edge也输出一个时钟周期的高电平。
计数器模块
该部分是对状态机状态转移的一个重要判断标志,因为按键的抖动时间在5ms~10ms之间,因此,该计数器为10ms,当计数器在计数过程,按键状态稳定,则判定按键进入稳定期。
verilog代码部分
//--------------------------------------------------------------------------------------------
// Component name : key_filter
// Author : 硬件嘟嘟嘟
// time : 2020.04.17
// Description : 按键消抖设计
// src : FPGA系统设计与验证实战指南_V1.2
//--------------------------------------------------------------------------------------------
module key_filter(
clk, //50m时钟
rst_n, //复位信号
key_in, //按键输入
key_flag, //按键状态切换标志
key_state //按键状态
);
input clk,rst_n;
input key_in;
output reg key_flag,key_state;
//对key_in作异步信号同步处理
reg key_in_a,key_in_b; //定义两个寄存器
always@(posedge clk,negedge rst_n)
if(!rst_n)begin
key_in_a <= 1'b1;
key_in_b <= 1'b1;
end
else begin //input : key_in -->output :key_in_b
key_in_a <= key_in;
key_in_b <=key_in_a;
end
//对key_in_b(异步信号同步处理输出)进行边沿检测
reg trigger_rf,trigger_rs;
wire pos_edge,neg_edge;
always@(posedge clk,negedge rst_n)
if(!rst_n) begin
trigger_rf <= 1'b0;
trigger_rs <= 1'b0;
end
else begin
trigger_rf <= key_in_b;
trigger_rs <= trigger_rf;
end
assign pos_edge = trigger_rf & (!trigger_rs);
assign neg_edge = (!trigger_rf) & trigger_rs;
//定义一个10ms计数器
reg [18:0]cnt; //10_000_000 ns /20ns =500000
reg en_cnt ; //定义使能寄存器,要让在按键抖动器件才开始计数
//使能信号产生,计数器开始计数
always@(posedge clk,negedge rst_n)
if(!rst_n)
cnt <= 19'd0;
else if(en_cnt)
cnt <= cnt + 1'b1;
else
cnt <= 19'd0;
// 计数器计满10ms后产生计满标志
reg cnt_full; //计满标志
always@(posedge clk,negedge rst_n)
if(!rst_n)
cnt_full <= 1'b0;
else if(cnt == 19'd499_999)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
//状态机状态转移
//localparam定义四种状态
localparam key_idle = 3'd1,
key_filter0 = 3'd2,
key_down = 3'd3,
key_filter1 = 3'd4;
reg [2:0] current_state; //定义当前态
//描述状态转移及输出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
en_cnt <= 1'b0;
current_state <= key_idle;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(current_state)
key_idle :
begin
key_flag <= 1'b0;
if(neg_edge)begin
current_state <= key_filter0;
en_cnt <= 1'b1;
end
else
current_state <= key_idle;
end
key_filter0:
if(cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b0;
en_cnt <= 1'b0;
current_state <= key_down;
end
else if(pos_edge)begin
current_state <= key_idle;
en_cnt <= 1'b0;
end
else
current_state <= key_filter0;
key_down:
begin
key_flag <= 1'b0;
if(pos_edge)begin
current_state <= key_filter1;
en_cnt <= 1'b1;
end
else
current_state <= key_down;
end
key_filter1:
if(cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b1;
current_state <= key_idle;
en_cnt <= 1'b0;
end
else if(neg_edge)begin
en_cnt <= 1'b0;
current_state <= key_down;
end
else
current_state <= key_filter1;
default:
begin
current_state <= key_idle;
en_cnt <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
endmodule
激励及仿真测试
该部分将在学习小项目中不断积累testbench书写技巧和仿真思想,深入理解testbench仿真模型后统一添加。
另附简单的testbench代码,仅供验证参考
//--------------------------------------------------------------------------------------------
// src : FPGA系统设计与验证实战指南_V1.2
//--------------------------------------------------------------------------------------------
`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);
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;
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;
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;
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;
$stop;
end
endmodule