多按键状态机的实现
Posted zzjan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多按键状态机的实现相关的知识,希望对你有一定的参考价值。
1. 简单按键检测
记得开始学习单片机的时候,写的按键扫描是这样的:
if(KEY1 == 0)
delay_ms(20);
if(KEY1 == 0)
while(KEY1 == 0);
// 按键按下处理代码
一看,有个20ms消除抖动时间,就是说我要在这里死等20ms,还有等待按键释放,我就是不放,你能怎么样?没办法只能做超时。那我想做长按1s呢?细思极恐,对于实际项目上的应用来说是很糟糕的事情,这不仅会拖慢你整个系统,还会出现,多个按键有时检测不到的问题。有没有更好的办法来实现呢?答案是肯定的,想想,如果这个20ms的延时用定时器来做,不就可以了吗!!!
2. 状态机
首先我们得了解什么是状态机?这个当然是问度娘了!!!
状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机的内在因果关系的考虑。"现态"和"条件"是因,"动作"和"次态"是果。详解如下:
现态:是指当前所处的状态。
条件:当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
次态:条件满足后要迁往的新状态。"次态"是相对于"现态"而言的,"次态"一旦被激活,就转变成新的"现态"了。
有了理论的支撑,有没有发现,状态机这种机制,其实可以运行到很多场景的啦,不仅仅局限于按键。
3. 按键枚举和结构体
按键的状态可以分为:未按、按下、长按、抬起四种。当然你也可以按自己需求去分。废话不多说,直接上代码分析。
typedef enum _KEY_STATUS_LIST
KEY_NULL = 0x00,
KEY_SURE = 0x01,
KEY_UP = 0x02,
KEY_DOWN = 0x04,
KEY_LONG = 0x08,
KEY_STATUS_LIST;
我这里定义了一个按键状态的枚举,包含5个元素,KEY_NULL
表示无动作,KEY_SURE
表示确认状态,KEY_UP
表示按键抬起,
KEY_DOWN
表示按键按下,KEY_LONG
表示长按。
typedef enum _KEY_LIST
KEY0,
KEY1,
KEY2,
KEY_NUM,
KEY_LIST;
再来一个枚举,这里列出你的按键,KEY_NUM
可以自动统计你按键个数,这里KEY_NUM的值为3,至于为什么我就不多说了,自己问度娘。为什么这里要用枚举?这里抛个砖,继续往下看。
typedef struct _KEY_COMPONENTS
uint8_t KEY_SHIELD; //按键屏蔽0:屏蔽,1:不屏蔽
uint8_t KEY_COUNT; //按键长按计数
uint8_t KEY_LEVEL; //虚拟当前IO电平,按下1,抬起0
uint8_t KEY_DOWN_LEVEL; //按下时IO实际的电平
uint8_t KEY_STATUS; //按键状态
uint8_t KEY_EVENT; //按键事件
uint8_t (*READ_PIN)(void);//读IO电平函数
KEY_COMPONENTS;
extern KEY_COMPONENTS Key_Buf[KEY_NUM];
KEY_SHIELD
按键屏蔽用的,0表示按键不使用,1表示使用;KEY_COUNT
长按计数器,好比秒表,按开始然后开始计数了;KEY_LEVEL
虚拟按键按下的电平,KEY_DOWN_LEVEL
,实际按键按下的IO电平,这两个变量,主要是为了函数封装进行统一,比如你一个按键按下高电平,一个按下低电平,我不管这么多,反正我就和你KEY_DOWN_LEVEL
值进行比较,相等我就认为你按下,然后把KEY_LEVEL
置位,相反就清零;KEY_STATUS
就是我们说的按键状态了,它负责记录某一时刻按键状态;KEY_EVENT
表示按键事件,我这里分了3个事件,有按下、抬起和长按。(*READ_PIN)
是一个函数指针变量,需要把你读IO的函数接口给它。
最后别忘了,用这个结构体定义变量(这里只是声明哦),有几个按键就定义几个结构类型变量。发现没有我们这里用到了KEY_NUM
,好处一,按键增加也不需要改动。
4. 按键IO函数
首先得读IO口的电平吧。
static uint8_t KEY0_ReadPin(void)
return _KEY0;
static uint8_t KEY1_ReadPin(void)
return _KEY1;
static uint8_t KEY2_ReadPin(void)
return _KEY2;
这个很简单,就是把你的IO口电平返回来给我就可以了。可以根据自己单片机去实现。有了这几个函数不就可以定义的结构体类型变量了吗。
KEY_COMPONENTS Key_Buf[KEY_NUM] =
1,0,0,0,KEY_NULL,KEY_NULL,KEY0_ReadPin,
1,0,0,0,KEY_NULL,KEY_NULL,KEY1_ReadPin,
1,0,0,0,KEY_NULL,KEY_NULL,KEY2_ReadPin,
;
这个就不多说了,对着上面结构体说明看就知道了,我这里按键按下的都是低电平。
真正的按键IO电平获取函数在这里
static void Get_Key_Level(void)
uint8_t i;
for(i = 0;i < KEY_NUM;i++)
if(Key_Buf[i].KEY_SHIELD == 0)
continue;
if(Key_Buf[i].READ_PIN() == Key_Buf[i].KEY_DOWN_LEVEL)
Key_Buf[i].KEY_LEVEL = 1;
else
Key_Buf[i].KEY_LEVEL = 0;
这个函数主要是实现封装,两步走,先判断按键是否使能,每一个按键IO电平。如果我添加按键这里要改动吗?完全不需要动。
5. 按键状态机
重点来了,准备了那么多,终于可以上按键状态机代码实现了。
void ReadKeyStatus(void)
uint8_t i;
Get_Key_Level();
for(i = 0;i < KEY_NUM;i++)
switch(Key_Buf[i].KEY_STATUS)
//状态0:没有按键按下
case KEY_NULL:
if(Key_Buf[i].KEY_LEVEL == 1)//有按键按下
Key_Buf[i].KEY_STATUS = KEY_SURE;//转入状态1
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
//状态1:按键按下确认
case KEY_SURE:
if(Key_Buf[i].KEY_LEVEL == 1)//确认和上次相同
Key_Buf[i].KEY_STATUS = KEY_DOWN;//转入状态2
Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
//状态2:按键按下
case KEY_DOWN:
if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
else if((Key_Buf[i].KEY_LEVEL == 1) && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
Key_Buf[i].KEY_STATUS = KEY_LONG;//转入状态3
Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
//状态3:按键连续按下
case KEY_LONG:
if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
Key_Buf[i].KEY_EVENT = KEY_NULL;
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
这个函数就是获取当前所有按键状态,每20ms(用定时器定时)调用一次,就可以了。
//状态0:没有按键按下
case KEY_NULL:
if(Key_Buf[i].KEY_LEVEL == 1)//有按键按下
Key_Buf[i].KEY_STATUS = KEY_SURE;//转入状态1
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
这里拿一个按键做说明,首先进来先获取按键IO电平,按键开始状态从KEY_NULL
开始(现态),此时按键按下(条件),转入KEY_SURE
(次态),第一步完成退出;
//状态1:按键按下确认
case KEY_SURE:
if(Key_Buf[i].KEY_LEVEL == 1)//确认和上次相同
Key_Buf[i].KEY_STATUS = KEY_DOWN;//转入状态2
Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
定时器到20ms,刚好去抖动,进来就从新的状态KEY_SURE
开始了,再判断当前按键是否还是按下的,如果没有按下,那就返回KEY_NULL
,说明上次是干扰,如果按键是按下的,那就进入真正按下的状态KEY_DOWN
,同时我们给KEY_EVENT
事件赋值,标识我触发了按下事件,KEY_COUNT
清零为长按计数做准备,此时退出,你就可以在外面判断事件这个变量决定是否要执行什么任务(动作);
//状态2:按键按下
case KEY_DOWN:
if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
Key_Buf[i].KEY_STATUS = KEY_LONG;//转入状态3
Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
又过20ms进来,这次是从状态KEY_DOWN
开始,判断按键是否释放,如果释放就转入状态KEY_NULL
,同时标记事件为KEY_UP
,如果没被释放,我们会进行计数,同时清空数据标志,其它不变,因为我们的条件
没有满足,不进行状态迁移,需要注意每次进来没有变化就清空事件,不然出去你判断的标记又触发动作了;
//状态2:按键按下
case KEY_DOWN:
if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
Key_Buf[i].KEY_STATUS = KEY_LONG;//转入状态3
Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
同样20ms后进来,假设长按,还是从KEY_DOWN
开始,计数值累加,当累加到设定值,比如25时,也就是500ms,满足长按条件
,迁移到长按状态KEY_LONG
,标记事件为KEY_LONG
;
//状态3:按键连续按下
case KEY_LONG:
if(Key_Buf[i].KEY_LEVEL != 1)//按键释放,端口高电平
Key_Buf[i].KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KEY_EVENT = KEY_UP;//松开事件
Key_Buf[i].KEY_EVENT = KEY_NULL;
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
Key_Buf[i].KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KEY_COUNT = 0;//计数器清零
else
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
break;
20ms后,进入状态KEY_LONG
,一样判断是否释放,释放就进入KEY_NULL
状态,标记松开事件,否则继续判断是否为长按。我这里做的是一直按下,每500ms就返回一个长按事件。
这就是整状态机实现,这个函数也不需要我们修改,按键增加或减少根本不影响我这个函数的实现,这就是我前面做一堆枚举、结构体、函数封装的好处,好处二。
6. 按键处理函数
前面我们做了一堆标记事件,就是给我们主函数做处理的,下面是我的按键处理,放在主循环里就可以了。
void Task_KEY_Scan(void)
ReadKeyStatus();
if(Key_Buf[KEY0].KEY_EVENT == KEY_UP)
printf("KEY0 Down\n");
else if(Key_Buf[KEY0].KEY_EVENT == KEY_LONG)
printf("KEY0 Long Down\n");
if(Key_Buf[KEY1].KEY_EVENT == KEY_UP)
printf("KEY1 Down\n");
else if(Key_Buf[KEY1].KEY_EVENT == KEY_LONG)
printf("KEY1 Long Down\n");
if(Key_Buf[KEY2].KEY_EVENT == KEY_UP)
printf("KEY2 Down\n");
else if(Key_Buf[KEY2].KEY_EVENT == KEY_LONG)
printf("KEY2 Long Down\n");
这个就比较简单,就一直循环判断每个事件的标记,你需要哪种事件就做对比,出现这个标记就执行你的代码。有没有发现,我这里可以清楚的知道是哪个按键标记的事件,这就是那个枚举的终极用处,好处三。
到这里就结束了,有什么不足的欢迎大家指出留言下方,有好的建议也可以提出,大家共同学习。好东西分享给大家,后面也会更新一些实用的东西给大家,喜欢就点关注哦!!!^_^
以上是关于多按键状态机的实现的主要内容,如果未能解决你的问题,请参考以下文章