• FPGA学习笔记(八)—— 状态机设计实例之独立按键消抖


    ###### 【该随笔中部分内容转载自小梅哥】 #########

      独立按键消抖自古以来在单片机和FPGA中都是个不可避免的问题,首先,解释一下什么叫做按键抖动,如图,按键在按下和松开的那个瞬间存在大概20ms的机械抖动:

      

      下面就是本篇的第一个重点 —— 什么时候需要按键消抖设计?如果是像复位按键这样,短时间内可以多次触发,就完全不需要设计消抖,但是如果是要设计按下按键使LED状态翻转,或者按下按键计数一次的话,就必须要设计消抖模块,否则就会带来不可预知的错误,因为在按下按键的那个时刻,可能已经触发了少则几次,多则几十次,可见按键消抖的必要性;

      那么,既然按键消抖如此重要,如何来进行消抖呢?

      1、硬件消抖 —— 0.1uF电容滤波

      

        这个104的电容就是起高频滤波的作用,在按键不是很多的情况下,可以使用这种设计,但是如果要做项目,会增加大量成本,所以接下来我们讲述如何进行软件消抖;

      2、软件消抖 —— delay

    if(key_in == 0)
    {
        deley(2000);
        if(key_in == 0)
        {
             //按键按下,执行相应操作 
         }   
    }

      在单片机中用C语言,可以这样设计按键消抖,同样的思路,在FPGA中,我们依然可以采用这种思想,将按键的这20ms抖动“屏蔽”,但是FPGA没有delay(2000),该如何设计呢?

      FPGA中控制延时可以采用计数器,因为工作时钟是已知的50M,所以要延时20_000_000ns(20ms),只需要对计数1_000_000个clk就可以,这样延时问题就解决了,按照之前的思路,设计如下:只需要在检测到key_in变为0,启动定时器,定时器时间到,再次检测key_in是否为0,若为0,表明按键按下稳定,关闭计数器并清零,然后等待按键释放,也就是key_in出现上升沿,再次启动定时器,时间到后检测key_in,若为1,则证明按键已释放,一次完整的按键过程结束

      大致思路有了,如何设计实现呢?貌似这是一个很复杂的设计,实则不然,FSM的本质就是对具有逻辑规律和时序逻辑的事物的描述,采用FSM设计,问题迎刃而解!

      1、从状态变量入手,分析状态变量

        IDLE:按键空闲状态(由于上拉电阻的作用,按键未被按下时保持高电平);

        FILTER_DOWN:按下滤波状态;

        DOWN:按下稳定状态;

        FILTER_UP:释放滤波状态;

      2、分析状态转移条件,绘制状态转移图(visio)

      3、照图施工,选用合适的描述方案

        在描述的时候,有两个重要问题需要解决:

        1)按键信号属于异步信号,在状态转移中需要对按键边沿敏感,所以首先采用一级D触发器将key_in与clk同步,产生pedge和nedge信号,也就是边沿检测电路,代码如下:

    //边沿检测电路
        always@(posedge clk)
            key_temp <= key_in;                                 //暂存上一个clk按键状态
        assign key_nedge = (key_temp)&&(!key_in);        //下降沿检测
        assign key_pedge = (!key_temp)&&(key_in);        //上升沿检测

        2)当20ms延时完毕后,应该输出一个脉冲,通知其它模块检测key_flag引脚电平;

        完整的verilog描述代码如下:

    `timescale 1ns / 1ps
    //////////////////////////////////////////////////////////////////////////////////
    // Module Name: key_filter
    // Description: //独立按键消抖模块
    //////////////////////////////////////////////////////////////////////////////////
    module key_filter(
        input clk,                    //50M时钟信号
        input rst,                    //低电平复位
        input key_in,                 //按键输入
        output reg key_flag,          //消抖完毕输出脉冲
        output reg key_state          //按键状态输出
    );
        reg  [3:0]NS;            //nextstate
        reg  key_temp;
        wire key_pedge;    
        wire key_nedge;
        reg en_cnt;
        reg [19:0]cnt;            //需要计数次数1_000_000
        
        //边沿检测电路
        always@(posedge clk)
            key_temp <= key_in;                          //暂存上一个clk按键状态
        assign key_nedge = (key_temp)&&(!key_in);        //下降沿检测
        assign key_pedge = (!key_temp)&&(key_in);        //上升沿检测
        
        //带使能端计数器,用于20ms延时
        always@(posedge clk,negedge rst)
            if(!rst)
                cnt <= 0;
            else if(en_cnt)
                cnt <= cnt + 1'b1;
            else
                cnt <= 0;
        
        //状态one-hot编码
        localparam 
            IDLE        = 4'b0001,        //空闲状态
            FILTER_DOWN = 4'b0010,        //按下消抖状态
            DOWN        = 4'b0100,        //按下稳定状态
            FILTER_UP   = 4'b1000;        //释放消抖状态
        
        //一段式状态机
        always@(posedge clk,negedge rst)
        if(!rst)begin
            NS     <= IDLE;
            en_cnt <= 0;
            key_flag <= 0;
            key_state <= 1;
        end
        else
            case(NS)
                IDLE:
                    begin
                        key_flag <= 0;
                        key_state <= 1;
                        if(key_nedge)begin
                            NS <= FILTER_DOWN;
                            en_cnt <= 1'b1;        //使能计数器
                        end
                        else
                            NS <= IDLE;
                    end
                FILTER_DOWN:
                    if(cnt >= 20'd999_999)begin
                        en_cnt <= 0;               //20ms时间到,失能计数器,进入稳定状态
                        key_flag <= 1'b1;          //key_flag输出一个clk高脉冲
                        NS <= DOWN;
                    end
                    else if(key_pedge)begin    
                        en_cnt <= 0;                //20ms时间内发生上升沿,失能计数器,保持空闲状态
                        NS <= IDLE;
                    end
                DOWN:
                    begin
                        key_flag <= 0;
                        key_state <= 0;
                        if(key_pedge)begin
                            NS <= FILTER_UP;
                            en_cnt <= 1'b1;         //使能计数器
                        end
                        else
                            NS <= DOWN;
                    end
                FILTER_UP:
                    if(cnt >= 20'd999_999)begin
                        en_cnt <= 0;
                        NS <= IDLE;                 //20ms时间到,失能计数器,进入稳定状态
                        key_flag <= 1;
                    end
                    else if(key_nedge)begin
                        en_cnt <= 0;                //20ms时间内发生上升沿,失能计数器,保持按下稳定状态
                        NS <= DOWN;            
                    end
                default:
                        NS <= IDLE;
            endcase
    endmodule
        

      4、编写tsetbench进行仿真测试(查看所生成的波形、状态转移图、RTL视图)

    `timescale 1ns / 1ps
    //////////////////////////////////////////////////////////////////////////////////
    // Module Name: key_filter_tb
    // Description: 
    //////////////////////////////////////////////////////////////////////////////////
    `define clk_period 20        //100M系统时钟
    
    module key_filter_tb();
        
        reg clk;                   //50M时钟信号
        reg rst;                   //低电平复位
        reg key_in;                //按键输入
        wire key_flag;             //消抖完毕输出脉冲
        wire key_state;            //按键状态输出
        
        //例化测试模块
        key_filter key_filter_test(
            .clk(clk),             //50M时钟信号
            .rst(rst),             //低电平复位
            .key_in(key_in),       //按键输入
            .key_flag(key_flag),   //消抖完毕输出脉冲
            .key_state(key_state)  //按键状态输出
    );
        //产生100M时钟信号
        initial clk = 1;
        always #(`clk_period / 2) clk <= ~clk;
        
        //开始测试
        initial begin
            rst <= 0;        //系统复位
            key_in <= 1;     //按键处于空闲状态
        #(`clk_period * 2);
            rst <= 1;
         #10_000_000;        //延时10ms,方便观察按键按下现象
        //开始模拟按键按下抖动
        key_in <= 0;    #1000;
        key_in <= 1;    #2000;
        key_in <= 0;    #1400;
        key_in <= 1;    #2600;
        key_in <= 0;    #1300;
        key_in <= 1;    #200;
        //产生一个稳定的低电平大于20ms,代表按键稳定
        key_in <= 0;   #30_000_000;
        //模拟释放抖动
        key_in <= 1;    #2000;
        key_in <= 0;    #1000;
        key_in <= 1;    #2600;
        key_in <= 0;    #1400;
        key_in <= 1;    #200;
        key_in <= 0;    #1300;
        //产生一个稳定的高电平大于20ms,代表释放稳定
        key_in <= 1;   #30_000_000;
        $stop;
        end
    endmodule 

       测试结果如下:

      对于testbench,这个文件写的很繁琐,可以进行一下优化,首先

      1、利用$random函数产生随机延时值模拟抖动,用法如下:

    reg [15:0]randnum;
    randnum = $random % 50;        //产生一个-49~49内的随机数
    randnum = {$random} %  50;     //产生一个0 ~ 50内的随机数

        2、利用task/endtask将重复代码进行封装,用法如下:

    task <任务名>;
        <语句1>
        <语句2>
        <语句3>
        ....
    endtask

      优化后的testbench测试文件如下:

    `timescale 1ns / 1ps
    //////////////////////////////////////////////////////////////////////////////////
    // Module Name: key_filter_tb
    // Description: 
    //////////////////////////////////////////////////////////////////////////////////
    `define clk_period 20        //100M系统时钟
    
    module key_filter_tb();
        
        reg clk;                   //50M时钟信号
        reg rst;                   //低电平复位
        reg key_in;                //按键输入
        wire key_flag;             //消抖完毕输出脉冲
        wire key_state;            //按键状态输出
        
        reg [15:0]rand_time;       //按键抖动时随机时长
        
        //例化测试模块
        key_filter key_filter_test(
            .clk(clk),                    //50M时钟信号
            .rst(rst),                    //低电平复位
            .key_in(key_in),              //按键输入
            .key_flag(key_flag),          //消抖完毕输出脉冲
            .key_state(key_state)         //按键状态输出
    );
        //产生100M时钟信号
        initial clk = 1;
        always #(`clk_period / 2) clk <= ~clk;
        
        //开始测试
        initial begin
            rst <= 0;        //系统复位
            key_in <= 1;     //按键处于空闲状态
        #(`clk_period * 2);
            rst <= 1;
         #10_000_000;          //延时10ms,方便观察按键按下现象
         press_key; #10000;    //第一次按下按键
         press_key; #10000;    //第二次按下按键
         press_key; #10000;    //第三次按下按键
        $stop;
        end
        
        task press_key;
           begin
               //开始模拟按键按下抖动
               repeat(50)begin
                  rand_time = {$random} % 65536;
                  #rand_time key_in = ~key_in; 
               end 
               //产生一个稳定的低电平大于20ms,代表按键稳定
               key_in = 0;     
               #30_000_000;
               //模拟释放抖动
               repeat(50)begin
                   rand_time = {$random} % 65536;
                   #rand_time key_in = ~key_in;  
               end
               //产生一个稳定的高电平大于20ms,代表释放稳定
               key_in = 1;
               #30_000_000;
           end
        endtask
    endmodule 

      测试结果如下(总共按键三次),可以看到,优化后的testbench比之前的测试更加精准,更加真实的模拟现实情况:

      这里,因为这是一个按键模型,对外提供的功能就是实际中按键的作用,这也就是仿真模型,所以可以独立写一个testbench作为按键模型,方便例化调用

      按键模型的仿真代码如下:

    `timescale 1ns / 1ps
    //////////////////////////////////////////////////////////////////////////////////
    // Module Name: key_module
    // Description: 仿真按键模型
    //////////////////////////////////////////////////////////////////////////////////
    
    module key_module(
        output reg key //对外输出按键信号 
        );
         
         reg [15:0]rand_time;       //按键抖动时随机时长
        
        initial begin
              key <= 1;    //按键处于空闲状态
              #10_000_000;          //延时10ms,方便观察按键按下现象
              press_key; #10000;    //第一次按下按键
              press_key; #10000;    //第二次按下按键
              press_key; #10000;    //第三次按下按键
             $stop;
             end
             
    task press_key;
             begin
             //开始模拟按键按下抖动
             repeat(50)begin
                rand_time = {$random} % 65536;
                #rand_time key = ~key; 
             end 
             //产生一个稳定的低电平大于20ms,代表按键稳定
             key = 0;     
             #30_000_000;
             //模拟释放抖动
             repeat(50)begin
                rand_time = {$random} % 65536;
                #rand_time key = ~key;  
             end
             //产生一个稳定的高电平大于20ms,代表释放稳定
             key = 1;
             #30_000_000;
             end
    endtask    
    endmodule

      这样一来,在testebench中测试就可以直接调用该仿真模型,优化到最后的testbench如下:

    `timescale 1ns / 1ps
    //////////////////////////////////////////////////////////////////////////////////
    // Module Name: key_filter_tb
    // Description: 基于按键模型进行测试
    //////////////////////////////////////////////////////////////////////////////////
    `define clk_period 20        //100M系统时钟
    
    module key_filter_tb();
        
        reg clk;                   //50M时钟信号
        reg rst;                   //低电平复位
        wire key_in;                   //按键输入
        wire key_flag;               //消抖完毕输出脉冲
        wire key_state;                //按键状态输出
        
        //例化测试模块
        key_filter key_filter_test(
            .clk(clk),                    //50M时钟信号
            .rst(rst),                    //低电平复位
            .key_in(key_in),            //按键输入
            .key_flag(key_flag),        //消抖完毕输出脉冲
            .key_state(key_state)    //按键状态输出
    );
        //例化按键模型
        key_module key1(
           .key(key_in) //对外输出按键信号 
            );
        //产生100M时钟信号
        initial clk = 1;
        always #(`clk_period / 2) clk <= ~clk;
        
        //开始测试
        initial begin
           rst = 0;    //系统复位
           #(`clk_period * 2);
           rst = 1;
        end
        
    endmodule 

      测试结果和之前完全一样,但testcench更加简洁,按键仿真也更加方便以后调用,至此,按键消抖模块就设计测试完毕,如有兴趣,可以进行进一步的设计,控制led或数码管计数。

      

     

  • 相关阅读:
    python随笔:邮箱题目
    05 小程序自定义组件
    04 小程序常用组件-view text rich-text icon swiger
    03 小程序语法-WXSS样式-尺寸-样式 -选择器
    02 小程序语法-数据绑定与事件绑定
    01 小程序入门与vscode开发加装插件
    JAVA25-Git、Vue.js
    JAVA14-File类、递归、字节流、字符流、缓冲流、转换流、序列化流、Files
    JAVA13-异常、线程、同步、等待与唤醒案例、线程池、Lambda表达式
    JAVA12-Scanner类、Random类、ArrayList类、String类、static、Arrays类、Math类、静态方法
  • 原文地址:https://www.cnblogs.com/Mculover666/p/9106477.html
Copyright © 2020-2023  润新知