實驗二:按鍵模塊① - 消抖
按鍵消抖實驗可謂是經典中的經典,按鍵消抖實驗雖曾在《建模篇》出現過,而且還惹來一堆麻煩。事實上,筆者這是在刁難各位同學,好讓對方的慣性思維短路一下,但是慘遭口水攻擊 ... 面對它,筆者宛如被甩的男人,對它又愛又恨。不管怎么樣,如今 I’ll be back,筆者再也不會重復一樣的悲劇。
按鍵消抖說傻不傻說難不難。所謂傻,它因為原理不僅簡單(就是延遲幾下下而已),而且順序語言(C語言)也有無數不盡的例子。所謂難,那是因為人們很難從單片機的思維跳出來 ... 此外,按鍵消抖也有許多細節未曾被人重視,真是讓人傷心。按鍵消抖一般有3段操作:
l 檢測電平變化;
l 過濾抖動(延遲);
l 產生有效按鍵。
假設C語言與單片機的組合想要檢測電平變化,它們一般是利用if查詢或者外部中斷。事后,如果這對組合想要過濾抖動,那么可以借用for延遲的力量,又或者依賴定時中斷產生精明的延遲效果。反觀有效案件的產生,這對組合視乎而外鍾情“按下有效”似的 ... 不管怎么樣,C語言與單片機這對組合在處理按鍵的時候,它們往往會錯過一些黃金。
“黃金?”,讀者震撼道。
所謂黃金時間就是電平發生變化那一瞬間,還有消抖(延遲)以后那一瞬間。按鍵按下期間,按鍵的輸入電平故會發生變化,如果使用if查詢去檢測,結果很容易浪費單片機的處理資源,因為單片機必須一直等待 ... 換之,如果反用外部中斷,中斷尋址也會耽誤諾干時間。
假設C語言與單片機這對組合挨過電平檢測這起難關,余下的困難卻是消抖動作。如果利用for循環實現去消抖,例如 Delay_ms(10) 之類的函數。For循環不僅計數不緊密,而且還會白白浪費單片機的處理資源。定時中斷雖然計數緊密,但是中斷觸發依然也會產生諾干的尋址延遲。補上,所謂尋址延遲是處理器處理中斷觸發的時候,它要事先保護現場之余,也要尋址中斷處理入口,然后執行中斷函數,完后回復現場,最后再返回當前的工作。
感覺上,筆者好似在欺負C語言以及單片機,死勁說它們的不是。親愛的讀者,千萬別誤會,筆者只是在陳述事實而已。單片機本來就是比較粗野的爺們,它很難做到緊湊又毫無空隙的操作,反觀FPGA卻異常在行。所以說,單片機的思路很難沿用在FPGA身上,否則會出現許多笑話。如今,這是描述語言以及FPGA的新時代,所謂后浪推前浪正是新舊時代的替換。
FPGA不僅沒有隱性處理,而且描述語言也是自由自身。我們只要方法得當,手段有效,“黃金”要多少就有多少 ... 哇哈哈!在此,筆者說了那么多廢話只是告知讀者,千萬別用單片機的思維去猜摸FPGA如何處理按鍵校抖,不然問號會沒完沒了。好了,廢話差不多說完了,讓我們切回主題吧。
圖2.1 按鍵活動的時序示意圖。
如圖2.1 所示,那是按鍵活動的時序圖。高電為平按鍵默認狀態,按鍵一旦按下,“按下事件”就發生了,電平隨之發生抖動,抖動周期大約為10ms。事后,如果按鍵依然按着不放,那么電平便會處於低電平。換之,如果按鍵此刻被釋放,那么“釋放事件”發生了,電平隨之由低變高,然后發生抖動,抖動周期大約為10ms。筆者曾在前面說過,按鍵消抖組一般有3個工作要做,亦即檢測電平變化,過濾抖動,還有產生有效按鍵。
檢測電平變化:
圖2.2 按鍵電平變化,按下事件與釋放事件。
顧名思義,檢測電平變化就是用來察覺“按下事件”還有“釋放事件”,或者監控電平的狀態變化。如圖2.2所示,筆者建立一組寄存器F1~F2,F1暫存當前的電平狀態,F2則暫存上一個時鍾的電平狀態。Verilog語言可以這樣表示,如代碼2.1所示:
reg F2,F1;
always @ ( posedge CLOCK )
{ F2,F1 } <= { F1,KEY };
代碼2.1所示
根據圖2.2的顯示,按下事件是 F2 為1值,F1為0值;釋放事件則是 F2為0值,F1為1值。為了不要錯過電平變化的黃金時間,“按下事件”還有“釋放事件”必須作為“即時“,為此 Verilog語言可以這樣表示:
wire isH2L = ( F2 == 1 && F1 == 0 );
wire isL2H = ( F2 == 0 && F1 == 1 );
過濾抖動:
圖2.3 過濾抖動。
過濾抖動就是也可以稱為延遲抖動,常規又廉價的機械按鍵,抖動時間大約是10ms之內,如圖2.3.所示。抖動一般都發生在“按下事件”或者“釋放事件”以后,過濾抖動就是延遲諾干時間即可。Verilog語言則可以這樣表示,如代碼2.2所示:
3:
if( C1 == T10MS -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
代碼2.2
產生有效按鍵:
圖2.4 產生有效按鍵。
產生有效按鍵亦即立旗有效的按鍵事件,如圖2.4所示,按下有效isPress信號,還有釋放有效isRelease信號,兩個信號分別都是拉高一個時鍾。除了常見的按下有效或者釋放有效以外,根據設計要求,有效按鍵按也有其它,例如:按鍵按下兩下有效(雙擊),按鍵按下一段時間有效(長擊)。至於Verilog語言則可以這樣表示,如代碼2.3所示:
1: begin isPress <= 1'b1; i <= i + 1'b1; end
2: begin isPress <= 1'b0; i <= i + 1'b1; end
...
1: begin isRelease <= 1'b1; i <= i + 1'b1; end
2: begin isRelease <= 1'b0; i <= i + 1'b1; end
代碼2.3
熱身完畢后,我們就可以進入實驗主題了。
圖2.5 實驗二建模圖。
如圖2.5所示,哪里有一塊名為 key_funcmod 的功能模塊,輸入端為 KEY信號,輸出端卻為 LED信號。KEY信號連接按鍵資源,LED信號分別驅動兩位LED資源。按鍵功能模塊的工作主要是過濾 KEY信號變化以后所發生的抖動,接着產生“按下有效”還有“釋放有效”兩個有效按鍵,然后點亮LED資源。
key_funcmod.v
1. module key_funcmod
2. (
3. input CLOCK, RESET,
4. input KEY,
5. output [1:0]LED
6. );
以上內容為出入端聲明。
7. parameter T10MS = 19'd500_000;
8.
9. /***************************/ //sub
10.
11. reg F2,F1;
12.
13. always @ ( posedge CLOCK or negedge RESET )
14. if( !RESET )
15. { F2, F1 } <= 2'b11;
16. else
17. { F2, F1 } <= { F1, KEY };
18.
19. /***************************/ //core
20.
21. wire isH2L = ( F2 == 1 && F1 == 0 );
22. wire isL2H = ( F2 == 0 && F1 == 1 );
以上內容為常量聲明以及電平檢測的周邊操作。第7行則是10ms的常量聲明,第11~17則是電平狀態檢測的周邊操作。至於第21~22行則是“按下事件”還有“釋放事件”的即時聲明。
23. reg [3:0]i;
24. reg isPress, isRelease;
25. reg [18:0]C1;
26.
27. always @ ( posedge CLOCK or negedge RESET )
28. if( !RESET )
29. begin
30. i <= 4'd0;
31. { isPress,isRelease } <= 2'b00;
32. C1 <= 19'd0;
33. end
34. else
以上內容為相關的寄存器聲明以及復位操作。i用來指向步驟,isPress與 isRelease則標示按下有效亦即釋放有效,C1則用來計數。
35. case(i)
36.
37. 0: // H2L check
38. if( isH2L ) i <= i + 1'b1;
39.
40. 1: // H2L debounce
41. if( C1 == T10MS -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
42. else C1 <= C1 + 1'b1;
43.
44. 2: // Key trigger prees up
45. begin isPress <= 1'b1; i <= i + 1'b1; end
46.
47. 3: // Key trigger prees down
48. begin isPress <= 1'b0; i <= i + 1'b1; end
49.
50. 4: // L2H check
51. if( isL2H ) i <= i + 1'b1;
52.
53. 5: // L2H debounce
54. if( C1 == T10MS -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
55. else C1 <= C1 + 1'b1;
56.
57. 6: // Key trigger prees up
58. begin isRelease <= 1'b1; i <= i + 1'b1; end
59.
60. 7: // Key trigger prees down
61. begin isRelease <= 1'b0; i <= 4’d0; end
62.
63. endcase
以上內容為核心操作。具體的核心操作過程如下:
步驟 0 等待電平由高變低;
步驟 2 用來過濾由高變低所引發的抖動;
步驟 1~3 用來產生按下有效的高脈沖;
步驟 4 等待電平由低變高;
步驟 5 用來過濾由低變高所引發的抖動;
步驟 6~7 用來產生釋放有效的高脈沖,然后返回步驟 0。
64.
65. /***********************/ // sub-demo
66.
67. reg [1:0]D1;
68.
69. always @ ( posedge CLOCK or negedge RESET )
70. if( !RESET )
71. D1 <= 2'b00;
72. else if( isPress )
73. D1[1] <= ~D[1];
74. else if( isRelease )
75. D1[0] <= ~D[0];
76.
77. assign LED = D1;
78.
79. endmodule
以上內容為演示用的周邊操作,它根據 isPress 還有 isRelease 的高脈沖,分別翻轉 D1[1] 還有 D1[0]的內容。至於第77行則是輸出驅動的聲明,D1驅動LED輸出端。編譯完后便下載程序。
我們會發現第一次按下 <KEY2> 會點亮 LED[1],釋放<KEY2>會點亮 LED[0]。第二次按下 <KEY2> 會消滅 LED[1],釋放 <KEY2> 則會消滅 LED[0]。如此一來,實驗二已經成功。實驗二未結束之前,讓筆者分析一下實驗二的諾干細節:
細節一:過分消抖
圖2.6 過分消抖(過分延遲)。
結果如圖2.6所示,假設筆者手癢將消抖時間拉長至1s,Verilog語言則可以這樣表示,如代碼2.4所示:
3:
if( C1 == T1S -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
代碼2.4
一般消抖期間,按鍵功能模塊的核心操作就會停留在消抖(延遲)步驟,如果消抖期間筆者釋放按鍵,那么釋放事件會被無視,然后核心操作會被打亂,最后整個功能模塊會跑飛。反之,如果筆者等待消抖完畢再釋放按鍵,那么釋放事件會照常發生,整個功能模塊也會照常運作。
細節二:精密控時
if( C1 == T10MS -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
2: // Key trigger prees up
begin isPress <= 1'b1; i <= i + 1'b1; end
3: // Key trigger prees down
begin isPress <= 1'b0; i <= i + 1'b1; end
代碼2.5
根據按鍵功能模塊,核心操作執行消抖之后都會產生有效按鍵,亦即為立旗寄存器
(isPress 或者 isRelease)拉高又拉低一個時鍾,如代碼 2.5 所示。如果筆者是一位精
密控時的狂人,這段拉高又拉低的時鍾消耗,筆者也會將其考慮進去消抖時間。為此,
筆者可以這樣修改,如代碼 2.6 所示:
1: // H2L debounce
if( C1 == T10MS -1 -2 ) begin C1 <= 19'd0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
2: // Key trigger prees up
begin isPress <= 1'b1; i <= i + 1'b1; end
3: // Key trigger prees down
begin isPress <= 1'b0; i <= i + 1'b1; end
代碼2.6
代碼2.6相較代碼2.6,消抖步驟部分的if判斷內多了 -2,其中 -2表示產生按鍵有效所消耗的時鍾。
細節三:完整的個體模塊
圖2.7 完整的按鍵功能模塊。
圖2.6是演示用的建模圖,然而圖2.7則是完整的建模圖,其中按鍵功能模塊有一個 KEY輸入端,主要連接按鍵資源。此外,按鍵功能模塊也有一組兩位的溝通信號Trig,亦即按下Trig[1]產生一個高脈沖,釋放Trig[0]產生一個高脈沖。
key_funcmod.v
2. (
3. input CLOCK, RESET,
4. input KEY,
5. output [1:0]oTrig
6. );
7. parameter T10MS = 19'd500_000;
8.
9. /***************************/ //sub
10.
11. reg F2,F1;
12.
13. always @ ( posedge CLOCK or negedge RESET )
14. if( !RESET )
15. { F2, F1 } <= 2'b11;
16. else
17. { F2, F1 } <= { F1, KEY };
18.
19. /***************************/ //core
20.
21. wire isH2L = ( F2 == 1 && F1 == 0 );
22. wire isL2H = ( F2 == 0 && F1 == 1 );
23. reg [3:0]i;
24. reg isPress, isRelease;
25. reg [18:0]C1;
26.
27. always @ ( posedge CLOCK or negedge RESET )
28. if( !RESET )
29. begin
30. i <= 4'd0;
31. { isPress,isRelease } <= 2'b00;
32. C1 <= 19'd0;
33. end
34. else
35. case(i)
36.
37. 0: // H2L check
38. if( isH2L ) i <= i + 1'b1;
39.
40. 1: // H2L debounce
41. if( C1 == T10MS -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
42. else C1 <= C1 + 1'b1;
43.
44. 2: // Key trigger prees up
45. begin isPress <= 1'b1; i <= i + 1'b1; end
46.
47. 3: // Key trigger prees down
48. begin isPress <= 1'b0; i <= i + 1'b1; end
49.
50. 4: // L2H check
51. if( isL2H ) i <= i + 1'b1;
52.
53. 5: // L2H debounce
54. if( C1 == T10MS -1 ) begin C1 <= 19'd0; i <= i + 1'b1; end
55. else C1 <= C1 + 1'b1;
56.
57. 6: // Key trigger prees up
58. begin isRelease <= 1'b1; i <= i + 1'b1; end
59.
60. 7: // Key trigger prees down
61. begin isRelease <= 1'b0; i <= 4’d0; end
62.
63. endcase
64.
65. /********************************/
66.
67. assign oTrig = { isPress, isRelease };
68.
69. endmodule
最后別向筆者要仿真了,因為按鍵消抖沒有仿真的意義,單是代碼已經足夠腦補時序了。