一、按键消抖
玩过单片机的都知道,机械按键在按下和松开的一段时间内会在高低电平间来回振荡数次。为了准确可靠地检测按键是否被触发,则需要对按键按下和松开的过程进行消抖处理。
消抖处理的核心就是用延时来跳过抖动阶段,从而在电平稳定时判定按键状态。下面以松开按键触发事件为例,编写按键消抖的Verilog代码。
首先用格雷码定义状态机的几种状态:
// 按键消抖状态机(格雷码)
localparam KEY_IDLE = 4'b0000; // key IDLE
localparam KEY_DELAY_1 = 4'b0001; // 按键下降沿延时
localparam KEY_DECIDE_1 = 4'b0011; // 判决延时后I/O是否为低电平
localparam KEY_WAIT_POSE = 4'b0010; // 等待按键释放,即等待上升沿出现
localparam KEY_DELAY_2 = 4'b0110; // 按键下降沿延时
localparam KEY_DECIDE_2 = 4'b0111; // 判决延时后I/O是否为高电平
localparam KEY_FINISH = 4'b0101; // 消抖完毕,输出一个时钟周期的高电平
KEY_IDLE:初始状态,在此状态中判断按键是否出现下降沿,即是否被按下,若被按下则开启延时定时器并跳转到下一状态。
KEY_DELAY_1:判断延时定时器是否溢出,溢出则跳转到下一状态。
KEY_DECIDE_1:延时结束后判断电平状态是否仍为低电平,若是则跳转到下一状态。
KEY_WAIT_POSE:在此状态中判断按键是否出现上升沿,即是否被松开,若被松开则开启延时定时器并跳转到下一状态。
KEY_DELAY_2:判断延时定时器是否溢出,溢出则跳转到下一状态。
KEY_DECIDE_2:延时结束后判断电平状态是否仍为高电平,若是则跳转到下一状态。
KEY_FINISH:消抖结束,输出一个时钟周期的脉冲。
状态转移图如下所示:
一段式状态机代码如下:
// 按键消抖主状态机
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)begin
state <= KEY_IDLE; // 状态寄存器清零
delay_en <= 1'b0; // 定时器失能
key_output <= 1'b0; // 按键输出脉冲清零
end
else begin
case(state)
// key IDLE
KEY_IDLE: begin
if(key_nege)begin // 按键出现下降沿跳转,使能定时器
delay_en <= 1'b1; // 定时器使能
state <= KEY_DELAY_1; // 跳转到 KEY_DELAY_1
end
else begin
delay_en <= 1'b0;
key_output <= 1'b0; // 按键输出脉冲清零
state <= KEY_IDLE;
end
end
// 消抖延时
KEY_DELAY_1: begin
if(delay_done)begin
delay_en <= 1'b0; // 延时结束失能定时器
state <= KEY_DECIDE_1; // 跳转到 KEY_DECIDE_1
end
else begin
delay_en <= 1'b1;
state <= KEY_DELAY_1;
end
end
// 判断I/O是否为低电平
KEY_DECIDE_1: begin
if(~key_input)
state <= KEY_WAIT_POSE; // 跳转到 KEY_WAIT_POSE
else
state <= KEY_IDLE;
end
// 等待按键松开,出现上升沿
KEY_WAIT_POSE: begin
if(key_pose)begin // 按键出现上升沿跳转,使能定时器
delay_en <= 1'b1; // 定时器使能
state <= KEY_DELAY_2; // 跳转到 KEY_DELAY_2
end
else begin
delay_en <= 1'b0;
state <= KEY_WAIT_POSE;
end
end
// 消抖延时
KEY_DELAY_2: begin
if(delay_done)begin
delay_en <= 1'b0; // 延时结束失能定时器
state <= KEY_DECIDE_2; // 跳转到 KEY_DECIDE_2
end
else begin
delay_en <= 1'b1;
state <= KEY_DELAY_2;
end
end
// 判断I/O是否为高电平
KEY_DECIDE_2: begin
if(key_input)
state <= KEY_FINISH; // 跳转到 KEY_FINISH
else
state <= KEY_IDLE;
end
// 消抖结束,输出脉冲
KEY_FINISH: begin
key_output <= 1'b1; // 按键已经按下,输出高电平脉冲
state <= KEY_IDLE; // 跳转回 KEY_IDLE
end
endcase
end
end
为了进行延时,需要设计一个门控的定时器,定时器中的参数(溢出值) DELAY_CNT 可配置,这个在后面叙述:
// 消抖延时定时器
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)begin
cnt <= 24'b0;
delay_done <= 1'b0;
end
else if(delay_en)begin
if(cnt == DELAY_CNT)begin
cnt <= 24'b0;
delay_done <= 1'b1;
end
else begin
cnt <= cnt + 24'b1;
delay_done <= 1'b0;
end
end
else begin
cnt <= 24'b0;
delay_done <= 1'b0;
end
end
因为我们目前设计的电路都是同步时序电路,边沿触发条件均应为输入时钟的边沿,所以按键的上升沿和下降沿不应写入敏感列表中;同时,按键输入相对于同步时钟来说是异步信号,对于单比特异步信号的同步化常用两个D触发器级联打两拍,故采用在两个级联的D触发器中提取边沿信息的方式判断按键边沿,其RTL电路如下:
由上图可见,当 key_input 产生下降沿时,必先在前一个时钟周期时有一高电平被打入 ff_a_reg ,紧接着下一个时钟周期时有一低电平被打入 ff_a_reg ,同时上一个周期的高电平被打入 ff_b_reg ,故此时将a触发器的输出取反后和b触发器输出相与,可得到下降沿信号,即当两个触发器的输出数据满足a为低电平、b为高电平时 key_nege_i 输出1,反之一直输出0。上升沿的判断与之相反。代码如下:
// 两拍跨时钟域同步寄存器
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)begin
ff_a <= 1'b0;
ff_b <= 1'b0;
end
else begin
ff_a <= key_input;
ff_b <= ff_a;
end
end
// 按键边沿检测
assign key_nege = ff_b && (~ff_a); // 高电平为下降沿
assign key_pose = ff_a && (~ff_b); // 高电平为上升沿
总体RTL代码如下(DELAY_CNT即为上述可配置的定时器参数):
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2020/10/08 22:30:22
// Design Name:
// Module Name: key_in
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module key_in #
(
parameter CLK_FRQ = 50_000_000, // 主晶振频率50MHz
parameter DELAY_TIME = 10 // 消抖延时时间(单位:ms)
)
(
input clk50M, // 全局时钟50MHz
input rst_n, // 异步复位
input key_input, // 按键输入:空闲高电平,按下低电平
output reg key_output, // 按键消抖 脉冲输出
output reg led // 测试LED
);
parameter DELAY_CNT = (CLK_FRQ*DELAY_TIME)/1000; // 消抖计数器溢出值
//
// 按键消抖状态机(格雷码)
localparam KEY_IDLE = 4'b0000; // key IDLE
localparam KEY_DELAY_1 = 4'b0001; // 按键下降沿延时
localparam KEY_DECIDE_1 = 4'b0011; // 判决延时后I/O是否为低电平
localparam KEY_WAIT_POSE = 4'b0010; // 等待按键释放,即等待上升沿出现
localparam KEY_DELAY_2 = 4'b0110; // 按键下降沿延时
localparam KEY_DECIDE_2 = 4'b0111; // 判决延时后I/O是否为高电平
localparam KEY_FINISH = 4'b0101; // 消抖完毕,输出一个时钟周期的高电平
//
reg[3:0] state; // 状态寄存器
reg[23:0] cnt; // 消抖延时24位计数器
reg delay_en; // 延时计数器使能
reg delay_done; // 延时结束标志寄存器
reg ff_a; // 同步寄存器A
reg ff_b; // 同步寄存器B
wire key_pose; // 按键上升沿:1为上升沿
wire key_nege; // 按键下降沿:1为下降沿
//
// 消抖延时定时器
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)begin
cnt <= 24'b0;
delay_done <= 1'b0;
end
else if(delay_en)begin
if(cnt == DELAY_CNT)begin
cnt <= 24'b0;
delay_done <= 1'b1;
end
else begin
cnt <= cnt + 24'b1;
delay_done <= 1'b0;
end
end
else begin
cnt <= 24'b0;
delay_done <= 1'b0;
end
end
// 两拍跨时钟域同步寄存器
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)begin
ff_a <= 1'b0;
ff_b <= 1'b0;
end
else begin
ff_a <= key_input;
ff_b <= ff_a;
end
end
// 按键边沿检测
assign key_nege = ff_b && (~ff_a); // 高电平为下降沿
assign key_pose = ff_a && (~ff_b); // 高电平为上升沿
//
// 按键消抖主状态机
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)begin
state <= KEY_IDLE; // 状态寄存器清零
delay_en <= 1'b0; // 定时器失能
key_output <= 1'b0; // 按键输出脉冲清零
end
else begin
case(state)
// key IDLE
KEY_IDLE: begin
if(key_nege)begin // 按键出现下降沿跳转,使能定时器
delay_en <= 1'b1; // 定时器使能
state <= KEY_DELAY_1; // 跳转到 KEY_DELAY_1
end
else begin
delay_en <= 1'b0;
key_output <= 1'b0; // 按键输出脉冲清零
state <= KEY_IDLE;
end
end
// 消抖延时
KEY_DELAY_1: begin
if(delay_done)begin
delay_en <= 1'b0; // 延时结束失能定时器
state <= KEY_DECIDE_1; // 跳转到 KEY_DECIDE_1
end
else begin
delay_en <= 1'b1;
state <= KEY_DELAY_1;
end
end
// 判断I/O是否为低电平
KEY_DECIDE_1: begin
if(~key_input)
state <= KEY_WAIT_POSE; // 跳转到 KEY_WAIT_POSE
else
state <= KEY_IDLE;
end
// 等待按键松开,出现上升沿
KEY_WAIT_POSE: begin
if(key_pose)begin // 按键出现上升沿跳转,使能定时器
delay_en <= 1'b1; // 定时器使能
state <= KEY_DELAY_2; // 跳转到 KEY_DELAY_2
end
else begin
delay_en <= 1'b0;
state <= KEY_WAIT_POSE;
end
end
// 消抖延时
KEY_DELAY_2: begin
if(delay_done)begin
delay_en <= 1'b0; // 延时结束失能定时器
state <= KEY_DECIDE_2; // 跳转到 KEY_DECIDE_2
end
else begin
delay_en <= 1'b1;
state <= KEY_DELAY_2;
end
end
// 判断I/O是否为高电平
KEY_DECIDE_2: begin
if(key_input)
state <= KEY_FINISH; // 跳转到 KEY_FINISH
else
state <= KEY_IDLE;
end
// 消抖结束,输出脉冲
KEY_FINISH: begin
key_output <= 1'b1; // 按键已经按下,输出高电平脉冲
state <= KEY_IDLE; // 跳转回 KEY_IDLE
end
endcase
end
end
always@(posedge clk50M or negedge rst_n)begin
if(!rst_n)
led <= 1'b0;
else if(key_output)
led <= ~led;
end
endmodule
二、按键消抖IP核打包教程
当我们上板测试无恙后,为方便以后快速使用此模块,可以将其打包为用户IP核供以后调用。这里我使用的平台是Xilinx的Kintex7 FPGA,EDA为Vivado 2019.2,故以Vivado 2019.2为例,演示如何将自己写好的按键消抖工程打包。
1、首先综合工程,无error后点击顶部菜单栏的 Tools ,下拉框选择 Create and Package New IP 选项,进入如下界面,直接点击 Next 。
然后进入如下图所示的界面,我们要打包工程,所以如图选择第一个按钮,然后点击 Next 。
然后进入如下图所示界面,选择IP核保存路径,注意不要有中文和空格,可以有合法英文标点。
点击 Next 。
点击 Next 。
检查一下IP核保存路径,点击 Finish 。
可以看到 Vivado 自动打开了一个临时工程,用于显示并编辑IP核的文本信息(当生成IP核后该临时工程会自动关闭),在当工程下可以看见如下界面。可以在此界面中编辑IP核的文本信息,注意文本也不能出现中文和空格,否则生成IP核时会报错。
然后在此界面的左面点击最后一个 Review and Package 按钮
在出现的界面中点击正下方的 Package IP 按钮,开始打包IP核。
出现如下提示信息,则代表IP核打包成功,点击 Yes 关闭临时工程。
IP核打包完成后,可以去刚才填写的保存路径下查看,可以看到IP核相关文件、原verilog和xdc文件都已经被打包。
至此,Verilog按键消抖并打包成IP核的工作已经全部完成了,今后再使用按键时无需二次编写,直接可以调用打包的IP核,节省开发周期。