单片机按键消抖

2019-04-15 18:47发布

转自:https://blog.csdn.net/elikang/article/details/77053845  一、按键分析   按键按下去分为以下几种:
1、按下立刻弹起,识别为一次按下动作
这种情况下,闭合时间取决于人手的按键速度,但是通常都在100ms以上
2、按下不抬起(保持一段时间),识别为单次点击或者连续点击
3、按下不抬起是一个状态(闭合保持),然后抬起是另一个状态(断开保持) 

检测按键可以用两种方法:
1、电压检测,需要不断扫描IO电平,比较消耗CPU资源
2、中断检测,不必持续检测,节省CPU的资源

按键抖动情况:
1、电压检测
按键刚按下和抬起的时候,电平是不稳定的,需要考虑去抖。
按键动作会有一段稳定的状态,一般采用延迟采样或者持续采样就能解决这个问题。
2、中断检测
中断状态下,有可能按下一次产生超过一次中断,比较多见的是触发两次中断。
解决这个问题,需要考虑按键动作的时间,在一段时间内发生的多次中断都要识别为一次。
并且,中断检测方式不能用于保持按下按键,持续产生按键动作的情况。   
  二、按键抖动原因
    通常按键所用的开关都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动.



按键稳定闭合时间长短是由操作人员决定的,通常都会在100ms以上,刻意快速按的话能达到40-50ms左右,很难再低了。

抖动时间是由按键的机械特性决定的,一般都会在10ms以内,为了确保程序对按键的一次闭合或者一次断开只响应一次,必须进行按键的消抖处理。当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。   三、按键去抖分析 按键消抖可分为硬件消抖和软件消抖。

硬件消抖:
利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。
但实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度,所以实际中使用的并不多。



软件实现消抖:
通过延时程序过滤。
最简单的消抖原理,就是当检测到按键状态变化后,先等待一个10ms左右的延时时间,让抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作了。

如果需要更精细的检测,可以考虑结合持续对IO状态进行采样来去抖。

======================

软件去抖的几个思路:

1、延迟
先定义一个变量记录按键的状态:char key;
然后轮询检测按键状态,当按键状态改变的时候,判断为有按键动作,接下来进入延迟函数;
等到延迟时间过了之后,再读取按键状态,如果按键状态仍为按下状态,则说明确实是有按键动作,不是抖动。

2、中断加延迟方式
单片机总是轮询状态会浪费很多资源,尤其是单片机运行的时间,可以通过中断方式来解决。
中断模式下,单片机不需要轮询按键状态,当有中断产生的时候才会进入延时函数,进行按键去抖程序。

这种情况下,延时函数在哪里进行处理就有两种选择:
一种是放在中断函数中进行,这个时候中断函数占用的时间就比较长,影响响应速度;
另一种是放在中断函数之外进行,这个时候就缩短了中断处理时间,但是这个时候就需要一个标志位来表明是否有中断产生,而且单片机也需要不断查询,只是节约了查询时候读取IO状态的步骤。

3、持续采样
持续采样会大大提高采样的准确度,但是同时也会增加CPU的开销。
在使用中,需要根据需要选择不同的采样频率,一般每10ms采集一次就足够了。

==
关于延迟方法:
简单的延时,可以采用空循环来实现,这个方法比较消耗CPU的资源,CPU任务较重的时候不建议使用。延时函数可以考虑使用定时器替换,但这又消耗了定时器资源,不过只要够用的话还是尽量用,毕竟可以减轻CPU的负担。

关于一次按键过程中产生多次中断的处理:
一次按键过程,可能产生多次中断,可以设置一个标志位来显示是否处于按键识别处理阶段,如果处于按键识别处理阶段即便产生中断也不进行任何响应,这样就可以忽略多余的中断了。

四、我的按键去抖方案
1、使用中断检测按键触发

只有在int_flag为0的时候才会设置int_flag的值为1,此时只有在后续的处理完成后,才会将ing_flag重新设为0,才会响应下一次按键中断。这就消除了一次按键动作会产生多次中断的情况。

2、产生中断后,后续的按键处理程序,可以保证正确识别按键按下还是按键抖动 


这里为了演示方便,直接使用了延时函数,实际使用中为了节省CPU开销,应该使用定时中断。 FIRE_Key.h /*! * COPYRIGHT NOTICE * Copyright (c) 2013,野火科技 * All rights reserved. * 技术讨论:野火初学论坛 http://www.chuxue123.com * * 除注明出处外,以下所有内容版权均属野火科技所有,未经允许,不得用于商业用途, * 修改内容时必须保留野火科技的版权声明。 * * @file FIRE_KEY.h * @brief KEY驱动头文件 * @author 野火科技 * @version v5.0 * @date 2013-07-10 */ #ifndef __FIRE_KEY_H__ #define __FIRE_KEY_H__ //下面是定义按键的时间,单位为 : 10ms(中断时间) #define KEY_DOWN_TIME 1 //消抖确认按下时间 #define KEY_HOLD_TIME 50 //长按hold确认时间,最多253,否则需要修改 keytime 的类型 //如果按键一直按下去,则每隔 KEY_HOLD_TIME - KEY_DOWN_TIME 时间会发送一个 KEY_HOLD 消息 //定义按键消息FIFO大小 #define KEY_MSG_FIFO_SIZE 20 //最多 255,否则需要修改key_msg_front/key_msg_rear类型 //按键端口的枚举 typedef enum { KEY_U, //上 KEY_D, //下 KEY_L, //左 KEY_R, //右 KEY_A, //取消 KEY_B, //选择 KEY_START, //开始 KEY_STOP, //停止 KEY_MAX, } KEY_e; //key状态宏定义 typedef enum { KEY_DOWN = 0, //按键按下时对应电平 KEY_UP = 1, //按键弹起时对应电平 KEY_HOLD, } KEY_STATUS_e; //按键消息结构体 typedef struct { KEY_e key; KEY_STATUS_e status; } KEY_MSG_t; void key_init(KEY_e key); // KEY初始化函数(key 小于 KEY_MAX 时初始化 对应端口,否则初始化全部端口) KEY_STATUS_e key_check(KEY_e key); //检测key状态(带延时消抖) //定时扫描按键 uint8 get_key_msg(KEY_MSG_t *keymsg); //获取按键消息,返回1表示有按键消息,0为无按键消息 void key_IRQHandler(void); //需要定时扫描的中断复位函数(定时时间为10ms) #endif //__FIRE_KEY_H__ FIRE_Key.c /*! * COPYRIGHT NOTICE * Copyright (c) 2013,野火科技 * All rights reserved. * 技术讨论:野火初学论坛 http://www.chuxue123.com * * 除注明出处外,以下所有内容版权均属野火科技所有,未经允许,不得用于商业用途, * 修改内容时必须保留野火科技的版权声明。 * * @file FIRE_KEY.c * @brief KEY驱动函数实现 * @author 野火科技 * @version v5.0 * @date 2013-07-10 */ /* * 包含头文件 */ #include "common.h" #include "MK60_port.h" #include "MK60_gpio.h" #include "FIRE_key.h" /* * 定义 KEY 编号对应的管脚 */ PTXn_e KEY_PTxn[KEY_MAX] = {PTD10, PTD14, PTD11, PTD12, PTD7, PTD13, PTC14, PTC15}; /*! * @brief 初始化key端口(key 小于 KEY_MAX 时初始化 对应端口,否则初始化全部端口) * @param KEY_e KEY编号 * @since v5.0 * Sample usage: KEY_init (KEY_U); //初始化 KEY_U */ void key_init(KEY_e key) { if(key < KEY_MAX) { gpio_init(KEY_PTxn[key], GPI, 0); port_init_NoALT(KEY_PTxn[key], PULLUP); //保持复用不变,仅仅改变配置选项 } else { key = KEY_MAX; //初始化全部 按键 while(key--) { gpio_init(KEY_PTxn[key], GPI, 0); port_init_NoALT(KEY_PTxn[key], PULLUP); //保持复用不变,仅仅改变配置选项 } } } /*! * @brief 获取key状态(不带延时消抖) * @param KEY_e KEY编号 * @return KEY_STATUS_e KEY状态(KEY_DOWN、KEY_DOWN) * @since v5.0 * Sample usage: if(key_get(KEY_U) == KEY_DOWN) { printf(" 按键按下") } */ KEY_STATUS_e key_get(KEY_e key) { if(gpio_get(KEY_PTxn[key]) == KEY_DOWN) { return KEY_DOWN; } return KEY_UP; } /*! * @brief 检测key状态(带延时消抖) * @param KEY_e KEY编号 * @return KEY_STATUS_e KEY状态(KEY_DOWN、KEY_DOWN) * @since v5.0 * Sample usage: if(key_check(KEY_U) == KEY_DOWN) { printf(" 按键按下") } */ KEY_STATUS_e key_check(KEY_e key) { if(key_get(key) == KEY_DOWN) { DELAY_MS(10); if( key_get(key) == KEY_DOWN) { return KEY_DOWN; } } return KEY_UP; } /********************* 如下代码是实现按键定时扫描,发送消息到FIFO ********************/ /* * 定义按键消息FIFO状态 */ typedef enum { KEY_MSG_EMPTY, //没有按键消息 KEY_MSG_NORMAL, //正常,有按键消息,但不满 KEY_MSG_FULL, //按键消息满 } key_msg_e; /* * 定义按键消息FIFO相关的变量 */ KEY_MSG_t key_msg[KEY_MSG_FIFO_SIZE]; //按键消息FIFO volatile uint8 key_msg_front = 0, key_msg_rear = 0; //接收FIFO的指针 volatile uint8 key_msg_flag = KEY_MSG_EMPTY; //按键消息FIFO状态 /*! * @brief 发送按键消息到FIFO * @param KEY_MSG_t 按键消息 * @since v5.0 * Sample usage: KEY_MSG_t *keymsg; keymsg.key = KEY_U; keymsg.status = KEY_HOLD; send_key_msg(keymsg); //发送 */ void send_key_msg(KEY_MSG_t keymsg) { uint8 tmp; //保存在FIFO里 if(key_msg_flag == KEY_MSG_FULL) { //满了直接不处理 return ; } key_msg[key_msg_rear].key = keymsg.key; key_msg[key_msg_rear].status = keymsg.status; key_msg_rear++; if(key_msg_rear >= KEY_MSG_FIFO_SIZE) { key_msg_rear = 0; //重头开始 } tmp = key_msg_rear; if(tmp == key_msg_front) //追到屁股了,满了 { key_msg_flag = KEY_MSG_FULL; } else { key_msg_flag = KEY_MSG_NORMAL; } } /*! * @brief 从FIFO里获取按键消息 * @param KEY_MSG_t 按键消息 * @return 是否获取按键消息(1为获取成功,0为没获取到按键消息) * @since v5.0 * Sample usage: KEY_MSG_t keymsg; if(get_key_msg(&keymsg) == 1) { printf(" 按下按键KEY%d,类型为%d(0为按下,1为弹起,2为长按)",keymsg.key,keymsg.status); } */ uint8 get_key_msg(KEY_MSG_t *keymsg) { uint8 tmp; if(key_msg_flag == KEY_MSG_EMPTY) //按键消息FIFO为空,直接返回0 { return 0; } keymsg->key = key_msg[key_msg_front].key; //从FIFO队首中获取按键值 keymsg->status = key_msg[key_msg_front].status; //从FIFO队首中获取按键类型 key_msg_front++; //FIFO队首指针加1,指向下一个消息 if(key_msg_front >= KEY_MSG_FIFO_SIZE) //FIFO指针队首溢出则从0开始计数 { key_msg_front = 0; //重头开始计数(循环利用数组) } tmp = key_msg_rear; if(key_msg_front == tmp) //比较队首和队尾是否一样,一样则表示FIFO已空了 { key_msg_flag = KEY_MSG_EMPTY; } else { key_msg_flag = KEY_MSG_NORMAL; } return 1; } /*! * @brief 定时检测key状态 * @since v5.0 * @note 此函数需要放入 定时中断复位函数里,定时10ms执行一次 */ void key_IRQHandler(void) { KEY_e keynum; static uint8 keytime[KEY_MAX]; //静态数组,保存各数组按下时间 KEY_MSG_t keymsg; //按键消息 for(keynum = (KEY_e)0 ; keynum < KEY_MAX; keynum ++) //每个按键轮询 { if(key_get(keynum) == KEY_DOWN) //判断按键是否按下 { keytime[keynum]++; //按下时间累加 if(keytime[keynum] <= KEY_DOWN_TIME) //判断时间是否没超过消抖确认按下时间 { continue; //没达到,则继续等待 } else if(keytime[keynum] == KEY_DOWN_TIME + 1 ) //判断时间是否为消抖确认按下时间 { //确认按键按下 keymsg.key = keynum; keymsg.status = KEY_DOWN; send_key_msg(keymsg); //把按键值和按键类型发送消息到FIFO } else if(keytime[keynum] <= KEY_HOLD_TIME) //是否没超过长按HOLD按键确认时间 { continue; //没超过,则继续等待 } else if(keytime[keynum] == KEY_HOLD_TIME + 1) //是否为长按hold确认时间 { //确认长按HOLD keymsg.key = keynum; keymsg.status = KEY_HOLD; send_key_msg(keymsg); //发送 keytime[keynum] = KEY_DOWN_TIME + 1; } else { keytime[keynum] = KEY_DOWN_TIME + 1; //继续重复检测 hold 状态 } } else { if(keytime[keynum] > KEY_DOWN_TIME) //如果确认过按下按键 { keymsg.key = keynum; keymsg.status = KEY_UP; send_key_msg(keymsg); //发送按键弹起消息 } keytime[keynum] = 0; //时间累计清0 } } }