最好的按键扫描和消抖方法,适用于复合、长按、按下或抬起响应按键 您所在的位置:网站首页 k480按键不灵敏 最好的按键扫描和消抖方法,适用于复合、长按、按下或抬起响应按键

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

2023-09-01 19:29| 来源: 网络整理| 查看: 265

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

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

/******************************************************/

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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有