用 C 语言实现有限状态机 FSM--基于表驱动

/用 C 语言实现有限状态机 FSM--基于表驱动 /

往期回顾:

    1. 用 C 语言实现简单工厂模式!

    2. 用 C 语言编写建造者模式!

    3. 用C语言实现原型模式!

    4. 用 C 语言实现一个静态代理模式 !

    5. C语言实现设计模式--装饰模式!

    6. 用C语言实现适配器模式!

    7. 用 C 语言编写设计模式--模板模式

👀

一、简介

1. 在传统的控制逻辑程序中,我们常常使用 if、else if、else 或者 switch case 来进行判断处理,但是当业务需求逻辑复杂了,使用这种方式实现往往会变得很复杂,且写出的代码不易维护。此时采用有限状态机,这个问题将会变得容易起来。

2. 有限状态机,把复杂的控制逻辑进行分解成有限个稳定状态,组成闭环系统,通过事件触发,让状态机按设定的顺序处理事务。状态机的原理如下:在当前状态下,发生某个事件后转移到下一个状态,然后决定执行的功能动作。

3. 有限状态机有四个点: 事件、当前状态、相应的动作、下一个状态,把这四个特性封装进结构体,建成一个表(结构体数组),循环遍历数组,根据事件触发,取出满足条件的状态项并执行对应动作函数。

👀

二、设计与实现

本文以我们平常使用打电话为例,画出状态图,使用状态机实现对应的逻辑程序。下图是根据接、打电话需求逻辑画出的状态图,最开始事件和状态处于空闲,当有事件触发,会使状态转移并触发执行相应的动作函数。

下图,圆圈中代表状态,箭头所指方向代表状态转移的方向,线条上的字则代表触发的事件。例如:初始是空闲状态,发生响铃事件,状态转移为响铃态,并执行响铃态的动作。

有限状态机程序设计,主要四大点: 事件,当前态,动作(执行函数),次态。   

事件: 即状态图中线条上的字,使用枚举类型进行定义,可以按照实际需求进行增加、修改。

typedef enum
{
    E_IDLE,       /* 空闲 */
    E_BELL,       /* 铃声 */
    E_WHITE_LIST, /* 白名单 */
    E_BLACK_LIST, /* 黑名单 */
    E_FINISH,     /* 通话结束 */
    E_BUSY,       /* 占线忙 */
    E_CONNECT,    /* 接通 */
    E_DIAL,       /* 拨号 */
    E_TIME_OUT    /* 超时 */
}Event; /* 触发事件,由外部到来 */

状态:状态,表示当前业务中所以的状态,即状态图圆圈中的字,使用枚举类型进行定义。​​​​​​​

typedef enum
{
    S_IDLE=0, /* 空闲 */
    S_BELL,   /* 响铃 */
    S_TALK,   /* 通话 */
    S_HANGUP, /* 挂断 */
    S_DIAL,   /* 拨号 */
    S_TIMEOUT /* 超时 */
}State; /* 当前状态 */

动作(执行的函数):使用函数指针进行定义,便于实现 C 语言的"多态",执行不同函数。

void (*event_action)(Event *event, void *);

状态表:有了以上的了解,便可以开始今天的重点了--状态表,使用结构体定义。​​​​​​​

typedef struct FsmTable
{
    Event event;                    /* 触发事件 */
    State cur_sta;                  /* 当前状态 */
    void (*event_action)(Event *event, void *);  /* 动作函数 */
    State next_sta;                 /* 跳转状态 */
}FsmTable;

根据这个定义,我们使用结构体数组来做一个驱动表,使用方式如下:这里包含接、打电话全部的处理流程,后续根据事件进行触发执行动作,替代大量 if、else 的逻辑判断,使其逻辑清晰。​​​​​​​

FsmTable fsmtb[] = {
    /* 事件         当前状态        动作        下一个状态 */
    { E_IDLE,       S_IDLE,     idle_func,      S_IDLE },
    { E_BELL,       S_IDLE,     bell_func,      S_BELL }, 
    { E_DIAL,       S_IDLE,     dial_func,      S_DIAL },
    { E_TIME_OUT,   S_DIAL,     timeout_func,   S_TIMEOUT },
    { E_TIME_OUT,   S_BELL,     timeout_func,   S_TIMEOUT },
    { E_BUSY,       S_DIAL,     hangup_func,    S_HANGUP },
    { E_CONNECT,    S_DIAL,     talk_func,      S_TALK },
    { E_WHITE_LIST, S_BELL,     talk_func,      S_TALK }, 
    { E_BLACK_LIST, S_BELL,     hangup_func,    S_HANGUP },
    { E_FINISH,     S_TALK,     hangup_func,    S_HANGUP },
    { E_IDLE,       S_HANGUP,   idle_func,      S_IDLE },
    { E_IDLE,       S_TIMEOUT,  idle_func,      S_IDLE  }
};
//限于篇幅,此处展示部分的动作函数
void talk_func(Event *event, void *args)
{
    int ret=0;
    printf("通话中... ");
    printf("【输入1-9, 通话结束】:");
    scanf("%d", &ret);
  
    *event = E_FINISH; //触发下一个事件
    //此处做演示,直接更新触发事件,
    //实际中可根据传入的参数,进行更新触发事件,
    //或根据外部条件进行触发
}
void hangup_func(Event *event, void *args)
{
    printf("\n挂断电话...\n\n");
    *event = E_IDLE; //更新触发下一个事件
}

有限状态机的数据结构:包含了以上定义的数据结构​​​​​​​

typedef struct FSM
{
    FsmTable    *fsmtb;     /* 状态迁移表 */
    State       cur_sta;    /* 状态机当前状态 */
    Event       event;      /* 当前的事件 */
    uint8_t     sta_max_n;  /* 状态机状态迁移数量 */
}FSM;

👀

三、有限状态机的实现

有限状态机的实现函数就两个,一个是创建创建有限状态机结构体指针并初始化的函数,一个是需要被循环执行的事件处理函数。​​​​​​​

/**
 * @breif: 遍历状态表,处理事件
 * @fsm: 创建好的FSM结构体指针
 * @args: 传入的参数
 * @return: 1:成功
*/
int run_fsm_action(FSM* fsm, void *args)
{
    int max_n = fsm->sta_max_n, i=0;
    State cur_sta = fsm->cur_sta;
    FsmTable *fsmtb = fsm->fsmtb;
    if(!fsm) return -1;
    for(i=0; i<max_n; ++i){
        if(fsmtb[i].cur_sta == cur_sta && fsmtb[i].event == fsm->event){
            fsmtb[i].event_action(&fsm->event, args);  /* 调用对应的处理函数 */
            fsm->cur_sta = fsmtb[i].next_sta;           /* 转移到下一个状态 */
            break;
        }
    }
    return 0;
}
/**
 * @brief: 创建一个FSM结构体指针
 * @fsmtb: 填充好的状态表
 * @state: 初始状态
 * @event: 初始事件
 * @num: 状态表项个数
 * @return: 返回一个FSM结构体指针
*/
FSM* create_fsm(FsmTable* fsmtb, State state, Event event, int num)
{
    FSM* fsm = (FSM*)malloc(sizeof(FSM));
    fsm->cur_sta = state;
    fsm->event = event;
    fsm->fsmtb = fsmtb;
    fsm->sta_max_n = num;
    return fsm;
}

👀

四、使用方式

1.定义有限状态机结构体表​​​​​​​

FsmTable fsmtb[] = {
    /* 事件         当前状态        动作        下一个状态 */
    { E_IDLE,       S_IDLE,     idle_func,      S_IDLE },
    { E_BELL,       S_IDLE,     bell_func,      S_BELL }, 
    { E_DIAL,       S_IDLE,     dial_func,      S_DIAL },
    ......

2.定义对应的动作函数​​​​​​​

//空闲处理函数
void idle_func(Event *event, void *args)
{
    //...
}
//响铃处理函数
void bell_func(Event *event, void *args)
{
    //...
}
//拨号处理函数
void dial_func(Event *event, void *args)
{
    //...
}

3.创建结构体并循环运行事件处理函数​​​​​​​

int main(void)
{
    int num = sizeof(fsmtb)/sizeof(fsmtb[0]);
    FSM *fsm = create_fsm(fsmtb, S_IDLE, E_IDLE, num);
    while(1)
    {
        run_fsm_action(fsm, NULL); //循环运行
    }
    
    return 0;
}

👀

五、测试

编译:在Linux环境中,输入make进行编译,mainApp是生成的可执行文件。

  1. 打电话流程测试图如下,通过输入进行触发事件,最开始是空闲,输入2拨号,触发拨号事件,进入拨号态,输入2接通接通电话,进入通话态,输入0接收通话,挂断电话,回到空闲态。可结合状态转换图一起看,更清晰。

  2. 接电话流程测试图如下,输入1触发响铃事件,进入响铃态,输入2接通电话,进入通话态,输入0结束通话,挂断电话,回到空闲态。

3.  其他流程也和状态转换图流程符合。

六、总结

有限状态机要点总结:

1.有多少个状态就有多少个处理函数。

2.状态图中有多少个事件连接线,表中就有多少项处理。

3.事件的作用是把状态从当前态转换到下一个状态。

4.发生事件后,执行的是下一个状态动作函数,即箭头所指的状态。例如:发生响铃事件,执行响铃态的动作函数。

    

    通过上述的了解,我们发现通过有限状态机可以实现业务中比较复杂的逻辑程序,且易于扩展,便于维护。

需要C语言有限状态机代码的小伙伴:在微信公众号【Linux编程用C】后台回复 【fsm】 即可获取!

欢迎大家加小C微信【LinuxCodeUseC】,我们一起交流讨论学习!

  • 21
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值