本文共 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 值变化了
输入部分,有 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;
当计数器到达 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;
用时钟,同步新/旧的采样数据,并且通过新旧的按键对比出到底是哪个被按下了:
// 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] 中就代表了上一次的采样数据了;
既然按下按键的信息被存储到了 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/