LinCoding

【读书笔记】详细解析基于FPGA的独立按键消抖

0
阅读(4457)

【主题】:详细解析基于FPGA的独立按键消抖

【作者】:LinCoding

【时间】:2016.11.17

      本文说一说我所了解的独立按键消除抖动的原理和程序。虽说独立按键抖动的原理大家已经都很熟悉了,但是消抖程序却还是有好几种。

方法一、检测按键已按下,延时20ms,再次检测,如果按键确实按下,则读取按键值,然后,检测按键已释放,延时20ms,再次检测,如果按键确实释放,则结束。

这是一种经典的按键消抖程序的原理,不管是在以前单片机的开发,还是FPGA的开发,这样的原理是通用的。

方法一的变型:在FPGA中,由于可以灵活的使用计数器,因此,可以产生一个周期为20ms的使能信号,利用使能信号和状态机来实现方法一。

方法二、检测到按键按下,延时20ms,然后直接读取按键值,结束。

这是一种对以上按键消抖原理的简化版,虽然实现起来更加简单,但是,笔者不推崇,因为,总感觉不太稳定。

方法三、利用计数器计数和清零的操作,连续20ms内检测到按键按下,则视为按键确实按下,连续20ms内检测到按键释放,则视为按键确实释放。

这种方法也非常好,尤其是在单片机的开发中,这样的方法可以利用定时器定时,如果在抖动期间,计数器一直清零,如果按键按下了20ms,则触发读取信号。这样利用一个定时器就可以完成按键的扫描,避免了使用delal_ms(20)这种延时函数。当然,在FPGA中,也可以使用这样的方法,比方法一更加的简单,而且也很稳定。

此外有一点需要注意,由于按键属于外设,而不是FPGA内部的东西,因此,可能会引起亚稳态,所以,对于按键的信号应做同步处理

本文只对方法一的变型方法三进行详细的程序解析。

(源码出自CrazyBingo,尊重版权)

方法一的变型:

module key_scan_jitter
#(
	parameter				KEY_WIDTH = 4
)
(
	input					clk,
	input					rst_n,
	
	input		[KEY_WIDTH-1'b1:0]	key_value,
	output	reg	[KEY_WIDTH-1'b1:0]	key_data,
	output					key_flag
);

第一部分是输入输出变量的定义,没什么可说的

//-------------------------------------
//counter for 20ms   
localparam	DELAY_TOP = 20'd1_000_000;
//localparam	DELAY_TOP = 20'd500;	//just for simulation
reg	[19:0]	delay_cnt;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		delay_cnt	<= 20'd0;
	else if ( delay_cnt < DELAY_TOP )
		delay_cnt	<= delay_cnt + 1'b1; 
	else
		delay_cnt	<= 20'd1;
end
wire	delay_done	= ( delay_cnt == DELAY_TOP ) ? 1'b1 : 1'b0;

第二部分是20ms的计数器,还是那个模式,计数器的输出采用组合逻辑

//-------------------------------------
//FSM: encoder
localparam	SCAN_DETECT1	= 3'd0;
localparam	SCAN_JITTER1	= 3'd1;
localparam	SCAN_READ	= 3'd2;
localparam	SCAN_DETECT2	= 3'd3;
localparam	SCAN_JITTER2	= 3'D4;

//-------------------------------------
//FSM
reg	[2:0]	key_state;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			key_data	<= {KEY_WIDTH{1'b0}};
			key_state	<= SCAN_DETECT1;
		end
	else if ( delay_done )
		case ( key_state )
			SCAN_DETECT1:
				if ( key_value	!= {KEY_WIDTH{1'b1}} )
					key_state	<= SCAN_JITTER1;
				else
					key_state	<= SCAN_DETECT1;		
			SCAN_JITTER1:
				if ( key_value	!= {KEY_WIDTH{1'b1}} )	
					key_state	<= SCAN_READ;
				else
					key_state	<= SCAN_DETECT1;		
			SCAN_READ	:
				begin
					key_data	<= ~ key_value;
					key_state	<= SCAN_DETECT2;
				end
			SCAN_DETECT2:
				if ( key_value	== {KEY_WIDTH{1'b1}} )	
					key_state	<= SCAN_JITTER2;
				else
					key_state	
			SCAN_JITTER2:
				if ( key_value	== {KEY_WIDTH{1'b1}} )	
					key_state	<= SCAN_DETECT1;
				else
					key_state	<= SCAN_JITTER2;		
			default:
				begin
					key_data	<= {KEY_WIDTH{1'b0}};
					key_state	<= SCAN_DETECT1;
				end
		endcase
	else
		begin
			key_data	<= key_data;
			key_state	<= key_state;
		end 
end	

 第三部分采用了一段式FSM,因为状态较少,而且大部分是状态的转移,因此使用一段式状态机比使用三段式更加方便。

 本段使用一个20ms定时的使能信号,作为状态转移的转移条件。保证了再每两次状态之间有20ms的时间,可以有效滤除抖动。

 有一点需要注意:这个按键消抖的程序并没有对按键的输入做同步处理,这样难道不会引起亚稳态吗?

 我的理解是,由于状态机是采用的20ms的时钟来进行采样的,并且肯定是在时钟沿上,一般情况下采样到的都会是高或者低,就算哪次碰巧,采样时候恰好采到了抖动,那也没关系,因为过20ms还会采样一次,因此,本段程序无需做同步化处理,当然,加上同步也是可以的。

assign	key_flag	= ( key_state == SCAN_JITTER2 && delay_done ) ? 1'b1 : 1'b0;

 最后一部分是按键输出标志,其他模块在收到这个标志信号以后,就可以读取按键的数据了。还是那个模式,输出采用组合逻辑,并且要加上使能信号(delay_done)


方法三: 

module key_scan_counter
#(
	parameter						KEY_WIDTH = 4
)
(
	input							clk,
	input							rst_n,
	
	input		[KEY_WIDTH-1'b1:0]	key_value,
	output	reg	[KEY_WIDTH-1'b1:0]	key_data,
	output	reg						key_flag
);

 第一部分是输入输出定义。

//------------------------------------------
//detect the key press or up
reg		[KEY_WIDTH-1'b1:0]	key_value_r1;
reg		[KEY_WIDTH-1'b1:0]	key_value_r2;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			key_value_r1	<= {KEY_WIDTH{1'b1}};
			key_value_r2	<= {KEY_WIDTH{1'b1}};
		end
	else
		begin
			key_value_r1	<= key_value;
			key_value_r2	<= key_value_r1;
		end
end

 第二部分是做用两级D触发器来做同步,以免引起亚稳态。

//------------------------------------------
//counter for 20ms
//localparam	T20MS	= 20'd1_000_000;
localparam	T20MS	= 20'd500;	//just for simulation
reg	[19:0]	delay_cnt;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		delay_cnt	<= 20'd0;
	else if ( key_value_r1 == key_value_r2 && key_value_r2 != {KEY_WIDTH{1'b1}} )	
		delay_cnt	<= ( delay_cnt < T20MS ) ? ( delay_cnt + 1'b1 ) : T20MS;
	else
		delay_cnt	<= 20'd1;
end
wire	key_trigger	= ( delay_cnt == T20MS - 1'b1 ) ? 1'b1 : 1'b0;

 第三部分判断如果按键按下,则计时器开启计时,计数满20ms之后,将不再继续。

计数器的输出采用了组合逻辑,并且提前一个clk,通知其他模块可以读取数据。

always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		key_data	<= {KEY_WIDTH{1'b0}};
	else if ( key_trigger )
		key_data	<= ~ key_value_r2;
	else	
		key_data	<= {KEY_WIDTH{1'b0}};
end

always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		key_flag	<= 1'b0;
	else
		key_flag	<= key_trigger;

 最后一部分是根据key_trigger信号,来读取按键值和触发输出使能信号。

blob.png

 由于key_trigger提前了一个clk,因此,key_flag与key_data保证了同步,这样时序非常完美。


总结:

如果是两个时序always块,不管是不是在一个module中,他们的沟通需要一个clk的时间,因此,必要的时候,要提前一个clk输出,以保证时序的同步。