刚参加工作的时候,看了一些同事采用的按键扫描和消抖方法,对比学校里和网上查到的按键处理,发现觉得不尽善尽美,有以下几点:
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消抖不是必须的,你可以根据自己的时基进行修改。
/**/
其他未尽说明,欢迎大家在下面留言,互相交流。
来源:51CTO
作者:JustLuwei1234
链接:https://blog.51cto.com/13719208/2107561