刚参加工作的时候,看了一些同事采用的按键扫描和消抖方法,对比学校里和网上查到的按键处理,发现觉得不尽善尽美,有以下几点:

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消抖不是必须的,你可以根据自己的时基进行修改。

/**/

其他未尽说明,欢迎大家在下面留言,互相交流。