###### 【該隨筆中部分內容轉載自小梅哥】 #########
獨立按鍵消抖自古以來在單片機和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或數碼管計數。