在嵌入式软件开发中,状态机编程是一个十分重要的编程思想,它也是嵌入式开发中一个常用的编程框架。掌握了状态机编程思想,可以更加逻辑清晰的实现复杂的业务逻辑功能。
状态机,或称有限状态机FSM(Finite State Machine),是一种重要的编程思想。
状态机有3要素:状态、事件与响应
状态机编程前,首先要根据需要实现的功能,整理出一个对应的状态转换图(状态机图),然后就可以根据这个状态转换图,套用状态机编程模板,实现对应是状态机代码了。
状态机编程主要有 3 种方法:switch-case 法、表格驱动法、函数指针法,本篇先介绍最简单也最易理解的switch-case 法。
下面以按键消抖功能,来介绍switch-case 法的状态机编程思路。
状态机机编程前,首先要明确的对应功能的状态机需要几个状态,本例的按键功能,只检测最基础的按下与松开状态(暂不实现长按、双击等状态),并增加对应的按钮去抖功能,因此,需要用到4个状态:
对应的状态转换图如下:
由于按键通常处于松开状态,这里让状态机的初始化状态为松开状态,然后在这4个状态中来回切换。
图中的VT代表按键检测到电平,VT=0即检测到低电平,可能是按键按下,由初始的“稳定松开”状态转为“按下抖动”状态
当持续检测到低电平(VT=0)一段时间后,认为消抖完成,由“按下抖动”状态转为“稳定按下”状态
在“按下抖动”状态时,在指定的一段时间内,再次检测到高电平(VT=1),说明确实是按钮抖动(比如按键被快速拨动了一下又弹起,或强烈震动导致的按键抖动),则由“按下抖动”状态转为“稳定松开”状态
对应上面的按钮状态图,可以知道需要用到4个状态:
这里使用枚举来定义这4个状态。为了在调试时,能够把对应状态名称以字符串的形式打印出来,这里使用宏定义的一个小技巧:
#符号+自定义的枚举名称
即可自动转变为字符串形式,再将这些字符串放到const char* key_status_name[]数组中,便可通过数组的形式访问这些状态的字符串名称形式。
此外,为了不重复书写枚举名称与对应的枚举字符串(#+枚举名称),进一步使用宏定义的方式,只定义一次状态,然后通过下面两条宏定义,实现对枚举项和枚举项对应的字符串的分别获取:
#define ENUM_ITEM(ITEM) ITEM,
#define ENUM_STRING(ITEM) #ITEM,
具体是宏定义、枚举定义与枚举名称数组声明如下:
#define ENUM_ITEM(ITEM) ITEM,
#define ENUM_STRING(ITEM) #ITEM,
#define KEY_STATUS_ENUM(STATUS) \
STATUS(KS_RELEASE) /*稳定松开状态*/ \
STATUS(KS_PRESS_SHAKE) /*按下抖动状态*/ \
STATUS(KS_PRESS) /*稳定按下状态*/ \
STATUS(KS_RELEASE_SHAKE) /*松开抖动状态*/ \
STATUS(KS_NUM) /*状态总数(无效状态)*/ \
typedef enum
{
KEY_STATUS_ENUM(ENUM_ITEM)
}KEY_STATUS;
const char* key_status_name[] = {
KEY_STATUS_ENUM(ENUM_STRING)
};
宏定义不便理解的,可以将宏定义分别带入,转为最终的结果,理解替代后的具体形式,比如下面的宏定义带入替换示意:
/*
KEY_STATUS_ENUM(STATUS) --> STATUS(KS_RELEASE) ... STATUS(KS_NUM)
KEY_STATUS_ENUM(ENUM_ITEM)
--> ENUM_ITEM(KS_RELEASE) ... ENUM_ITEM(KS_NUM)
--> KS_RELEASE, ... KS_NUM,
KEY_STATUS_ENUM(ENUM_STRING)
--> ENUM_STRING(KS_RELEASE) ... ENUM_STRING(KS_NUM)
--> #KS_RELEASE, ... #KS_NUM,
*/
下面是状态机的具体实现:
KEY_STATUS g_keyStatus = KS_RELEASE; //当前按键的状态
KEY_STATUS g_lastKeyStatus = KS_NUM; //上一状态
int g_DebounceCnt = 0; //消抖时间计数
void key_status_check()
{
switch(g_keyStatus)
{
//按键释放(初始状态)
case KS_RELEASE:
{
//检测到低电平,先进行消抖
if (KEY0 == 0)
{
g_keyStatus = KS_PRESS_SHAKE;
g_DebounceCnt = 0;
}
}
break;
//按下抖动
case KS_PRESS_SHAKE:
{
g_DebounceCnt++;
//确实是抖动
if (KEY0 == 1)
{
g_keyStatus = KS_RELEASE;
}
//消抖完成
else if (g_DebounceCnt == 5)
{
g_keyStatus = KS_PRESS;
printf("=====> key press\r\n");
}
}
break;
//稳定按下
case KS_PRESS:
{
//检测到高电平,先进行消抖
if (KEY0 == 1)
{
g_keyStatus = KS_RELEASE_SHAKE;
g_DebounceCnt = 0;
}
}
break;
//松开抖动
case KS_RELEASE_SHAKE:
{
g_DebounceCnt++;
//确实是抖动
if (KEY0 == 0)
{
g_keyStatus = KS_PRESS;
}
//消抖完成
else if (g_DebounceCnt == 5)
{
g_keyStatus = KS_RELEASE;
printf("=====> key release\r\n");
}
}
break;
default:break;
}
if (g_keyStatus != g_lastKeyStatus)
{
g_lastKeyStatus = g_keyStatus;
printf("new key status:%d(%s)\r\n", g_keyStatus, key_status_name[g_keyStatus]);
}
}
int main(void)
{
delay_init(); //延时函数初始化
KEY_Init();
uart_init(115200);
printf("hello\r\n");
while(1)
{
key_status_check();
delay_ms(10);
}
}
注:本例程需要使用一个按键,需要初始化对应的GPIO,这里不再贴代码。
将完整的代码编译后烧录到板子中,连接串口,按下与松开按键,观察串口输出信息。
我的测试输出信息如下:
前两次拨动按键模拟按钮抖动的情况,可以看到串口打印出两次从松开到按下抖动的状态切换。
然后是按下按键,再松开按键,可以看到状态的变化:松开 -> 按下抖动 -> 按下 -> 松开抖动 -> 松开
本篇介绍了嵌入式软件开发中常用的状态机编程实现,并通过按键消抖实例,以常用的switch-case形式,实现了对应的状态机编程代码实现,并通过测试,串口打印对应状态,分析状态机的状态跳转过程。