所谓按键消抖,其实就是过滤掉按键的抖动信息,单片机开发过程中,按键的消抖处理是比较频繁的,常用的消抖方式有延时或者滤波。说到这里就不得不提一下阻塞和非阻塞的编程思想,一名优秀的软件工程师是绝对不允许自己的工程存在阻塞的(特殊情况除外),例如:_NOP();、while(xxx == 1);的使用,这样写不仅会占用大量的单片机资源,而且在实时性要求较高的场合下使用往往会打乱运行逻辑,造成数据错误等(当然,如果简单的做粗延时使用,_NOP();封装的延时函数还是比较方便的);非阻塞的延时大多用系统定时器来封装,一般时基为1ms(对于系统定时器的使用,不建议把时基设置到1ms以下,通用定时器的时基最好也不要低于100us!!!如果时基过小,频繁的时钟中断会占用太多的系统资源,尤其是对于低功耗的mcu来讲,这会大大减少其使用寿命),中断函数内部计数标志一直自加,通过对计数标志的判断来实现延时的目的。
下边整理了我用过的几种按键消抖方法,希望可以给大家一些启示:
阻塞型延时消抖
#define Delay_nms(x) Delay_nus(x*1000)
/************************
函数名称:Delay_1us
函数作用:nop延时1微秒
函数入口:无
函数出口:无
函数作者:cxj
补充说明:
主频72MHz 72个nop就是1us
************************/
void Delay_1us(void)
{
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
__NOP();__NOP();
}
/************************
函数名称:Delay_nus
函数作用:nop延时n微秒
函数入口:
num 延时时长
函数出口:无
函数作者:cxj
************************/
void Delay_nus(uint32_t num)
{
while(num--)
Delay_1us();
}
/************************
函数名称:KEY_GetVal
函数作用:获取键值
函数入口:无
函数出口:
0 无按键按下
1 KEY1按下
函数作者:cxj
************************/
uint8_t KEY_ret = 0;
uint8_t KEY_GetVal(void)
{
if(KEY1() == 0)//按键可能有人按下
{
Delay_nms(10); //延时消抖
if(KEY1() == 0)//的确有人按下
{
while(KEY1() == 0);//松手检测
KEY_ret = 1;
}
}
return KEY_ret; //key_ret为真即为按键按下
}
这段代码是在stm32f103的环境下实现的,其实后来发现在别的mcu(如华大、nxp)上,按照这个写法延时并不准确,甚至相差很大, 所以建议用作粗延时使用。
非阻塞型消抖
这种消抖方式不会使程序造成阻塞,一般配合系统定时器使用,由它派生出很多种消抖滤波方式,但原理都是一样的——定时器每过一段时间对按键电平采样一次,如果采样n次或者大于n/2次都是按下的状态,说明按键按下,这种方式也被常用于数据滤波。使用前需要初始化系统定时器,对于ARM内核的mcu,有一个通用的初始化系统定时器的函数SysTick_Config(uint32_t time);,如果要定义一个1ms的时基,只需要初始化SysTick_Config(CoreClock/1000);即可,CoreClock为系统时钟源。
SysTick_Config(SystemCoreClock/1000); //1ms 一次中断
void SysTick_IRQHandler(void)
{
gSysTickCount++;
}
状态机消抖1:
/******************************************************************************/
/**
* Description: 循环检测check电平
* @param
* @return
* @throws
*/
/******************************************************************************/
void LoopCheck(void)
{
if(FALSE == Gpio_GetInputIO(TWR4_UP_CHECK_PORT, TWR4_UP_CHECK_PIN))
{
if(cHeckflag==FALSE) //等待过程中
{
cHeckflag=TRUE;
if(cHeckstatus==CHECK_FREE)
cHeckstatus=CHECK_FIRST;
else if(cHeckstatus==CHECK_FIRST)
cHeckstatus=CHECK_SECOND;
else if(cHeckstatus==CHECK_SECOND)
cHeckstatus=CHECK_THIRD;
else if(cHeckstatus==CHECK_THIRD)
cHeckstatus=CHECK_OVER;
if(cHeckstatus==CHECK_OVER) //当防抖检查完
{
cHeckstatus=CHECK_FREE;
if(FALSE == Gpio_GetInputIO(TWR4_UP_CHECK_PORT, TWR4_UP_CHECK_PIN)) //动作确认
{
KEY_Flag = 1;
}
}
else
{
gTimebase=gSysTickCount;
Flag = 1;
}
}
}
else
{
cHeckstatus=CHECK_FREE;
cHeckflag=FALSE;
KEY_Flag = 0;
}
}
/******************************************************************************/
/**
* Description: 系统延时函数,依据参数gSysTickCount来参考,gSysTickCount 1ms加1
* LED 延时闪烁函数 单独放在外面。因为LED 是一直需要闪烁,规律闪烁
* 此函数 都是单次延时事件处理,不单独用定时器来做,浪费资源、
* @param
* @return
* @throws
*/
/******************************************************************************/
void LoopSysDelay(void)
{
uint32_t timesverfy=0;
//处理按键防抖事件
if(Flag)
{
if(gSysTickCount>=gTimebase)
timesverfy=gSysTickCount-gTimebase;
else //防止定时器溢出
timesverfy=(0xffffffff-gTimebase)+gSysTickCount;
if(timesverfy>20)
{
Flag = 0;
cHeckflag=FALSE;
}
}
}
状态机消抖2:
/******************************************************************************/
/**
* Description: 系统延时函数,依据参数gSysTickCount来参考,gSysTickCount 1ms加1
* isOverTime2()函数参考延时篇
* @param
* @return
* @throws
*/
/******************************************************************************/
void MenuButton(void)
{
static uint32_t Delay = 0;
if(isOverTime2(&ButtonDelay,10)) //按键滤波10ms采样一次
{
MenuButtonStatusF[2] = MenuButtonStatusF[1];
MenuButtonStatusF[1] = MenuButtonStatusF[0];
MenuButtonStatusF[0] = GetMenuButtonStatus();
MenuButtonStatus = (MenuButtonStatusF[2] & MenuButtonStatusF[1]) |
(MenuButtonStatusF[0] & (MenuButtonStatusF[2] |
MenuButtonStatusF[1])); //按键滤波
MenuButtonStatusP = (~MenuButtonStatusLast) & MenuButtonStatus; //按键上升沿脉冲
MenuButtonStatusLast = MenuButtonStatus;
}
这段代码又稍微复杂了一些(加了状态机的思想),整体思路就是:每一个计时事件都有一个对应的标志位(如:Flag)、一个当前记录时间(如:gTimebase),当计时开始时,gTimebase记录当前定时器的值gSysTickCount,gSysTickCount在中断中自加,gSysTickCount-gTimebase的值达到超时值(20)时,切换到下一个状态继续计时,再过20ms又会采样一次,再切换状态,直到状态机运行结束。这个模板不仅可以做消抖使用,一些滤波、采样的操作都可以添加进去,还是很方便的。
其它
网上还有一些三行代码的消抖方式也可以借鉴一下,不管怎么说,适合自己的才是最好的,相信大家也都有一套自己的编程习惯,欢迎留言分享。