關(guān)注公眾號,回復(fù)“入門資料”獲取單片機入門到高級開掛教程
?開發(fā)板帶你入門,我們帶你飛
文 | 無際(微信:2777492857)
全文約4589字,閱讀大約需要?15?分鐘
說起按鍵檢測,在座的各位,哪個沒被它折磨過?
? ? ? ? ?? ?
我剛?cè)腴T時,為了實現(xiàn)一個簡單的按鍵功能,硬生生寫了幾十行代碼,各種?if...else?嵌套,邏輯繞得我自己都頭暈。
? ? ? ? ?? ?
更可氣的是,辛辛苦苦寫完,一測試,發(fā)現(xiàn)按鍵跟“抽風(fēng)”似的,時不時失靈,有時候按一下,它給我響應(yīng)好幾次,好想把板子砸了!
? ? ? ? ?? ?
后來我才知道,這叫按鍵抖動,是硬件的“鍋”,得軟件來“背”。為了解決這個問題,我嘗試了各種方法,什么延時消抖、多次采樣,但效果都不盡如人意。要么系統(tǒng)響應(yīng)變慢,要么代碼臃腫不堪,維護起來簡直是噩夢。
? ? ? ? ?? ?
如果你也曾被按鍵檢測折磨得死去活來,那么恭喜你,這篇文章將帶你實現(xiàn)一個穩(wěn)定性高,擴展性強的按鍵框架。
? ? ? ? ?? ?
當然,更重要的是提供一種編程思維,起到拋磚引玉的作用,掌握這種思維,才能萬變不離其宗,適應(yīng)千變?nèi)f化的需求。
? ? ? ? ?? ?
1.?C語言里的面向?qū)ο?/span>
這個方法的核心是用面向?qū)ο蠛蜖顟B(tài)機。
? ? ? ? ?? ?
先聊聊面向?qū)ο?OOP)的核心是啥?是把數(shù)據(jù)和操作數(shù)據(jù)的代碼打包在一起,形成一個“對象”,然后讓這些對象自己管自己。
? ? ? ? ?? ?
C語言雖然沒有class關(guān)鍵字,但我們可以用結(jié)構(gòu)體(struct)和函數(shù)指針來山寨一個面向?qū)ο蟮娘L(fēng)格。
? ? ? ? ?? ?
我們用一個結(jié)構(gòu)體來定義這個按鍵對象:
typedef?struct?{? ??int?pin; ? ? ? ? ??// 按鍵連的引腳? ??int?state; ? ? ? ??// 當前狀態(tài)? ??void?(*press)(); ??// 按下時調(diào)用的函數(shù)? ??void?(*release)();?// 松開時調(diào)用的函數(shù)} Key;
看到?jīng)]?pin和state是按鍵的“個人信息”,而press和release是函數(shù)指針,指向按鍵的行為。
? ? ? ? ?? ?
啥是函數(shù)指針?簡單說,就是一個“遙控器”,你告訴它按下時要干啥,它就去干啥。
? ? ? ? ?? ?
比如,按鍵1按下時點亮LED,按鍵2按下時播放音樂,完全可以各干各的,不用擠在一個大函數(shù)里亂七八糟地if-else。
? ? ? ? ?? ?
這樣設(shè)計的好處是啥?每個按鍵都是一個獨立的對象,你可以隨便創(chuàng)建多少個,互不干擾。以后加個新按鍵,直接復(fù)制粘貼一個Key實例,改改引腳和回調(diào)函數(shù)就行,代碼復(fù)用性拉滿。
? ? ? ? ?? ?
2.?狀態(tài)機
光有面向?qū)ο筮€不夠,按鍵的狀態(tài)變化得有個清晰的邏輯。這時候,狀態(tài)機就派上用場了。? ??
? ? ? ? ?? ?
狀態(tài)機是個啥?它就像一個流程圖,告訴你系統(tǒng)當前在哪個狀態(tài),下一步能去哪兒,全程有條不紊。
? ? ? ? ?? ?
對于按鍵檢測,我們可以定義幾個常見狀態(tài):
?Idle(空閑):按鍵沒被按下,啥也沒發(fā)生。
?Pressed(按下):按鍵剛被按下,可能需要去抖動。
?Hold(持續(xù)按下):按鍵按著不放,可能是長按。
?Released(釋放):按鍵被松開,結(jié)束一次操作。
這些狀態(tài)之間怎么跳來跳去呢?靠輸入信號決定。
? ? ? ? ?? ?
比如,Idle狀態(tài)下檢測到引腳變低(按下),就跳到Pressed;Pressed狀態(tài)下如果一直低電平,就進入Hold;Hold狀態(tài)下引腳變高(松開),就跳到Released,最后回到Idle。
? ? ? ? ?? ?
聽起來是不是有點像玩“超級馬里奧”,每踩一個磚頭就換個場景?
? ? ? ? ?? ?
用狀態(tài)機的好處是啥?邏輯清晰啊!你不用寫一堆嵌套的if-else去判斷按了多久?抖不抖之類的問題,狀態(tài)機自帶"導(dǎo)航",每一步該干啥一目了然。
? ? ? ? ?? ?
3.?組合拳:面向?qū)ο?狀態(tài)機設(shè)計按鍵檢測
好了,現(xiàn)在我們把面向?qū)ο蠛蜖顟B(tài)機捏到一起,設(shè)計一個通用的按鍵檢測模塊。
? ? ? ? ?? ?
思路是這樣的:用結(jié)構(gòu)體定義按鍵對象,用狀態(tài)機控制它的行為。
? ? ? ? ?? ?
第一步:定義狀態(tài)和結(jié)構(gòu)體
先用枚舉定義狀態(tài):
typedef?enum?{? ? IDLE, ? ? ??// 空閑? ? PRESSED, ? ?// 按下? ? HOLD, ? ? ??// 持續(xù)按下? ? RELEASED ? ?// 釋放} KeyState;
?? ?
然后,稍微升級一下之前的Key結(jié)構(gòu)體,把狀態(tài)加上:
typedef?struct?{? ??int?pin; ? ? ? ? ? ?// 引腳? ? KeyState state; ? ??// 當前狀態(tài)? ??void?(*onPress)(); ?// 按下回調(diào)? ??void?(*onRelease)();?// 釋放回調(diào)} Key;
第二步:寫狀態(tài)更新函數(shù)
? ? ? ? ?? ?
接下來,核心是個updateKeyState函數(shù),負責根據(jù)引腳信號更新狀態(tài)。
? ? ? ? ?? ?
我們假設(shè)有個readPin函數(shù)能讀引腳狀態(tài)(高電平表示沒按,低電平表示按下),邏輯如下:? ??
void?updateKeyState(Key *key)?{? ? int currentInput =?readPin(key->pin);?// 讀取引腳狀態(tài)? ??switch?(key->state)?? ? {? ? ? ??case?IDLE:? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? {?// 低電平,按下? ? ? ? ? ? ? ? key->state =?PRESSED;? ? ? ? ? ? ? ??if?(key->onPress) key->onPress();? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?PRESSED:? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? {?// 還是按著? ? ? ? ? ? ? ? key->state =?HOLD;? ? ? ? ? ? }?else?? ? ? ? ? ? {?// 松開了? ? ? ? ? ? ? ? key->state =?RELEASED;? ? ? ? ? ? ? ??if?(key->onRelease) key->onRelease();? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?HOLD:? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? {?// 松開了? ? ? ? ? ? ? ? key->state =?RELEASED;? ? ? ? ? ? ? ??if?(key->onRelease) key->onRelease();? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?RELEASED:? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? {?// 確認松開? ? ? ? ? ? ? ? key->state =?IDLE;? ? ? ? ? ? }? ? ? ? ? ??break;? ? }}
這個函數(shù)干了啥?它每次被調(diào)用時,檢查當前引腳狀態(tài),然后根據(jù)狀態(tài)機規(guī)則跳到下一個狀態(tài),順便觸發(fā)對應(yīng)的回調(diào)函數(shù)。
? ? ? ? ?? ?
比如,從IDLE到PRESSED時調(diào)用onPress,從HOLD到RELEASED時調(diào)用onRelease。
? ? ? ? ?? ?
簡單吧?但已經(jīng)能處理基本的按下和釋放了。
? ? ? ? ?? ?
? ? ? ? ?? ?
第三步:讓代碼跑起來
咱們寫個例子跑跑看。
? ? ? ? ?? ?
假設(shè)有兩個按鍵:
一個連在P1引腳,按下時打印“開燈”,松開時打印“關(guān)燈”。
另一個連在P2引腳,按下時打印“啟動”,松開時打印“停止”。
? ? ? ? ?? ?
先定義回調(diào)函數(shù):
void?key1Press()?{? ??printf("開燈\n");}void?key1Release()?{? ??printf("關(guān)燈\n");}void?key2Press()?{? ??printf("啟動\n");}void?key2Release()?{? ??printf("停止\n");}
? ? ? ? ?? ?
然后初始化兩個按鍵對象:
Key?key1?=?{P1, IDLE, key1Press, key1Release};Key?key2?=?{P2, IDLE, key2Press, key2Release};
? ? ? ? ?? ?
主循環(huán)里定期更新狀態(tài):
void?main()?{? ??while?(1)?? ? {? ? ? ??updateKeyState(&key1);? ? ? ??updateKeyState(&key2);? ? ? ??delay(10);?// 每10ms檢查一次? ? }}
跑起來后,按下P1會打印“開燈”,松開打印“關(guān)燈”,P2同理。
? ? ? ? ?? ?
每個按鍵獨門獨戶,互不干擾,想加第三個按鍵?再定義一個Key key3就行,so easy!
? ? ? ? ?? ?
? ? ? ? ?? ?
3.1?加點料:支持長按功能
這時候你可能會問:“這也太基礎(chǔ)了吧,長按咋辦?”別急,面向?qū)ο蟮暮锰幘驮谟跀U展性強。
? ? ? ? ?? ?
咱們給Key結(jié)構(gòu)體加個計時器,專門記錄按下的時間:
typedef?struct?{? ??int?pin;? ? KeyState state;? ??int?holdTime; ? ? ??// 按下持續(xù)時間? ??void?(*onPress)();? ??void?(*onHold)(); ??// 長按回調(diào)? ??void?(*onRelease)();} Key;
然后改一下updateKeyState,增加長按邏輯:? ??
void?updateKeyState(Key *key)?{? ? int currentInput =?readPin(key->pin);? ??switch?(key->state)?? ? {? ? ? ??case?IDLE:? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? {? ? ? ? ? ? ? ? key->state =?PRESSED;? ? ? ? ? ? ? ? key->holdTime =?0;? ? ? ? ? ? ? ??if?(key->onPress) key->onPress();? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?PRESSED:? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? {? ? ? ? ? ? ? ? key->holdTime +=?10;?// 每次循環(huán)加10ms? ? ? ? ? ? ? ??if?(key->holdTime >=?1000)?? ? ? ? ? ? ? ? {?// 按了1秒算長按? ? ? ? ? ? ? ? ? ? key->state =?HOLD;? ? ? ? ? ? ? ? ? ??if?(key->onHold) key->onHold();? ? ? ? ? ? ? ? }? ? ? ? ? ? }?else?? ? ? ? ? ? {? ? ? ? ? ? ? ? key->state =?RELEASED;? ? ? ? ? ? ? ??if?(key->onRelease) key->onRelease();? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?HOLD:? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? {? ? ? ? ? ? ? ? key->state =?RELEASED;? ? ? ? ? ? ? ??if?(key->onRelease) key->onRelease();? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?RELEASED:? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? {? ? ? ? ? ? ? ? key->state =?IDLE;? ? ? ? ? ? }? ? ? ? ? ??break;? ? }}
? ? ? ? ?? ?
現(xiàn)在,按下不到1秒是短按,超過1秒觸發(fā)長按回調(diào)。想改成2秒?把1000改成2000就行。這種設(shè)計改起來是不是跟玩似的?
? ? ? ? ?? ?
? ? ? ? ?? ?
3.2?去抖動怎么辦?
說到按鍵檢測,繞不過去抖動問題。機械按鍵按下或松開時,信號會抖個幾毫秒到幾十毫秒。
? ? ? ? ?? ?
咋解決?其實狀態(tài)機已經(jīng)幫我們鋪好路了。
? ? ? ? ?? ?
去抖動的核心思想是:在檢測到按鍵狀態(tài)變化后,不立即做出反應(yīng),而是等待一段時間(通常為10ms到50ms,稱為“去抖動時間”),然后再次檢測按鍵狀態(tài),以確認變化是真實的。這樣可以過濾掉抖動帶來的短暫信號波動。
? ? ? ? ?? ?
不過為了保持通用性,咱們可以把去抖動時間做成參數(shù),加到Key結(jié)構(gòu)體里,留給用戶自己調(diào)。? ??
? ? ? ? ?? ?
下面繼續(xù)完善下代碼:
3.2.1?狀態(tài)定義:
為了支持去抖動,我們擴展狀態(tài)機的狀態(tài),加入DEBOUNCE_PRESSED和DEBOUNCE_RELEASED兩個去抖動狀態(tài):
typedef?enum?{? ? IDLE, ? ? ? ? ? ? ??// 空閑? ? DEBOUNCE_PRESSED, ??// 按下去抖動? ? PRESSED, ? ? ? ? ? ?// 按下? ? HOLD, ? ? ? ? ? ? ??// 持續(xù)按下? ? DEBOUNCE_RELEASED, ?// 釋放去抖動? ? RELEASED ? ? ? ? ? ?// 釋放} KeyState;
? ? ? ? ?? ?
3.2.2?Key結(jié)構(gòu)體
在Key結(jié)構(gòu)體中加入去抖動計時器debounceTimer,用于跟蹤等待時間:
typedef?struct?{? ??int?pin; ? ? ? ? ? ?// 引腳? ? KeyState state; ? ??// 當前狀態(tài)? ??int?debounceTimer; ?// 去抖動計時器(單位:ms)? ??int?holdTime; ? ? ??// 按下持續(xù)時間(單位:ms)? ??void?(*onPress)(); ?// 按下回調(diào)函數(shù)? ??void?(*onHold)(); ??// 長按回調(diào)函數(shù)? ??void?(*onRelease)();?// 釋放回調(diào)函數(shù)} Key;
? ? ? ? ?? ?
3.2.3?更新狀態(tài)函數(shù)
以下是實現(xiàn)去抖動的updateKeyState函數(shù),假設(shè)按鍵低電平表示按下,高電平表示松開:
void?updateKeyState(Key *key)?{? ??int?currentInput =?readPin(key->pin);?// 讀取當前引腳狀態(tài)(0為按下,1為松開)? ??switch?(key->state)?? ? {? ? ? ??case?IDLE:? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? {?// 檢測到按下? ? ? ? ? ? ? ? key->state = DEBOUNCE_PRESSED;? ? ? ? ? ? ? ? key->debounceTimer =?0;?// 重置去抖動計時器? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?DEBOUNCE_PRESSED:? ? ? ? ? ? key->debounceTimer +=?10;?// 假設(shè)每次循環(huán)10ms? ? ? ? ? ??if?(key->debounceTimer >= DEBOUNCE_TIME)?? ? ? ? ? ? {?// 去抖動時間到? ? ? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? ? ? {?// 確認按下? ? ? ? ? ? ? ? ? ? key->state = PRESSED;? ? ? ? ? ? ? ? ? ? key->holdTime =?0;?// 重置長按計時器? ? ? ? ? ? ? ? ? ??if?(key->onPress) key->onPress();?// 觸發(fā)按下回調(diào)? ? ? ? ? ? ? ? }?else?? ? ? ? ? ? ? ? {?// 是抖動? ? ? ? ? ? ? ? ? ? key->state = IDLE;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?PRESSED:? ? ? ? ? ??if?(currentInput ==?0)?? ? ? ? ? ? {?// 持續(xù)按下? ? ? ? ? ? ? ? key->holdTime +=?10;?// 累加按下時間? ? ? ? ? ? ? ??if?(key->holdTime >=?1000)?? ? ? ? ? ? ? ? {?// 長按1秒? ? ? ? ? ? ? ? ? ? key->state = HOLD;? ? ? ? ? ? ? ? ? ??if?(key->onHold) key->onHold();?// 觸發(fā)長按回調(diào)? ? ? ? ? ? ? ? }? ? ? ? ? ? }?else?? ? ? ? ? ? {?// 檢測到松開? ? ? ? ? ? ? ? key->state = DEBOUNCE_RELEASED;? ? ? ? ? ? ? ? key->debounceTimer =?0;?// 重置去抖動計時器? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?HOLD:? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? {?// 檢測到松開? ? ? ? ? ? ? ? key->state = DEBOUNCE_RELEASED;? ? ? ? ? ? ? ? key->debounceTimer =?0;? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?DEBOUNCE_RELEASED:? ? ? ? ? ? key->debounceTimer +=?10;? ? ? ? ? ??if?(key->debounceTimer >= DEBOUNCE_TIME)?? ? ? ? ? ? {?// 去抖動時間到? ? ? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? ? ? {?// 確認松開? ? ? ? ? ? ? ? ? ? key->state = RELEASED;? ? ? ? ? ? ? ? ? ??if?(key->onRelease) key->onRelease();?// 觸發(fā)釋放回調(diào)? ? ? ? ? ? ? ? }?else?{?// 是抖動? ? ? ? ? ? ? ? ? ? key->state = PRESSED;?// 返回PRESSED狀態(tài)? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?RELEASED:? ? ? ? ? ??if?(currentInput ==?1)?? ? ? ? ? ? {?// 確認松開? ? ? ? ? ? ? ? key->state = IDLE;?// 返回空閑狀態(tài)? ? ? ? ? ? }? ? ? ? ? ??break;? ? }}
3.2.4?代碼工作流程
3.2.4.1?IDLE(空閑狀態(tài))
?如果檢測到引腳變低(按下),進入DEBOUNCE_PRESSED狀態(tài),重置去抖動計時器。
? ? ? ? ?? ?
3.2.4.2?DEBOUNCE_PRESSED(按下去抖動狀態(tài))
?累加計時器,等待DEBOUNCE_TIME(20ms)。
?時間到后再次檢測引腳:?
?若仍為低電平,確認按下,進入PRESSED狀態(tài)并觸發(fā)onPress回調(diào)。
?若變?yōu)楦唠娖剑J為是抖動,返回IDLE。
? ? ? ? ?? ?
3.2.4.3?PRESSED(按下狀態(tài))
?若引腳持續(xù)低電平,累加holdTime;若達到1秒,進入HOLD狀態(tài)并觸發(fā)onHold回調(diào)。
?若檢測到引腳變高(松開),進入DEBOUNCE_RELEASED狀態(tài)。
? ? ? ? ?? ?
3.2.4.4?HOLD(持續(xù)按下狀態(tài))
?若檢測到引腳變高,進入DEBOUNCE_RELEASED狀態(tài)。
? ? ? ? ?? ?
3.2.4.5?DEBOUNCE_RELEASED(釋放去抖動狀態(tài))
?累加計時器,等待DEBOUNCE_TIME。
?時間到后再次檢測引腳:?
?若仍為高電平,確認松開,進入RELEASED狀態(tài)并觸發(fā)onRelease回調(diào)。
?若變?yōu)榈碗娖剑J為是抖動,返回PRESSED。
? ? ? ? ?? ?
3.2.4.6?RELEASED(釋放狀態(tài))
?若引腳保持高電平,返回IDLE狀態(tài)。
? ? ? ? ?? ?
代碼假設(shè)updateKeyState每10ms調(diào)用一次,debounceTimer每次加10ms。在實際應(yīng)用中,建議使用單片機的硬件定時器以獲得更精確的時間控制。
? ? ? ? ?? ?
4.?這架構(gòu)好在哪里?
? ? ? ? ?? ?
用面向?qū)ο蠛蜖顟B(tài)機搞按鍵檢測,好在哪兒?
? ? ? ? ?? ?
第一,模塊化,每個按鍵自成體系,想加功能只改自己的結(jié)構(gòu)體和回調(diào),不用動全局邏輯。
第二,可讀性高,狀態(tài)機把流程畫得明明白白,比if-else嵌套強多了。
第三,擴展性好,長按、雙擊、組合按,只要加狀態(tài)和變量就能搞定。
? ? ? ? ?? ?
更重要的是,這種思路不只適用于按鍵檢測。LED閃爍、傳感器采集、通信協(xié)議解析,凡是有狀態(tài)變化的模塊,都能套用這個套路。
? ? ? ? ?? ?
學(xué)會了這一招,你寫單片機代碼的水平絕對能上一個臺階,所以在文章開頭,我說這個編程思維能起到拋磚引玉的作用。
? ? ? ? ?? ?
4.最后想和大家說的話
別小看按鍵檢測功能,看似簡單,其實是個磨煉設(shè)計能力的好機會,對編程思維和代碼水平是一個考驗。
? ? ? ? ?? ?
用面向?qū)ο蠛蜖顟B(tài)機,能讓你的代碼從“能跑”變成“跑得好”。
? ? ? ? ?? ?
當然,實際項目里,你可能還得考慮功耗、中斷、定時器精度之類的問題,但核心思路不變:把復(fù)雜問題拆成小塊兒,交給對象和狀態(tài)機去管。
? ? ? ? ?? ?
所以,下次寫代碼時,別再一股腦兒堆if-else了,試試這套“組合拳”,保證你會愛上這種清晰又靈活的感覺。畢竟,好的設(shè)計不僅能解決問題,還能讓你少掉點頭發(fā),對吧?
? ? ? ? ?? ?

? ? ? ? ?? ?end
