狀態機設計——從簡單的按鍵消抖開始


  目前筆者正在接受明德揚FPGA網上培訓班的培訓,講的內容非常適合新手,且以練習和互動答疑的教學模式讓我學到了很多東西。由於是根據自身時間安排進度的,所以戰線拉的比較長,發現做些設計總結非常重要,可以幫助自己理清思路,同時也能得到很好的復習。

  之前一直在做altera FPGA的相關學習,對xilinx還不是很熟悉,借着這個契機,將比較基礎常用的設計在VIVADO開發環境中過一遍,對我來說是個不錯的選擇。進入今天的正題,本篇博文旨在通過一個小例子掌握狀態機的設計方法。由於設計非常簡單,采用常見的三段式狀態機來規范設計。后續復雜的例子中,將采用明德揚提出的四段式狀態機,個人理解雖然與三段式基本思想相同,但有助於簡化設計,理清思路。

  眾所周知,硬件按鍵都存在機械抖動。所以一次人為按下的動作會觸發數次按鍵按下的行為。所謂“按鍵消抖”模塊的功能就是將抖動濾除掉,保證對按鍵狀態的有效識別。單片機的設計思想比較通用,即檢測到按鍵連接端口為低電平(低電平有效)后,延遲一段時間再次確認是否為低。若是則說明此次低電平確實為一次按鍵行為,否則視為抖動。按鍵松手檢測同理。其大體設計流程如下:

 

   這是典型的順序設計思想,但FPGA是並行的。所以這種時間有先后,且操作差異較大的處理過程要用到狀態機進行設計。簡化后可將上述過程分為四個狀態:初始空閑狀態、延遲並檢測低電平狀態、檢測釋放狀態和延遲並檢測高電平狀態。以下是狀態轉移圖:

  空閑狀態下如檢測到按鍵接口低電平進入延遲並確認低電平狀態,延遲計數時間設定為10ms。若計數完成且依然為低電平則按下有效進入檢測釋放狀態,若計數期間按鍵出現高電平說明為抖動回到初始狀態。在檢測釋放狀態中若出現高電平進入延遲確認狀態,否則持續檢測高電平。在延遲確認高電平狀態若計數完成且為高電平視為有效松手行為,此時置位有效標志位,按鍵完成了一次按下到松手的完整有效過程回到IDLE狀態再檢測下一次按下。如果計數期間出現低電平同樣為抖動回到檢測釋放狀態重新檢測。

  1 `timescale 1ns / 1ps
  2 
  3 module key_jitter#
  4 (
  5 parameter DELAY_TIME = 2000_000  //延遲10ms
  6 )
  7 (
  8     input clk,
  9     input rst_n,
 10     
 11     input key_i,
 12     output reg led_o
 13     );
 14     
 15     localparam IDLE          = 4'b0001,
 16                DELAY_LOW     = 4'b0010,
 17                CHECK_RELEASE = 4'b0100,
 18                DELAY_HIGH    = 4'b1000;
 19                
 20     reg [20:0] div_cnt;
 21     reg [3:0] state_c,state_n;
 22     reg key_tmp0,key_tmp1;
 23     
 24     wire add_cnt,end_cnt;
 25     wire vld_flag;
 26     wire cnt_during;
 27 
 28     //消除亞穩態
 29     always@(posedge clk or negedge rst_n)begin
 30         if(!rst_n)begin
 31             key_tmp0 <= 0;
 32             key_tmp1 <= 0;
 33         end
 34         else begin
 35             key_tmp0 <= key_i;
 36             key_tmp1 <= key_tmp0;
 37         end
 38     end
 39     
 40     //狀態機
 41     always@(posedge clk or negedge rst_n)begin
 42         if(!rst_n)
 43             state_c <= IDLE;
 44         else 
 45             state_c <= state_n;
 46     end
 47     
 48     always@(*)begin
 49         case(state_c)
 50             IDLE:begin  //初始狀態檢測是否有按鍵按下 //4'b0001
 51                 if(key_tmp1 == 0)//有按鍵按下進入延時后再次確認低電平狀態
 52                     state_n <= DELAY_LOW;
 53                 else 
 54                     state_n <= state_c;
 55             end
 56             
 57             DELAY_LOW:begin  //延時並再次確認低電平狀態  //4'b0010
 58                 if(end_cnt && key_tmp1 == 0)//10ms后依然是低電平則有按鍵按下,此時檢測是否松手
 59                     state_n <= CHECK_RELEASE;
 60                 else if(cnt_during && key_tmp1 == 1)
 61                     state_n <= IDLE;//若未計數完成出現高電平則視為抖動,重新檢測按下
 62                 else 
 63                     state_n <= state_c;//計數未完成繼續
 64             end
 65             
 66             CHECK_RELEASE:begin  //4'b0100
 67                 if(key_tmp1 == 1)//為高電平則等待並再次確認
 68                     state_n <= DELAY_HIGH;
 69                 else 
 70                     state_n <= state_c;//若沒有高電平則持續檢測
 71             end
 72             
 73             DELAY_HIGH:begin  //4'b1000
 74                 if(vld_flag)//10ms后依然高電平則按鍵釋放
 75                     state_n <= IDLE;//釋放后回到初始狀態再次檢測下一次的按下
 76                 else if(cnt_during && key_tmp1 == 0)//若延時后為0則松手過程視為抖動
 77                     state_n <= CHECK_RELEASE;
 78                 else 
 79                     state_n <= state_c;//繼續計數
 80             end
 81             
 82             default:
 83                 state_n <= IDLE;
 84         endcase
 85     end
 86     
 87     assign cnt_during = add_cnt && div_cnt < DELAY_TIME;
 88     
 89     //延遲計數器
 90     always@(posedge clk or negedge rst_n)begin
 91         if(!rst_n)
 92             div_cnt <= 0;
 93         else if(add_cnt)begin
 94             if(end_cnt)
 95                 div_cnt <= 0;
 96             else 
 97                 div_cnt <= div_cnt + 1'b1;
 98         end
 99         else 
100             div_cnt <= 0;
101     end
102     
103     assign add_cnt = state_c == DELAY_HIGH || state_c == DELAY_LOW;
104     assign end_cnt = add_cnt && div_cnt == DELAY_TIME - 1;
105     //按下一次並釋放后表示一次有效的操作,此時led翻轉
106     always@(posedge clk or negedge rst_n)begin
107         if(!rst_n)
108             led_o <= 0;//上電復位點亮
109         else if(state_c == DELAY_HIGH && vld_flag)//可將()內條件作為按鍵有效輸出
110             led_o <= ~led_o;
111     end
112     
113     
114     assign vld_flag = end_cnt && key_tmp1 == 1;
115     
116 endmodule

   需要注意的知識點是狀態機的設計技巧和參數設定。采用三段式狀態機設計:一個always塊用同步時序方式描述狀態轉移,另一個模塊采用組合邏輯判斷狀態轉移條件,最后給每一個狀態輸出分配一個時序邏輯塊。其優勢在於它將同步時序和組合邏輯分別放到不同的always 程序塊中實現。這樣做的好處不僅僅是便於閱讀、理解、維護,更重要的是利於綜合器優化代碼,利於用戶添加合適的時序約束條件,利於布局布線器實現設計。同時采用時序邏輯輸出消除了“毛刺”現象,提高設計穩定性。

  另外,參數化設計幫助提高代碼可讀性和靈活性。verilog中經常使用parameter 和localparam兩個關鍵字定義參數,兩者之間有一定的區別:parameter可用作在頂層模塊中例化底層模塊時傳遞參數的接口,localparam的作用域僅僅限於當前module,不能作為參數傳遞的接口。所以這里將延遲時間設定為可傳遞參數接口,便於頂層模塊修改。而狀態參數不能改動,只使其作用於當前模塊。

  在FPGA設計中,仿真環節必不可少,甚至占用設計周期的大半,極大提高開發效率,讓問題盡量在設計前期解決。現在編寫測試激勵,用modelsim仿真觀察按鍵消抖模塊是否完成預期功能。

 1 `timescale 1ns / 1ps
 2 
 3 module key_jitter_tb();
 4     
 5     // reg sys_clk_n,sys_clk_p;
 6     reg clk;
 7     reg rst_n;
 8     reg key_i;
 9     reg [15:0] myrand;
10     
11     wire led_o;
12     
13     key_jitter key_jitter
14 (
15     
16     .clk(clk),
17     .rst_n(rst_n),
18     
19     .key_i(key_i),
20     .led_o(led_o)
21 );
22 
23     defparam key_jitter.DELAY_TIME = 50000;//參數重定義 有效時間改為50us便於仿真
24     parameter RST_TIME = 2,
25                CYCLE = 5;
26     
27     initial begin
28         clk = 0;
29         forever #(CYCLE /2) clk = ~clk;
30     end
31     
32     initial begin
33         rst_n = 1;
34         #1;
35         rst_n = 0;
36         #(CYCLE * RST_TIME);
37         rst_n = 1;
38     end
39     
40     initial begin
41         #1;
42         key_i = 1;//初始未按下
43         #(CYCLE * RST_TIME);
44         #(CYCLE * 10);
45         press_key;
46         #10_000;
47         press_key;
48         $stop;
49     end
50     
51     task press_key;
52     begin
53         repeat(20)begin//模擬抖動過程
54             myrand = {$random}%50000;
55             #myrand key_i = ~key_i;
56         end
57         key_i = 0;
58         #300_000;
59         repeat(20)begin
60             myrand = {$random}%50000;
61             #myrand key_i = ~key_i;
62         end
63         key_i = 1;
64         #300_000;
65     end
66     endtask
67     
68 endmodule

 

  其中使用defparam實現參數重定義,讓延遲時間縮短,可以在完成功能驗證的前提下縮短仿真時間,提升開發效率。(仿真真的是慢!)在測試激勵的編寫中,task任務封裝絕對是一項利器,可以非常方便地將某項功能封裝后反復調用。測試文件中將按鍵按下釋放以及中間的抖動過程作為一個task,在仿真過程中多次調用模擬多次按下釋放的行為。通過觀察輸出波形即可得知功能是否正確。

  在仿真之前,一定要設置好仿真工具、編譯庫等選項。注意:如果按下Run xx simulation之后一直卡在執行仿真過程中,說明代碼中有錯誤。此時要檢查tcl console和log日志文件查看仿真相關警告和錯誤提示並作出修改。

 

modelsim仿真波形:

   可以看到兩處紅圈處為一次按下釋放的有效動作,使led輸出翻轉。以上便是FPGA實現按鍵消抖模塊的全部設計過程。由於設計比較簡單,此處略去上板驗證並在線調試的過程,在之后的設計中將給出此處的具體操作流程。這是筆者第一次撰寫技術博文,希望大家給出寶貴建議,相互交流學習!


免責聲明!

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



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