【说明】按键消抖常用于机械按键的抖动消除,由于我们平时用的按键(实验板上的按键)都是弹簧结构,在按下后弹簧会来回抖动几次,这样如果用按键信号作为时钟触发的话就会导致多次触发(可以试一下用按键信号作为一个计数器的时钟试一试),因此实际使用时是需要按键消抖控制的。
按键消抖的Verilog程序和Modelsim仿真:
【摘录:https://mp.weixin.qq.com/s/bsevwum0t86IQzb_4GS77Q, 原创:张俊涛,人工智能科学与技术】
在数字电路中,开关用于用于产生高、低电平,按键用于产生单次脉冲。由于开关和按键为机械部件,每次按下或者释放时,由于簧片的弹性会产生短暂的抖动,然后才能稳定接通或者断开。抖动现象会导致按键电路的输出产生毛刺,如图1所示,从而可能导致系统产生误动作。开关和按键的抖动时间一般在20ms以内。为了防止因按键抖动引起的系统误动作,必须对按键电路进行消抖,只在按键闭合或者断开稳定后才允许输出。
图1 按键抖动现象
按键消抖有软件消抖和硬件消抖两类方法。软件消抖是在嵌入式系统中,检测到按键按下时,应用软件延时20ms后再次检测按键的状态,如果两次状态相同,则确认按键已经按下。这种处理方式虽然简单,但是会浪费CPU资源。
硬件消抖有多种方法。第一种方法是应用施密特电路的回差特性配合积分电路实现按键消抖,应用电路如图2所示。
图2 应用积分电路实现按键消抖
第二种方法是应用锁存器的保持功能实现开关消抖,应用电路如图3所示。
图3 应用锁存器实现开关消抖
除了上述两种按键消抖方法外,在基于FPGA的数字系统设计中,也可以应用状态机设计按键消抖电路,在FPGA内部实现。
基于状态机设计按键消抖电路时,需要将按键的一次动作分解为:按下前、按下时、稳定期和释放时4个状态,如图1中所示,分别用KEY_IDLE、KEY_PRESSED、KEY_ACTIVE和KEY_RELEASE表示。设按键输入用key_in表示,低电平有效,设计消抖时间为20ms,则按键消抖状态机的状态转换关系如图4所示。
图4 按键消抖状态机
根据上述状态转换关系,描述按键消抖模块的Verilog HDL代码参考如下:
-----------------------------------------------
module KEY_debounce #(parameter DEBOUNCE_TIME = 1000_000 )(
// 50MHz时钟时,对应消抖时间为20ms
input clk_50, // 50MHz时钟,周期为20ns
input rst_n, // 复位信号
input key_in, // 按键输入
output reg key_out // 消抖后输出
);
// 内部状态定义,循环编码方式
localparam KEY_IDLE = 2'b00, // 按下前
KEY_PRESSED = 2'b01, // 按下时
KEY_ACTIVE = 2'b11, // 稳定期
KEY_RELEASE = 2'b10; // 释放时
// 内部变量定义
reg [19:0] debounce_cnt; // 消抖计数变量
reg [1:0] current_state,next_state; // 现态和次态
reg [0:1] keytmp; // 同步寄存器
// 内部线网定义
wire cnt_en,cnt_end; // 计数允许和停止计数标志
wire cnt_flag; // 消抖计数标志
wire release_flag; // 按键释放标志
// 允许消抖计数逻辑:按键按下时或者释放时,cnt_en有效。
assign cnt_en = current_state == KEY_PRESSED
|| current_state == KEY_RELEASE;
// 停止计数标志:cnt_en有效并且debounce_cnt达到最大值,则cnt_end有效。
assign cnt_end = cnt_en && ( debounce_cnt == DEBOUNCE_TIME - 1 );
// 正在计数标志:cnt_en有效并且debounce_cnt未达到最大值,则cnt_flag有效。
assign cnt_flag = cnt_en && ( debounce_cnt < DEBOUNCE_TIME );
// 按键已释放标志: cnt_end有效,并且keytmp[1]为高电平,则release_flag有效。
assign release_flag = cnt_end && keytmp[1];
// 按键输入两级同步寄存过程,以消除亚稳态。
always @(posedge clk_50 or negedge rst_n)
if ( !rst_n )
keytmp <= 2'b00; // 清零
else
keytmp[0:1] <= {key_in,keytmp[0]}; // 右移
// 时序逻辑过程,描述状态转换
always @(posedge clk_50 or negedge rst_n)
if ( !rst_n )
current_state <= KEY_IDLE;
else
current_state <= next_state;
// 组合逻辑过程,定义次态
always @(*) begin
case ( current_state )
KEY_IDLE: if ( !keytmp[1] ) // 按键按下时,进入KEY_PRESSED
next_state = KEY_PRESSED;
else // 否则,保持KEY_IDLE
next_state = current_state;
KEY_PRESSED: if ( cnt_end && !keytmp[1] )
// 消抖时间到且keytmp[1]为0,确认按下有效
next_state = KEY_ACTIVE;
else if ( cnt_flag && keytmp[1] )
// 正在计数,但keytmp[1]为1,则为抖动
next_state = KEY_IDLE;
else // 否则状态保持
next_state = current_state;
KEY_ACTIVE: if ( keytmp[1] ) // keytmp[1]跳变为1则进入释放状态
next_state = KEY_RELEASE;
else
next_state = current_state; // 否则状态保持
KEY_RELEASE: if ( release_flag ) // 按键已释放,返回
next_state = KEY_IDLE;
else if ( cnt_flag && !keytmp[1] )
// 正在计数,但keytmp[1]为0,则为抖动
next_state = KEY_ACTIVE;
else // 否则状态保持
next_state = current_state;
default: next_state = KEY_IDLE;
endcase
end
// 时序逻辑过程,消抖计时
always @( posedge clk_50 or negedge rst_n )
if ( !rst_n )
debounce_cnt <= 20'b0;
else if ( cnt_en ) // 计数允许信号有效
if ( cnt_end ) // 消抖时间到
debounce_cnt <= 20'b0;
else // 消抖时间未到
debounce_cnt <= debounce_cnt + 1'b1;
else // 计数允许信号无效
debounce_cnt <= 20'b0;
// 时序逻辑过程,按键消抖后输出
always @( posedge clk_50 or negedge rst_n )
if ( !rst_n )
key_out <= 1'b1;
else
case ( current_state )
KEY_IDLE : key_out <= 1'b1;
KEY_PRESSED: key_out <= 1'b1;
KEY_ACTIVE : key_out <= 1'b0;
KEY_RELEASE: key_out <= 1'b0;
default: key_out <= 1'b1;
endcase
endmodule
---------------------------------------------------
对上述代码进行仿真验证时,需要建立testbench文件,应用系统函数$random产生随机数,以模拟不规则的抖动脉冲间隔。
num = $random%b
num ={$random}%b
--------------------------------------------------------------
`timescale 1ns/1ps
module KEY_debounce_vlg_tst();
reg clk;
reg rst_n;
reg key_in;
wire key_out;
// 模块参数重定义,减少计数容量,以缩短仿真时间
defparam KEY_debounce.DEBOUNCE_TIME = 50000;
// 内部变量定义
reg [15:0] rand_num;
// 仿真参数定义
parameter RESET_TIME = 2, STEP = 5;
// 按键消抖模块例化
KEY_debounce i1
( .clk (clk),
.rst_n (rst_n),
.key_in (key_in),
.key_out (key_out));
// 设置复位信号波形
initial begin
rst_n = 1;
#1;
rst_n = 0;
#(STEP * RESET_TIME);
rst_n = 1;
end
// 设置按键输入
initial begin
#1; key_in = 1; //按下前
#(STEP * 10); press_key; // 第1次按键过程
#10_000; press_key; // 第2次按键过程
end
// 设置时钟信号
initial clk_50 = 0;
always #(STEP/2) clk_50 = ~clk_50;
// 监测任务
initial
$monitor($time,"clk_50=%b rst_n=%b key_in=%b key_out=%b",
clk_50,rst_n,key_in,key_out);
// 按键任务定义
task press_key;
begin
repeat (20) begin // 模拟前沿抖动过程
rand_num = {$random}%5000;
#rand_num key_in = ~key_in;
end
key_in = 0;
#300_000;
repeat (20) begin // 模拟后沿抖动过程
rand_num = {$random}%5000;
#rand_num key_in = ~key_in;
end
key_in = 1;
#300_000;
end
endtask
endmodule
--------------------------------------------------------------
上述代码中使用了defparam语句用于对KEY_debounce模块中的DEBOUNCE_TIME参数进行重定义,在确保功能验证的前提下缩短消抖时间。启动modelsim进行仿真,结果如图5所示。
图5 按键消抖模块仿真波形
从波形图中可以看出,消抖电路对按键按下和释放产生的4次抖动都能实现有效消抖,因此验证基于状态机设计的按键消抖模块功能正确。