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或數碼管計數。

 

  

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM