写在前面


正文

背景介绍及回顾

我们在互联网上经常会看到这种按键防抖的Verilog设计,那就是大概每20ms读取一次开关。

事实上,不同的按键,其抖动时间是不一样的,参考资料:debouncing中,作者测试了不同的按键的抖动时间:

Verilog设计实例(8)按键防抖设计之软件防抖_sed

Verilog设计实例(8)按键防抖设计之软件防抖_按键去抖_02
用于打开和关闭每个开关(数字A至R)的抖动时间(以微秒为单位)。开关E被排除在外,因为其157毫秒的跳动会严重扭曲图形。

有兴趣的可以看下!

一种常见的按键的电路如下:

Verilog设计实例(8)按键防抖设计之软件防抖_参考资料_03

按键未按下键值信号为高,按下为低,物理按键都存在着时间或短或长的按键抖动!如下图所示:

Verilog设计实例(8)按键防抖设计之软件防抖_Verilog_04

如上面所说,按键抖动一般公认为20ms,如果从软件或者说逻辑设计的方式去消除抖动,就是先检测到按键信号的边沿,之后每计数20ms采样一次键值!这样就实现了按键消抖的目的!

单个按键

一个按键的消抖设计

先从一个按键为例:

如下图为设计框图:
Verilog设计实例(8)按键防抖设计之软件防抖_键值_05
设计文件:

`timescale 1ns / 1ps
/
// Engineer: Reborn Lee
// Module Name: debounce_1b


module debounce_1b(
	input clk, //50MHz
	input rst,
	input sw_in_n,
	output reg sw_out_n
    );


	reg sw_mid_r1, sw_mid_r2, sw_valid;

	always@(posedge clk or posedge rst) begin
		if(rst) begin
			sw_mid_r1 <= 1; // synchronize 1 clock 
			sw_mid_r2 <= 1; // delay 1 clock
			sw_valid <= 0; // gen negedge
		end
		else begin
			sw_mid_r1 <= sw_in_n;
			sw_mid_r2 <= sw_mid_r1;
			sw_valid <= sw_mid_r2 & (~sw_mid_r1);
		end
	end

	reg [19:0] key_cnt;

	always@(posedge clk or posedge rst) begin
		if(rst) begin
			key_cnt <= 0;
		end
		else if(sw_valid) begin
			key_cnt <= 0;
		end
		else begin
			key_cnt <= key_cnt + 1; //20ms
		end
	end

	always@(posedge clk or posedge rst) begin
		if(rst) begin
			sw_out_n <= 1;
		end
		else if(key_cnt == 20'hfffff) begin
			sw_out_n <= sw_in_n;
		end
	end

endmodule

很容易理解,就是先检测到键值有没有变化,检测键值的下降沿,如果检测到了下降沿则对计数器清零,否则计数,计数到20ms(20’dfffff),采样一次键值作为输出。

下面自己设计抖动来测试下这个设计。

测试文件

`timescale 1ns / 1ps
module debounce1_tb(
    );
	reg clk;
	reg rst;
	reg sw_in_n;
	wire sw_out_n;

	initial begin
		clk = 0;
		forever begin
			#5 clk = ~clk;
		end
	end

	initial begin
		rst = 1;
		sw_in_n = 1;
		#50 
		rst = 0;
		@(negedge clk)
		sw_in_n = 0;
		repeat(3) @(negedge clk);
		sw_in_n = 1;
		repeat(20000) begin
			repeat(5) @(negedge clk);
			sw_in_n = 0;
			repeat(2) @(negedge clk);
			sw_in_n = 1;
			repeat(3) @(negedge clk);
			sw_in_n = 0;
		end

		repeat(3000000) begin
			@(negedge clk);
		end
		
		sw_in_n = 1;

	end


	debounce_1b inst0(
		.clk(clk),
		.rst(rst),
		.sw_in_n(sw_in_n),
		.sw_out_n(sw_out_n)
		);


endmodule

仿真波形为:

Verilog设计实例(8)按键防抖设计之软件防抖_Verilog_06

Verilog设计实例(8)按键防抖设计之软件防抖_键值_07

设计的RTL原理图:

Verilog设计实例(8)按键防抖设计之软件防抖_Verilog_08

单按键的其他设计版本

不得不说明的是,资料给出的除抖动原理图:

Verilog设计实例(8)按键防抖设计之软件防抖_键值_09
其实就是简化了上述设计的RTL图,我觉得比第一个设计要精妙一些。

这个版本设计没有检测按键的下降沿,而是直接对键值同步两拍,检测这两拍数据的异或,如果异或值为1,则代表键值变化了,这时计数值清零;否则计数,直到计数到规定值时采样键值数据。

下面给出VHDL设计,很简单,这里没必要转换成verilog了(如果实在需要,关注我微信公众号 FPGA LAB,后台给我消息,我看人多了之后抽空转下):

----------------------------------------------------------------
--   FileName:         debounce.vhd
--   Version History
--   Version 1.0 3/26/2012 Scott Larson
----------------------------------------------------------------

LIBRARY ieee;
USE ieee.std_logic_1164.all;
USE ieee.std_logic_unsigned.all;

ENTITY debounce IS
  GENERIC(
    counter_size  :  INTEGER := 19); --counter size (19 bits gives 10.5ms with 50MHz clock)
  PORT(
    clk     : IN  STD_LOGIC;  --input clock
    button  : IN  STD_LOGIC;  --input signal to be debounced
    result  : OUT STD_LOGIC); --debounced signal
END debounce;

ARCHITECTURE logic OF debounce IS
  SIGNAL flipflops   : STD_LOGIC_VECTOR(1 DOWNTO 0); --input flip flops
  SIGNAL counter_set : STD_LOGIC;                    --sync reset to zero
  SIGNAL counter_out : STD_LOGIC_VECTOR(counter_size DOWNTO 0) := (OTHERS => '0'); --counter output
BEGIN

  counter_set <= flipflops(0) xor flipflops(1);   --determine when to start/reset counter
  
  PROCESS(clk)
  BEGIN
    IF(clk'EVENT and clk = '1') THEN
      flipflops(0) <= button;
      flipflops(1) <= flipflops(0);
      If(counter_set = '1') THEN                  --reset counter because input is changing
        counter_out <= (OTHERS => '0');
      ELSIF(counter_out(counter_size) = '0') THEN --stable input time is not yet met
        counter_out <= counter_out + 1;
      ELSE                                        --stable input time is met
        result <= flipflops(1);
      END IF;    
    END IF;
  END PROCESS;
END logic;

还有上一个版本的改进版:

Verilog设计实例(8)按键防抖设计之软件防抖_按键去抖_10

LIBRARY ieee;
USE ieee.std_logic_1164.all;

ENTITY debounce IS
  GENERIC(
    clk_freq    : INTEGER := 50_000_000;  --system clock frequency in Hz
    stable_time : INTEGER := 10);         --time button must remain stable in ms
  PORT(
    clk     : IN  STD_LOGIC;  --input clock
    reset_n : IN  STD_LOGIC;  --asynchronous active low reset
    button  : IN  STD_LOGIC;  --input signal to be debounced
    result  : OUT STD_LOGIC); --debounced signal
END debounce;

ARCHITECTURE logic OF debounce IS
  SIGNAL flipflops   : STD_LOGIC_VECTOR(1 DOWNTO 0); --input flip flops
  SIGNAL counter_set : STD_LOGIC;                    --sync reset to zero
BEGIN

  counter_set <= flipflops(0) xor flipflops(1);  --determine when to start/reset counter
  
  PROCESS(clk, reset_n)
    VARIABLE count :  INTEGER RANGE 0 TO clk_freq*stable_time/1000;  --counter for timing
  BEGIN
    IF(reset_n = '0') THEN                        --reset
      flipflops(1 DOWNTO 0) <= "00";                 --clear input flipflops
      result <= '0';                                 --clear result register
    ELSIF(clk'EVENT and clk = '1') THEN           --rising clock edge
      flipflops(0) <= button;                        --store button value in 1st flipflop
      flipflops(1) <= flipflops(0);                  --store 1st flipflop value in 2nd flipflop
      If(counter_set = '1') THEN                     --reset counter because input is changing
        count := 0;                                    --clear the counter
      ELSIF(count < clk_freq*stable_time/1000) THEN  --stable input time is not yet met
        count := count + 1;                            --increment counter
      ELSE                                           --stable input time is met
        result <= flipflops(1);                        --output the stable value
      END IF;    
    END IF;
  END PROCESS;
  
END logic;

有兴趣可以转到链接:去抖动逻辑电路(以VHDL为例)查看(打不开的除外)!

好吧,单个按键就到这里了,下面给出多个按键的设计。


多个按键

多个按键进行消抖的设计如下:

使用一种相当简单的方法来查找开关的n个连续稳定读数,其中n是一个从1(完全没有反跳)到看似无穷大的数字。 通常,代码会先检测到跳变,然后开始递增或递减计数器,每次重新读取输入时,直到n达到一些安全的,无抖动的计数。 如果状态不稳定,则计数器会重置为其初始值。

`timescale 1ns / 1ps
 
//说明:当三个独立按键的某一个被按下后,相应的LED被点亮;
//		再次按下后,LED熄灭,按键控制LED亮灭
 
module sw_debounce(
    		clk,rst_n,
			sw1_n,sw2_n,sw3_n,
	   		led_d1,led_d2,led_d3
    		);
 
input   clk;	//主时钟信号,50MHz
input   rst_n;	//复位信号,低有效
input   sw1_n,sw2_n,sw3_n; 	//三个独立按键,低表示按下
output  led_d1,led_d2,led_d3;	//发光二极管,分别由按键控制
 
//---------------------------------------------------------------------------
reg key_rst;  
 
always @(posedge clk  or negedge rst_n)
    if (!rst_n) key_rst <= 1'b1;
    else key_rst <= sw3_n&sw2_n&sw1_n;
 
reg key_rst_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
 
always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) key_rst_r <= 1'b1;
    else key_rst_r <= key_rst;
   
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期 
wire key_an = key_rst_r & (~key_rst);
/*
key_rst     1 1 1 0 0 1
~key_rst    0 0 0 1 1 0
key_rst_r     1 1 1 0 0 1
key_an        0 0 1 0 0
*/
//---------------------------------------------------------------------------
reg[19:0]  cnt;	//计数寄存器
 
always @ (posedge clk  or negedge rst_n)
    if (!rst_n) cnt <= 20'd0;	//异步复位
	else if(key_an) cnt <=20'd0;
    else cnt <= cnt + 1'b1;
  
reg[2:0] low_sw;
 
always @(posedge clk  or negedge rst_n)
    if (!rst_n) low_sw <= 3'b111;
    else if (cnt == 20'hfffff) 	//满20ms,将按键值锁存到寄存器low_sw中	 cnt == 20'hfffff
      low_sw <= {sw3_n,sw2_n,sw1_n};
      
//---------------------------------------------------------------------------
reg  [2:0] low_sw_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
 
always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) low_sw_r <= 3'b111;
    else low_sw_r <= low_sw;
/*
low_sw		111 111 111 110 110 110  
~low_sw     000 000 000 001 001 001
low_sw_r        111 111 111 110 110 110
led_ctrl	000 000 000 001 000 000 
   */
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期 
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);
 
reg d1;
reg d2;
reg d3;
  
always @ (posedge clk or negedge rst_n)
    if (!rst_n) begin
        d1 <= 1'b0;
        d2 <= 1'b0;
        d3 <= 1'b0;
      end
    else begin		//某个按键值变化时,LED将做亮灭翻转
        if ( led_ctrl[0] ) d1 <= ~d1;	
        if ( led_ctrl[1] ) d2 <= ~d2;
        if ( led_ctrl[2] ) d3 <= ~d3;
      end
 
assign led_d3 = d1 ? 1'b1 : 1'b0;		//LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;
  
endmodule
 

按键消抖的部分,从开头到这里:

always @(posedge clk  or negedge rst_n)
    if (!rst_n) low_sw <= 3'b111;
    else if (cnt == 20'hfffff) 	//满20ms,将按键值锁存到寄存器low_sw中	 cnt == 20'hfffff
      low_sw <= {sw3_n,sw2_n,sw1_n};

已经结束 ,剩下的都是控制LED的部分!

我们来简单看下这个代码:

always @(posedge clk  or negedge rst_n)
    if (!rst_n) key_rst <= 1'b1;
    else key_rst <= sw3_n&sw2_n&sw1_n;

只要检测到键值有变化(哪怕是抖动),在这里是变低,就会令key_rst拉低!

reg key_rst_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中
 
always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) key_rst_r <= 1'b1;
    else key_rst_r <= key_rst;
   
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期 
wire key_an = key_rst_r & (~key_rst);

对key_rst延迟一拍之后,在做下降沿检测,如果检测到下降沿key_an有效!
也就是说,如果键值变化,key_an就能有效!

reg[19:0]  cnt;	//计数寄存器
 
always @ (posedge clk  or negedge rst_n)
    if (!rst_n) cnt <= 20'd0;	//异步复位
	else if(key_an) cnt <=20'd0;
    else cnt <= cnt + 1'b1;

在key_an有效后,我们就将计数值清零,之后计数(key_an无效),如果在计数过程中,键值处于抖动阶段,则key_an会再次有效,计数值再次清零。直到抖动结束后,一直计数,计数到20ms,取键值信号!

always @(posedge clk  or negedge rst_n)
    if (!rst_n) low_sw <= 3'b111;
    else if (cnt == 20'hfffff) 	//满20ms,将按键值锁存到寄存器low_sw中	 cnt == 20'hfffff
      low_sw <= {sw3_n,sw2_n,sw1_n};

这段程序的意思就是计数满,如果系统时钟频率为50MHz,则意味着计数20ms左右,将采样键值,这里以三个按键为例。
到这里为止,也就结束了按键消抖的部分!

写在最后

其实对于按键抖动消除问题,还可以通过硬件的方式去抖动,但不在本文的讨论范围之内,可以参考资料!

 


交个朋友

Verilog设计实例(8)按键防抖设计之软件防抖_参考资料_11