目前筆者正在接受明德揚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實現按鍵消抖模塊的全部設計過程。由於設計比較簡單,此處略去上板驗證並在線調試的過程,在之后的設計中將給出此處的具體操作流程。這是筆者第一次撰寫技術博文,希望大家給出寶貴建議,相互交流學習!