一、按键的消抖
按键消抖:通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,因而在闭合及断开的瞬间均伴随有一连串的抖动,按键抖动会引起一次按键被误读多次。
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。
软件消抖:硬件方法将导致系统硬件电路设计复杂化,常采用软件方法进行消抖。
软件方法去抖,即检测出键闭合后执行一个延时程序,5ms~10ms的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。
二、按键状态机实现
0.状态机模式
简单理解为:将一个事件划分为有限个状态,满足相应的条件,在有限个状态之间跳转;可以使用状态图来描述事件处理过程,这种方式使得程序逻辑思路更加清晰严谨。以按键为例,按键检测的过程可以分为三个状态:按键检测状态、按键确认状态、按键释放状态;而在这三个状态之间跳转的条件为当前状态下按键的值。
在单片机中实现状态机最常用的语句便是switch case语句。
【状态机中如何非阻塞消抖】:使用定时器中断,定时每10ms执行一次switch case语句,即两个状态之间跳转的时间为10ms,这样便代替了delay延时。当定时中断发生时,才跳转到中断服务函数执行。
1.单个按键检测
独立按键电路
单个按键的状态转移图如下:
S1状态为按键检测,S2为按键确认,S3为释放按键;状态跳转条件为当前状态下读取到的按键高低电平Key,当Result为1时,表示按键已经成功按下。
单个按键检测的代码实现:
#ifdef SingleKeyEvent
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2
}KEY_STATE;
KEY_STATE KeyState =KEY_CHECK; // 初始化按键状态为检测状态
u8 g_KeyFlag = 0; // 按键有效标志,0: 按键值无效; 1:按键值有效
/**
* 单个按键检测事件
* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
* 状态机使用switch case语句实现状态之间的跳转
*
*/
void Key_Scan(void)
{
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
break;
//按键按下状态:
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
KeyState = KEY_RELEASE; //进入下一个释放状态
g_KeyFlag = 1; //按键有效值为1, 按键确认按下,按下就执行按键任务;
}
else //当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; //返回上一个状态
}
break;
//按键释放状态
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
// g_KeyFlag = 1; //如果置于此,则在按键释放状态后,才执行按键任务;
}
break;
default: break;
}
}
#endif
2.单个按键实现长按和短按
单个按键实现短按和长按是个很常用的方式,区别单个按键是否是长按和短按依靠检测按键按下的持续时间。
此处将短按时间T设为 10ms < T <1 s;长按时间的T设置为:T > 1s.
在上面的按键实现基础上可继续实现长按和短按判断,具体程序如下:
代码如下(示例):
#ifdef SingleKey_LongShort_Event
/**
* 单个按键检测短按和长按事件
* 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s
* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
* 状态机使用switch case语句实现状态之间的跳转
* lock变量用于判断是否是第一次进行按键确认状态
* :按键释放后才执行按键事件
*/
void Key_Scan(void)
{
static u8 TimeCnt = 0;
static u8 lock = 0;
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
TimeCnt = 0; //计数复位
lock = 0;
break;
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
if(!lock) lock = 1;
TimeCnt++;
}
else
{
if(lock) // 不是第一次进入, 释放按键才执行
{
/*按键时长判断*/
if(TimeCnt > 100) // 长按 1 s
{
g_KeyActionFlag = LONG_KEY;
TimeCnt = 0;
}
else // Key值变为了1,说明此处动作为短按
{
g_KeyActionFlag = SHORT_KEY; // 短按
}
/*按键时长判断*/
KeyState = KEY_RELEASE; // 需要进入按键释放状态
}
else // 当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; // 返回上一个状态
}
}
break;
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
}
break;
default: break;
}
}
#endif
按键释放之后,才检测不太合理,做如下调整,长按事件需要达到时长就执行,短按可以在按键释放后执行。
/**
* 单个按键检测短按和长按事件
* 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s
* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
* 状态机使用switch case语句实现状态之间的跳转
* lock变量用于判断是否是第一次进行按键确认状态
* :长按键事件提前执行,短按键事件释放后才执行
*/
void Key_Scan(void)
{
static u8 TimeCnt = 0;
static u8 lock = 0;
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
TimeCnt = 0; //计数复位
lock = 0;
break;
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
if(!lock) lock = 1;
TimeCnt++;
/*按键时长判断*/
if(TimeCnt > 100) // 长按 1 s
{
g_KeyActionFlag = LONG_KEY;
TimeCnt = 0;
lock = 2; //防止退出长按时,又执行一次短按
}
}
else
{
if(1==lock) // 不是第一次进入, 释放按键才执行
{
g_KeyActionFlag = SHORT_KEY; // 短按
KeyState = KEY_RELEASE; // 需要进入按键释放状态
}
else // 当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; // 返回上一个状态
}
}
break;
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
}
break;
default: break;
}
}
三、长按和短按测试示例
头文件
/**
* 使用定时器来轮询Key_Scan()函数,定时节拍为2ms,
* 状态转换时间为10ms,即每次进入switch case语句的时间差为10ms
* 利用该10ms的间隙跳过按键抖动
*/
#ifndef __BUTTON_H
#define __BUTTON_H
#include "stm32f10x.h"
//按键对应的IO管脚 KEY1 PA.15
#define KEY_IO_RCC RCC_APB2Periph_GPIOA
#define KEY_IO_PORT GPIOA
#define KEY_IO_PIN GPIO_Pin_15
//Key: 1:高电平,按键未按下, 0:低电平,按键按下
#define Key GPIO_ReadInputDataBit(KEY_IO_PORT,KEY_IO_PIN)
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2
}KEY_STATE;
typedef enum
{
NULL_KEY = 0,
SHORT_KEY =1,
LONG_KEY
}KEY_TYPE;
//extern u8 g_KeyFlag;
//extern KEY_TYPE g_KeyActionFlag;
//单个按键事件
//#define SingleKeyEvent
//单个按键实现长按和短按
#define SingleKey_LongShort_Event 1
void Key_Init(void);
void Key_Scan(void);
main函数
KEY_STATE KeyState =KEY_CHECK; // 初始化按键状态为检测状态
u8 g_KeyFlag = 0; // 按键有效标志,0: 按键值无效; 1:按键值有效
KEY_TYPE g_KeyActionFlag; //用于区别长按和短按
int main()
{
Key_Init();
Timer_init(19,7199);//10Khz的计数频率,计数到20为2ms
while(1)
{
switch(g_KeyActionFlag)
{
case SHORT_KEY:
/*
执行短按对应的事件
*/
g_KeyActionFlag = 0; //状态回到空
break;
case LONG_KEY:
/*
执行长按对应的事件
*/
g_KeyActionFlag = 0; //状态回到空
default: break;
}
}
}
void TIM3_IRQHandler(void) //TIM3 每 2ms 中断一次
{
static u8 cnt;
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
cnt++;
if(cnt>5) // 每10ms 执行一次按键扫描程序
{
Key_Scan();
cnt = 0;
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIMx的中断待处理位:TIM 中断源
}
}
四 、多按键检测
同样的,多按键也是一样的;
示例如下:
Buttion.h 头文件中只需要把Key做下修改;
#define KEY0 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)//读取按键0
#define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取按键1
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键2
#define Key (KEY0 && KEY1 && (!WK_UP))
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2,
}KEY_STATE;
//对应的按键值,
typedef enum
{
KEY_NULL = 0,
KEY_0,
KEY_1,
KEY_WK_UP
}KEY_VALUE;
对应的状态机中对按键值进行区分即可:
void Key_Scan(void)
{
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
break;
//按键按下状态:
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
KeyState = KEY_RELEASE; //进入下一个释放状态
//g_KeyFlag = 1; //按键有效值为1, 按键确认按下,按下就执行按键任务;
// 多按键判断
if(0 == KEY0)
g_Key = KEY_0;
else if(0 == KEY1)
g_Key = KEY_1;
else if(1 == WK_UP)
g_Key = KEY_WK_UP;
}
else //当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; //返回上一个状态
}
break;
//按键释放状态
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
}
break;
default: break;
}
}
main函数中也是一样,使用switch case 语句执行按键事件即可:
extern KEY_VALUE g_Key;
switch(g_Key)
{
case KEY_0:
/*
KEY 0 按键事件
*/
g_Key=KEY_NULL;
break;
case KEY_1:
/*
KEY 1 按键事件
*/
g_Key=KEY_NULL;
break;
case KEY_WK_UP:
/*
KEY_WK_UP 按键事件
*/
g_Key=KEY_NULL;
break;
default: break;
}
总结
以上便是对按键状态机程序的总结,对长按和短按的判断实现……
一、按键的消抖
按键消抖:通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,因而在闭合及断开的瞬间均伴随有一连串的抖动,按键抖动会引起一次按键被误读多次。
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。
软件消抖:硬件方法将导致系统硬件电路设计复杂化,常采用软件方法进行消抖。
软件方法去抖,即检测出键闭合后执行一个延时程序,5ms~10ms的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。
二、按键状态机实现
0.状态机模式
简单理解为:将一个事件划分为有限个状态,满足相应的条件,在有限个状态之间跳转;可以使用状态图来描述事件处理过程,这种方式使得程序逻辑思路更加清晰严谨。以按键为例,按键检测的过程可以分为三个状态:按键检测状态、按键确认状态、按键释放状态;而在这三个状态之间跳转的条件为当前状态下按键的值。
在单片机中实现状态机最常用的语句便是switch case语句。
【状态机中如何非阻塞消抖】:使用定时器中断,定时每10ms执行一次switch case语句,即两个状态之间跳转的时间为10ms,这样便代替了delay延时。当定时中断发生时,才跳转到中断服务函数执行。
1.单个按键检测
独立按键电路
单个按键的状态转移图如下:
S1状态为按键检测,S2为按键确认,S3为释放按键;状态跳转条件为当前状态下读取到的按键高低电平Key,当Result为1时,表示按键已经成功按下。
单个按键检测的代码实现:
#ifdef SingleKeyEvent
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2
}KEY_STATE;
KEY_STATE KeyState =KEY_CHECK; // 初始化按键状态为检测状态
u8 g_KeyFlag = 0; // 按键有效标志,0: 按键值无效; 1:按键值有效
/**
* 单个按键检测事件
* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
* 状态机使用switch case语句实现状态之间的跳转
*
*/
void Key_Scan(void)
{
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
break;
//按键按下状态:
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
KeyState = KEY_RELEASE; //进入下一个释放状态
g_KeyFlag = 1; //按键有效值为1, 按键确认按下,按下就执行按键任务;
}
else //当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; //返回上一个状态
}
break;
//按键释放状态
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
// g_KeyFlag = 1; //如果置于此,则在按键释放状态后,才执行按键任务;
}
break;
default: break;
}
}
#endif
2.单个按键实现长按和短按
单个按键实现短按和长按是个很常用的方式,区别单个按键是否是长按和短按依靠检测按键按下的持续时间。
此处将短按时间T设为 10ms < T <1 s;长按时间的T设置为:T > 1s.
在上面的按键实现基础上可继续实现长按和短按判断,具体程序如下:
代码如下(示例):
#ifdef SingleKey_LongShort_Event
/**
* 单个按键检测短按和长按事件
* 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s
* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
* 状态机使用switch case语句实现状态之间的跳转
* lock变量用于判断是否是第一次进行按键确认状态
* :按键释放后才执行按键事件
*/
void Key_Scan(void)
{
static u8 TimeCnt = 0;
static u8 lock = 0;
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
TimeCnt = 0; //计数复位
lock = 0;
break;
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
if(!lock) lock = 1;
TimeCnt++;
}
else
{
if(lock) // 不是第一次进入, 释放按键才执行
{
/*按键时长判断*/
if(TimeCnt > 100) // 长按 1 s
{
g_KeyActionFlag = LONG_KEY;
TimeCnt = 0;
}
else // Key值变为了1,说明此处动作为短按
{
g_KeyActionFlag = SHORT_KEY; // 短按
}
/*按键时长判断*/
KeyState = KEY_RELEASE; // 需要进入按键释放状态
}
else // 当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; // 返回上一个状态
}
}
break;
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
}
break;
default: break;
}
}
#endif
按键释放之后,才检测不太合理,做如下调整,长按事件需要达到时长就执行,短按可以在按键释放后执行。
/**
* 单个按键检测短按和长按事件
* 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s
* 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
* 状态机使用switch case语句实现状态之间的跳转
* lock变量用于判断是否是第一次进行按键确认状态
* :长按键事件提前执行,短按键事件释放后才执行
*/
void Key_Scan(void)
{
static u8 TimeCnt = 0;
static u8 lock = 0;
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
TimeCnt = 0; //计数复位
lock = 0;
break;
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
if(!lock) lock = 1;
TimeCnt++;
/*按键时长判断*/
if(TimeCnt > 100) // 长按 1 s
{
g_KeyActionFlag = LONG_KEY;
TimeCnt = 0;
lock = 2; //防止退出长按时,又执行一次短按
}
}
else
{
if(1==lock) // 不是第一次进入, 释放按键才执行
{
g_KeyActionFlag = SHORT_KEY; // 短按
KeyState = KEY_RELEASE; // 需要进入按键释放状态
}
else // 当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; // 返回上一个状态
}
}
break;
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
}
break;
default: break;
}
}
三、长按和短按测试示例
头文件
/**
* 使用定时器来轮询Key_Scan()函数,定时节拍为2ms,
* 状态转换时间为10ms,即每次进入switch case语句的时间差为10ms
* 利用该10ms的间隙跳过按键抖动
*/
#ifndef __BUTTON_H
#define __BUTTON_H
#include "stm32f10x.h"
//按键对应的IO管脚 KEY1 PA.15
#define KEY_IO_RCC RCC_APB2Periph_GPIOA
#define KEY_IO_PORT GPIOA
#define KEY_IO_PIN GPIO_Pin_15
//Key: 1:高电平,按键未按下, 0:低电平,按键按下
#define Key GPIO_ReadInputDataBit(KEY_IO_PORT,KEY_IO_PIN)
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2
}KEY_STATE;
typedef enum
{
NULL_KEY = 0,
SHORT_KEY =1,
LONG_KEY
}KEY_TYPE;
//extern u8 g_KeyFlag;
//extern KEY_TYPE g_KeyActionFlag;
//单个按键事件
//#define SingleKeyEvent
//单个按键实现长按和短按
#define SingleKey_LongShort_Event 1
void Key_Init(void);
void Key_Scan(void);
main函数
KEY_STATE KeyState =KEY_CHECK; // 初始化按键状态为检测状态
u8 g_KeyFlag = 0; // 按键有效标志,0: 按键值无效; 1:按键值有效
KEY_TYPE g_KeyActionFlag; //用于区别长按和短按
int main()
{
Key_Init();
Timer_init(19,7199);//10Khz的计数频率,计数到20为2ms
while(1)
{
switch(g_KeyActionFlag)
{
case SHORT_KEY:
/*
执行短按对应的事件
*/
g_KeyActionFlag = 0; //状态回到空
break;
case LONG_KEY:
/*
执行长按对应的事件
*/
g_KeyActionFlag = 0; //状态回到空
default: break;
}
}
}
void TIM3_IRQHandler(void) //TIM3 每 2ms 中断一次
{
static u8 cnt;
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
cnt++;
if(cnt>5) // 每10ms 执行一次按键扫描程序
{
Key_Scan();
cnt = 0;
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIMx的中断待处理位:TIM 中断源
}
}
四 、多按键检测
同样的,多按键也是一样的;
示例如下:
Buttion.h 头文件中只需要把Key做下修改;
#define KEY0 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)//读取按键0
#define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取按键1
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键2
#define Key (KEY0 && KEY1 && (!WK_UP))
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2,
}KEY_STATE;
//对应的按键值,
typedef enum
{
KEY_NULL = 0,
KEY_0,
KEY_1,
KEY_WK_UP
}KEY_VALUE;
对应的状态机中对按键值进行区分即可:
void Key_Scan(void)
{
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(!Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为0,说明按键开始按下,进入下一个状态
}
break;
//按键按下状态:
case KEY_COMFIRM:
if(!Key) //查看当前Key是否还是0,再次确认是否按下
{
KeyState = KEY_RELEASE; //进入下一个释放状态
//g_KeyFlag = 1; //按键有效值为1, 按键确认按下,按下就执行按键任务;
// 多按键判断
if(0 == KEY0)
g_Key = KEY_0;
else if(0 == KEY1)
g_Key = KEY_1;
else if(1 == WK_UP)
g_Key = KEY_WK_UP;
}
else //当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; //返回上一个状态
}
break;
//按键释放状态
case KEY_RELEASE:
if(Key) //当前Key值为1,说明按键已经释放,返回开始状态
{
KeyState = KEY_CHECK;
}
break;
default: break;
}
}
main函数中也是一样,使用switch case 语句执行按键事件即可:
extern KEY_VALUE g_Key;
switch(g_Key)
{
case KEY_0:
/*
KEY 0 按键事件
*/
g_Key=KEY_NULL;
break;
case KEY_1:
/*
KEY 1 按键事件
*/
g_Key=KEY_NULL;
break;
case KEY_WK_UP:
/*
KEY_WK_UP 按键事件
*/
g_Key=KEY_NULL;
break;
default: break;
}
总结
以上便是对按键状态机程序的总结,对长按和短按的判断实现……
举报