FPGA 14 按键滤波模块设计实验(有限状态机【FSM】实现)
主要功能: 当按键按下时,我们在按下和松开的时候,不可避免的会出现按键抖动的情况,我们设计了一个按键消抖模块,让按键经过消抖后,输出一个稳定的电平。
实现(设计)流程: 模块设计了处理亚稳态电路,并在内部使用了20ms定时器,对输入的 key_in 信号进行消抖,最后在输出段,得到稳定的输出按键状态的电平(key_state)和按键按下的滤波信号(key_flag)。在模块中使用了常用的FSM来设计整体的按键按下滤波和按键松开滤波的状态转移图。
实验目的 : 了解异步电路的输入信号处理方式、常用的FSM(线性状态机的使用)的简单控制过程,了解状态转移图的使用过程.
按键消抖状态转移图:
按键消抖我们一共分成4个状态
分别是 : 【IDEL】 【 FITEL】 【 DOWN 】 【FITEL1】
一、第一个状态是 IDEL(空闲状态) —》此时 key_in 的电平是高电平。当我们检测到下降沿的时候,我们进入到第二个状态 STATE = FITEL ,即:抖动滤波状态。在这里,我们还会启动一个20ms的 计时定时器。
二、在20ms内,我们要判断两种状态,在20ms 计时定时器以内,状态机信号 STATE 有两个方向:
① 如果在20ms内出现上升沿,那么就会再次进入STATE = IDEL状态,此时也就表明 这是一个按键按下抖动信号,我们把定时器使能信号关闭,且always块中,让计数器寄存器重新清零。 key_state = 1 , key_flag= 0
②如果到达20ms后,再次检测没有出现上升沿,那么表示这次按键按下状态已经稳定,我们将 STATE = DOWN 即:按键按下稳定状态。并且定时器【使能信号关闭】,always计数器寄存器重新清零。 key_state = 0,key_flag= 1
三、在 STATE = DOWN 状态时,我们【只要】判断是否出现高电平(上升沿)即可,出现高电平,则进入【按键松开】消抖滤波状态,即 STATE = FITEL_1 ,另外,我们还要开启20ms 计时定时器进行滤波 。 key_state = 0,key_flag= 0
四、 在 STATE = FITEL_1 中,我们仍然会出现两种状态
① 20ms内,出现了下降沿,则我们重新重新返回到状态 STATE = DOWN 中,表示这个是一个按键松开的抖动信号,另外定时【使能信号】关闭,计数器寄存器清零。 key_state = 0, key_flag= 0
②到达20ms,key_in 仍然还是高电平,则表示按键已经松开,此时 STATE = IDEL初始状态,此时【使能信号】关闭,计数器寄存器清零 key_state = 1,
key_flag= 1 。 重新开始下一轮的按键检测。
注: 这里有一个输出的key_flag信号,这个时一个脉冲信号,在本次设计的电路中,按键按下成功会出现两次key_flag脉冲信号,分别在按键按下时刻和按键松开时刻出现。也可以考虑设计只有一次key_flag信号,即只要有key_flag脉冲,就代表按键按下(应该也是可以的)。
key_state 输出信号可以理解为key_in的输入信号经过滤波得到的,但是呢,在输出的时候时经过了20ms的延时得到的输出滤波信号。
使用状态机来设计按键消抖实验:
边缘检测电路实际模型:
key_filter.v文件
//定义按键函数端口
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 ;
//定义状态机
localparam
IDEL = 4'b0001 ,
FILTER0 = 4'b0010 ,
DOWN = 4'b0100 ,
FILTER1 = 4'b1000 ;
//定义使用到的寄存器
reg [3:0]state ;
reg key_tmp0 , key_tmp1 ; // key_in 的状态寄存器
wire pedge , nedge ;
reg en_cnt_20ms ;
reg [19:0]cnt_20ms ;
reg cnt_20ms_full ;
//对外部输入的异步信号key_in进行同步处理
//key_in 异步信号,指的是在输入端的输入并不会跟着时钟来发生改变的,更通俗来讲
// 输入信号的输入在任意一个时钟边沿来保持这个变化,所以,如果在边沿的时候,输入信号key_in 发生了变化
// 我们就不知道自己得到的是高电平或者低电平,对于这种情况,我们一般的处理方法就是在,输入端增加多级寄存器,将输入信号进行
// 寄存,从而得到稳定的输入信号
// 这个always块里面,主要是为了得到稳定状态的输入信号: key_in_sb
reg key_in_sa,key_in_sb;
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)begin
key_in_sa <= 1'b0;
key_in_sb <= 1'b0;
end
else begin
key_in_sa <= key_in;
key_in_sb <= key_in_sa;
end
//获取上一个周期的 按键电平 和 当前按键电平
//目的: 获取,按键状态判断是下降沿还是上升沿到来
always@(posedge Clk or negedge Rst_n)
if(Rst_n == 1'b0) begin
key_tmp0 <= 1'b0 ;
key_tmp1 <= 1'b0 ;
end
else begin //每次时钟上升沿到来,两个寄存器读取上一次的数据的值
key_tmp0 <= key_in_sb;
key_tmp1 <= key_tmp0;
end
// 下降沿和上升沿到来标志位信号
assign nedge = ((~key_tmp0 ) & ( key_tmp1)) ; //判断下降沿是否到来?
// 分析:: 下降沿到来的清况是高电平-->低电平的变换过程,也就是说:
// key_tmp1 存的前1个时刻的电平,(理解这个信号端口很重要)
// key_tmp0 存的是当前时刻的电平,
// 要满足该条件,也就是说, key_tmp0 <= 0 且 key_tmp1 <= 1; 此时得到 nedge = 1 ;
assign pedge = key_tmp0 & (!key_tmp1) ; //判断上升沿是否到来?
//状态机判断
always@(posedge Clk or negedge Rst_n)
if(!Rst_n) //复位状态下,状态机处于空闲状态
begin
state <= IDEL ; //复位状态下,状态机处于空闲模式下
en_cnt_20ms <= 1'b0 ;//消抖计数器关闭
key_flag <= 1'b0 ; //按键成功标志位清零
key_state <= 1'b1 ; //默认输入按键状态高电平
end
else
begin
case(state)
IDEL :
begin
key_flag <= 1'b0 ; //空闲状态下,按键成功标志位永远为0
if(nedge == 1'b1) begin //按键检测到下降沿到来
state <= FILTER0 ;//进入下一个案件滤波状态
en_cnt_20ms <= 1 ;//开启使能计数20ms
end
else
state <= IDEL ; //未检测到下降沿到来,信号状态保持不变
end
FILTER0:
if(cnt_20ms_full == 1'b1) begin //看消除抖动20ms后,且20ms内没有检测到上升沿到来 = 20ms
key_flag <= 1'b1; //表示按键 已经成功按下
key_state<= 1'b0; //此时输入按键按下输出状态为低电平
state <= DOWN ; //进入下一状态
en_cnt_20ms <= 0 ; //20ms计数器关闭
end
else if(pedge == 1'b1) begin //没有满20ms 以内,有上升沿的到来,表明此时是抖动信号,非按键按下信号
state <= IDEL ;
en_cnt_20ms <= 0 ;//计数器关闭
end
else //上述两者情况均为出现,则状态机保持不变
state <= FILTER0 ;
DOWN:
begin
key_flag <= 1'b0; //检测到有按键按下,发送一个脉冲信号,由上一个状态的高电平变为低电平
if(pedge == 1'b1) begin //上一个状态时间超过20ms,此时需要一直等待上升沿的到来(做为按键松开的标志)
state <= FILTER1 ; //进入到下一状态
en_cnt_20ms <= 1'b1 ;//开启下一次20ms 延时计数
end
else //没有,则保持该状态
state <= DOWN ;
end
FILTER1:
if(cnt_20ms_full == 1'b1) begin//表示 20ms计数又一次到来,此时按键稳定
key_flag <= 1'b1; //表示上升沿的信号稳定的信号,随后到 IDEL又会设置为低电平,产生一个脉冲信号
key_state<= 1'b1; //此时输入按键按下状态为低电平
en_cnt_20ms <= 1'b0 ; //关闭20ms定时器
state <=IDEL ;
end
else if(nedge)begin // 20ms内又出现了下降沿,表示这是一次抖动
en_cnt_20ms <= 1'b0 ;
state <= DOWN ;
end
else //20ms内状态保持不变
state <=FILTER1 ;
default:
begin
state <= IDEL ;
key_flag <= 1'b0;
key_state<= 1'b1; //默认状态时 1 高电平
en_cnt_20ms <= 1'b0 ; //默认状态时 0 低电平
end
endcase
end
// 定时器启动 always块
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
cnt_20ms <= 20'd0;
else if(en_cnt_20ms)
cnt_20ms <= cnt_20ms + 1'b1;
else // 关闭时,计时器清零
cnt_20ms <= 20'd0;
//定时器20ms always块 50Mhz 20ms = cnt_20ms : 1_000_000
always@(posedge Clk or negedge Rst_n)
if(Rst_n == 1'b0)
cnt_20ms_full <= 0 ;
else if(cnt_20ms == 20'd999_999)
cnt_20ms_full <= 1'b1 ; //计数值计满标志位
else
cnt_20ms_full <= 1'b0 ; // 未挤满,输出0
endmodule
解决亚稳态的两级D触发器
仿真测试文件
`timescale 1ns/1ns
`define clk_period 20
module key_filter_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_model.v 文件
`timescale 1ns/1ns
module key_model(press,key);
input press;
output reg key;
reg [15:0]myrand;
initial begin
key = 1'b1;
// press_key;
// #10000;
// press_key;
// #10000;
// press_key;
// #10000;
// $stop;
end
always@(posedge press)
press_key;
task press_key;
begin
repeat(50)begin
myrand = {$random}%65536;//0~65535; 产生一个随机数
#myrand key = ~key; // 延时 myrand 时间, 输出 key 信号翻转
end
key = 0;
#25000000;
repeat(50)begin
myrand = {$random}%65536;//0~65535;
#myrand key = ~key;
end
key = 1;
#25000000;
end
endtask
endmodule
按键输入控制led四个以加减法的形式输出实验top文件:
module led_ctrl(
Clk ,
Rst_n ,
key_flag0,
key_flag1,
key_state0,
key_state1,
led
);
input Clk ;
input Rst_n ;
input key_flag0 , key_flag1 ;
input key_state0 , key_state1 ;
output [3:0]led ;
reg [3:0]led_r ;
always@(posedge Clk or negedge Rst_n)
if(Rst_n == 1'b0)
led_r <= 4'b0000 ;
else if(key_flag0 && !key_state0)
//key_flag0 表示的是按键按下的一个标志
//key_state0 表示的是按键现在所处的状态
//key_0 按下,led 做加法
led_r <= led_r + 1'b1 ;
else if(key_flag1 && !key_state1)
led_r <= led_r - 1'b1 ;
//key_0 按下,led 做减法
else
led_r <= led_r ;
assign led = ~led_r ;
endmodule
本章要学习的内容有:
1、按键的实际状态和状态机的相结合
2、20ms循环定时器中的两个 信号, en_cnt_20ms 是使能计数器, cnt_20ms 寄存器,
我们可以看到,只要我们 使 en_cnt_20ms =0 ,那么在定时器的 always块语句中,cnt_20ms 就会被清零。
所以在顶层状态机设计的时候,cnt_20ms 并未在这里面出现,而是通过控制 en_cnt_20ms 信号达到我们所需要的效果。
3、在信号输入之前,我们又加了两级D触发器,分别是key_in_sa和key_in_sb,我们是使用的非阻塞语句进行操作的,这个是解决亚稳态的两级寄存器。后面跟的key_temp_1 和 key_temp_0是两个时刻的信号状态,为了判断上升沿和下降沿的目的,所以二者功能是不同的。