###### 【该随笔中部分内容转载自小梅哥】 #########
独立按键消抖自古以来在单片机和FPGA中都是个不可避免的问题,首先,解释一下什么叫做按键抖动,如图,按键在按下和松开的那个瞬间存在大概20ms的机械抖动:
下面就是本篇的第一个重点 —— 什么时候需要按键消抖设计?如果是像复位按键这样,短时间内可以多次触发,就完全不需要设计消抖,但是如果是要设计按下按键使LED状态翻转,或者按下按键计数一次的话,就必须要设计消抖模块,否则就会带来不可预知的错误,因为在按下按键的那个时刻,可能已经触发了少则几次,多则几十次,可见按键消抖的必要性;
那么,既然按键消抖如此重要,如何来进行消抖呢?
1、硬件消抖 —— 0.1uF电容滤波
这个104的电容就是起高频滤波的作用,在按键不是很多的情况下,可以使用这种设计,但是如果要做项目,会增加大量成本,所以接下来我们讲述如何进行软件消抖;
2、软件消抖 —— delay
if(key_in == 0) { deley(2000); if(key_in == 0) { //按键按下,执行相应操作
} }
在单片机中用C语言,可以这样设计按键消抖,同样的思路,在FPGA中,我们依然可以采用这种思想,将按键的这20ms抖动“屏蔽”,但是FPGA没有delay(2000),该如何设计呢?
FPGA中控制延时可以采用计数器,因为工作时钟是已知的50M,所以要延时20_000_000ns(20ms),只需要对计数1_000_000个clk就可以,这样延时问题就解决了,按照之前的思路,设计如下:只需要在检测到key_in变为0,启动定时器,定时器时间到,再次检测key_in是否为0,若为0,表明按键按下稳定,关闭计数器并清零,然后等待按键释放,也就是key_in出现上升沿,再次启动定时器,时间到后检测key_in,若为1,则证明按键已释放,一次完整的按键过程结束。
大致思路有了,如何设计实现呢?貌似这是一个很复杂的设计,实则不然,FSM的本质就是对具有逻辑规律和时序逻辑的事物的描述,采用FSM设计,问题迎刃而解!
1、从状态变量入手,分析状态变量:
IDLE:按键空闲状态(由于上拉电阻的作用,按键未被按下时保持高电平);
FILTER_DOWN:按下滤波状态;
DOWN:按下稳定状态;
FILTER_UP:释放滤波状态;
2、分析状态转移条件,绘制状态转移图(visio)
3、照图施工,选用合适的描述方案
在描述的时候,有两个重要问题需要解决:
1)按键信号属于异步信号,在状态转移中需要对按键边沿敏感,所以首先采用一级D触发器将key_in与clk同步,产生pedge和nedge信号,也就是边沿检测电路,代码如下:
//边沿检测电路
always@(posedge clk) key_temp <= key_in; //暂存上一个clk按键状态
assign key_nedge = (key_temp)&&(!key_in); //下降沿检测
assign key_pedge = (!key_temp)&&(key_in); //上升沿检测
2)当20ms延时完毕后,应该输出一个脉冲,通知其它模块检测key_flag引脚电平;
完整的verilog描述代码如下:
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module Name: key_filter // Description: //独立按键消抖模块 ////////////////////////////////////////////////////////////////////////////////// module key_filter( input clk, //50M时钟信号 input rst, //低电平复位 input key_in, //按键输入 output reg key_flag, //消抖完毕输出脉冲 output reg key_state //按键状态输出 ); reg [3:0]NS; //nextstate reg key_temp; wire key_pedge; wire key_nedge; reg en_cnt; reg [19:0]cnt; //需要计数次数1_000_000 //边沿检测电路 always@(posedge clk) key_temp <= key_in; //暂存上一个clk按键状态 assign key_nedge = (key_temp)&&(!key_in); //下降沿检测 assign key_pedge = (!key_temp)&&(key_in); //上升沿检测 //带使能端计数器,用于20ms延时 always@(posedge clk,negedge rst) if(!rst) cnt <= 0; else if(en_cnt) cnt <= cnt + 1'b1; else cnt <= 0; //状态one-hot编码 localparam IDLE = 4'b0001, //空闲状态 FILTER_DOWN = 4'b0010, //按下消抖状态 DOWN = 4'b0100, //按下稳定状态 FILTER_UP = 4'b1000; //释放消抖状态 //一段式状态机 always@(posedge clk,negedge rst) if(!rst)begin NS <= IDLE; en_cnt <= 0; key_flag <= 0; key_state <= 1; end else case(NS) IDLE: begin key_flag <= 0; key_state <= 1; if(key_nedge)begin NS <= FILTER_DOWN; en_cnt <= 1'b1; //使能计数器 end else NS <= IDLE; end FILTER_DOWN: if(cnt >= 20'd999_999)begin en_cnt <= 0; //20ms时间到,失能计数器,进入稳定状态 key_flag <= 1'b1; //key_flag输出一个clk高脉冲 NS <= DOWN; end else if(key_pedge)begin en_cnt <= 0; //20ms时间内发生上升沿,失能计数器,保持空闲状态 NS <= IDLE; end DOWN: begin key_flag <= 0; key_state <= 0; if(key_pedge)begin NS <= FILTER_UP; en_cnt <= 1'b1; //使能计数器 end else NS <= DOWN; end FILTER_UP: if(cnt >= 20'd999_999)begin en_cnt <= 0; NS <= IDLE; //20ms时间到,失能计数器,进入稳定状态 key_flag <= 1; end else if(key_nedge)begin en_cnt <= 0; //20ms时间内发生上升沿,失能计数器,保持按下稳定状态 NS <= DOWN; end default: NS <= IDLE; endcase endmodule
4、编写tsetbench进行仿真测试(查看所生成的波形、状态转移图、RTL视图)
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module Name: key_filter_tb // Description: ////////////////////////////////////////////////////////////////////////////////// `define clk_period 20 //100M系统时钟 module key_filter_tb(); reg clk; //50M时钟信号 reg rst; //低电平复位 reg key_in; //按键输入 wire key_flag; //消抖完毕输出脉冲 wire key_state; //按键状态输出 //例化测试模块 key_filter key_filter_test( .clk(clk), //50M时钟信号 .rst(rst), //低电平复位 .key_in(key_in), //按键输入 .key_flag(key_flag), //消抖完毕输出脉冲 .key_state(key_state) //按键状态输出 ); //产生100M时钟信号 initial clk = 1; always #(`clk_period / 2) clk <= ~clk; //开始测试 initial begin rst <= 0; //系统复位 key_in <= 1; //按键处于空闲状态 #(`clk_period * 2); rst <= 1; #10_000_000; //延时10ms,方便观察按键按下现象 //开始模拟按键按下抖动 key_in <= 0; #1000; key_in <= 1; #2000; key_in <= 0; #1400; key_in <= 1; #2600; key_in <= 0; #1300; key_in <= 1; #200; //产生一个稳定的低电平大于20ms,代表按键稳定 key_in <= 0; #30_000_000; //模拟释放抖动 key_in <= 1; #2000; key_in <= 0; #1000; key_in <= 1; #2600; key_in <= 0; #1400; key_in <= 1; #200; key_in <= 0; #1300; //产生一个稳定的高电平大于20ms,代表释放稳定 key_in <= 1; #30_000_000; $stop; end endmodule
测试结果如下:
对于testbench,这个文件写的很繁琐,可以进行一下优化,首先
1、利用$random函数产生随机延时值模拟抖动,用法如下:
reg [15:0]randnum; randnum = $random % 50; //产生一个-49~49内的随机数
randnum = {$random} % 50; //产生一个0 ~ 50内的随机数
2、利用task/endtask将重复代码进行封装,用法如下:
task <任务名>; <语句1>
<语句2>
<语句3> .... endtask
优化后的testbench测试文件如下:
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module Name: key_filter_tb // Description: ////////////////////////////////////////////////////////////////////////////////// `define clk_period 20 //100M系统时钟 module key_filter_tb(); reg clk; //50M时钟信号 reg rst; //低电平复位 reg key_in; //按键输入 wire key_flag; //消抖完毕输出脉冲 wire key_state; //按键状态输出 reg [15:0]rand_time; //按键抖动时随机时长 //例化测试模块 key_filter key_filter_test( .clk(clk), //50M时钟信号 .rst(rst), //低电平复位 .key_in(key_in), //按键输入 .key_flag(key_flag), //消抖完毕输出脉冲 .key_state(key_state) //按键状态输出 ); //产生100M时钟信号 initial clk = 1; always #(`clk_period / 2) clk <= ~clk; //开始测试 initial begin rst <= 0; //系统复位 key_in <= 1; //按键处于空闲状态 #(`clk_period * 2); rst <= 1; #10_000_000; //延时10ms,方便观察按键按下现象 press_key; #10000; //第一次按下按键 press_key; #10000; //第二次按下按键 press_key; #10000; //第三次按下按键 $stop; end task press_key; begin //开始模拟按键按下抖动 repeat(50)begin rand_time = {$random} % 65536; #rand_time key_in = ~key_in; end //产生一个稳定的低电平大于20ms,代表按键稳定 key_in = 0; #30_000_000; //模拟释放抖动 repeat(50)begin rand_time = {$random} % 65536; #rand_time key_in = ~key_in; end //产生一个稳定的高电平大于20ms,代表释放稳定 key_in = 1; #30_000_000; end endtask endmodule
测试结果如下(总共按键三次),可以看到,优化后的testbench比之前的测试更加精准,更加真实的模拟现实情况:
这里,因为这是一个按键模型,对外提供的功能就是实际中按键的作用,这也就是仿真模型,所以可以独立写一个testbench作为按键模型,方便例化调用:
按键模型的仿真代码如下:
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module Name: key_module // Description: 仿真按键模型 ////////////////////////////////////////////////////////////////////////////////// module key_module( output reg key //对外输出按键信号 ); reg [15:0]rand_time; //按键抖动时随机时长 initial begin key <= 1; //按键处于空闲状态 #10_000_000; //延时10ms,方便观察按键按下现象 press_key; #10000; //第一次按下按键 press_key; #10000; //第二次按下按键 press_key; #10000; //第三次按下按键 $stop; end task press_key; begin //开始模拟按键按下抖动 repeat(50)begin rand_time = {$random} % 65536; #rand_time key = ~key; end //产生一个稳定的低电平大于20ms,代表按键稳定 key = 0; #30_000_000; //模拟释放抖动 repeat(50)begin rand_time = {$random} % 65536; #rand_time key = ~key; end //产生一个稳定的高电平大于20ms,代表释放稳定 key = 1; #30_000_000; end endtask endmodule
这样一来,在testebench中测试就可以直接调用该仿真模型,优化到最后的testbench如下:
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Module Name: key_filter_tb // Description: 基于按键模型进行测试 ////////////////////////////////////////////////////////////////////////////////// `define clk_period 20 //100M系统时钟 module key_filter_tb(); reg clk; //50M时钟信号 reg rst; //低电平复位 wire key_in; //按键输入 wire key_flag; //消抖完毕输出脉冲 wire key_state; //按键状态输出 //例化测试模块 key_filter key_filter_test( .clk(clk), //50M时钟信号 .rst(rst), //低电平复位 .key_in(key_in), //按键输入 .key_flag(key_flag), //消抖完毕输出脉冲 .key_state(key_state) //按键状态输出 ); //例化按键模型 key_module key1( .key(key_in) //对外输出按键信号 ); //产生100M时钟信号 initial clk = 1; always #(`clk_period / 2) clk <= ~clk; //开始测试 initial begin rst = 0; //系统复位 #(`clk_period * 2); rst = 1; end endmodule
测试结果和之前完全一样,但testcench更加简洁,按键仿真也更加方便以后调用,至此,按键消抖模块就设计测试完毕,如有兴趣,可以进行进一步的设计,控制led或数码管计数。