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