一种状态机编程思想

在嵌入式软件的开发过程中,当我们遇到比较复杂庞大的系统,各个功能模块之间相互耦合,在没有清晰的代码结构的情况下,整个代码会非常混乱且难以维护,对于新增的需求也难以顺利的加入进去。

本文介绍一种状态机的编程思想,旨在使代码结构更加清晰,可扩展性更强。整体思想就是:对于任何一个复杂系统,实际上我们都可以把它分解成一个一个状态,系统的运行实际上就是在不同状态之间发生切换,切换的过程应满足一定的条件。本文介绍一种实际的方法论,使得状态机的运行过程——状态的切换变成一个自动的过程。

例子:我们想要控制一辆小汽车需要直行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),一个状态下可能有几条路径,对应到表里面就是有几行,查就完了。

结束,再见

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注