目录

  • 一丶按键原理
  • 二丶按键消抖
  • 三丶消抖方式
  • 1.延迟采样
  • ①任务描述
  • ②编写代码
  • ③时序图分析
  • ④仿真
  • 2.抖动稳定后采样
  • ①任务描述
  • ②编写代码
  • ③代码分析
  • ④仿真
  • 四丶消抖场景
  • 五丶消抖应用
  • 一丶按键原理

    我们首先来看原理图

    可以看到有4条输入线接到FPGA的IO口(最左边四个KEY)上,分两种情况:
    1.当按键KEY1按下时,D3V3(也就是电源)通过电阻R(原理图上折线的那一段)然后再通过按键KEY1最终进入GND形成一条通路,那么这条线路的全部电压都加到了R这个电阻上,KEY1(最左边四个IO口)这个引脚就是个低电平。
    2.当松开按键后,线路断开,就不会有电流通过,那么KEY1D3V3就应该是等电位,是一个高电平。我们就可以通过KEY1这个IO口的高低电平来判断是否有按键按下。

    二丶按键消抖

    由于机械按键的物理特性,按键被按下的过程中,存在一段时间的抖动,同
    时在释放按键的过程中也存在抖动,这就导致在识别的时候可能检测为多次的按键按下,而通常检测到一次按键输入信号的状态为低电平,就可以确认按键被按下了,所以我们在使用按键时往往需要消抖,以确保按键被按下一次只检测到一次低电平

    按键消抖解决方案1:延迟采样。(图片采用作者stark-lin)
    按键消抖解决方案2:信号变化频率平稳后并且持续20ms则采样。

    三丶消抖方式

    1.延迟采样

    ①任务描述

    任务:

    我们需要在检测到按键抖动的时刻延时20ms再采样

    思路:我们首先需要一个模块来检测按键是否抖动,如果抖动,计时模块一个标志位开始计时,记满20ms, 再给输出消抖后按键信号模块一个标志位进行采样。
    理清思路,整个程序分为三个模块,模块之间相互关联,关联之处需要一个起到连接作用的器件,也就是标志位。比如将flag作为标志位,检测到按键抖动之后,将flag作为计时开始的条件。下面开始编写代码,之后进行时序以及代码分析。

    ②编写代码

    key_debounce.v

    module key_debounce (   
        input  wire     clk,     //系统时钟 50MHz
        input  wire     rst_n,   //复位信号
        input  wire     key,     //按键输入信号
        output reg      key_done //消抖之后的按键信号
    );
    
    reg                 key_r0;  //同步信号(滤波作用,滤除小于一个周期的抖动)
    reg                 key_r1;  //打拍
    reg                 flag;    //标志位
    wire                nedge;   //下降沿检测(检测到下降沿代表开始抖动)
    
    //计时器定义
    reg [19:0]          cnt;
    wire                add_cnt;  //计时器开启
    wire                end_cnt;  //计时记满
    
    parameter           MAX_CNT=20'd1_000_000;  //20ms延时
    
    //同步
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_r0<=1'b1;
        end
        else
            key_r0<=key;
    end
    
    //打拍
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_r1<=1'b1;    
        end
        else
            key_r1<=key_r0;
    end
    
    assign nedge = ~key_r0 & key_r1;  //检测到下降沿拉高
    
    //标志位
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            flag<=1'b0; 
        end
        else if (nedge) begin
            flag<=1'b1; 
        end
        else if (end_cnt) begin
            flag<=1'b0;
        end
    end
    
    //延时模块
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt<=20'b0;
        end
        else if (add_cnt) begin
            if (end_cnt) begin
                cnt<=20'b0;
            end
            else
                cnt<=cnt+1;
        end
    end
    
    assign add_cnt=flag;                    //计时器开启
    assign end_cnt=add_cnt&&cnt==MAX_CNT-1; //计时器关闭
    
    //key_done输出
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_done<=1'b0; 
        end
        else if (end_cnt) begin            //延时满20ms采样
            key_done<=~key_r0;
        end
        else
            key_done<=1'b0;
    end
    
    endmodule 
    

    ③时序图分析

    clock:我们的整个程序都是在时钟的控制下运行的,所有的always模块都对时钟的上升沿敏感,本人使用的开发板时钟频率是50MHz,也就是一秒震动50_000_000次,一个周期就是20ns

    always @(posedge clk or negedge rst_n) 
    

    key_in:这是按键输入信号,低电平有效
    key_r0:为了滤除掉小于一个周期的抖动,对key_in推迟一个周期进行同步。看图
    方框位置key_in输入的按键信号抖动为低电平,小于一个周期,在时钟上升沿(posedge 代表上升沿敏感)到来的时候信号已经恢复高电平,key_r0还是保持高电平,这样就达到了滤波的效果

    key_r1:这个信号是把同步信号再延时一个周期,主要是为了保存key_r0上一个周期的值,来判断是否出现下降沿
    nedge:检测key_r0是否出现下降沿,若出现,则将标志位flag设为1,也是计时器开启的条件
    cnt:20ms计时器
    add_cnt:计时器开启条件,用flag表示
    end_cnt:计时器结束条件,记满20ms,也是采样模块的开启条件
    key_done:计时器记满,则开始采样,key_done拉高一个周期,代表按键按下

    ④仿真

    代码:

    `timescale 1ns/1ps
    
    module tb_key_debounce ();
    reg         tb_clk;
    reg         tb_rst_n;
    reg         tb_key;
    wire        tb_key_done;
    
    integer i,j;
    defparam u_key_debounce.MAX_CNT=5;    //将延时的时间修改为5个周期,也就是100ns
    
    key_debounce u_key_debounce(   
        .clk             (tb_clk)        ,       //系统时钟 50MHz
        .rst_n           (tb_rst_n)      ,       //复位信号
        .key             (tb_key)        ,       //按键输入信号
        .key_done        (tb_key_done)           //消抖之后的按键信号
    );
    
    always #10 tb_clk=~tb_clk;
    initial begin
        tb_clk=1'b1;
        tb_rst_n=1'b1;
        tb_key=1'b1;
        #100;
    
        //复位
    
        tb_rst_n=1'b0;
        #100;
    
    
        //恢复
        tb_rst_n=1'b1;
    
        //模拟按键抖动
        for (i = 0; i<=15; i=i+1) begin
            j=({$random}%15);
            #(j);
            tb_key={$random};
        end
    
        //按键按下
    
        tb_key=1'b0;
        #150;
    
        tb_key=1'b1;
        #2000;
        $stop;
    end
    
    endmodule
    

    方框标红处延时20ms开始采样,拉高一个周期

    2.抖动稳定后采样

    ①任务描述

    任务:

    我们需要不停的检测到按键抖动,直到信号稳定之后再延时20ms,之后采样

    思路:我们首先需要一个模块来检测按键是否抖动,如果抖动,计时模块一个标志位开始计时,记满20ms, 再给输出消抖后按键信号模块一个标志位进行采样。
    计时模块与方式1不同,只要还在抖动就把计数器清零,重新计数20ms,直到抖动稳定

    ②编写代码

    key_debounce.v

    module key_debounce(
    	input  wire  clk,
    	input  wire  rst_n,
    	input  wire  key_in,
    	
    	output reg   key_flag,               //判断抖动是否消除的标志信号,0为抖动,1为抖动结束
    	output reg   key_value               //消抖后稳定的按键值给到蜂鸣器模块
    );
    
    //定义20ms延迟计数器,0.2s,1_000_000次
    reg [19:0] delay_cnt;
    
    //寄存依次key的值用来判断按键是否消抖成功
    reg key_reg;
    
    parameter MAX_CNT=20'd1_000_000;  //20ms
    
    //20ms延时计数器
    always@(posedge clk or negedge rst_n)begin
    	if(!rst_n)
    		begin
    			key_reg <= 1'b1;                        //复位信号,设置按键无效
    			delay_cnt <= 1'b0;                      //计数器设置为0
    		end
    	else
    		begin
    			key_reg <= key_in; 
    			if(key_reg == 1 && key_in == 0)            //当这一次key值和上一次key值不一样,证明正在抖动
    				delay_cnt <= MAX_CNT;          //延迟时间20ms
    			else if(delay_cnt > 0)
    				delay_cnt <= delay_cnt - 1;          //没有抖动,开始20ms倒计时
    			else
    				delay_cnt <= 1'b0;                  
    		end
    end
    
    
    //根据延时计数器获取按键状态以及按键值
    always@(posedge clk or negedge rst_n)begin
    	if(!rst_n)
    		begin
    		   key_flag <= 1'b0;                               //复位信号,设置信号标志为抖动
    			key_value <= 1'b1;                          //设置抽样值为1
    		end
    	else
    		begin
    			if(delay_cnt == 20'd1)                      //倒计时1_000_000到1
    				begin
    					key_flag <= 1'b1;
    					key_value <= key_in;                     //稳定20ms后将key值给到key_value
    				end
    			else	
    				begin
    					key_flag <= 1'b0;
    					key_value <= key_value;               //20ms内先不取样
    				end
    		end
    end
    endmodule
    

    ③代码分析

    这里因为没有同步信号,和打拍信号,比较容易理解,就不画时序图做分析,可以对照仿真进行理解

    计时模块中此处

    key_reg相当于消抖方式1的同步信号,同样检测到下降沿开始计时(即计时器清零,我们这里使用倒计时,效果一样)
    红框中的条件也可以换成key_reg != key_in,但是意义就不一样了,更改了之后就是只要前一个周期跟现在的信号有差异就计数,也就是出现下降沿和上升沿都计数,实际上差别不大,因为抖动时间在5-10ms内,有抖动一定会出现上升沿和下降沿

    ④仿真

    代码:

    `timescale 1ns/1ps
    
    module tb_key_debounce ();
    reg         tb_clk;
    reg         tb_rst_n;
    reg         tb_key_in;
    wire        tb_key_flag;
    wire        tb_key_value;
    
    integer i,j;
    defparam u_key_debounce.MAX_CNT=5;    //将延时的时间修改为5个周期,也就是100ns
    
    key_debounce u_key_debounce(   
        .clk                 (tb_clk)            ,       //系统时钟 50MHz
        .rst_n               (tb_rst_n)          ,       //复位信号
        .key_in              (tb_key_in)         ,       //按键输入信号
        .key_flag            (tb_key_flag)       ,       //标志位
        .key_value           (tb_key_value)              //消抖之后的按键信号
    );
    
    always #10 tb_clk=~tb_clk;
    initial begin
        tb_clk=1'b1;
        tb_rst_n=1'b1;
        tb_key_in=1'b1;
        #100;
    
        //复位
    
        tb_rst_n=1'b0;
        #100;
    
    
        //恢复
        tb_rst_n=1'b1;
    
        //模拟按键抖动
        for (i = 0; i<=15; i=i+1) begin
            j=({$random}%15);
            #(j);
            tb_key_in={$random};
        end
    
        //按键按下
    
        tb_key_in=1'b0;
        #150;
    
        tb_key_in=1'b1;
        #2000;
        $stop;
    end
    
    endmodule
    

    红框处,计时器从我修改的值延时5个周期(100ns),倒计时到0,key_value采样,电平拉低一个周期,代表按键低有效

    四丶消抖场景

    上面所提到的按键消抖方式我们可以想象:
    验证密码

    假设我们的密码是10101,设置key[1]代表密码1,key[0]代表密码0

    我们第一次按下key[1],经过消抖之后,系统只会检测到一个按键低电平,代表只按了一次key[1],否则,不消抖的话,按一次,系统能检测到多次低电平,输入的密码就会出现错误

    按键控制LED灯状态切换

    按下一个按键并松开,LED保持对应状态,切换按键,LED切换状态

    如果跟①一样,那么按键必须按住不放LED才会显示对应的状态,因为按照我们的按键消抖,按一下只会输出一个周期的低电平,所以需要一直按下按键,才能保持低电平,在不改变按键消抖的前提下,我们可以设置一个标志位,在按键消抖模块输出一个周期低电平的时候给标志位flag赋值为1,让flag作为LED状态的条件

    五丶消抖应用

    1.按键消抖+识别字符串(类似于验证密码)
    2.按键消抖+led灯状态切换(切换LED灯和呼吸灯)

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【FPGA】按键消抖

    发表评论