【读书笔记】详细解析基于FPGA的独立按键消抖
0赞【主题】:详细解析基于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信号,来读取按键值和触发输出使能信号。
由于key_trigger提前了一个clk,因此,key_flag与key_data保证了同步,这样时序非常完美。
总结:
如果是两个时序always块,不管是不是在一个module中,他们的沟通需要一个clk的时间,因此,必要的时候,要提前一个clk输出,以保证时序的同步。