在实际应用中,用按键控制一些操作的场景十分常见,但由于机械按键的物理特性导致按下和释放瞬间会有大约20ms的抖动,这就会给应用时带来一些不方便的地方。当然硬件上可以使用RS触发器合理设计消抖电路,但这里记录的是一种软件消抖方法。由于是基于FPGA的应用,延时是不可综合的,故采用一种状态机电路对机械按键进行消抖。模块图:
key_in是按键信号的输入。
key_flag信号在滤波稳定后拉高一个时钟周期,合理设计的话,一次按键事件,该信号应被分别拉高一个时钟周期。
key_state是按键稳定后,为0或者为1指示按键的所处状态。
如assign key = !key_state & key_flag 可作为按键按下的标志。
采用状态机进行设计,状态转移图以及相关条件如下图。
分为四个状态:
IDLE:空闲态,复位后,没有任何按键行为,按键值为高电平。在检测到下降沿后,进入按下滤波状态FILTER0,否则一直等待下降沿的到来。
FILTER0:按下滤波态,进入该态后,启动20ms的计数器,如在计满期间检测到上升沿,则判定为是在抖动中,按键并未稳定为低电平,则返回IDLE态。如果计满时还未检测到上升沿,则认为按键稳定在低电平。进入DOWN态
DOWN:按键按下稳定态,是类似于IDLE态的孪生态,在该态中检测到上升沿后,进入释放滤波态FILTER1,否则一直等待上升沿的到来。
FILTER1:释放滤波态,进入该态后,启动20ms的计数器,如在计满期间检测到下降沿,则判定为是在抖动中,按键并未稳定为高电平,则返回DOWN态。如果计满时还未检测到下降沿,则认为按键稳定在高电平。进入IDLE态。
一、逻辑设计及仿真
两个要注意的点:
在表示状态机的状态时,由于状态较少条件相对较多,采用one-hot编码,虽然多使用了几个bit的寄存器,但由于one-hot编码实际上已经是译码后的状态,减少综合后的组合逻辑,提高电路的运行效率。由于不涉及外部传参,故采用localparam进行编码定义。
采用两级寄存器对按键输入信号进行寄存,在20ns的时钟速度下,足以捕捉到按键按下的瞬间前后的电平状态,再用一级组合电路对边沿进行判断即可。
逻辑设计
module key_filter
(
input wire clk ,
input wire rst_n ,
input wire key_in ,
output reg key_flag ,
output reg key_state
);
//状态机参数定义
localparam IDLE = 4'b0001;
localparam FILTER0 = 4'b0010;
localparam DOWN = 4'b0100;
localparam FILTER1 = 4'b1000;
reg [3:0] state;
//20ms受控计数器参数定义
reg cnt_en;
reg [19:0] cnt;
//计数满20ms信号定义
reg cnt_full;
//边沿捕获参数定义
wire nedge;
wire pedge;
reg key_reg0;
reg key_reg1;
//两级寄存器寄存key_in状态
always@(posedge clk or negedge rst_n)
if(!rst_n)
key_reg0 <= 1'b0;
else
key_reg0 <= key_in;
always@(posedge clk or negedge rst_n)
if(!rst_n)
key_reg1 <= 1'b0;
else
key_reg1 <= key_reg0;
//边沿判断
assign nedge = (key_reg1) & (!key_reg0);
assign pedge = (!key_reg1) & (key_reg0);
//20ms受控计数器
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 20'd0;
else if(cnt_en)begin
if(cnt == 20'd999_999)
cnt <= 20'd0;
else
cnt <= cnt + 1'b1;
end
else
cnt <= 20'd0;
//cnt_full信号赋值
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;
//状态机部分
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
state <= IDLE;
cnt_en <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
IDLE : begin
key_flag <= 1'b0;
if(nedge)begin
state <= FILTER0;
cnt_en = 1;
end
else
state <= IDLE;
end
FILTER0 : begin
if(cnt_full)begin
state <= DOWN;
cnt_en = 1'b0;
key_flag = 1;
key_state = 0;
end
else if(pedge) begin
state <= IDLE;
cnt_en <= 1'b0;
end
else
state <= FILTER0;
end
DOWN : begin
key_flag <= 1'b0;
if(pedge)begin
state <= FILTER1;
cnt_en = 1;
end
else
state <= DOWN;
end
FILTER1 : begin
if(cnt_full) begin
state <= IDLE;
cnt_en <= 1'b0;
key_flag = 1;
key_state = 1;
end
else if(nedge)begin
state <= DOWN;
cnt_en = 1'b0;
end
else
state <= FILTER1;
end
default : begin
state <= IDLE;
cnt_en <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
endmodule
tb
这里使用了一个仿真模型,如果直接在tb中写代码进行按键行为的模拟,代码非常臃肿。写一个单独的的“模块”模拟几次按键行为,也方便以后调用。
`timescale 1ns/1ns
module key_model
(
output reg key
);
reg [15:0] rand_value;
task press_key; begin
repeat(50) begin
rand_value = {$random} % 65536;
#rand_value;
key = !key;
end
key = 0;
#50_000_000;
repeat(50) begin
rand_value = {$random} % 65536;
#rand_value;
key = !key;
end
key = 1;
#50_000_000;
end
endtask
initial begin
key = 1;
press_key;
#200;
press_key;
#200;
press_key;
#200;
$stop;
end
endmodule
这里涉及到task语法和系统函数random的使用。
task类似函数打包调用,不赘述。
random函数实际上生成一个范围内的随机数,适合用来模拟抖动每个高低电平的持续时间。贴出官方文档截图。
modelsim仿真
填坑:设计到仿真模型时要注意simulation的设置,在原有的tb选项中添加key_model文件,而不是创建一个key_model为仿真文件的test bench。
可以看到经历一串抖动之后,分别合理地产生了key_flag信号和key_state信号。利用好这两个信号便从功能上滤除了机械按键地抖动。
二、总结
又一次熟悉了状态机的操作。
关于状态机的状态定义是使用格雷码还是独热码,更多理解可以看另一位博主的文章,下面给出链接。