一种状态机编程思想
在嵌入式软件的开发过程中,当我们遇到比较复杂庞大的系统,各个功能模块之间相互耦合,在没有清晰的代码结构的情况下,整个代码会非常混乱且难以维护,对于新增的需求也难以顺利的加入进去。
本文介绍一种状态机的编程思想,旨在使代码结构更加清晰,可扩展性更强。整体思想就是:对于任何一个复杂系统,实际上我们都可以把它分解成一个一个状态,系统的运行实际上就是在不同状态之间发生切换,切换的过程应满足一定的条件。本文介绍一种实际的方法论,使得状态机的运行过程——状态的切换变成一个自动的过程。
例子:我们想要控制一辆小汽车需要直行100米,然后右转200米,到达目的地。
我可以把小汽车分为以下几种状态:一、静止状态(也是小汽车的初始状态);二、右转状态;三、直行状态。
小汽车的整个运动过程我们可以理解为:
1、最初汽车处于静止状态;
2、按下开始(状态从静止到直行的切换条件满足),汽车状态切换为直行状态;
3、汽车GPS检测到100米位置到达(直行状态切换到右转状态的条件满足),汽车切换到右转状态;
4、GPS检测到转向完成之后(右转状态切换到执行状态的条件满足),汽车回正方向盘,进入直行状态;
5、GPS检测到200米位置到达(直行状态到静止状态的切换条件满足),汽车刹车,进入停止状态。
可以看到整个过程都可以分解为状态和状态的切换,下面开始介绍实现方法。
我们以四个状态为例,状态切换图如下:
一、状态
以一个枚举类型数据,记录所有状态
typedef enum
{
SYSTEM_STATE_1 = 0,//状态1
SYSTEM_STATE_2, //状态2
SYSTEM_STATE_3, //状态3
SYSTEM_STATE_4, //状态4
}SYSTEM_STATE_e;
二、状态动作和状态切换
以两个结构体记录状态机的:当前状态、当前状态所要执行的动作(包括初始化动作和轮询动作)、是否满足状态切换条件、条件满足后应切换到下一个是什么状态。
首先看第一个结构体,
typedef struct
{
uint8_t mu8_workState; //工作状态
void (*ctlInit)( uint8_t param ); //此工作状态下的初始化函数指针
void (*ctlLoop)( uint8_t param ); //此工作状态下的轮询函数指针
}SYSTEM_STATE_WORK_t;
其中包含:
工作状态:mu8_workState,非系统实际运行的状态,是一个数据表,不断查询系统状态与此数据表对比
此工作状态下所要直行的Init动作:
void (*ctlInit)( uint8_t param );
此工作状态下所要执行的Loop动作:
void (*ctlLoop)( uint8_t param );
其中,后两者为函数指针,具体的函数实现,根据各个状态下的动作自行实现。
应注意,从结构体仅仅为一个数据结构,内容只包含一个状态下的信息,一个系统包含很多状态,后面会使用此数据结构,定义一个结构体数组,类似一个列表,存储多个状态下的信息,状态机去查询这个表格,判定系统状态,执行对应状态的函数。
可以看到第一个结构体内存储的信息,包含了各个状态和状态下的执行动作。
但不涉及状态的切换,这就是第二个结构体的功能了。
第二个结构体:
typedef struct
{
uint8_t mu8_workState; //工作状态
bool (*stateCheck)( uint8_t param ); //状态切换的判定函数
uint8_t mu8_nextWorkState; //满足切换条件之后的下一个状态
}SYSTEM_STATE_CHECK_t;
其中包含:
工作状态:mu8_workState,非系统运行的实时状态,系统运行的实时状态由另一个全局变量存储,此处的状态只是数据表,通过轮询比对系统运行状态全局变量与此数据表进行对比,确定应执行什么动作,进行什么判断。
状态机在此状态下,应执行的判断状态切换的函数指针:bool (*stateCheck)( uint8_t param );
满足切换条件后的应切换到的下一个状态:mu8_nextWorkState
注意,此数据结构也仅是为列表使用,这个数据表的一个巨大作用是,可以规定从某一个状态切换到另一个状态的切换路径(切换前的状态,满足某些条件,切换后的状态),在此数据表内未记录的状态切换路径,是不可能发生的,由此我们可以精确的控制所有的状态切换。
三、状态数据列表
前面说到,两个数据结构都是对单一状态的信息,其目的是为了列结构体数组表
static SYSTEM_STATE_WORK_t workPerform[4] = {
//状态1下 初始化函数:State_1_Init 循环体:State_1_Loop {SYSTEM_STATE_1,State_1_Init,State_1_Loop},
//状态2下 初始化函数:State_2_Init 循环体:State_2_Loop {SYSTEM_STATE_2,State_2_Init,State_2_Loop},
//状态3下 初始化函数:State_3_Init 循环体:State_3_Loop {SYSTEM_STATE_3,State_3_Init,State_3_Loop},
//状态4下 初始化函数:State_4_Init 循环体:State_4_Loop {SYSTEM_STATE_4,State_4_Init,State_4_Loop},
};
第一张表记录了四种状态下的,初始化函数和循环函数的函数指针,初始化函数内执行进入此状态后只需要执行一次的动作,如变量清零等,循环函数下执行需要一直轮询的动作。接下来看第二张表
由状态切换图我们可以看到状态的切换情况:
状态2可以切换的路径有:
状态2—>状态1;
状态2—>状态3;
状态2—>状态4;
转化为代码如下:
static SYSTEM_STATE_CHECK_t stateCheckPerform[] = {
{SYSTEM_STSTE_2,System_State_1_Check,SYSTEM_STATE_1},
{SYSTEM_STSTE_2,System_State_3_Check,SYSTEM_STATE_3},
{SYSTEM_STATE_2,System_State_4_Check,SYSTEM_STATE_4},
};
代码解释:状态2下,状态机不断查询此表格,是否满足切换到状态1、3、4的条件,System_State_x_Check为判断函数,判断状态切换的逻辑可以写在此函数内,若满足条件则返回true,否则返回false,若返回true,状态则切换到第三个元素记录的状态(状态1或状态3或状态4)
同样的:
状态3可以切换的路径有:
状态3—>状态1;
状态3—>状态4;
状态4可以切换的路径有:
状态4—>状态1 ;
我们把所有的切换路径合成一张大表如下:
static SYSTEM_STATE_CHECK_t stateCheckPerform[] = {
{SYSTEM_STSTE_2,System_State_1_Check,SYSTEM_STATE_1},
{SYSTEM_STSTE_2,System_State_3_Check,SYSTEM_STATE_3},
{SYSTEM_STATE_2,System_State_4_Check,SYSTEM_STATE_4},
{SYSTEM_STATE_3,System_State_1_Check,SYSTEM_STATE_1},
{SYSTEM_STATE_3,System_State_4_Check,SYSTEM_STATE_4},
{SYSTEM_STATE_4,System_State_1_Check,SYSTEM_STATE_1},
};
四、状态机逻辑函数
两张表格列完以后,我们可以开始使用这两张表格来进行状态机逻辑的编写:
void systemRunning( void )
{
/*循环查询系统状态*/
for(uint8_t i = 0 ; i < (sizeof(workPerform)/sizeof(workPerform[0])) ; i ++)
{
/*u8_systemState记录系统当前状态,与表内每个结构体元素的第一个成员对比*/
if(u8_systemState == workPerform[i].mu8_workState)
{
/*u8_systemStateLast记录上一次状态,状态的一阶历史,用于状态初始化执行判断*/
if(u8_systemStateLast != u8_systemState[encoder])
{
/*历史状态和当前状态不同,状态刚发生切换,执行一次初始化*/
workPerform[i].ctlInit(encoder);
/*同步状态历史数据,防止状态初始化函数重复执行*/
u8_systemStateLast[encoder] = u8_systemState[encoder];
}
/*执行当前状态下的循环函数*/
workPerform[i].ctlLoop(encoder);
/*状态切换函数*/
systemStateSwitch();
break;
}
}
}
可以看出,我们使用一个存储系统当前状态和前一次状态的变量,不断去查表,查到系统当前状态与把表内吻合时,就执行对应的初始化和循环函数。其中还包含一个状态自动切换的函数,这就需要使用前面所说的第二个结构体的内容了,下面开始介绍状态切换函数是如何实现状态的自动切换的。
void systemStateSwitch( void )
{
/*遍历状态切换表*/
for ( uint8_t i = 0;i < (sizeof(stateCheckPerform)/sizeof(stateCheckPerform[0])) ;i ++ )
{
/*只查询当前状态下的路径*/
if(u8_systemState == stateCheckPerform[i].mu8_workState)
{
/*状态切换条件是否满足*/
if(stateCheckPerform[i].stateCheck(encoder) == true)
{
/*满足则切换状态*/
u8_systemState[encoder] = stateCheckPerform[i].mu8_nextWorkState;
break;
}
}
}
}
这里其实就是查第二张表格,那系统当前状态与第二张表的每个元素的第一个成员进行对比,相当于查询当前状态下所有的状态切换路径,看其Check函数的返回值是否是true,若返回true,则状态发生切换,路走通了,然后继续查询新的状态下对应的所以有切换路径。
这里就可以体会到,前面两张表格的巨大作用,所有的状态切换路径都已经在表格里面规定好了,我们要做的只是去查询这个路径能不能走得通(Check函数是否返回true),一个状态下可能有几条路径,对应到表里面就是有几行,查就完了。
结束,再见