基于状态机的按键检测
一般在学单片机的时候,最基础的一个内容就是学习按键的输入扫描,最简单的方式当然是读IO电平然后再加上一段延时做消抖。今天要分享的是我自己写的一个基于状态机检测的按键扫描程序,使用状态机可以根据按键按下时长的不同状态实现短按单击、长按连击、长时间长按高速连击(我叫超级连击)的区分。
今天我所说的按键扫描程序全部是基于这种5向开关来做的,如果是矩阵按键扫描的话得做一些相应的修改
原理图如下,一共6个引脚。一个公共端上拉,其他5个引脚接单片机IO口就可以了。
首先是最简单的按键输入方式,哪个按键按下就返回哪个按键的值,没有按下的时候就返回空格键,这里是纯粹的按键扫描,一般还要加入一个20ms到150ms之间的延迟函数来做按键消抖:
uint8_t GetKeyInpt(){
uint8_t Key_x;
if(Key_Up) Key_x = 'U';
else if(Key_Down) Key_x = 'D';
else if(Key_Left) Key_x = 'L';
else if(Key_Rignt) Key_x = 'R';
else if(Key_Center) Key_x = 'C';
else Key_x = ' ';
return Key_x;
}
因为一个比较大延迟的加入,这样的按键检测也是缺点非常明显的,无论你有没有按下按键都会有一个固定的100ms左右的延迟,它大大的降低了整个程序的执行的速度。之前做智能车的时候有一次学弟跟我说他的舵机感觉不管怎么调总是反应特别慢,最后检测才发现他的按键检测和舵机控制是放在main函数里的,而罪魁祸首就是按键检测里的Delay();
而基于状态机做的按键检测在没有按键按下的时候完全不需要做延迟,几乎不花时间,实测我的按键检测程序在48M主频的KEAZ128上之花了13us。
在说程序之前先说说什么是状态机,这是百度百科上的说法:
简单的理解就是通过不停的检测系统输入的状态,然后根据不同的条件让系统处于不同的状态,然后得到不同的输出。
对于按键的输入,我的按键扫描状态转移图可以由下表示:
这只是一个按键的输入扫描,如果多个按键怎么办?其实也很简单弄一个数组然后用循环不就可以啦!
具体实现的代码如下:
///////////////////////////////////////////// 按键输入检测 ////////////////////////////////////////////////////////////////
//功能:5向开关检测按键
//返回值 : 'U' 'D' 'L' 'R' 'C' 分别代表5向按键的 上、下、左、右、中 5个方向上的某个被按下了 ,没有任何按键按下返回 ' '
//
// 注意:
// 1、需要在头文件中配置按键的引脚,以及按下/松开按键时的电平状态
// 2、按键输入分为不按,短按单击,长按连击,超级连击四种模式。可以在头文件中把短按配置成按下时返回按键值或松开的时候返回按键值
// 短按的时候在 按下/松开 的时候返回一次按键值(需要头文件配置),短按超过一段时间之后,自动进入连击和超级连击模式(进入的时间
// 以及连击的速度可以在头文件中配置),连击模式下持续返回按键值
// 3、5个按键是有优先级的,分别为 : 上 > 下 > 左 > 右 > 中
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
uint8_t GetKeyInpt(){
//这个数组表示按键的状态,0表示已经松开,1表示短按,2表示长按进入连击模式,3表示超级连击(连击速度超快)
static uint8_t key_state[] = {0 , 0 , 0 , 0 , 0 };
// 对应的按键 'U' 'D' 'L' 'R' 'C'
uint8_t key_input[5] = {0}; //按键当前的输入状态
#if Key_Trigger_Rising_Edge == 1
static uint8_t key_input_1[5] = {0}; //上一次按键的输入
#endif
static uint8_t key_con[5] = {0}; //按键计数,控制按下的持续时间
static uint16_t key_con_s[5] = {0}; //进入超级连击的计数
static uint8_t Key_x[] = {'U','D','L','R','C'}; //返回值
uint8_t i = 0;
//获取当前键盘状态
key_input[0] = Key_Up;
key_input[1] = Key_Down;
key_input[2] = Key_Left;
key_input[3] = Key_Right;
key_input[4] = Key_Center;
for(i = 0;i<5;i++){
if(key_state[i]==0 && key_input[i] == Key_Press){//某个按键的之前状态是松开的,然后又被按下了,这是短按模式
KEY_Delay(50); //消抖
key_state[i] = 1; //按键状态改为按下
#if Key_Trigger_falling_Edge == 1
KEY_Delay(50);
return Key_x[i]; //返回按键值,在这返回相当于是下降沿触发(短按按下的时候返回值)
#elif Key_Trigger_Rising_Edge == 1
return ' '; //上升沿在这不返回按键值
#endif
}
else if(key_state[i] == 1 && key_input[i] == Key_Press){ //上次某个按键被按下了,这次还是按下的,检测长按模式
key_con[i]++;
KEY_Delay(Delay_Tim);
if(key_con[i]>=Time_Multiple_Hit){ //设置计数时间
key_state[i] = 2; //该按键状态改为长按连击模式
key_con[i] = 0; //清空计数值
#if Key_Trigger_Rising_Edge == 1
return Key_x[i]; //上升沿模式下,在进入连击模式之前先返回一次单击的按键值
#endif
}
}
else if(key_state[i] == 2 && key_input[i] == Key_Press){ //如果还一直按着这个按键,进入长按模式
key_con[i]++;
key_con_s[i]++; //超级连击计数
KEY_Delay(Delay_Tim);
if(key_con[i]>=Speed_Multiple_Hit){ //设置计数时间
key_con[i] = 0; //清空计数值
return Key_x[i]; //返回按键值
}
if(key_con_s[i] >= Time_Super_Multiple_Hit){
key_con[i] = 0; //清空计数值
key_con_s[i] = 0; //清空计数值
key_state[i] = 3; //进入超级连击模式
return Key_x[i]; //返回按键值
}
}
else if(key_state[i] == 3 && key_input[i] == Key_Press){ //处于超级连击状态还按着按键
key_con_s[i]++;
KEY_Delay(Delay_Tim);
if(key_con_s[i] >= Super_Speed_Multiple_Hit){
key_con_s[i] = 0; //清计数值
return Key_x[i]; //返回按键值
}
}
else if(key_state[i] == 2 && key_input[i] == Key_Release){ //某按键上次状态是长按,但是这次没有按键按下
key_state[i] = 0; //状态改为没按下
key_con[i] = 0; //检测长按计数清0
}
#if Key_Trigger_Rising_Edge == 1
//这一次检测某按键是松开的,但是上一次是按下的,而且按键的状态是短按的时候,返回按键值(这个if相当于上升沿,短按松开的时候返回按键值)
else if((key_input[i] == Key_Release && key_input_1[i] == Key_Press) && (key_state[i] == 1)){
key_state[i] = 0;
KEY_Delay(20); //消抖
return Key_x[i]; //返回按键值
}
#endif
else if(key_input[i] == Key_Release){ //某按键没有按下
key_state[i] = 0; //状态改为没按下
key_con[i] = 0; //检测长按连击计数清0
key_con_s[i] = 0; //检测超级连击计数清0
}
#if Key_Trigger_Rising_Edge == 1
key_input_1[i] = key_input[i]; //记录按键
#endif
}
return ' '; //没有任何按键按下时返回空格' '
}
这里面用了一些条件编译,实现了按键单击是下降沿触发(按键按下的时间就返回按键值)还是上升沿触发(按键松开的时候才返回按键值),还有按键从单击转换到连击所需的时间、连击的速度、进入高速连击的时间、进入高速连击的速度都是用宏在头文件中定义的,方便做相应的修改。
关于按键输入的IO口,输入时候触发电平是高电平还是低电平也可以通过宏定义修改。
头文件键里相关的宏定义如下:
///////////////////////////////////// 按键I/O相关配置 ////////////////////////////////////////////////////////////////////
#define Key_Up gpio_get(E0)
#define Key_Down gpio_get(E3)
#define Key_Left gpio_get(E2)
#define Key_Right gpio_get(I3)
#define Key_Center gpio_get(I2)
#define KeyUPin E0
#define KeyDPin E3
#define KeyLPin E2
#define KeyRPin I3
#define KeyCPin I2
#define Hight 1 //高电平
#define Low 0 //低电平
//配置按键按下的时候和松开的时候的电平状态
#define Key_Press Hight //按键按下时的电平状态
#define Key_Release Low //按键松开是的电平状态
///////////////////////////////////// 按键检测相关配置 ////////////////////////////////////////////////////////////////////
//按键短按的时候是下降沿触发还是上升沿触发触发方式同时只能存在一个,两个都存在的时候上升沿优先级比较高,下降沿无效
//注意:两个触发方式都为0的时候短按会失效
#define Key_Trigger_Rising_Edge 0 //上升沿触发:按键短按松开的时候才返回按键值
#define Key_Trigger_falling_Edge 1 //下降沿触发:按键短按按下的时候就返回按键值
//按键短按多久进入连击模式的时间,这个时间大概 = Delay_Tim * Time_Multiple_Hit,最大为255(用的uint8_t型变量来计数)
#define Time_Multiple_Hit 8
//进入连击模式后连击的速度,每次连击之间的间隔为 Speed_Multiple_Hit * Delay_Tim ms
#define Speed_Multiple_Hit 5
//在长按连击的模式下,一直长按进入超级连击模式的时间,时间大概 = 10ms * Time_Super_Multiple_Hit
#define Time_Super_Multiple_Hit 65
//进入超级连击模式后连击的速度,每次连击之间的间隔为 Super_Speed_Multiple_Hit * Delay_Tim ms 如果速度和长按连击一样,相当于没有超级连击
#define Super_Speed_Multiple_Hit 2
//替换延迟函数
#define KEY_Delay systick_delay_ms
#define Delay_Tim 5