最好的按鍵掃描和消抖方法,適用於復合、長按、按下或擡起響應按鍵
1. 消抖復雜,效率低。有人直接在電平判斷後使用delay()函數,進行消抖,耽誤時間;有人在按鍵電平中斷中進行消抖和處理,導致其他的服務反應慢,不適合做實時系統; 2. 許多功能在不同界面下是不同的,把按鍵處理在中斷進行,導致分支很多,業務流不清晰。 3. 特殊功能按鍵的處理麻煩。在需要長按作為特殊按鍵、復合按鍵響應、復合按鍵長按響應的時候,需要增加很多的標誌位,反復使用if..else判斷,流程看起來很亂。 4. 跟硬件設計或業務關聯很深,不便於移植和修改,導致每個項目都要更改一次。
想了很久之後,我結合PC的鍵盤處理方法,編寫了自己的按鍵函數,經過幾次修改,定了下來。這十多年來,無論更換單片機,還是采用端口/掃描方式,還是采用前後臺或操作系統,都一直在用,方便移植,也比較清晰。
/****/
它主要有幾個特點:
-
按鍵掃描和取值分開。
在中斷中,每隔10ms調用keyScan()進行按鍵掃描,多次掃描進行消抖,獲得的按鍵值不返回,作為消息放到全局變量中;
在業務層需要判斷的地方使用getKeyValue()獲取當前的鍵值,進行處理。
-
每一個按鍵,都有單獨的標誌位和計時變量。
消抖計時:
每調用一次10ms中斷,如果按鍵按下,gucKeyOkTimer(以OK按鍵為例)增加; gucKeyOkTimer超過消抖的閥值(我一般10次,即100ms),則確認有按鍵了。 任何一次掃描到按鍵沒有按下,gucKeyOkTimer清零,重新開始;
標誌位:
如果按下的電平時間超過閾值,一直按著,會有gfOkPressing的標誌,表明按鍵一直有效中; 如果按下過一次,需要響應,會有gfOkNeedAck,這個標誌只置位一次;
-
復合按鍵的響應:
因為每個按鍵,都有自己的標誌位和計時變量。復合按鍵的判斷,使用多個按鍵pressing的標誌判斷是否有效。同樣每個復合按鍵有自己pressing的標誌,和NeedAck的標誌;
-
長按鍵的響應:
按鍵超過指定時間,則作為新的按鍵,也會有pressing標誌,和NeedAck標誌。
我沒有使用怪癖詭異的編程方法。有很多取巧的方法可使實現按鍵的掃描,甚至有人寫了三行代碼就實現消抖。——我個人不喜歡這樣的程序風格。我喜歡思路清晰的編程方法,易於維護和移植。當然代價就是多了一些ROM和RAM占用,但我覺得時間和代碼的質量更重要。
如果你跟我的思路相同,也遇見過這樣的困惑,可以考慮我的按鍵掃描方法。
/**硬件說明**/
這是個常用的按鍵定義,四個按鍵:上、下、確認、取消;長按確認為開關機按鍵;開機後同時按下上下按鍵,為菜單按鍵。
/*****軟件代碼**/
首先是按鍵掃描,需要每10ms調用一次,在使用STM32的系統中,可以直接使用SysTick,累積10秒調用一次按鍵掃描函數。
在void SysTick_Handler(void)中,添加以下代碼:
//key sacn, each 10ms
giKeyScanTimer++;
if(giKeyScanTimer>=10)
{
giKeyScanTimer=0;
keyScan();
}
在按鍵掃描文件key.c中,以下為按鍵端口的宏定義。項目使用了HAL庫,但為了節約時間,端口掃描直接調用了GPIO寄存器。
#define PORT_KOK ((GPIOA->IDR)&(uint32_t)GPIO_IDR_IDR4)
#define PORT_KUP ((GPIOA->IDR)&(uint32_t)GPIO_IDR_IDR5)
#define PORT_KDOWN ((GPIOA->IDR)&(uint32_t)GPIO_IDR_IDR6)
#define PORT_KCANCEL ((GPIOA->IDR)&(uint32_t)GPIO_IDR_IDR7)
按鍵掃描需要的變量。因為使用的STM32的RAM較大,所以標誌位直接用uint8_t,在RAM緊張的地方,可以改為位定義。
uint32_t gucKeyOkTimer, gucKeyUpTimer,gucKeyDownTimer, gucKeyCancelTimer, gucKeyMenuTimer; //按鍵消抖需要的掃描計時器
uint8_t gfOkPressing, gfOkNeedAck; //OK按鍵的按下標誌、需要響應的標誌
uint8_t gfUpPressing, gfUpNeedAck; //UP按鍵的按下標誌、需要響應的標誌;
uint8_t gfDownPressing, gfDownNeedAck; //DN按鍵的按下標誌、需要響應的標誌;
uint8_t gfCancelPressing, gfCancelNeedAck; //CANCEL按鍵的按下標誌、需要響應的標誌;
uint8_t gfMenuPressing, gfMenuNeedAck; //MENU按鍵(同時按下UP、DOWN)的按下標誌、需要響應的標誌;
uint8_t gfONOFFPressing, gfONOFFNeedAck; //ONOFF按鍵(按下OK超過3秒)的按下標誌、需要響應的標誌;
以下為keyScan函數,我將1個按鍵、1個長按按鍵、1個復合按鍵的代碼完整copy下來,其他的不占用篇幅了。
//Key scan time, based on 10ms
#define KEY_100MS 10
#define KEY_200MS 20
#define KEY_500MS 50
#define KEY_1S 100
#define KEY_2S 200
/*********************函數說明*********************
函數作用:按鍵掃描函數
註意事項:每10ms被中斷調用一次,判斷是否有按鍵按下
消抖時間:100ms
**********************************************/
void keyScan()
{
//OK key
if(PORT_KOK==0)
{
gucKeyOkTimer++;
//100ms消抖後,確認需要處理
if(gucKeyOkTimer>KEY_100MS)
{
//gfOkPressing代表這個按鍵一直被按下中
gfOkPressing=1;
//確認按下後,置待響應標誌,這個標誌只置一次,防止業務流重復處理
if(gfOkPressing==0)
gfOkNeedAck=1;
}
//如果連續按下1s,則為ONOFF按鍵,同樣有pressing標誌,和needack標誌
if(gucKeyOkTimer>KEY_1S)
{
gfONOFFPressing=1;
if(gfONOFFPressing==0)
gfONOFFNeedAck=1;
}
}
else
{
//如果沒有被按下,定時器、pressing標誌都清零。needack標誌不能清。
gucKeyOkTimer=0;
gfOkPressing=0;
gfONOFFPressing=0;
}
//Up key ...
//Dn key ...
//Cancel key ...
//三個按鍵的處理方法相同,只是沒有長按的處理。
//如果UP和DOWN按鍵同時按下超過1秒,則為Menu按鍵;
if(gfUpPressing&&gfDownPressing)
{
gucKeyMenuTimer++;
if(gucKeyMenuTimer>KEY_1S)
{
gfMenuPressing=1;
if(gfMenuPressing==0)
gfMenuNeedAck=1;
}
}
else
{
gucKeyMenuTimer=0;
gfMenuPressing=0;
}
}
在業務流的程序處理中,調用getKeyValue()獲得有效鍵值。一般是在某個界面的loop中。
/*********************函數說明*********************
函數作用:根據掃描結果,返回按鍵值
註意事項:需要判斷按鍵的時候,調用此函數
**********************************************/
uint8_t getKeyValue()
{
if(gfUpNeedAck)
{
gfUpNeedAck=0;
return KEY_UP;
}
... ...
if(gfMenuNeedAck)
{
gfMenuNeedAck=0;
return KEY_MENU;
}
if(gfONOFFNeedAck)
{
gfONOFFNeedAck=0;
return KEY_ONOFF;
}
return KEY_NONE;
}
當然,在進入某個界面前,需要清空一下按鍵標誌,否則在上一個界面沒響應的按鍵會影響下一個界面:
/*********************函數說明*********************
函數作用:清空按鍵緩沖區
註意事項:
**********************************************/
void flushKeyBuf(void)
{
gfUpNeedAck=0;
gfDownNeedAck=0;
gfOkNeedAck=0;
gfCancelNeedAck=0;
gfMenuNeedAck=0;
gfONOFFNeedAck=0;
}
OK了,這篇文章我在51hei發表過,但是沒有說得這麽詳細。
/**寫在後面**/
有幾個特殊的按鍵處理要求,我簡單收一下:
-
是按下響應還是擡起響應。
業務要求不一樣,就會有不一樣的要求。以上代碼是按下響應的,如果需要擡起響應,就在if(PORT_KOK==0)的代碼裏不處理needack標誌。在else分支裏面,如果擡起之前pressing是置位的,那就置位needack。
-
先後順序,或連擊多少次的密碼操作。
建議還是放在業務流裏面吧,沒必要在按鍵掃描裏面處理。
-
一個按鍵按不同時間,進行不同提示進入不同隱藏功能。
這個情況下不建議再keyscan中進行處理了,因為可能會先處理按鍵時間短的功能。請在業務流直接判斷pressing的時間吧。
-
按鍵行列掃描。
很容易改動,把PORT_KOK==0改動一下即可。
-
時間問題
10ms掃描一次,100ms消抖不是必須的,你可以根據自己的時基進行修改。
/**/
其他未盡說明,歡迎大家在下面留言,互相交流。
最好的按鍵掃描和消抖方法,適用於復合、長按、按下或擡起響應按鍵