最好的按键扫描和消抖方法,适用于复合、长按、按下或抬起响应按键

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

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

/**/

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

原文地址:http://blog.51cto.com/13719208/2107561

时间: 2024-10-10 12:07:32

最好的按键扫描和消抖方法,适用于复合、长按、按下或抬起响应按键的相关文章

按键的硬件消抖小结

最近没事,对按键的硬件消抖进行了一下了解,现对其总结如下: 对于单刀双掷开关,使用RS触发器进行硬件去抖. 原理:图中两个“与非”门构成一个RS触发器.当按键未按下时,输出为1;当键按下时,输出为0.此时即使按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开B),按键也不会返回原始状态A,双稳态电路的状态不改变,输出保持为0,不会产生抖动的波形.也就是说,即使B点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波. 而实际应用当中,最常用的按键是两个接线端的按键.对于此类按键的硬

基础项目(6)基于尖峰脉冲的按键消抖程序设计讲解

 写在前面的话 我们通常所用的按键开关为机械弹性开关,当机械触点断开.闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会马上断开.因而在闭合及断开的瞬间均伴随有一连串的抖动,为了避免这种现象造成的干扰而作的措施就是按键消抖. 抖动时间的长短由按键的机械特性决定,一般为5ms-10ms.按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒.键抖动会引起一次按键被误读多次.为确保智能单元对按键的一次闭合仅作一次处理,必须消除键抖动.在按键闭合

从简单的按键消抖开始

笔者正在接受FPGA的线上培训,以接近尾声,就水平来说算是入门.设计时发现做些设计总结非常重要,可以帮助自己理清思路,同时也能得到很好的复习,便于日后回顾.之前一直在做altera FPGA的相关学习,对xilinx还不是很熟悉,借着这个契机,将比较基础常用的设计在VIVADO开发环境中过一遍,对我来说是个不错的选择.废话不多说,进入今天的正题. 众所周知,硬件按键都存在机械抖动.所以一次人为按下的动作会触发数次按键按下的行为.所谓"按键消抖"模块的功能就是将抖动滤除掉,保证对按键状态

新型的按键扫描程序

不过我在网上游逛了很久,也看过不少源程序了,没有发现这种按键处理办法的踪迹,所以,我将他共享出来,和广大同僚们共勉.我非常坚信这种按键处理办法的便捷和高效,你可以移植到任何一种嵌入式处理器上面,因为C语言强大的可移植性. 同时,这里面用到了一些分层的思想,在单片机当中也是相当有用的,也是本文的另外一个重点. 对于老鸟,我建议直接看那两个表达式,然后自己想想就会懂的了,也不需要听我后面的自吹自擂了,我可没有班门弄斧的意思,hoho--但是对于新手,我建议将全文看完.因为这是实际项目中总结出来的经验

单片机按键扫描程序,仅三行代码(转)

以下假设你懂C语言,因为纯粹的C语言描述,所以和处理器平台无关,你可以在MCS-51,AVR,PIC,甚至是ARM平台上面测试这个程序性能.当然,我自己也是在多个项目用过,效果非常好的.      好了,工程人员的习惯,废话就应该少说,开始吧.以下我以AVR的MEGA8作为平台讲解,没有其它原因,因为我手头上只有AVR的板子而已没有51的.用51也可以,只是芯片初始化部分不同,还有寄存器名字不同而已.核心算法:unsigned char Trg;unsigned char Cont;void K

09A-独立按键消抖实验01——小梅哥FPGA设计思想与验证方法视频教程配套文档

芯航线--普利斯队长精心奉献 ? 实验目的: 1.复习状态机的设计思想并以此为基础实现按键消抖 2.单bit异步信号同步化以及边沿检测 3.在激励文件中学会使用随机数发生函数$random 4.仿真模型的概念 实验平台:芯航线FPGA核心板 实验原理: ????按键在电子设计中使用的最多,从复位到控制设置均可以看到其身影.现在按键的功能也种类也越来越多,例如多向按键.自锁按键.薄膜按键等.普通按键其硬件示意图如图9-1所示. 图9-1 按键示意图 芯航线开发板所载的为两脚贴片按键,分别位于开发板

09B-独立按键消抖实验02——小梅哥FPGA设计思想与验证方法视频教程配套文档

芯航线--普利斯队长精心奉献 ? 实验目的: 1.复习按键的设计 2.用模块化设计的方式实现每次按下按键0,4个LED显示状态以二进制加法格式加1,每次按下按键1,4个LED显示状态以二进制加法格式减1 实验平台:芯航线FPGA核心板 实验原理:???? ????在上一讲中设计并验证了独立按键的消抖,这里基于上一讲的按键消抖模块来实现一个加减法计数器,并以此学习模块化的设计方式. ????在设计过程中,相对大一点的工程经常通常不会写在一个设计文件中,通常会针对不同的功能设计出不同的子文件,最后在

按键消抖之终极解决方案

1.按键消抖的原理 图1.按键抖动示意图 我们平常所用的按键为机械弹性开关,由于触点的弹性作用,按键在闭合时不会马上稳定的接通,而是有一段时间的抖动,在断开时也不会立即断开.抖动时间由按键的机械特性所决定,一般为5ms~10ms.所以我们在做按键检测时都要加一个消抖的过程. 按键消抖主要有两种方案: 一是延时重采样:二是持续采样. 从理论上来说,延时(如10ms)重采样的准确率肯定低于持续采样. 2.按键消抖的方法 (1)延时重采样 延时重采样的意思是,当第一次检测到键值由'1'变为'0'时,再

什么叫状态机:按键消抖实例

原创  https://jingyan.baidu.com/article/14bd256e112e25bb6d261211.html 貌似没有教程讲到过状态机的概念和编程思路,特从别的论坛转贴一篇很好的实例,献给广大初学者.用状态机做键盘消抖,很好用,不必延时等待键盘稳定,当检测到有键按下或弹起时能发出相应的键盘消息 步骤/方法 1 设置状态机有4种状态,A0,A1,A2,A3 初始时处于A0状态,当扫描发现有键按下时,转入到A1状态. 当处于A1状态时,当扫描发现有键按下并且键值等于A1状态