一个学妹写的按键检测函数把我秀翻了!
摘要:今年實驗室來了三個學妹,其中一個學妹以前是物聯網專業的,進了實驗室老師二話沒說:先把STM32單片機過一遍
上來第一個例程就是使用按鍵點亮一個LED燈,好家伙。點燈小師弟比較在行,畢竟32、FPGA、Linux的小燈都被小師弟點了一遍。哈哈哈!所以今天還是來說一說按鍵檢測吧!
一、如何進行按鍵檢測
檢測按鍵有中斷方式和GPIO查詢方式兩種。推薦大家用GPIO查詢方式。
1.從裸機的角度分析
中斷方式:中斷方式可以快速地檢測到按鍵按下,并執行相應的按鍵程序,但實際情況是由于按鍵的機械抖動特性,在程序進入中斷后必須進行濾波處理才能判定是否有效的按鍵事件。如果每個按鍵都是獨立的接一個 IO 引腳,需要我們給每個 IO 都設置一個中斷,程序中過多的中斷會影響系統的穩定性。中斷方式跨平臺移植困難。
查詢方式:查詢方式有一個最大的缺點就是需要程序定期的去執行查詢,耗費一定的系統資源。實際上耗費不了多大的系統資源,因為這種查詢方式也只是查詢按鍵是否按下,按鍵事件的執行還是在主程序里面實現。
2.從OS的角度分析
中斷方式:在 OS 中要盡可能少用中斷方式,因為在RTOS中過多的使用中斷會影響系統的穩定性和可預見性。只有比較重要的事件處理需要用中斷的方式。
查詢方式:對于用戶按鍵推薦使用這種查詢方式來實現,現在的OS基本都帶有CPU利用率的功能,這個按鍵FIFO占用的還是很小的,基本都在1%以下。
二、最簡單的按鍵檢測程序
先給他說了一種經典的按鍵檢測代碼,相信大多數人使用按鍵函數都見過它,很簡單就不過多介紹了!
#define?KEY0_PRES?1??//KEY0?? #define?KEY1_PRES?2??//KEY1? #define?WKUP_PRES?3??//WK_UP?u8?KEY_Scan(u8?mode) {??static?u8?key_up=1;//按鍵按松開標志if(mode)key_up=1;??//支持連按????if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)){delay_ms(10);//去抖動?key_up=0;if(KEY0==0)return?KEY0_PRES;else?if(KEY1==0)return?KEY1_PRES;else?if(WK_UP==1)return?WKUP_PRES;?}else?if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1;???????return?0;//?無按鍵按下 }int?main(void) {?u8?t=0;???delay_init();???????//延時函數初始化???LED_Init();???????//初始化與LED連接的硬件接口KEY_Init();???????????//初始化與按鍵連接的硬件接口LED=0;?????//點亮LEDwhile(1){t=KEY_Scan(0);??//得到鍵值switch(t){?????case?KEY0_PRES:??//如果KEY0按下LED=!LED;break;default:delay_ms(10);?}?} }如果你在工作中使用這種代碼,有可能會被同事笑話。當然我這里并不是說這種代碼不好,不管黑貓白貓,能抓住老鼠就是好貓。只要能滿足項目需求實現對應的功能就是好代碼。但是如果你使用下面這種個人感覺可能會更好。
其實也并沒有什么神秘感,就是使用了FIFO機制。參考的就是安富萊的按鍵例程,不過源代碼相對比較復雜,對于初學者并不友好,所以小小的修改了一下,僅供參考!
在前面分享了使用系統滴答定時器實現了多個軟件定時器,在按鍵FIFO中也需要使用這個定時器。在系統的開始我們會啟動一個10ms的軟件定時器。在這個10ms的軟件定時器中不斷的進行按鍵掃描,與其他的任務互不影響。
三、為什么要了解FIFO
要回答什么是FIFO,先要回答為什么要使用FIFO。只有搞清楚使用FIFO的好處,你才會有意無意的使用FIFO。學習FIFO機制和狀態機機制一樣,都是在裸機編程中非常重要的編程思想。編程思想很重要。初級coder總是在關注代碼具體是怎么寫,高級coder關注的是程序的框架邏輯,而不是某個細節。只要你框架邏輯通了,則一通百通。
四、什么是FIFO
FIFO是先入先出的意思,即誰先進入隊列,誰先出去。比如我們需要串口打印數據,當使用緩存將該數據保存的時候,在輸出數據時必然是先進入的數據先出去,那么該如何實現這種機制呢?首先就是建立一個緩存空間,這里假設為10個字節空間進行說明。
從這張圖就知道如果要使用FIFO,就要定義一個結構體,而這個結構體至少應該有三個成員。數組buf、讀指針read、寫指針write。
typedef?struct {uint8_t?Buf[10];??/*?緩沖區?*/uint8_t?Read;???/*?緩沖區讀指針*/uint8_t?Write;???/*?緩沖區寫指針?*/ }KEY_FIFO_T;緩存一開始沒有數據,并且用一個變量write指示下一個寫入緩存的索引地址,這里下一個存放的位置就是0,用另一個變量read 指示下一個讀出緩存的索引地址,并且下一個讀出數據的索引地址也是0。目前隊列中是沒有數據的,也就是不能讀出數據,隊列為空的判斷條件在這里就是兩個索引值相同。
現在開始存放數據:
在這里可以看到隊列中加入了9個數據,并且每加入一個數據后隊尾索引加 1,隊頭不變,這就是數據加入隊列的過程。但是緩存空間只有10個,如何判斷隊列已滿呢?如果只是先一次性加數據到隊列中,然后再讀出數據,那這里的判斷條件顯然是隊尾索引為9。
好了這就是FIFO的基本原理,下面來看一下按鍵FIFO是怎么操作的。
我們這里以5個字節的FIFO空間進行說明。Write變量表示寫位置,Read 變量表示讀位置。初始狀態時,Read = Write = 0。
我們依次按下按鍵 K1,K2,那么FIFO中的數據變為:
如果 Write!= Read,則我們認為有新的按鍵事件。我們通過函數KEY_FIFO_Get()讀取一個按鍵值進行處理后,Read 變量變為 1。Write 變量不變。
繼續通過函數KEY_FIFO_Get()讀取 3 個按鍵值進行處理后,Read 變量變為 4。此時Read = Write= 4。兩個變量已經相等,表示已經沒有新的按鍵事件需要處理。
有一點要特別的注意,如果 FIFO 空間寫滿了,Write 會被重新賦值為 0,也就是重新從第一個字節空間填數據進去,如果這個地址空間的數據還沒有被及時讀取出來,那么會被后來的數據覆蓋掉,這點要引起大家的注意。我們的驅動程序開辟了 10 個字節的 FIFO 緩沖區,對于一般的應用足夠了。
五、按鍵FIFO的優點
可靠地記錄每一個按鍵事件,避免遺漏按鍵事件。特別是需要實現按鍵的按下、長按、自動連發、彈起等事件時。
讀取按鍵的函數可以設計為非阻塞的,不需要等待按鍵抖動濾波處理完畢。
按鍵 FIFO 程序在嘀嗒定時器中定期的執行檢測,不需要在主程序中一直做檢測,這樣可以有效地降低系統資源消耗。
六、按鍵 FIFO 的實現
1.定義結構體
在我們的key.h文件中定義一個結構體類型為KEY_FIFO_T的結構體。就是前面說的那個結構體。這只是類型聲明,并沒有分配變量空間。
typedef?struct {uint8_t?Buf[10];??/*?緩沖區?*/uint8_t?Read;???/*?緩沖區讀指針*/uint8_t?Write;???/*?緩沖區寫指針?*/ }KEY_FIFO_T;接著在key.c 中定義 s_tKey 結構變量, 此時編譯器會分配一組變量空間。
static?KEY_FIFO_T?s_tKey;/*?按鍵FIFO變量,結構體?*/好了按鍵FIFO的結構體數據類型就定義完了,很簡單吧!
2.將鍵值寫入FIFO
既然結構體都定義好了,接著就是往這個FIFO的數組中寫入數據,也就是按鍵的鍵值,用來模擬按鍵的動作了。
/* ********************************************************** *?函?數?名:?KEY_FIFO_Put *?功能說明:?將1個鍵值壓入按鍵FIFO緩沖區。可用于模擬一個按鍵。 *?形????參:??_KeyCode?:?按鍵代碼 *?返?回?值:?無 ********************************************************** */ void?KEY_FIFO_Put(uint8_t?_KeyCode) {s_tKey.Buf[s_tKey.Write]?=?_KeyCode;if?(++s_tKey.Write??>=?KEY_FIFO_SIZE){s_tKey.Write?=?0;} }函數的主要功能就是將按鍵代碼_KeyCode寫入到FIFO中,而這個FIFO就是我們定義結構體的這個數組成員,每寫一次,就是每調用一次KEY_FIFO_Put()函數,寫指針write就++一次,也就是向后移動一個空間,如果FIFO空間寫滿了,也就是s_tKey.Write >= KEY_FIFO_SIZE,Write會被重新賦值為 0。
3.從FIFO讀出鍵值
有寫入鍵值當然就有讀出鍵值。
/* *********************************************************** *?函?數?名:?KEY_FIFO_Get *?功能說明:?從按鍵FIFO緩沖區讀取一個鍵值。 *?形????參:?無 *?返?回?值:?按鍵代碼 ************************************************************ */ uint8_t?KEY_FIFO_Get(void) {uint8_t?ret;if?(s_tKey.Read?==?s_tKey.Write){return?KEY_NONE;}else{ret?=?s_tKey.Buf[s_tKey.Read];if?(++s_tKey.Read?>=?KEY_FIFO_SIZE){s_tKey.Read?=?0;}return?ret;} }如果寫指針和讀出的指針相等,那么返回值就為0,表示按鍵緩沖區為空,所有的按鍵時間已經處理完畢。如果不相等就說明FIFO的緩沖區不為空,將Buf中的數讀出給ret變量。同樣,如果FIFO空間讀完了,沒有緩存了,也就是s_tKey.Read >= KEY_FIFO_SIZE,Read也會被重新賦值為 0。按鍵的鍵值定義在key.h 文件,下面是具體內容:
typedef?enum {KEY_NONE?=?0,???/*?0?表示按鍵事件?*/KEY_1_DOWN,????/*?1鍵按下?*/KEY_1_UP,????/*?1鍵彈起?*/KEY_1_LONG,????/*?1鍵長按?*/KEY_2_DOWN,????/*?2鍵按下?*/KEY_2_UP,????/*?2鍵彈起?*/KEY_2_LONG,????/*?2鍵長按?*/KEY_3_DOWN,????/*?3鍵按下?*/KEY_3_UP,????/*?3鍵彈起?*/KEY_3_LONG,????/*?3鍵長按?*/ }KEY_ENUM;必須按次序定義每個鍵的按下、彈起和長按事件,即每個按鍵對象占用 3 個數值。推薦使用枚舉enum, 不用#define的原因是便于新增鍵值,方便調整順序。使用{ } 將一組相關的定義封裝起來便于理解。編譯器也可幫我們避免鍵值重復。
4.按鍵檢測程序
上面說了如何將按鍵的鍵值存入和讀出FIFO,但是既然是按鍵操作,就肯定涉及到按鍵消抖處理,還有按鍵的狀態是按下還是彈起,是長按還是短按。所以為了以示區分,我們用還需要給每一個按鍵設置很多參數,就需要再定義一個結構體KEY_T,讓每個按鍵對應1個全局的結構體變量。
typedef?struct {/*?下面是一個函數指針,指向判斷按鍵手否按下的函數?*/uint8_t?(*IsKeyDownFunc)(void);?/*?按鍵按下的判斷函數,1表示按下?*/uint8_t??Count;???/*?濾波器計數器?*/uint16_t?LongCount;??/*?長按計數器?*/uint16_t?LongTime;??/*?按鍵按下持續時間,?0表示不檢測長按?*/uint8_t??State;???/*?按鍵當前狀態(按下還是彈起)?*/uint8_t??RepeatSpeed;?/*?連續按鍵周期?*/uint8_t??RepeatCount;?/*?連續按鍵計數器?*/ }KEY_T;在key.c 中定義s_tBtn結構體數組變量。
static?KEY_T???s_tBtn[3]?=?{0};每個按鍵對象都分配一個結構體變量,這些結構體變量以數組的形式存在將便于我們簡化程序代碼行數。因為我的硬件有3個按鍵,所以這里的數組元素為3。使用函數指針IsKeyDownFunc可以將每個按鍵的檢測以及組合鍵的檢測代碼進行統一管理。
因為函數指針必須先賦值,才能被作為函數執行。因此在定時掃描按鍵之前,必須先執行一段初始化函數來設置每個按鍵的函數指針和參數。這個函數是void KEY_Init(void)。
void?KEY_Init(void) {KEY_FIFO_Init();??/*?初始化按鍵變量?*/KEY_GPIO_Config();??/*?初始化按鍵硬件?*/ }下面是KEY_FIFO_Init函數的定義:
static?void?KEY_FIFO_Init(void) {uint8_t?i;/*?對按鍵FIFO讀寫指針清零?*/s_tKey.Read?=?0;s_tKey.Write?=?0;/*?給每個按鍵結構體成員變量賦一組缺省值?*/for?(i?=?0;?i?<?HARD_KEY_NUM;?i++){s_tBtn[i].LongTime?=?100;/*?長按時間?0?表示不檢測長按鍵事件?*/s_tBtn[i].Count?=?5/?2;?/*?計數器設置為濾波時間的一半?*/s_tBtn[i].State?=?0;/*?按鍵缺省狀態,0為未按下?*/s_tBtn[i].RepeatSpeed?=?0;/*?按鍵連發的速度,0表示不支持連發?*/s_tBtn[i].RepeatCount?=?0;/*?連發計數器?*/}/*?判斷按鍵按下的函數?*/s_tBtn[0].IsKeyDownFunc?=?IsKey1Down;s_tBtn[1].IsKeyDownFunc?=?IsKey2Down;s_tBtn[2].IsKeyDownFunc?=?IsKey3Down; }我們知道按鍵會有機械抖動,你以為按鍵按下就是低電平,其實在按下的一瞬間會存在機械抖動,如果不做延時處理,可能會出錯,一般如果按鍵檢測到按下后再延時50ms檢測一次,如果還是檢測低電平,才能說明按鍵真正的被按下了。反之按鍵彈起時也是一樣的。所以我們程序設置按鍵濾波時間50ms, 因為代碼每10ms掃描一次按鍵,所以按鍵的單位我們可以理解為10ms,濾波的次數就為5次。這樣只有連續檢測到50ms狀態不變才認為有效,包括彈起和按下兩種事件,即使按鍵電路不做硬件濾波(沒有電容濾波),該濾波機制也可以保證可靠地檢測到按鍵事件。
判斷按鍵是否按下,用一個HAL_GPIO_ReadPin就可以搞定。
static?uint8_t?IsKey1Down(void)? {if?(HAL_GPIO_ReadPin(GPIOE,?GPIO_PIN_4)?==?GPIO_PIN_RESET)?return?1;else?return?0; } static?uint8_t?IsKey2Down(void)? {if?(HAL_GPIO_ReadPin(GPIOE,?GPIO_PIN_3)?==?GPIO_PIN_RESET)?return?1;else?return?0; }static?uint8_t?IsKey3Down(void)? {if?(HAL_GPIO_ReadPin(GPIOE,?GPIO_PIN_2)?==?GPIO_PIN_RESET)?return?1;else?return?0; }下面是KEY_GPIO_Config函數的定義,這個函數就是配置具體的按鍵GPIO的,就不需要過多的解釋了。
static?void?KEY_GPIO_Config(void) {?GPIO_InitTypeDef?GPIO_InitStructure;/*?第1步:打開GPIO時鐘?*/__HAL_RCC_GPIOE_CLK_ENABLE();/*?第2步:配置所有的按鍵GPIO為浮動輸入模式(實際上CPU復位后就是輸入狀態)?*/GPIO_InitStructure.Mode?=?GPIO_MODE_INPUT;??????/*?設置輸入?*/GPIO_InitStructure.Pull?=?GPIO_NOPULL;?????????????????/*?上下拉電阻不使能?*/GPIO_InitStructure.Speed?=?GPIO_SPEED_FREQ_VERY_HIGH;??/*?GPIO速度等級?*/GPIO_InitStructure.Pin?=?GPIO_PIN_4;HAL_GPIO_Init(GPIOB,?&GPIO_InitStructure);GPIO_InitStructure.Pin?=?GPIO_PIN_3;HAL_GPIO_Init(GPIOB,?&GPIO_InitStructure);GPIO_InitStructure.Pin?=?GPIO_PIN_2;HAL_GPIO_Init(GPIOB,?&GPIO_InitStructure); }5.按鍵掃描
按鍵掃描函數KEY_Scan()每隔 10ms 被執行一次。RunPer10ms函數在 systick中斷服務程序中執行。
void?RunPer10ms(void) {KEY_Scan(); }void?KEY_Scan(void) {uint8_t?i;for?(i?=?0;?i?<?HARD_KEY_NUM;?i++){KEY_Detect(i);} } /*每隔10ms所有的按鍵GPIO均會被掃描檢測一次。KEY_Detect函數實現如下:
static?void?KEY_Detect(uint8_t?i) {KEY_T?*pBtn;pBtn?=?&s_tBtn[i];if?(pBtn->IsKeyDownFunc()){//這個里面執行的是按鍵按下的處理if?(pBtn->Count?<?KEY_FILTER_TIME){//按鍵濾波前給?Count?設置一個初值pBtn->Count?=?KEY_FILTER_TIME;}else?if(pBtn->Count?<?2?*?KEY_FILTER_TIME){//實現?KEY_FILTER_TIME?時間長度的延遲pBtn->Count++;}else{if?(pBtn->State?==?0){pBtn->State?=?1;/*?發送按鈕按下的消息?*/KEY_FIFO_Put((uint8_t)(3?*?i?+?1));}if?(pBtn->LongTime?>?0){if?(pBtn->LongCount?<?pBtn->LongTime){/*?發送按鈕持續按下的消息?*/if?(++pBtn->LongCount?==?pBtn->LongTime){/*?鍵值放入按鍵FIFO?*/KEY_FIFO_Put((uint8_t)(3?*?i?+?3));}}else{if?(pBtn->RepeatSpeed?>?0){if?(++pBtn->RepeatCount?>=?pBtn->RepeatSpeed){pBtn->RepeatCount?=?0;/*?長按鍵后,每隔10ms發送1個按鍵?*/KEY_FIFO_Put((uint8_t)(3?*?i?+?1));}}}}}}else{//這個里面執行的是按鍵松手的處理或者按鍵沒有按下的處理if(pBtn->Count?>?KEY_FILTER_TIME){pBtn->Count?=?KEY_FILTER_TIME;}else?if(pBtn->Count?!=?0){pBtn->Count--;}else{if?(pBtn->State?==?1){pBtn->State?=?0;/*?發送按鈕彈起的消息?*/KEY_FIFO_Put((uint8_t)(3?*?i?+?2));}}pBtn->LongCount?=?0;pBtn->RepeatCount?=?0;} }這個函數還是比較難以理解的,主要是結構體的操作。所以好好學習結構體,不要見了結構體就跑。
分析:首先讀取相應按鍵的結構體地址賦值給結構體指針變量pBtn ,因為程序里面每個按鍵都有自己的結構體,只有通過這個方式才能對具體的按鍵進行操作。(在前面我們使用軟件定時器時也使用了這中操作,在滴答定時器的中斷服務函數中)。
static?KEY_T?s_tBtn[3];//程序里面每個按鍵都有自己的結構體,有三個按鍵 KEY_T?*pBtn;//定義一個結構體指針變量pBtn pBtn?=?&s_tBtn[i];//將按鍵的結構體地址賦值給結構體指針變量pBtn然后接著就是給按鍵濾波前給Count設置一個初值,前面說按鍵初始化的時候已經設置了Count =5/2。然后判斷是否按下的標志位,如果按鍵按下了,這里就將其設置為 1,如果沒有按下這個變量的值就會一直是 0。這里可能不理解是就是按鍵按下發送的鍵值是3 * i + 1。按鍵彈起發送的鍵值是3 * i + 2,按鍵長按發送的鍵值是3 * i + 3。也就是說按鍵按下發送的鍵值是1和4和7。按鍵彈起發送的鍵值是2和5和8,按鍵長按發送的鍵值是3和6和9。看下面這個枚舉enum你就明白了。
typedef?enum {KEY_NONE?=?0,???/*?0?表示按鍵事件?*/KEY_1_DOWN,????/*?1鍵按下?*/KEY_1_UP,????/*?1鍵彈起?*/KEY_1_LONG,????/*?1鍵長按?*/KEY_2_DOWN,????/*?2鍵按下?*/KEY_2_UP,????/*?2鍵彈起?*/KEY_2_LONG,????/*?2鍵長按?*/KEY_3_DOWN,????/*?3鍵按下?*/KEY_3_UP,????/*?3鍵彈起?*/KEY_3_LONG,????/*?3鍵長按?*/}KEY_ENUM;7.試驗演示
int?main(void) {uint8_t?KeyCode;/*?按鍵代碼?*/KEY_Init();while?(1){??/*?按鍵濾波和檢測由后臺systick中斷服務程序實現,我們只需要調用KEY_FIFO_Get讀取鍵值即可。?*/KeyCode?=?KEY_FIFO_Get();?/*?讀取鍵值,?無鍵按下時返回?KEY_NONE?=?0?*/if?(KeyCode?!=?KEY_NONE){switch?(KeyCode){case?KEY_DOWN_K1:???/*?K1鍵按下?*/printf("K1鍵按下\r\n");break;case?KEY_UP_K1:????/*?K1鍵彈起?*/printf("K1鍵彈起\r\n");break;case?KEY_DOWN_K2:???/*?K2鍵按下?*/printf("K2鍵按下\r\n");break;case?KEY_UP_K2:????/*?K2鍵彈起?*/printf("K2鍵彈起\r\n");break;case?KEY_DOWN_K3:???/*?K3鍵按下?*/printf("K3鍵按下\r\n");break;case?KEY_UP_K3:????/*?K3鍵彈起?*/printf("K3鍵彈起\r\n");break;????????default:/*?其它的鍵值不處理?*/break;}}} }不知道學妹看懂沒,沒看懂就多看幾遍。代碼例程已上傳至Gitee。
https://gitee.com/zhiguoxin/Wechat-Data.gitEnd
推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語言
我的知識小密圈
關注公眾號,后臺回復「1024」獲取學習資料網盤鏈接。
歡迎點贊,關注,轉發,在看,您的每一次鼓勵,我都將銘記于心~
嵌入式Linux
微信掃描二維碼,關注我的公眾號
總結
以上是生活随笔為你收集整理的一个学妹写的按键检测函数把我秀翻了!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQL Server 2008 r2数据
- 下一篇: 无法找到 Adobe PDF资源文件。“