一、按键抖动原理
按键抖动原理:按键存在一个反作用弹簧,因此当按下或者松开时均会产生额外的物理抖动,物理抖动会产生电平的抖动。
消抖方法:一般情况下,抖动的总时间会持续20ms以内,按下按键后,等20ms过去了再取键值就行了。
市面上有多种按键消抖的方法,我对比了各家的代码,发现有两种方法非常好用,其原理略微的不同。同时将小梅哥FPGA中的task任务和仿真模型的概念一并记录下来。
二、第1种按键消抖
只对按下侧的抖动进行消除,弹起的就不管了,因为我们使用按键时要的也是按下后的键值。输出为1clk的按键值。
1 //====================================================================== 2 // --- 名称 : key_filter 3 // --- 作者 : xianyu_FPGA 4 // --- 日期 : 2018-11-02 5 // --- 描述 : 按键消抖,输出为1个clk的输入,只关注按下侧的消抖 6 //====================================================================== 7 8 module key_filter 9 //---------------------<参数定义>--------------------------------------- 10 #( 11 parameter TIME_20MS = 1000000 , //20ms时间 12 parameter TIME_W = 20 , //20ms时间位宽 13 parameter KEY_W = 4 //按键个数 14 ) 15 //---------------------<端口声明>--------------------------------------- 16 ( 17 input clk , //时钟,50Mhz 18 input rst_n , //复位,低电平有效 19 input [KEY_W-1:0] key , //按键输入 20 output reg [KEY_W-1:0] key_vld //按键消抖后的输出 21 ); 22 //---------------------<信号定义>--------------------------------------- 23 reg [TIME_W-1:0] cnt ; 24 wire add_cnt ; 25 wire end_cnt ; 26 reg [KEY_W -1:0] key_r0 ; 27 reg [KEY_W -1:0] key_r1 ; 28 reg flag ; 29 30 //---------------------------------------------------------------------- 31 //-- 信号同步 + 消除亚稳态 32 //---------------------------------------------------------------------- 33 always @(posedge clk or negedge rst_n)begin 34 if(!rst_n)begin 35 key_r0 <= 0; 36 key_r1 <= 0; 37 end 38 else begin 39 key_r0 <= key; //信号同步 40 key_r1 <= key_r0; //打拍,防亚稳态 41 end 42 end 43 44 //---------------------------------------------------------------------- 45 //-- 20ms计时 46 //---------------------------------------------------------------------- 47 always @(posedge clk or negedge rst_n)begin 48 if(!rst_n) 49 cnt <= 0; 50 else if(add_cnt)begin 51 if(end_cnt) 52 cnt <= 0; 53 else 54 cnt <= cnt + 1; 55 end 56 else 57 cnt <= cnt; 58 end 59 60 assign add_cnt = flag==0 && key_r1!=0 ; //允许计数 且 按键按下 61 assign end_cnt = add_cnt && cnt==TIME_20MS-1; //计到20ms 62 63 //计满指示 64 always @(posedge clk or negedge rst_n)begin 65 if(!rst_n) //复位 66 flag <= 0; //flag=0允许计数 67 else if(end_cnt) //20ms到 68 flag <= 1; //flag=1不再计数 69 else if(key_r1==0) //按键松开 70 flag <= 0; //flag=0,为下次计数做准备 71 else //否则 72 flag <= flag; //维持自身 73 end 74 75 //---------------------------------------------------------------------- 76 //-- 按键消抖完成,输出按键有效信号 77 //---------------------------------------------------------------------- 78 always @(posedge clk or negedge rst_n)begin 79 if(!rst_n) 80 key_vld <= 0; 81 else if(end_cnt) //20ms到 82 key_vld <= key_r1; //按键已消抖,可以使用 83 else 84 key_vld <= 0; 85 end 86 87 88 endmodule
现在编写仿真代码。由代码可以看到这里使用了task任务,用其定义一个完整的按下弹起的按键过程。
task的语法如下:
task <任务名>;<端口及数据类型声明语句> <语句1> <语句2> . . . <语句n> endtask
task调用的语法如下:
<任务名> (端口1, 端口2 ... 端口n);
在task任务中,模拟抖动时采用了随机数发生函数来产生抖动。$random这一系统函数可以产生一个有符号的32位随机整数。一般的用法是“$random%b”,其中b > 0。这样就会生成一个范围在 [-(b-1),b-1] 中的随机数。如果只得到正数的随机数,这可采用“{$random}%b”来产生,这样就会生成一个方位在 [0,b-1] 中的随机数。
1 `timescale 1ns/1ps //时间精度 2 `define Clock 20 //时钟周期 3 4 module key_filter_tb; 5 //---------------------<端口声明>--------------------------------------- 6 reg clk ; 7 reg rst_n ; 8 reg [3:0] key ; 9 wire [3:0] key_vld ; 10 11 //---------------------------------------------------------------------- 12 //-- 模块例化 13 //---------------------------------------------------------------------- 14 key_filter 15 #( //参数传递 16 .TIME_20MS (100 ) 17 ) 18 u_key_filter //模块例化 19 ( 20 .clk (clk ), 21 .rst_n (rst_n ), 22 .key (key ), 23 .key_vld (key_vld ) 24 ); 25 26 //---------------------------------------------------------------------- 27 //-- 时钟信号和复位信号 28 //---------------------------------------------------------------------- 29 initial begin 30 clk = 1; 31 forever 32 #(`Clock/2) clk = ~clk; 33 end 34 35 initial begin 36 rst_n = 0; #(`Clock*20+1); 37 rst_n = 1; 38 end 39 40 //---------------------------------------------------------------------- 41 //-- task函数编写,模拟按键抖动 42 //---------------------------------------------------------------------- 43 reg [15:0] rand ; 44 45 task press_key; 46 begin 47 repeat(50) begin //50次按下随机时间抖动 48 rand = {$random}%70; 49 #rand; 50 key = ~key; 51 end 52 key = 4'b1001; 53 #10000; 54 55 repeat(50) begin //50次释放随机时间抖动 56 rand = {$random}%70; 57 #rand; 58 key = ~key; 59 end 60 key = 0; 61 #10000; 62 end 63 endtask 64 65 //---------------------------------------------------------------------- 66 //-- 设计输入信号 67 //---------------------------------------------------------------------- 68 initial begin 69 #1; 70 key = 0 ; #(`Clock*20+1); //初始化完成 71 press_key; #10000; 72 press_key; #10000; 73 press_key; #10000; 74 $stop; 75 end 76 77 78 79 endmodule
Modelsim仿真波形如下所示:
可以看到按键抖动被非常完美的仿真出来了。但是消抖后的按键值好像有点问题,没有变化一直为0?并非如此!是因为此设计的key_vld只维持1个clk的按键值,我们放大看看,喏,出现了!
三、第2种按键消抖
按下和弹起的抖动都消除掉
1 //====================================================================== 2 // --- 名称 : key_filter 3 // --- 作者 : xianyu_FPGA 4 // --- 日期 : 2018-11-02 5 // --- 描述 : 按键消抖,输出为消抖后的输入,计数器一直在工作 6 //====================================================================== 7 8 module key_filter 9 //---------------------<参数定义>--------------------------------------- 10 #( 11 parameter TIME_20MS = 1000000 , //20ms时间 12 parameter TIME_W = 20 , //20ms时间位宽 13 parameter KEY_W = 4 //按键个数 14 ) 15 //---------------------<端口声明>--------------------------------------- 16 ( 17 input clk , //时钟,50Mhz 18 input rst_n , //复位,低电平有效 19 input [KEY_W-1:0] key , //按键输入 20 output reg [KEY_W-1:0] key_vld //消抖后的按键输出 21 ); 22 //---------------------<信号定义>--------------------------------------- 23 reg [TIME_W-1:0] cnt ; 24 wire add_cnt ; 25 wire end_cnt ; 26 reg [KEY_W -1:0] key_r0 ; 27 reg [KEY_W -1:0] key_r1 ; 28 reg [KEY_W -1:0] key_r2 ; 29 wire key_press ; 30 31 //---------------------------------------------------------------------- 32 //-- 边沿检测 33 //---------------------------------------------------------------------- 34 always @(posedge clk or negedge rst_n)begin 35 if(!rst_n)begin 36 key_r0 <= 0; 37 key_r1 <= 0; 38 key_r2 <= 0; 39 end 40 else begin 41 key_r0 <= key; //信号同步 42 key_r1 <= key_r0; //打拍,防亚稳态 43 key_r2 <= key_r1; 44 end 45 end 46 47 assign key_press = key_r1 ^ key_r2; //按键状态变化检测 48 49 //---------------------------------------------------------------------- 50 //-- 20ms计时 51 //---------------------------------------------------------------------- 52 always @(posedge clk or negedge rst_n)begin 53 if(!rst_n) 54 cnt <= 0; 55 else if(add_cnt)begin 56 if(end_cnt) 57 cnt <= 0; 58 else 59 cnt <= cnt + 1; 60 end 61 else 62 cnt <= cnt; 63 end 64 65 assign add_cnt = 1 ; //一直处于计数状态 66 assign end_cnt = key_press || (cnt== TIME_20MS-1); //按键仍在抖动或计到了20ms,则清0 67 68 //---------------------------------------------------------------------- 69 //-- 按键消抖完成,输出按键有效信号 70 //---------------------------------------------------------------------- 71 always @(posedge clk or negedge rst_n)begin 72 if(!rst_n) 73 key_vld <= 0; 74 else if(cnt==TIME_20MS-1) //cnt计到20ms 75 key_vld <= key_r2; //按键已消抖,可以使用 76 else 77 key_vld <= key_vld; 78 end 79 80 81 endmodule
现在编写仿真代码,和上面略微不同,我们使用一个新玩意:仿真模型。示意图如下,key_filter_tb是仿真文件,key_module模块是key_filter模块的仿真模型。
实际代码如下:
1 `timescale 1ns/1ps //时间精度 2 `define Clock 20 //时钟周期 3 4 module key_filter_tb; 5 //---------------------<端口声明>--------------------------------------- 6 reg clk ; 7 reg rst_n ; 8 wire [3:0] key ; //本是输入现在变成了内部信号,故改成wire型 9 wire [3:0] key_vld ; 10 11 //---------------------------------------------------------------------- 12 //-- 模块例化 13 //---------------------------------------------------------------------- 14 //按键消抖仿真模型 15 key_module u_key_module 16 ( 17 .key (key ) 18 ); 19 20 //按键消抖设计文件 21 key_filter 22 #( //参数传递 23 .TIME_20MS (100 ) 24 ) 25 u_key_filter //模块例化 26 ( 27 .clk (clk ), 28 .rst_n (rst_n ), 29 .key (key ), 30 .key_vld (key_vld ) 31 ); 32 33 //---------------------------------------------------------------------- 34 //-- 时钟信号和复位信号 35 //---------------------------------------------------------------------- 36 initial begin 37 clk = 1; 38 forever 39 #(`Clock/2) clk = ~clk; 40 end 41 42 initial begin 43 rst_n = 0; #(`Clock*20+1); 44 rst_n = 1; 45 end 46 47 48 endmodule
1 //====================================================================== 2 //--名称 : key_module 3 //--作者 : xianyu_FPGA 4 //--日期 : 2018-11-02 5 //--描述 : key按键消抖模块的仿真模型 6 //====================================================================== 7 `timescale 1ns/1ps 8 9 module key_module 10 //---------------------<端口声明>--------------------------------------- 11 ( 12 output reg [15:0] key 13 ); 14 15 //---------------------------------------------------------------------- 16 //-- task函数编写,模拟按键抖动 17 //---------------------------------------------------------------------- 18 reg [15:0] rand ; 19 20 task press_key; 21 begin 22 repeat(50) begin //50次按下随机时间抖动 23 rand = {$random}%70; 24 #rand; 25 key = ~key; 26 end 27 key = 4'b1001; 28 #10000; 29 30 repeat(50) begin //50次释放随机时间抖动 31 rand = {$random}%70; 32 #rand; 33 key = ~key; 34 end 35 key = 0; 36 #10000; 37 end 38 endtask 39 40 //---------------------------------------------------------------------- 41 //-- 设计输入信号 42 //---------------------------------------------------------------------- 43 initial begin 44 #1; 45 key = 0 ; #401; //初始化完成 46 press_key; #10000; 47 press_key; #10000; 48 press_key; #10000; 49 $stop; 50 end 51 52 53 endmodule
Modelsim仿真波形如下,输出波形刚好是消抖后的按键值,完美!
这两种按键消抖的本质都是一样的。第一种适用于只采1clk按键值的场景,例如按一下按键,计数器加1一次。第二种按键适用于各种场合,如果也只需要1clk的按键值,则在消抖后、使用前再用一次下降沿检测即可实现和第一种按键消抖一样的效果。
参考资料:
[1]小梅哥FPGA教程
[2]锆石科技FPGA教程