1. 程式人生 > >最好的按鍵掃描和消抖方法,適用於復合、長按、按下或擡起響應按鍵

最好的按鍵掃描和消抖方法,適用於復合、長按、按下或擡起響應按鍵

按鍵消抖 按鍵掃描 C語言按鍵

剛參加工作的時候,看了一些同事采用的按鍵掃描和消抖方法,對比學校裏和網上查到的按鍵處理,發現覺得不盡善盡美,有以下幾點:

1. 消抖復雜,效率低。有人直接在電平判斷後使用delay()函數,進行消抖,耽誤時間;有人在按鍵電平中斷中進行消抖和處理,導致其他的服務反應慢,不適合做實時系統;

2. 許多功能在不同界面下是不同的,把按鍵處理在中斷進行,導致分支很多,業務流不清晰。

3. 特殊功能按鍵的處理麻煩。在需要長按作為特殊按鍵、復合按鍵響應、復合按鍵長按響應的時候,需要增加很多的標誌位,反復使用if..else判斷,流程看起來很亂。

4. 跟硬件設計或業務關聯很深,不便於移植和修改,導致每個項目都要更改一次。

想了很久之後,我結合PC的鍵盤處理方法,編寫了自己的按鍵函數,經過幾次修改,定了下來。這十多年來,無論更換單片機,還是采用端口/掃描方式,還是采用前後臺或操作系統,都一直在用,方便移植,也比較清晰。

/****/

它主要有幾個特點:

  1. 按鍵掃描和取值分開。

    在中斷中,每隔10ms調用keyScan()進行按鍵掃描,多次掃描進行消抖,獲得的按鍵值不返回,作為消息放到全局變量中;

    在業務層需要判斷的地方使用getKeyValue()獲取當前的鍵值,進行處理。

  2. 每一個按鍵,都有單獨的標誌位和計時變量。

    消抖計時:

    每調用一次10ms中斷,如果按鍵按下,gucKeyOkTimer(以OK按鍵為例)增加;
    gucKeyOkTimer超過消抖的閥值(我一般10次,即100ms),則確認有按鍵了。
    
    任何一次掃描到按鍵沒有按下,gucKeyOkTimer清零,重新開始;

    標誌位:

    如果按下的電平時間超過閾值,一直按著,會有gfOkPressing的標誌,表明按鍵一直有效中;
    
    如果按下過一次,需要響應,會有gfOkNeedAck,這個標誌只置位一次;
  3. 復合按鍵的響應:

    因為每個按鍵,都有自己的標誌位和計時變量。復合按鍵的判斷,使用多個按鍵pressing的標誌判斷是否有效。同樣每個復合按鍵有自己pressing的標誌,和NeedAck的標誌;
  4. 長按鍵的響應:

    按鍵超過指定時間,則作為新的按鍵,也會有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發表過,但是沒有說得這麽詳細。

/**寫在後面**/

有幾個特殊的按鍵處理要求,我簡單收一下:

  1. 是按下響應還是擡起響應。

    業務要求不一樣,就會有不一樣的要求。以上代碼是按下響應的,如果需要擡起響應,就在if(PORT_KOK==0)的代碼裏不處理needack標誌。在else分支裏面,如果擡起之前pressing是置位的,那就置位needack。

  2. 先後順序,或連擊多少次的密碼操作。

    建議還是放在業務流裏面吧,沒必要在按鍵掃描裏面處理。

  3. 一個按鍵按不同時間,進行不同提示進入不同隱藏功能。

    這個情況下不建議再keyscan中進行處理了,因為可能會先處理按鍵時間短的功能。請在業務流直接判斷pressing的時間吧。

  4. 按鍵行列掃描。

    很容易改動,把PORT_KOK==0改動一下即可。

  5. 時間問題

    10ms掃描一次,100ms消抖不是必須的,你可以根據自己的時基進行修改。

/**/

其他未盡說明,歡迎大家在下面留言,互相交流。

最好的按鍵掃描和消抖方法,適用於復合、長按、按下或擡起響應按鍵