本帖最后由 王朗的誘惑 于 2020-4-14 22:26 編輯
前接[失敗]狀態(tài)機+事件驅(qū)動 按鍵掃描。(如果懶得看前面的內(nèi)容可以直接看本帖,無妨)
繼2018年10月實驗過一個按鍵掃描失敗,時隔將近一年半,后續(xù)它終于來了!
這次換用了IAP15W4K61S4,把之前的程序搞好了。必須要承認一個錯誤:之前單片機死機其實不是單片機速度問題,而是程序設(shè)計問題[慚愧……],現(xiàn)在已經(jīng)解決。這次的程序添加了事件隊列,進一步釋放定時器資源。定時器只負責(zé)添加任務(wù),由主程序清空任務(wù)隊列+執(zhí)行事件函數(shù)。所以芯片慢點也沒關(guān)系啦。
正文狀態(tài)機、事件隊列 狀態(tài)機還是挺常見,不過為什么要用事件隊列呢?因為中斷函數(shù)不能拖得太長,如果定時器中斷執(zhí)行的事件函數(shù)還沒完,下一次中斷又來了可怎么辦……所以加入事件隊列機制,中斷只負責(zé)把需要執(zhí)行的事件入隊,由主函數(shù)執(zhí)行并清空隊列里面的事件函數(shù),也算是某種“異步”了吧。 假如主函數(shù)清空隊列的速度還沒有定時器入隊快,那么隊列滿了以后,之后的按鍵事件直接拋棄,不會響應(yīng)。 參考資料資源占用功能說明支持button和switch 支持button單擊、多擊、長按、多按鍵組合 支持任意IO的按鍵連接
文件說明使用方法以下步驟在主函數(shù)main.c中操作。 把KeyScan.h, KeyScan.c, KeyScanConfig.h放到工程目錄下。 在主函數(shù)中引入頭文件 - #include "main.h"
- #include "Uart.h"
- #include "KeyScan.h"
復(fù)制代碼main.h包含基本數(shù)據(jù)類型的typedef定義,一些C庫的頭文件引入和系統(tǒng)時鐘設(shè)置; Uart.h是單片機串口模塊頭文件; KeyScan.h是按鍵掃描模塊頭文件。 假如現(xiàn)在有4個按鍵A,B,C,D,給按鍵編號,放到枚舉類型里。 - enum EnumUserKey{ //按鍵編號 從0開始 不得超過(MAX_KEY_NUMBER-1)
- EnumKey_A = 0,
- EnumKey_B = 1,
- EnumKey_C = 2,
- EnumKey_D = 3
- };
復(fù)制代碼
定義按鍵相關(guān)的兩個結(jié)構(gòu)體,作為全局變量。GPIO_KEY_NUM是第3步中按鍵的數(shù)量,這里是4;FUNC_KEY_NUM是第5步中事件函數(shù)的數(shù)量,這里是3,功能與按鍵是獨立的,數(shù)量可以不相等。 - #define GPIO_KEY_NUM 4 // 按鍵總數(shù),即enum EnumUserKey定義的按鍵數(shù)量
- xdata KeyIO_t SingleKey[GPIO_KEY_NUM]; // 按鍵IO數(shù)組
- #define FUNC_KEY_NUM 3 // 用戶自定義的功能總數(shù)
- xdata KeyFunc_t KeyFuncs[FUNC_KEY_NUM]; // 按鍵功能數(shù)組
復(fù)制代碼
定義按鍵功能函數(shù)。比如,需要按鍵A,B單擊分別觸發(fā),按鍵C,D同時按下觸發(fā),功能函數(shù)可以定義成下面這樣,函數(shù)名字隨意。 - void KeyAPressEvent(void){
- P40 = ~P40;
- }
- void KeyBPressEvent(void){
- Delay100ms();
- }
- void KeyCDPressEvent(void){
- P41 = ~P41;
- // printf發(fā)送長串被中斷打斷會死機,使用UartSendString
- // 如果很短可以使用printf
- UartSendString("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
- Delay100ms(); // 長延時也不會死機了,哈哈
- UartSendString("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
- Delay100ms();
- }
復(fù)制代碼
好的,現(xiàn)在按鍵有了,功能也有了,但是還沒聯(lián)系到一起。下面是按鍵掃描初始化函數(shù),把它們聯(lián)系起來。 - //按鍵掃描初始化
- void KeyInit(void){
- u8 i;
- // 函數(shù)指針必須全部初始化為NULL
- for(i=0; i<FUNC_KEY_NUM; i++){
- KeyFuncs.fp_singleClick = NULL;
- KeyFuncs.fp_comboClick = NULL;
- KeyFuncs.fp_longPress = NULL;
- KeyFuncs.fp_multiPress = NULL;
- }
-
- // 注冊按鍵 Port1必須是IO口 Port2是IO口或"GND"
- SingleKey[EnumKey_A].IOPort1 = "P36"; SingleKey[EnumKey_A].IOPort2 = "GND";
- SingleKey[EnumKey_B].IOPort1 = "P52"; SingleKey[EnumKey_B].IOPort2 = "GND";
- SingleKey[EnumKey_C].IOPort1 = "P54"; SingleKey[EnumKey_C].IOPort2 = "GND";
- SingleKey[EnumKey_D].IOPort1 = "P53"; SingleKey[EnumKey_D].IOPort2 = "GND";
-
- // 需要響應(yīng)的鍵值 注意是鍵值! 不是鍵編號! 組合按鍵用或
- KeyFuncs[0].triggerValue = TRIGGER_VALUE(EnumKey_A);
- // 注冊回調(diào)函數(shù)為單擊功能
- KeyFuncs[0].fp_singleClick = KeyAPressEvent;
-
- // 需要響應(yīng)的鍵值 注意是鍵值! 不是鍵編號! 組合按鍵用或
- KeyFuncs[1].triggerValue = TRIGGER_VALUE(EnumKey_B);
- // 注冊回調(diào)函數(shù)為單擊功能
- KeyFuncs[1].fp_singleClick = KeyBPressEvent;
-
- // 需要響應(yīng)的鍵值 注意是鍵值! 不是鍵編號! 組合按鍵用或
- KeyFuncs[2].triggerValue = TRIGGER_VALUE(EnumKey_C) | TRIGGER_VALUE(EnumKey_D);
- // 注冊回調(diào)函數(shù)為組合鍵功能
- KeyFuncs[2].fp_multiPress = KeyCDPressEvent;
-
- KeyScanInit((KeyIO_t*)&SingleKey, GPIO_KEY_NUM, (KeyFunc_t*)&KeyFuncs, FUNC_KEY_NUM);
- }
復(fù)制代碼初始化過程可以分為4個步驟:
初始化函數(shù)指針為NULL,這里的函數(shù)指針變量來自第4步定義的xdata KeyFunc_t KeyFuncs[FUNC_KEY_NUM] 告訴單片機按鍵的硬件連線位置,假如按鍵A,B,C,D的一端分別連到單片機的P36,P52,P54,P53上,另一端接地,就按照上面的程序設(shè)置。如果按鍵是矩陣的,沒有接地,就把按鍵兩端的IO都對應(yīng)寫成字符串。
(這么做的好處就是可以把按鍵隨便亂接,畢竟有的封裝,比如SOP-16是沒有完整的一組8bit IO引出的,假如在這個單片機上用傳統(tǒng)的方式應(yīng)用4×4矩陣鍵盤,位處理是不是特別難受?)
這一步把按鍵和一個事件函數(shù)聯(lián)系起來。 需要用到TRIGGER_VALUE宏,把按鍵編號轉(zhuǎn)換成觸發(fā)值,如果用到組合鍵,把各個按鍵的觸發(fā)值用|連接即可。 還需要注意的就是按鍵的功能是靠結(jié)構(gòu)體成員的名字來區(qū)分的,有fp_singleClick, fp_comboClick, fp_longPress, fp_multiPress共4種,給哪個賦值,對應(yīng)的事件函數(shù)就是什么功能。 把剛才設(shè)置好的結(jié)構(gòu)體給到按鍵掃描程序,開始按鍵掃描。
編寫主函數(shù),把KeyInit()放到初始化里,把KeyEventProcess()放到while(1)里。 - void main(){
- EA = 1;
- UartInit();
- KeyInit(); //按鍵掃描初始化
- // printf發(fā)送長串被中斷打斷會死機,使用UartSendString
- // printf("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
- UartSendString("testtesttesttesttesttesttesttesttesttesttesttesttest\r\n");
- while(1){
- KeyEventProcess();
- }
- }
復(fù)制代碼
KeyEventProcess()檢查事件隊列,執(zhí)行隊列里所有函數(shù)。如果隊列滿了,那么再有按鍵事件的話,就會被忽略。所以,慢點按,哈哈。(其實單片機速度夠,快點按也沒事,而且隊列長度能改,在KeyScanConfig.h里)
注意事項printf發(fā)送字符串有bug。 具體發(fā)生在printf發(fā)送過程中被中斷打斷的時候,現(xiàn)象是死機,串口不斷發(fā)送亂碼。調(diào)節(jié)串口中斷優(yōu)先級為最高沒有改善。 如果沒有中斷打斷,不會有bug。 系統(tǒng)主頻率在main.h里對應(yīng)實際主頻率修改。 串口波特率在Uart.h里對應(yīng)實際波特率修改。 目前僅在IAP15W4K61S4上實驗,15系列的單片機可以直接用,如果更換其他系列單片機需要修改Timer0的初始化和中斷函數(shù)。(在KeyScan.c里定義) 編譯報了3個警告,不影響。
其他害,原諒我偷懶下吧……其實文檔已經(jīng)在github上面寫好了,這里不知道怎么用markdown編輯器,搞得排版很亂……所以剩下的設(shè)計思路參考Wiki吧。
|