文章目录
- 一、按键消抖简介
- 1.1、为什么要按键消抖
- 二、C4开发板原理图
- 三、按键消抖源码
- 3.1、方案一(每当检测到下降沿便开始重新计数)
- 3.2、方案二(检测到第一次下降沿后便开始计数)
- 四、仿真代码及仿真波形图
- 五、拓展:按键消抖版按键控制LED状态
一、按键消抖简介
1.1、为什么要按键消抖
生活中常用的按键为机械按键,而机械按键在按下后就会产生按键抖动
按键抖动:按键抖动通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动。当按下一次按键,可能在A点检测到一次低电平,在B点检测到一次高电平,在C点又检测到一次低电平。同时抖动是随机,不可测的。那么按下一次按键,抖动可能会误以为按下多次按键。
一般我们认为抖动存在时间保持在5-10ms左右,而在5-10ms后才会保持稳定,
因此我们可以很容易意识到可以通过延时采样稳定的按键信号进行消抖。而本次设计中博主在消抖后输出的是一个脉冲信号。
本文博主提出两种按键消抖的解决方案:
- 第一种:每当检测到下降沿便开始重复进行20ms计数
- 第二种:检测到第一次下降沿后,进行一次并只进行这一次20ms计数
第一种方案为博主自行编写,代码略显繁琐‘第二种方案为博主老师的方法,较容易理解(但博主认为从第一次下降沿开始便检测有些许不太严谨),大家可以依据自己的喜好进行选择。
二、C4开发板原理图
由原理图可以看出,博主的开发板按键按下后低电平有效,博主接下来提供的代码请各位根据自己实际的开发板进行适当的修改
三、按键消抖源码
3.1、方案一(每当检测到下降沿便开始重新计数)
源码分析:
-
在方案一中,博主设计了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
3.2、方案二(检测到第一次下降沿后便开始计数)
源码分析:
- 原理与第一种方案类似,但是此方案引入了中间变量flag,初始值为0,检测到下降沿后便将flag置为1,此时再将flag作为开始计数的条件,同时计满20ms后再将flag置为0,也就省去了在最大值-1时赋值key_out并保持计数器最大值的繁琐。
- 此方法与方案一的区别就在于,方案一相当于是在抖动过程中最后一次下降沿才开始计数,而此方案是在抖动过程中的第一次下降沿便开始计数。
/**************************************功能介绍***********************************
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 <= 'd0;
end
else if(end_cnt_20ms)begin
key_down <= 1'b1;
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
五、拓展:按键消抖版按键控制LED状态
按键消抖模块:
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
六、参考资料
https://blog.csdn.net/weixin_43828944/article/details/122360794