生活中常用的按键为机械按键,而机械按键在按下后就会产生按键抖动
按键抖动:按键抖动通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动。当按下一次按键,可能在A点检测到一次低电平,在B点检测到一次高电平,在C点又检测到一次低电平。同时抖动是随机,不可测的。那么按下一次按键,抖动可能会误以为按下多次按键。
一般我们认为抖动存在时间保持在5-10ms左右,而在5-10ms后才会保持稳定,
因此我们很容易意识到可以通过延时采样稳定的按键信号进行消抖。而本次设计中博主在消抖后输出的是一个脉冲信号。
本文博主提出两种按键消抖的解决方案:
第一种方案为博主自行编写,代码略显繁琐。第二种方案为博主老师的方法,较容易理解(但博主认为从第一次下降沿开始便检测有些许不太严谨),大家可以依据自己的喜好进行选择。
由原理图可以看出,博主的开发板按键按下后低电平有效,博主接下来提供的代码请各位根据自己实际的开发板进行适当的修改
源码分析:
在方案一中,博主设计了key_r0、key_r1、key_r2三个中间寄存器,分别用于同步按键信号key_in并进行打两拍。大家可以认为同步打拍就是在时序电路中,分别将前一个信号的值寄存下来,第一次为同步,后续即为打拍(这里打一拍还是打两拍取决于各位,甚至只同步不打拍也可以进行下降沿检测)。
由于是在时序电路中进行非阻塞赋值,因此三个中间信号分别相差了一个时钟周期,而博主就是利用后两个信号进行下降沿检测,具体如图:
由波形图可以看出,我们可以通过key_r1取反并逻辑与上key_r2的运算,检测出下降沿就,即:assign nedge = ~key_r1 && key_r2
在本方案中,博主设计的20ms计数器在不断的计数,但是通过前面的分析我们得知,要想按键消抖,应该在检测到下降沿的时候才开始计数,因此博主在计时器中添加了检测nedge的条件,一旦检测到nedge就会自动将进行计数器清零,以此确保计数器检测到下降沿后是从0开始计数。
但大家又会想到,按键按下的时间如果远远大于20ms,计数器就会不止一次地计数到20ms,而我们也将采样多次,前文博主说道要求输出脉冲信号,因此在按键按下的低电平持续时间内会产生多个脉冲信号,并不符合要求。对此博主的解决方案是:在计数器计数到最大值-1(代码中是MAX - 2 因为计数是从0开始)时便输出脉冲信号,而一旦计数器计数到最大值(MAX - 1)便将cnt保持在最大值,从而避免产生多个脉冲。(最大值-1与最大值只相差一个时钟周期20ns,因此影响可以忽略不计)。
同时注意在一切条件都满足后,输出的key_out的值不应该置1。如果只是单纯置1,假设抖动后按键仍保持在高电平,或者松开按键时产生的释放抖动也会输出一个脉冲信号,很明显在这两种情况下是不成立的,因此key_out的值应该赋为key_in(或者三个中间信号均可)并取反(按键低电平有效需要取反),并且在一个时钟周期后置0(初始值为0)才是输出脉冲信号。
本方案每当检测到下降沿便会重新开始20ms计数
module key_filter(
input wire clk ,
input wire rst_n ,
input wire key_in ,//按键输入信号
output reg key_out //输出稳定的脉冲信号
);
parameter MAX = 20'd1_000_000;
reg [19:0] cnt_delay ; //20ms延时计数寄存器
wire add_cnt_delay ; //开始计数的标志
wire end_cnt_delay ; //结束计数的标志
reg key_r0 ; //同步
reg key_r1 ; //打一拍
reg key_r2 ; //打两拍
wire nedge ; //下降沿寄存器
//同步打拍
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
key_r0 <= 1'b1;
key_r1 <= 1'b1;
key_r2 <= 1'b1;
end
else begin
key_r0 <= key_in; //同步
key_r1 <= key_r0; //寄存一拍
key_r2 <= key_r1; //寄存两拍
end
end
//20ms计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_delay <= 1'b0;
end
else if(add_cnt_delay )begin
if(nedge)begin //检测到下降沿从0开始计数
cnt_delay <= 1'b0;
end
else if(cnt_delay == MAX - 1'b1)begin
cnt_delay <= cnt_delay; //计数计满结束后保持,避免产生多个输出脉冲
end
else begin
cnt_delay <= cnt_delay + 1'b1;
end
end
else begin
cnt_delay <= 1'b0;
end
end
assign nedge = ~key_r1 && key_r2; //下降沿检测
assign add_cnt_delay = 1'b1;
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX - 1'b1;
//key_out脉冲信号赋值
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_out <= 1'b0;
end
else if(cnt_delay == MAX - 2'd2)begin //计数到最大值减1时产生按键脉冲
key_out <= ~key_in;//不可直接置为1!!!!!!!!!
end
else begin
key_out <= 1'b0;
end
end
endmodule
源码分析:
/**************************************功能介绍***********************************
Date : 2023-07-26 14:43:33
Author : majiko
Version : 1.0
Description: 按键消抖模块(1位按键)
*********************************************************************************/
//---------<模块及端口声名>------------------------------------------------------
module key_filter(
input clk ,
input rst_n ,
input key_in ,
output reg key_down //输出脉冲信号(按键按下1次)
);
//---------<参数定义>---------------------------------------------------------
parameter TIME_20MS = 1000_000;//20ms
//---------<内部信号定义>-----------------------------------------------------
reg key_r0 ;//同步
reg key_r1 ;//打两拍
reg key_r2 ;
wire n_edge ;//下降沿
reg flag ;//计数器计数的标志信号(按键按下抖动标志)
reg [19:0] cnt_20ms ;//20ms计数器
wire add_cnt_20ms ;
wire end_cnt_20ms ;
//****************************************************************
//--同步打拍
//****************************************************************
//同步,将key_in信号,同步到clk时钟域下面
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_r0 <= 1'b1;
end
else begin
key_r0 <= key_in;
end
end
//打两拍
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_r1 <= 1'b1;
key_r2 <= 1'b1;
end
else begin
key_r1 <= key_r0;
key_r2 <= key_r1;
end
end
//****************************************************************
//--n_edge
//****************************************************************
assign n_edge = ~key_r1 & key_r2;//下降沿检测
// assign p_edge = key_r1 & ~key_r2;//上升沿检测
//****************************************************************
//--cnt_20ms
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_20ms <= 'd0;
end
else if(add_cnt_20ms)begin
if(end_cnt_20ms)begin
cnt_20ms <= 'd0;
end
else begin
cnt_20ms <= cnt_20ms + 1'b1;
end
end
end
assign add_cnt_20ms = flag;
assign end_cnt_20ms = add_cnt_20ms && cnt_20ms == TIME_20MS - 1;
//****************************************************************
//--flag
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
flag <= 'd0;
end
else if(n_edge)begin
flag <= 1'b1;
end
else if(end_cnt_20ms)begin
flag <= 1'b0;
end
end
//****************************************************************
//--key_down
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_down <= 1'd0;
end
else if(end_cnt_20ms)begin
key_down <= ~key_in;
end
else begin
key_down <= 1'b0;
end
end
endmodule
本次仿真较为随意,只是为了展示逻辑功能
源码:
`timescale 1ns/1ns
module tb_key_filter();
reg clk ;
reg rst_n ;
reg key_in ;
wire key_out;
parameter CYCLE = 20;
always #(CYCLE / 2) clk = ~clk;
initial begin
clk = 1'b0;
rst_n = 1'b1;
key_in = 1'b1;
#20
rst_n = 1'b0;
#20
rst_n = 1'b1;
#60
key_in = 1'b0;
#(30*CYCLE);
$stop;
end
key_filter#(.MAX(10)) u_key_filter(
.clk (clk ) ,
.rst_n (rst_n ) ,
.key_in (key_in ) ,
.key_out(key_out)
);
endmodule
可以看出,成功输出了一次脉冲信号key_out
按键消抖模块:
module key_filter(
input wire clk ,
input wire rst_n ,
input wire [3:0] key_in ,//按键输入信号
output reg [3:0] key_out //输出稳定的脉冲信号
);
parameter MAX = 20'd1_000_000;
reg [19:0] cnt_delay ; //20ms延时计数寄存器
wire add_cnt_delay ; //开始计数的标志
wire end_cnt_delay ; //结束计数的标志
reg [3:0] key_r0 ; //同步
reg [3:0] key_r1 ; //打一拍
reg [3:0] key_r2 ; //打两拍
wire nedge ; //下降沿寄存器
//同步打拍
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
key_r0 <= 4'b1111;
key_r1 <= 4'b1111;
key_r2 <= 4'b1111;
end
else begin
key_r0 <= key_in; //同步
key_r1 <= key_r0; //寄存一拍
key_r2 <= key_r1; //寄存两拍
end
end
//20ms计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_delay <= 1'b0;
end
else if(add_cnt_delay )begin
if(nedge)begin //检测到下降沿从0开始计数
cnt_delay <= 1'b0;
end
else if(cnt_delay == MAX - 1'b1)begin
cnt_delay <= cnt_delay; //计数计满结束后保持,避免产生多个输出脉冲
end
else begin
cnt_delay <= cnt_delay + 1'b1;
end
end
else begin
cnt_delay <= 1'b0;
end
end
assign nedge = (~key_r1[0] && key_r2[0]) || (~key_r1[1] && key_r2[1]) || (~key_r1[2] && key_r2[2]) || (~key_r1[3] && key_r2[3]); //下降沿检测
assign add_cnt_delay = 1'b1;
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX - 1'b1;
//key_out脉冲信号赋值
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_out <= 4'b0000;
end
else if(cnt_delay == MAX - 2'd2)begin //计数到最大值减1时产生按键脉冲
key_out <= ~key_in;
end
else begin
key_out <= 4'b0000;
end
end
endmodule
LED控制模块:
module led (
input wire clk ,
input wire rst_n ,
input wire [3:0] key_in ,
output reg [3:0] led
);
parameter MAX = 15_000_000 ;
reg [25:0] cnt ;
reg [3:0] flag;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
cnt <= 1'b0;
end
else if(cnt == MAX - 1'b1)begin
cnt <= 1'b0;
end
else begin
cnt <= cnt + 1'b1;
end
end
reg [2:0] state ;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
state <= 1'b0;
end
else if(cnt == MAX - 1'b1)begin
state <= state + 1'b1;
end
else begin
state <= state;
end
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
flag <= 4'b0000;
end
else if(key_in[0] == 1'b1)begin
flag <= 4'b0001;
end
else if(key_in[1] == 1'b1)begin
flag <= 4'b0010;
end
else if(key_in[2] == 1'b1)begin
flag <= 4'b0100;
end
else if(key_in[3] == 1'b1)begin
flag <= 4'b1000;
end
else begin
flag <= flag;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
led <= 4'b0001;
end
else if(flag == 4'b0001)begin
case(state)
3'd0 : led <= 4'b0000;
3'd1 : led <= 4'b0001;
3'd2 : led <= 4'b0011;
3'd3 : led <= 4'b0111;
3'd4 : led <= 4'b1111;
3'd5 : led <= 4'b0111;
3'd6 : led <= 4'b0011;
3'd7 : led <= 4'b0001;
default:;
endcase
end
else if(flag == 4'b0010 && cnt == MAX - 1'b1)begin
led <= {led[2:0],led[3]};
end
else if(flag == 4'b0100 && cnt == MAX - 1'b1)begin
led <= {led[2:0],~led[3]};
end
else if(flag == 4'b1000 && cnt == MAX - 1'b1)begin
led <= 4'b1111;
end
end
endmodule
顶层模块:
module top_key_led(
input wire clk ,
input wire rst_n ,
input wire [3:0] key_in ,
output wire [3:0] led
);
wire [3:0] key_out;
key_filter u_key_filter(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_in),
.key_out (key_out)
);
led u_led(
.clk (clk ),
.rst_n (rst_n ),
.key_in (key_out),
.led (led )
);
endmodule
注意:led模块有一点问题,在key0跑马灯开始后再按下key1,key2时,呈现效果也许不是流水灯,因为跑马灯开始后led状态不再是4’b0001
代码分析:
博主一共设计了四种状态:
由时序图我们可以分析得出:
module fsm_key#(parameter WIDTH = 4) (
input wire clk ,
input wire rst_n ,
input wire [WIDTH - 1:0] key_in ,//按键信号输入
output reg [WIDTH - 1:0] key_out //消抖后稳定的脉冲信号输出
);
//参数定义
parameter IDLE = 4'b0001;//空闲状态
parameter FILTER_DOWN = 4'b0010;//按键按下抖动状态
parameter HOLD_DOWN = 4'b0100;//按键稳定状态
parameter FILTER_UP = 4'b1000;//按键释放抖动状态
parameter MAX = 20'd1_000_000 ;//延时20ms采样
//内部信号定义
reg [19:0] cnt_20ms ;//20ms计数寄存器
reg [WIDTH - 1:0] key_r0 ;//同步
reg [WIDTH - 1:0] key_r1 ;//打一拍
reg [WIDTH - 1:0] key_r2 ;//打两拍
reg [3:0] cstate ;//现态寄存器
reg [3:0] nstate ;//次态寄存器
//状态转移条件定义
wire idle2down ;
wire down2hold ;
wire hold2up ;
wire up2idle ;
wire add_cnt_20ms;//开始计时的标志
wire end_cnt_20ms;//结束计时的标志
wire [WIDTH - 1:0] nedge ;//下降沿寄存器
wire [WIDTH - 1:0] pedge ;//上升沿寄存器
//同步打拍
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
key_r0 <= {WIDTH{1'b1}};
key_r1 <= {WIDTH{1'b1}};
key_r2 <= {WIDTH{1'b1}};
end
else begin
key_r0 <= key_in;
key_r1 <= key_r0;
key_r2 <= key_r1;
end
end
//20ms计数器
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_20ms <= 1'b0;
end
else if(add_cnt_20ms)begin
if(end_cnt_20ms)begin
cnt_20ms <= 1'b0;
end
else begin
cnt_20ms <= cnt_20ms + 1'b1;
end
end
else begin
cnt_20ms <= 1'b0;
end
end
assign add_cnt_20ms = cstate == FILTER_DOWN || cstate == FILTER_UP;
assign end_cnt_20ms = add_cnt_20ms && cnt_20ms == MAX - 1'b1;
//下降沿 上升沿检测
assign nedge = ~key_r1 & key_r2;
assign pedge = key_r1 & ~key_r2;
//三段式状态机
//第一段,时序逻辑,描述状态转移
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cstate <= IDLE;
end
else begin
cstate <= nstate;
end
end
//第二段,组合逻辑,状态转移规律及状态转移条件
always @(*)begin
case (cstate)
IDLE : begin
if(idle2down)begin
nstate <= FILTER_DOWN;
end
else begin
nstate <= cstate;
end
end
FILTER_DOWN : begin
if(down2hold)begin
nstate <= HOLD_DOWN;
end
else begin
nstate <= cstate;
end
end
HOLD_DOWN : begin
if(hold2up)begin
nstate <= FILTER_UP;
end
else begin
nstate <= cstate;
end
end
FILTER_UP : begin
if(up2idle)begin
nstate <= IDLE;
end
else begin
nstate <= cstate;
end
end
default : nstate <= IDLE;
endcase
end
assign idle2down = cstate == IDLE && nedge ;
assign down2hold = cstate == FILTER_DOWN && end_cnt_20ms ;
assign hold2up = cstate == HOLD_DOWN && pedge ;
assign up2idle = cstate == FILTER_UP && end_cnt_20ms ;
//第三段 时序逻辑或组合逻辑 描述输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_out <= {WIDTH{1'b0}};
end
else if(down2hold)begin
key_out <= ~key_in;
end
else begin
key_out <= {WIDTH{1'b0}};
end
end
endmodule
六、参考资料
https://blog.csdn.net/weixin_43828944/article/details/122360794