【原创】详解Verilog一段式状态机实现按键消抖(含打包成IP核教程)

一、按键消抖

  玩过单片机的都知道,机械按键在按下和松开的一段时间内会在高低电平间来回振荡数次。为了准确可靠地检测按键是否被触发,则需要对按键按下和松开的过程进行消抖处理。

消抖处理的核心就是用延时来跳过抖动阶段,从而在电平稳定时判定按键状态。下面以松开按键触发事件为例,编写按键消抖的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核,节省开发周期。

 

 

 

 

 

 

 

 

 

 

  • 12
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值