消抖原因
按键分为两种:
-
无需消抖:如复位按键,短时间内可以多次触发;
-
需要消抖:如计数按键,在按下按键的一瞬间,可能已经触发了许多次。
消抖方法
硬件消抖
0.1uF电容滤波:
- 104的电容起高频滤波的作用。在按键不多,要求不高时,可以使用。
缺点:增加成本,电路复杂。不如软件消抖好
软件消抖
delay函数
if(key_in == 0)
{
deley(2000); //延时20ms
if(key_in == 0)
{
//按键按下,执行相应操作
}
}
以上,是C中常用的消抖方式。FPGA中可采用计数器延时:sys_clk = 50MHz T=20ns 故计数1_000_000个clk即可,采用FSM设计。
- FSM( finite state machine)是对具有逻辑规律和时序逻辑的事物的描述。设计如下:
1、分析状态变量
IDLE:按键空闲状态(由于上拉电阻的作用,按键未被按下时保持高电平);
FILTER_DOWN:按下滤波状态;
DOWN:按下稳定状态;
FILTER_UP:释放滤波状态;
2、分析状态转移条件,绘制状态转移图
3、实现方案
my_key_filter
module my_key_filter(clk,rst_n,key_in,key_flag,key_state);
input clk,rst_n,key_in;
output reg key_flag; //消抖完毕输出脉冲
output reg key_state; //按键状态输出
//状态one-hot编码
localparam
IDEL = 4'b0001, //空闲状态
FILTER_DOWN = 4'b0010, //按下消抖状态
DOWN = 4'b0100, //按下稳定状态
FILTER_UP = 4'b1000; //释放消抖状态
reg [3:0] state; //nextstate
reg key_tmp0,key_tmp1;
reg [19:0] counter; //需要计数次数1_000_000
reg en_counter; //enble counter
reg counter_full; //20ms flag
wire pedge,nedge; //assign 左值必须是一个标量或向量线网
//边沿检测电路
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;
key_tmp1 <= key_tmp0;
end
assign nedge = !key_tmp0 & key_tmp1; //assign语句瞬时发生(忽略门延时)
assign pedge = key_tmp0 & (!key_tmp1);
// 补充一个边沿检测电路(详见博客链接)
// always@(posedge clk)
// key_temp <= key_in; //暂存上一个clk按键状态
// assign key_nedge = (key_temp)&&(!key_in); //下降沿检测
// assign key_pedge = (!key_temp)&&(key_in); //上升沿检测
//
always @ (posedge clk or negedge rst_n)
if (!rst_n) begin
state <= IDEL; //绌洪棽
en_counter <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
IDEL:
begin
key_flag <= 1'b0;
key_state <= 1'b1;
if(nedge)begin
state <= FILTER_DOWN;
en_counter <= 1'b1;
end
else
state <= IDEL;
end
FILTER_DOWN:
if(counter_full)begin
state <= DOWN;
en_counter <= 1'b0;
key_flag <= 1'b1;
key_state <= 1'b0;
end
else if(pedge)begin
state <= IDEL;
en_counter <= 1'b0;
end
else
state <= FILTER_DOWN;
DOWN:
begin
key_flag <= 1'b0;
if(pedge) begin
state <= FILTER_UP;
en_counter <= 1'b1;
end
else
state <= DOWN;
end
FILTER_UP:
if(counter_full)begin
state <= IDEL;
//en_counter <= 1'b0; // IDEL清零了
key_flag <= 1'b1;
key_state <= 1'b1;
end
else if(nedge)begin
state <= DOWN;
en_counter <= 1'b0;
end
else
state <= FILTER_UP;
default:
begin
state <= IDEL;
en_counter <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
//带使能端计数器 delay 20ms
always @ (posedge clk or negedge rst_n)
if(!rst_n)
counter <= 20'd0;
else if(en_counter)
counter <= counter + 1'b1;
else
counter <= 20'd0;
always @ (posedge clk or negedge rst_n)
if(!rst_n)
counter_full <= 1'b0;
else if(counter == 999_999)
counter_full <= 1'b1;
else
counter_full <= 1'b0;
endmodule
my_key_filter_tb
`timescale 1ns/1ns
`define clk_period 20
module my_key_filter_tb;
reg clk,rst_n,key_in; //对应输入
wire key_flag,key_state; //对应输出
my_key_filter my_key_filter0(
.clk (clk),
.rst_n (rst_n),
.key_in (key_in),
.key_flag (key_flag),
.key_state (key_state)
);
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);key_in = 1'b0;
//一次按下
#1000; key_in = 1'b1; //模拟抖动 20ms内
#2000; key_in = 1'b0;
#1000; key_in = 1'b1;
#2400; key_in = 1'b0;
#1060; key_in = 1'b1;
#2400; key_in = 1'b0;
#1700; key_in = 1'b1;
#2570; key_in = 1'b0;
#1040; key_in = 1'b1;
#2040; key_in = 1'b0;
#1000; key_in = 1'b1;
#2000; key_in = 1'b0;
#20_001_000 //低电平了
#1000_000 //保持
key_in = 1'b1;
#2400; key_in = 1'b0;
#1700; key_in = 1'b1;
#2570; key_in = 1'b0;
#1040; key_in = 1'b1;
#2040; key_in = 1'b0;
#1000; key_in = 1'b1; //释放
#20_001_000 //高电平了
#50_001_000;
//二次按下
#1000; key_in = 1'b1; //模拟抖动 20ms内
#2000; key_in = 1'b0;
#1000; key_in = 1'b1;
#2400; key_in = 1'b0;
#1060; key_in = 1'b1;
#2400; key_in = 1'b0;
#1700; key_in = 1'b1;
#2570; key_in = 1'b0;
#1040; key_in = 1'b1;
#2040; key_in = 1'b0;
#1000; key_in = 1'b1;
#2000; key_in = 1'b0;
#20_001_000 //低电平了
#1000_000 //保持
key_in = 1'b1;
#2400; key_in = 1'b0;
#1700; key_in = 1'b1;
#2570; key_in = 1'b0;
#1040; key_in = 1'b1;
#2040; key_in = 1'b0;
#1000; key_in = 1'b1; //释放
#20_001_000 //高电平了
#50_001_000;
//三次按下
#1000; key_in = 1'b1; //模拟抖动 20ms内
#2000; key_in = 1'b0;
#1000; key_in = 1'b1;
#2400; key_in = 1'b0;
#1060; key_in = 1'b1;
#2400; key_in = 1'b0;
#1700; key_in = 1'b1;
#2570; key_in = 1'b0;
#1040; key_in = 1'b1;
#2040; key_in = 1'b0;
#1000; key_in = 1'b1;
#2000; key_in = 1'b0;
#20_001_000 //低电平了
#1000_000 //保持
key_in = 1'b1;
#2400; key_in = 1'b0;
#1700; key_in = 1'b1;
#2570; key_in = 1'b0;
#1040; key_in = 1'b1;
#2040; key_in = 1'b0;
#1000; key_in = 1'b1; //释放
#20_001_000 //高电平了
#50_001_000;
$stop;
end
endmodule
这个文件写的很繁琐,可以进行一下优化,利用$random函数产生随机延时值模拟抖动,用task/endtask将重复代码进行封装。见参考博客。
状态图
可以看到,与我们描述的一致。