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