最近看到几个关于状态机编程的帖子,发现大家更多的是在辩论状态机编程好不好。这里,我以原子提供的《实验3 按键输入》为基础,着重以key.c中的按键扫描函数KEY_Scan()为例,写一个的基于状态机形式的KEY_Scan(),来扫描KEY_UP、KEY0、KEY1和KEY2,希望能起到抛砖引玉的作用。工程项目文件我打包挂在下面,大家可以下载到自己的板子上运行,效果很好。
一般情况下,在按键扫描函数中最让人蛋疼的就是按键消抖延时。这是可靠程序所必须的。否则,按键一次,MCU读到的却是N多次。但是用延时来给按键消抖,代价是很大的。对于我们用户来说,KEY_Scan()中的“delay_ms(10); //抖动延时”并无大碍,但是对于STM32这样的72MHz的MCU来说,delay_ms(10)是很漫长的。所以,如果你的系统里需要数码管显示数据,同时还要实时扫描按键,而你的KEY_Scan()中含有类似delay_ms(10)这样的语句的话,那么数码管的数据显示肯定会有问题,除非你搭载了uC/OS-II。除此之外,“delay_ms(10)”至少还有以下两点不足:
第一,如果需要实时扫描按键,那么扫描时间间隔一定要小于按键消抖延时,否则,会丢键;
第二,由于按键本身的机械性能不同,对某些按键而言,delay_ms(10)可能因延时过长而丢键,也可能因延时不足而一键多发。
归根结底,delay_ms(10)对要求不太严格的系统是可以用的,而且效果也不错。但是还是治标不治本。而状态机的优势就是不用delay_ms(10)这种方法。
下面,以状态机的方法,来写一个KEY_Scan()函数(其实状态机的方法真的用的很广,尤其是在多任务运行的情况下)。
用状态机思想去进行单片机编程,比较通用的方法就是用switch分支语句来进行状态的跳转,不仅编程模式相对固定,而且程序可读性也很好。就像下面这种框架:
switch(KeyStatus)
{
case KEY_SEARCH_STATUS: //按键查询状态
{
……
KeyStatus=KEY_ACK_STATUS; //按键下一个状态为确认状态
……
}
case KEY_ACK_STATUS: //按键确认状态
{
……
KeyStatus=KEY_REALEASE_STATUS; //按键下一个状态为释放状态
……
}
case KEY_REALEASE_STATUS: //按键释放状态
{
……
KeyStatus=KEY_SEARCH_STATUS; //按键下一个状态为释放状态
……
}
default: break;
}
在key.c中,状态机形式的KEY_Scan()如下所示,注意,整个函数是没有delay的。另外,原子提供的KEY_Scan()通过入口参数是可以选择是否支持长按的,这里我只把注意力集中于状态机上,你也可以实现类似选择是否支持长按,加一个静态变量来标识按键的状态即可,并不难。工程文件在下面。
u8 KEY_Scan(void)
{
static u8 KeyStatus=KEY_SEARCH_STATUS; //静态变量,保存按键状态
static u8 KeyCurPress=0; //静态变量,保存当前按键的键值
u8 KeyValue=0;
if(KEY0==0||KEY1==0||KEY2==0||KEY3==1) //如果有按键按下
{
if(KEY0==0) KeyValue=KEY_RIGHT;
else if(KEY1==0) KeyValue=KEY_DOWN;
else if(KEY2==0) KeyValue=KEY_LEFT;
else if(KEY3==1) KeyValue=KEY_UP;
}
switch(KeyStatus)
{
case KEY_SEARCH_STATUS: //按键查询状态
{
if(KeyValue) //如果有按键按下(暂时不管到底是哪个Key按下)
KeyStatus=KEY_ACK_STATUS; //则按键下一个状态为确认状态
return 0;
}
case KEY_ACK_STATUS: //按键确认状态
{
if(!KeyValue) //如果没有按键按下
KeyStatus=KEY_SEARCH_STATUS; //按键状态返回为查询状态
else //否则有按键按下
{
if(KEY0==0) KeyCurPress=KEY_RIGHT;
else if(KEY1==0) KeyCurPress=KEY_DOWN;
else if(KEY2==0) KeyCurPress=KEY_LEFT;
else if(KEY3==1) KeyCurPress=KEY_UP;
KeyStatus=KEY_REALEASE_STATUS; //按键下一个状态为释放状态
}
return 0;
}
case KEY_REALEASE_STATUS: //按键释放状态
{
if(!KeyValue) //按键释放
{
KeyStatus=KEY_SEARCH_STATUS; //按键下一个状态为释放状态
return KeyCurPress; //返回当前按键
}
return 0;
}
default: return 0;
}
}
修改:发现 KEY_Scan() 修改为以下代码更简洁(对应修改后的工程项目文件也在下面):
u8 KEY_Scan(void)
{
static u8 KeyStatus=KEY_SEARCH_STATUS; //静态变量,保存按键状态
static u8 KeyCurPress=0; //静态变量,保存当前按键的键值
u8 HasKeyPressed ;
/* 如果有按键按下,HasKeyPressed置1.否则,置0 */
HasKeyPressed=(KEY0==0||KEY1==0||KEY2==0||KEY3==1)?1:0;
switch(KeyStatus)
{
case KEY_SEARCH_STATUS: //按键查询状态
{
if( HasKeyPressed ) //如果有按键按下(暂时不管到底是哪个Key按下)
KeyStatus=KEY_ACK_STATUS; //则按键下一个状态为确认状态
return 0;
}
case KEY_ACK_STATUS: //按键确认状态
{
if(! HasKeyPressed ) //如果没有按键按下
KeyStatus=KEY_SEARCH_STATUS; //按键状态返回为查询状态
else //否则有按键按下
{
if(KEY0==0) KeyCurPress=KEY_RIGHT;
else if(KEY1==0) KeyCurPress=KEY_DOWN;
else if(KEY2==0) KeyCurPress=KEY_LEFT;
else if(KEY3==1) KeyCurPress=KEY_UP;
KeyStatus=KEY_REALEASE_STATUS; //按键下一个状态为释放状态
}
return 0;
}
case KEY_REALEASE_STATUS: //按键释放状态
{
if(! HasKeyPressed ) //按键释放
{
KeyStatus=KEY_SEARCH_STATUS; //按键下一个状态为释放状态
return KeyCurPress;//返回当前按键
}
return 0;
}
default: return 0;
}
}
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
---------------------------------
比如,你的MCU上电后第一步就是要判断按键值,然后才根据按键值去执行不同的操作,这就是你说的情况了吧。如下:
u8 KeyVal=0;
main()
{
while( ( KeyVal = Key_Scan() ) ==0 ); //其实这句中的while不也是个循环么,依然会消抖
switch(KeyVal )
{
//根据 KeyVal 去执行不同的操作
}
}
一周热门 更多>