博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Verilog 按键消抖的一些分析和想法
阅读量:786 次
发布时间:2019-03-24

本文共 11813 字,大约阅读时间需要 39 分钟。

最近在网上看了下 Verilog 按键消抖方面的设计,有一些个人的想法,做一些分析和记录;

电路板上,通常会提供若干按键,每个按键下赋予了不同的含义,按键的含义由具体的场景来定义;

打个比方,一组电路板上的按键定义如下所示:

在这个例子中,可以看到,硬件原理图中提供了 5 个信号:

KEY_UP

KEY_DOWN

KEY_LEFT

KEY_RIGHT

KEY_ENTER

当 S2~S6 没有被按下的时候,I/O 管脚被通过上拉电阻到 Vcc,即逻辑 1;

当 S2~S6 被按下的时候,这些 I/O 信号直接到 GND,即逻辑 0;

即,按下按键,读 I/O 状态为 0;否则为 1;

当然,这是理想情况,但是现实却很骨感,我们把这个简单的按键操作无限放大后,事实的真相是:按键的时候,可能会存在抖动因素,即需要判断是否是真的按下了按键?是否是因为抖动的因素,导致了误报?这就是按键去抖;

其实按键去抖比较常见,一个比较简易的判断抖动的算法是:如果发现按下了,那么间隔一个很短的时间,再去采集该 I/O 的状态,发现还是按下的状态,那么就认为是真的按键,否则认为是抖动;这里,间隔的这个很短的时间,一般情况可以取 20ms;那么在翻译一下,发现 I/O 状态改变后,隔 20ms,再次采样 I/O 状态,如果发现当前的状态与之前的状态一致,那么说明,I/O 状态改变是不争的事实!

对应的,如果在单片机上,发现有按键按下,那么可以起一个 20ms 的 Timer,到期后再次检测按键即可;

那么 FPGA 上,纯硬件逻辑应该如何应对呢?这就是本章需要了解的部分;

这里分析了几种实现方案:

方案一

具体实现上,有一些技巧,参考了一些设计,具体的实现如下所示(这里分了几个部分,每个部分逐步讲述):

1、定义一个 20ms 的计数器,不断的从 0 ~ 20ms 进行计数

2、20 ms 到期后,将 key 数据进行采样并缓存到 key 0,并在下一个时钟周期,将 key 0 同步到内部缓存的 key 1 中;

3、key 0 与 key 1 有一个时钟周期的差别,利用这一点,进行判断,是否 key 值变化了

 

Part 1

输入部分,有 4 个按键,分别控制 4 个 LED,按下其中一个,对应的 LED 亮,再次按下,LED 灭

输入时钟频率 25MHz,输入有复位;

首先实现的是 20ms 的计数器,这个不多说了,Verilog 逻辑如下:

module keyscan(    input clk,    input n_rst,    input [3:0] key,    output [3:0] led    );    // Input Clock is 25MHz, so in order to get 20ms     // The Counter should be 500,000 - 1    reg [19:0] cnt;always @(posedge clk or negedge n_rst)    if(!n_rst) cnt <= 20'd0;    else if (cnt == 20'd499999) cnt <= 20'd0;    else cnt <= cnt + 1'b1;

 

Part 2

当计数器到达 20ms 的时候,将按键的 I/O 信息进行采样,存储到内部 key_sample_new[3:0] 寄存器:

// Sample Key input every 20ms    reg [3:0] key_sample_new;always @(posedge clk or negedge n_rst)    if(!n_rst) key_sample_new <= 4'b0000;    else if(cnt == 20'd499999) key_sample_new <= key;

 

Part 3

用时钟,同步新/旧的采样数据,并且通过新旧的按键对比出到底是哪个被按下了:

// Sync the latest one to old one    reg [3:0] key_sample_last;always @(posedge clk or negedge n_rst)    if(!n_rst) key_sample_last <= 4'b0000;    else if(cnt == 20'd499999) key_sample_last <= key_sample_new;    // latch one clock cycle and check which key have been press down    wire [3:0] key_pressed = key_sample_last[3:0] & (~key_sample_new[3:0]);

我们按照时间顺序来进行流程分析:

1、首先,clk 的 20ms 的计数器到期了,会将当前的 key 的 I/O 状态进行采样到 key_sample_new[3:0],注意,此刻的 key_sample_last[3:0] 还是上一次的值,并没发生变化,因为此刻的数据刚刚打入 key_sample_new[3:0];

2、组合逻辑生效 key_pressed 信号会根据当前 20ms 拿到的最新的 I/O 状态 key_sample_new[3:0] 取反和上一次的 key_sample_last[3:0] 进行与操作,这里多说一下:

wire [3:0] key_pressed = key_sample_last[3:0] & (~key_sample_new[3:0]);

如果对应的 bit 之前为 1 (也就是 last 中为 1),新采样拿到的数据为 0(也就是 new 中为 0),那么 key_pressed 中对应的 bit 被置为 1,否则,其他任何情况,key_pressed 中对应的 bit 都为 0;换句话来说,只有检测到对应的 bit 由 1 -> 0 的过程,那么就说明被按下了,并将其记录到 key_pressed 中;

3、下一个 clk 的上升沿到来的时候,key_sample_new[3:0] 被同步到了 key_sample_last[3:0] 中,也就是说,如果有真实的按下按键的话,这两个内部寄存器的值,只有一个时钟周期是不一样的;利用这个时钟周期,将数据存储到了 key_pressed;数据完成了同步后,key_sample_last[3:0] 中就代表了上一次的采样数据了;

 

Part 4

既然按下按键的信息被存储到了 key_pressed 中,那么控制 LED 就靠它了:

reg [3:0] temp_led;always @(posedge clk or negedge n_rst)     if(!n_rst) temp_led <= 4'b1111;    else begin        if ( key_pressed[0] ) temp_led[0] <= ~temp_led[0];        if ( key_pressed[1] ) temp_led[1] <= ~temp_led[1];        if ( key_pressed[2] ) temp_led[2] <= ~temp_led[2];        if ( key_pressed[3] ) temp_led[3] <= ~temp_led[3];    end    assign led[0] = temp_led[0];    assign led[1] = temp_led[1];    assign led[2] = temp_led[2];    assign led[3] = temp_led[3];endmodule

总的来说,这种方案就是将 20ms 前获取到的 key 值(key_sample_last[3:0]) 和 20ms 拿到的新的 key 值(key_sample_new[3:0]) 进行按位来对比,判断是否有被置位的情况;

典型的情况是:

当然,还可能存在一些比较极限的情况,就是 20ms 到期的时候,保存最新的 key 值的时候,正好处于抖动期间;

极限的真实按下按键键的情况:

如果此次是真实的按键,而且正好采样时刻 B 位于抖动期间,如果采集到了 0,那么判断条件生效,将会认为有按键按下,如果采集到的是 1,那么相当于认为当前还是未按下的状态,并且会在 C 点判断出已经按键,显然,这种 Case 没有问题;

 

如果只有抖动,没有按键的情况,如果此次采集到的 key 是 1,那么相安无事,如果采集到的是 0,那么判断就会失效;所以方案一是存在失效风险的;

 

方案二

针对方案一的缺陷,可以设计成为采样 cnt 一直累加,但是一旦检测到 key 有下降沿的时候,立即重置 cnt 到 0,重新计数;

// Solution 2    parameter SAMPLE_RATE=6'd10;// ------------- Key negedge detect Logic Start -------------    reg [3:0]key_0; always @(posedge clk or negedge n_rst)    if(!n_rst) key_0 <= 4'b1111;    else key_0 <= key;    reg [3:0]key_1; always @(posedge clk or negedge n_rst)    if(!n_rst) key_1 <= 4'b1111;    else key_1 <= key_0;    // Check key negedge    wire [3:0]key_neg = key_1[3:0] & (~key_0[3:0]);// ------------- Key negedge detect Logic End -------------// ------------- 4 cnt Logic Start -------------    reg [3:0] cnt_k0; always @(posedge clk or negedge n_rst)    if(!n_rst) cnt_k0 <= 4'd0;    else if(key_neg[0]) cnt_k0 <= 4'd0;    else if(cnt_k0 == SAMPLE_RATE) cnt_k0 <= 4'd0;    else cnt_k0 <= cnt_k0 + 1'b1;        reg [3:0] cnt_k1; always @(posedge clk or negedge n_rst)    if(!n_rst) cnt_k1 <= 4'd0;    else if(key_neg[1]) cnt_k1 <= 4'd0;    else if(cnt_k1 == SAMPLE_RATE) cnt_k1 <= 4'd0;    else cnt_k1 <= cnt_k1 + 1'b1;    reg [3:0] cnt_k2; always @(posedge clk or negedge n_rst)    if(!n_rst) cnt_k2 <= 4'd0;    else if(key_neg[2]) cnt_k2 <= 4'd0;    else if(cnt_k2 == SAMPLE_RATE) cnt_k2 <= 4'd0;    else cnt_k2 <= cnt_k2 + 1'b1;    reg [3:0] cnt_k3; always @(posedge clk or negedge n_rst)    if(!n_rst) cnt_k3 <= 4'd0;    else if(key_neg[3]) cnt_k3 <= 4'd0;    else if(cnt_k3 == SAMPLE_RATE) cnt_k3 <= 4'd0;    else cnt_k3 <= cnt_k3 + 1'b1;// ------------- 4 cnt Logic End -------------    // Sample Key input every SAMPLE_RATE    reg [3:0] key_sample_new;always @(posedge clk or negedge n_rst)    if(!n_rst) key_sample_new <= 4'b1111;    else begin        if(cnt_k0 == SAMPLE_RATE) key_sample_new[0] <= key[0];        if(cnt_k1 == SAMPLE_RATE) key_sample_new[1] <= key[1];        if(cnt_k2 == SAMPLE_RATE) key_sample_new[2] <= key[2];        if(cnt_k3 == SAMPLE_RATE) key_sample_new[3] <= key[3];    end    // Sync the latest one to old one    reg [3:0] key_sample_last;always @(posedge clk or negedge n_rst)    if(!n_rst) key_sample_last <= 4'b1111;    else key_sample_last <= key_sample_new;    // latch one clock cycle and check which key have been press down    wire [3:0] key_pressed = key_sample_last[3:0] & (~key_sample_new[3:0]);    reg [3:0] temp_led;always @(posedge clk or negedge n_rst)     if(!n_rst) temp_led <= 4'b0000;    else begin        if ( key_pressed[0] ) temp_led[0] <= ~temp_led[0];        if ( key_pressed[1] ) temp_led[1] <= ~temp_led[1];        if ( key_pressed[2] ) temp_led[2] <= ~temp_led[2];        if ( key_pressed[3] ) temp_led[3] <= ~temp_led[3];    end    assign led[0] = temp_led[0];    assign led[1] = temp_led[1];    assign led[2] = temp_led[2];    assign led[3] = temp_led[3];

首先使用一个边缘检测电路,检测到 key 的下降沿,一旦发现下降沿,那么 cnt 立马置 0,这样就万无一失了吧?

这种方案的确是比方案一稳一些了,但是在还是有问题:

当 cnt 计数正好计数满的时候,此刻来下降沿了,此刻判断失效!

 

方案三

针对方案一和方案二的问题,那么是否可以这样设计,计数器开始就别工作了,等到下降沿检测好了,在开始进入工作状态,在计数的过程中如果发现有上升沿,那么重新计数,如果一直计数到满(20ms)为止,那么判定,此次是真的按下了!

上述想法可以使用状态机进行设计,刚刚开始,处于 IDLE 状态,通过边沿检测电路来检测 key 的下降沿;

当检测到 key 下降沿后,进入 SAMPLING 采样状态,一旦发现采样过程有上升沿,那么返回到 IDLE,如果计数器满足后,判定为真实按下,进入 DOWN 状态,并输出相应的输出!

这样就能够解决上面两个方案中遇到的问题啦,Verilog 如下(为了仿真,采样时间定在了 10 个 clk,并且只针对了一个 key):

`timescale 1ns / 1ps//// Company: // Engineer:       StephenZhou// // Create Date:    10:02:46 11/25/2019 // Design Name: // Module Name:    keyscan // Project Name: // Target Devices: SP6// Tool versions: // Description: //// Dependencies: //// Revision: 0.01// Revision 0.01 - File Created// Additional Comments: ////module keyscan(    input clk,    input n_rst,    input key,    output reg led    );    // Key posedge and negedge detective logic    reg key_in_0;    reg key_in_1;    always @(posedge clk or negedge n_rst)    if(!n_rst) begin        key_in_0 <= 1'b1;        key_in_1 <= 1'b1;    end    else begin        key_in_0 <= key;        key_in_1 <= key_in_0;    end        wire key_posedge;    wire key_negedge;    assign key_posedge = key_in_0 & (~key_in_1);    assign key_negedge = key_in_1 & (~key_in_0);        // State Machine    reg [2:0] state;    parameter IDLE =     3'b001;    parameter SAMPLING = 3'b010;    parameter DOWN =     3'b100;    reg en_cnt;    reg led_pressed;    always @(posedge clk or negedge n_rst)    if(!n_rst) begin        state  <= IDLE;        en_cnt <= 1'b0;        //led    <= 1'b0;        led_pressed    <= 1'b0;    end    else begin        case(state)            IDLE : begin                if(key_negedge) begin                    state  <= SAMPLING;                    en_cnt <= 1'b1;                    //led    <= 1'b0;                    led_pressed <= 1'b0;                end                else begin                    state  <= IDLE;                    en_cnt <= 1'b0;                    //led    <= 1'b0;                    led_pressed <= 1'b0;                end            end            SAMPLING : begin                if(key_posedge) begin                    state  <= IDLE;                    en_cnt <= 1'b0;                    led_pressed <= 1'b0;                end                else begin                    if(cnt_full) begin                        state  <= DOWN;                        en_cnt <= 1'b0;                        led_pressed <= 1'b0;                    end                    else begin                        state  <= SAMPLING;                        en_cnt <= 1'b1;                        led_pressed <= 1'b0;                    end                end            end            DOWN : begin                state  <= IDLE;                en_cnt <= 1'b0;                led_pressed <= 1'b1;            end            default : begin                state  <= IDLE;                en_cnt <= 1'b0;                led_pressed <= 1'b0;                            end       endcase    end    // Counter    reg [3:0] cnt;    always @(posedge clk or negedge n_rst)    if(!n_rst) cnt <= 4'b0000;    else if (en_cnt) cnt <= cnt + 1'b1;    else cnt <= 4'b0000;        // Counter Full Logic    reg cnt_full;    parameter SAMP_CNT = 4'd10;    always @(posedge clk or negedge n_rst)    if(!n_rst) cnt_full <= 1'b0;    else if(cnt == SAMP_CNT) cnt_full <= 1'b1;    else cnt_full <= 1'b0;    // Check posedge of led and keep output    always @(posedge clk or negedge n_rst)    if(!n_rst) led <= 1'b0;    else if(led_pressed) led <= ~led;    //else led <= 1'b0;endmodule

代码中均有注释,还是来解释一下:

1、首先使用 key_in_0 和 key_in_1 进行下降沿和上升沿检测电路()

2、定义状态机,独热码,三个状态 IDLE、SAMPLING、DOWN,检测下降沿状态为 IDLE,一旦有下降沿,则进入 SAMPLING,在此状态下,如果有上升沿,那么认为是抖动,返回 IDLE,继续检测下降沿,同时计数器停止计时

3、在 SAMPLING 状态下计时器到期,那么认为,稳定时间到,则认为有按键按下,并走到 DOWN 状态;

4、DOWN 状态,认为已经检测 OK,那么 led_pressed 赋值为高,led 亮

testbench 为:

`timescale 1ns / 1ps// Company: // Engineer:    StephenZhou//// Create Date:   13:44:47 11/22/2019// Design Name:   keyscan// Module Name:   D:/Xlinx_ISE_Projects/keyscan/tb/key_scan_tb.v// Project Name:  test// Target Device:  // Tool versions:  // Description: //// Verilog Test Fixture created by ISE for module: keyscan//// Dependencies:// // Revision:// Revision 0.01 - File Created// Additional Comments:// module key_scan_tb;	// Inputs	reg clk;	reg n_rst;	reg key;	// Outputs	wire led;	// Instantiate the Unit Under Test (UUT)	keyscan uut (		.clk(clk), 		.n_rst(n_rst), 		.key(key), 		.led(led)	);    // Clock Generator freq @20    always #5 clk = ~clk;	initial begin		// Initialize Inputs		clk = 0;		n_rst = 0;		key = 1;		// Wait 100 ns for global reset to finish		#100;        		// Add stimulus here        // Release Reset signal        n_rst = 1;                // Wait 100        #100;                // Key Input Signal Jitter        #20 key = 1'b0;        #15 key = 1'b1;        #20 key = 1'b0;        #5  key = 1'b1;        #15 key = 1'b0;        #15 key = 1'b1;        #10 key = 1'b0;        #5  key = 1'b1;                #400;        // Key Input Signal Jitter        #18 key = 1'b0;        //#30 key[0] = 1'b0;        #15 key = 1'b1;        #20 key = 1'b0;        #5  key = 1'b1;        #15 key = 1'b0;        #15 key = 1'b1;        #10 key = 1'b0;        #5  key = 1'b1;        // Real Push down        #10 key = 1'b0;        // Up key        #200 key = 1'b1;	end      endmodule

仿真波形全貌为:

第一个抖动,并没有认为是按键,

放大第一个逻辑为:

 

第二个是的确按下了,所以逻辑正确,放大第二段逻辑:

 

转载地址:http://xquuk.baihongyu.com/

你可能感兴趣的文章