一种轻量级的函数指针状态机框架--EFSM

       

前言


        注:本文的目标读者是有过状态机编程经验,并希望提升代码可阅读性或者实现状态模式里的进入/退出动作的软件工程师

有限状态机(Finite State Machin FSM)是一种很有用的编程手段,常用的实现方式有三种

        1.switch...case状态机

        2.二维状态转移表

        3.函数指针状态机

        其中第一种方式是最常见的实现状态机实现方式,缺点是粒度过大,状态机全部的代码集中在一个switch...case结构下,当代码规模较大或者状态较多的时候,就不太方便于阅读(可通过将将对应状态下的代码打包成函数的方式,提升可阅读性),容易写出意大利面条式的代码。

        第二种实现方式是通过构建一个二维数组,一个维度的索引为状态(state_idx),另外一个维度的索引是事件(evt_idx),数组内部的数据是对应状态-事件下应该执行的函数指针。优点是执行速度快,缺点是粒度过细,需要为每个状态下的每个事件都编写函数或者填入NULL,当状态和事件较多的时候,函数数量会非常多,不利于阅读。状态转移表的一种改进的方式是改为一维状态转移表,这里不做展开了。

        如果说前面两种实现方式,状态都是使用一个常量来表示的话,第三种实现方式则直接使用函数指针作为状态,进而在执行速度和粒度(每个状态对应一个函数)之间取得平衡。并且第三种实现方式也容易将状态机的调度部分提取出来,制作成状态机框架。

        QP内部的事件编程框架(QF)就带有一个使用函数指针来实现的状态机,它支持使用状态模式来编写代码,不仅能实现FSM还可以实现HSM(hierarchicalFinite State Machine层次状态机),同时还有诸如进入/退出动作,历史状态之类的功能。

        QP的入门可以参考我的两篇博文:

        QM基础教程

        QP/C初步入门

        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秒计时,仅用作示范,因为电脑的性能和平台的不同,实际延时时间会有不同。

        执行效果:

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值