前言
注:本文的目标读者是有过状态机编程经验,并希望提升代码可阅读性或者实现状态模式里的进入/退出动作的软件工程师
有限状态机(Finite State Machin FSM)是一种很有用的编程手段,常用的实现方式有三种
1.switch...case状态机
2.二维状态转移表
3.函数指针状态机
其中第一种方式是最常见的实现状态机实现方式,缺点是粒度过大,状态机全部的代码集中在一个switch...case结构下,当代码规模较大或者状态较多的时候,就不太方便于阅读(可通过将将对应状态下的代码打包成函数的方式,提升可阅读性),容易写出意大利面条式的代码。
第二种实现方式是通过构建一个二维数组,一个维度的索引为状态(state_idx),另外一个维度的索引是事件(evt_idx),数组内部的数据是对应状态-事件下应该执行的函数指针。优点是执行速度快,缺点是粒度过细,需要为每个状态下的每个事件都编写函数或者填入NULL,当状态和事件较多的时候,函数数量会非常多,不利于阅读。状态转移表的一种改进的方式是改为一维状态转移表,这里不做展开了。
如果说前面两种实现方式,状态都是使用一个常量来表示的话,第三种实现方式则直接使用函数指针作为状态,进而在执行速度和粒度(每个状态对应一个函数)之间取得平衡。并且第三种实现方式也容易将状态机的调度部分提取出来,制作成状态机框架。
QP内部的事件编程框架(QF)就带有一个使用函数指针来实现的状态机,它支持使用状态模式来编写代码,不仅能实现FSM还可以实现HSM(hierarchicalFinite State Machine层次状态机),同时还有诸如进入/退出动作,历史状态之类的功能。
QP的入门可以参考我的两篇博文:
QP虽然很强大,但由于编程的思路和主流的RTOS编程截然不同,在实际项目中使用的时候,受到了部分同事的抵制。几年前在离职的时候为了让同事便于接手一个项目,需要一个简单的状态机框架来"翻译"原有的为QP而编写的代码,EFSM的前身就这样诞生了。
今年年初因为项目的需求比较适合使用状态机建模,于是抽空在周末编写了这个EFSM(Extended Finite State Machin--扩展有限状态机)。EFSM作为一个轻量级的状态机框架,它适用于嵌入式环境,相对普通的FSM扩展出了entry/exit动作。。
相关信息
下载地址
https://download.csdn.net/download/chenbb8/86721947
https://github.com/chenbb8/efsm
百度网盘: https://pan.baidu.com/s/1h9d6zV-TkvnGWDVeSK6Tgw?pwd=fcbe 提取码: fcbe
目录结构
│ version.txt
│
├─efsm //efsm本体代码
│ efsm.c //efsm主体代码
│ efsm.h
│ efsm_config.h //efsm配置
│ efsm_hal.c //efsm的平台相关代码
│ efsm_hal.h
│
└─examples
└─blink //闪灯例子
│ .blink
│ blink.qm //blink的状态图
│
├─codeblocks //windows下的codeblocks工程
│ blink.cbp
│ blink.depend
│ blink.layout
│ main.c
│
└─linux_gcc //linux下的makefile工程
main.c
makefile
原理解析
注:为阅读方便,仅提取主要代码进行解说,一些辅助性的代码,例如函数的参数检查,抢占资源保护之类的内容已经删除
EFSM的事件仅支持flag标志位,它默认是一个uint16_t类型的变量,变量对应的bit上置1则表示对应事件有效。事件flag的低3位是由框架内部自动产生的:
enum {
EFSM_EVT_ENTRY = _BIT(0),//内部:进入事件
EFSM_EVT_EXIT = _BIT(1),//内部:退出事件
EFSM_EVT_COMMON = _BIT(2),//内部:通用事件,每次调用Efsm_Hand()时候自动产生
EFSM_EVT_TICK = _BIT(3),//用户:已定义的tick事件
EFSM_EVT_USER = _BIT(4),//用户:自定义事件的起始bit
};
其中事件EFSM_EVT_ENTRY/EFSM_EVT_EXIT与进入/退出动作有关系。
用户通过_EFSM_USER_BIT()宏自定义事件,例如
#define DOOR_S_CLOSE _EFSM_USER_BIT(0)
用户在编写状态处理函数的时候,根据需要添加被处理的事件分支:
void DoorStateOpen(door_t *ao, uint16_t evt)
{
switch (evt)
{
case EFSM_EVT_ENTRY:
开门
break;
case DOOR_S_CLOSE:
状态转移到关门
break;
}
}
上面的DoorStateOpen既是状态处理函数,同时也作为door状态机的一个状态。其中door_t是用户自定义的类型,它继承自efsm_t,因此当door_t*类型通过强制类型转换,赋值给efsm_t类型的时候并不会导致错误。
typedef struct structEfsm efsm_t;
typedef void (*efsmState_t)(efsm_t *ao, uint16_t evt);//状态的类型
struct structEfsm {
efsmState_t state;//当前状态
efsmState_t nextState;//需要转换的状态
uint16_t evt;//事件
};
//以下是用户自定义个类型
typedef struct {
efsm_t super;
xxx
yyy
} door_t;
efsm_t类型是EFSM内部用来记录当前状态机的一些相关信息,用户每建立一个状态机,都应该通过继承efsm_t,生成一个新的类型,比如这里的door_t和后面的led_t。
efsm_t中的state存放的是当前的状态,比如这里的DoorStateOpen状态,而当它需要切换状态的时候,需要调用Efsm_Trans()函数:
/**
* @brief 切换状态
* @param ao[in/out]: 被操作的对象
* @param state: 被切换的状态
*/
void Efsm_Trans(efsm_t *ao, efsmState_t state)
{
ao->nextState = state;
Efsm_EvtTrig(ao, EFSM_EVT_EXIT);
}
/**
* @brief 发送事件
* @param ao[in/out]: 被操作的对象
* @param evt: 被发送的事件
*/
void Efsm_EvtTrig(efsm_t *ao, uint16_t evt)
{
ao->evt |= evt;
}
需要注意的一点是EFSM使用的状态类型和用户定义的状态处理函数类型可能不一致,比如这里的DoorStateOpen类型为:
void (*)(door_t *ao, uint16_t evt);
因此需要在调用相关函数的时候对状态处理函数进行强制类型转换,也可以使用带有强制类型转换的函数宏来实现同样功能,它们的使用方式参考后面的闪灯实例:
#define EFSM_REG_STATE(STATE) (Efsm_RegState((AO), (efsmState_t)(STATE)))//为当前对象注册state
#define EFSM_EVT_TRIG(EVT) (Efsm_EvtTrig((AO), (EVT)))//为当前对象发送事件
#define EFSM_TRANS(STATE) (Efsm_Trans((AO), (efsmState_t)(STATE)))//为当前对象切换状态
状态机的运行,需要用户手动调用Efsm_Hand(),这是一个非阻塞式的函数(包括用户编写的状态处理函数也应该是非阻塞式的函数),每次调用都会在内部通过轮询,自动的将被设置过的事件flag进行处理:
/**
* @brief 运行EFSM
* @param ao[in/out]: 被操作的对象
* @note state: 非阻塞函数
*/
void Efsm_Hand(efsm_t *ao)
{
uint16_t evtBit;
evtBit = EFSM_EVT_COMMON;//自动产生EFSM_EVT_COMMON事件
ao->state(ao, evtBit);
while (ao->evt!=0)//循环查询事件
{
evtBit = Efsm_GetEvt(ao->evt);//获取最低位的事件
if (evtBit)
{
ao->state(ao, evtBit);//调用用户编写的状态处理函数
if ((evtBit==EFSM_EVT_EXIT)&&(ao->nextState!=NULL)) {//判断是否需要切换状态
ao->state = ao->nextState;
ao->nextState = NULL;
Efsm_EvtTrig(ao, EFSM_EVT_ENTRY);//切换状态后,自动产生EFSM_EVT_ENTRY事件
}
ao->evt &= ~evtBit;//清除已处理的事件
}
}
}
这20来行就是EFSM实现状态转换的核心代码。
闪灯实例
这是一个闪灯状态机,以2秒为周期进行亮灭闪烁,对应的状态图(这里为了清晰的展示调用的是EFSM_EVT_ENTRY事件,就没使用qm的状态图自带的entry动作):
首先继承efsm_t实现led对象,并为后面能直接调用EFSM_REG_STATE()/EFSM_EVT_TRIG()/EFSM_TRANS()而定义宏AO
typedef struct {
efsm_t super;
uint16_t timeCount;
uint32_t globalTick;
} led_t;
led_t ao_led;
#define AO (efsm_t*)led
其中属性timeCount用于辅助计时,而globalTick则用来打印累计计时时间。ao_led是led对象的实例,其中ao这个命名习惯来自QP,意思是active object。
接下来定义led对象的构造函数,它用于实现状态图中的初始转换。
void Led_Ctor(led_t *led)
{
Efsm_Ctor((efsm_t*)led, (efsmState_t)Led_StateOn);
led->globalTick = 0;
EFSM_REG_STATE(Led_StateOn);//提前注册state,用于Efsm_Trans()的参数检测
EFSM_REG_STATE(Led_StateOff);
}
其中传入到Efsm_Ctor()函数的参数Led_StateOn,是这个led对象的初始状态。
然后分别定义Led_StateOn和Led_StateOff两个状态处理函数:
void Led_StateOn(led_t *led, uint16_t evt)
{
switch (evt)
{
case EFSM_EVT_EXIT:
printf("%8d:Led_StateOn, Exit\n",led->globalTick);
break;
case EFSM_EVT_ENTRY:
led->timeCount = 0;
printf("%8d:Led_StateOn, Entry\n",led->globalTick);
break;
case EFSM_EVT_TICK:
++led->globalTick;
if (++led->timeCount >= 2000) {
EFSM_TRANS(Led_StateOff);
}
break;
}
}
void Led_StateOff(led_t *led, uint16_t evt)
{
switch (evt)
{
case EFSM_EVT_EXIT:
printf("%8d:Led_StateOff, Exit\n",led->globalTick);
break;
case EFSM_EVT_ENTRY:
led->timeCount = 0;
printf("%8d:Led_StateOff, Entry\n",led->globalTick);
break;
case EFSM_EVT_TICK:
++led->globalTick;
if (++led->timeCount >= 2000) {
EFSM_TRANS(Led_StateOn);
}
break;
}
}
为了方便观察运行信息,在EFSM_EVT_EXIT和EFSM_EVT_ENTRY动作下增加了打印运行信息的功能。
这个状态机还比较简单,不太容易看出在在EFSM_EVT_ENTRY下增加了初始化led->timeCount的功能的优点,但在状态机的规模较大,状态转换较多的时候,通过执行状态的进入或者离开动作来初始化或者注销资源,可阅读性和安全性会显著提高。
最后是led对象在main函数下的初始化和调用的例子
int main()
{
Led_Ctor(&ao_led);
while (1)
{
Efsm_EvtTrig((efsm_t*)&ao_led, EFSM_EVT_TICK);
Efsm_Hand((efsm_t*)&ao_led);
usleep(1000);
}
system("pause");
return 0;
}
需要注意,这里使用usleep(1000)的延时1ms,并通过led->timeCount累加2000次代表2秒计时,仅用作示范,因为电脑的性能和平台的不同,实际延时时间会有不同。
执行效果: