/用 C 语言实现有限状态机 FSM--基于表驱动 /
往期回顾:
3. 用C语言实现原型模式!
6. 用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是生成的可执行文件。
-
打电话流程测试图如下,通过输入进行触发事件,最开始是空闲,输入2拨号,触发拨号事件,进入拨号态,输入2接通接通电话,进入通话态,输入0接收通话,挂断电话,回到空闲态。可结合状态转换图一起看,更清晰。
-
接电话流程测试图如下,输入1触发响铃事件,进入响铃态,输入2接通电话,进入通话态,输入0结束通话,挂断电话,回到空闲态。
3. 其他流程也和状态转换图流程符合。
六、总结
有限状态机要点总结:
1.有多少个状态就有多少个处理函数。
2.状态图中有多少个事件连接线,表中就有多少项处理。
3.事件的作用是把状态从当前态转换到下一个状态。
4.发生事件后,执行的是下一个状态动作函数,即箭头所指的状态。例如:发生响铃事件,执行响铃态的动作函数。
通过上述的了解,我们发现通过有限状态机可以实现业务中比较复杂的逻辑程序,且易于扩展,便于维护。
需要C语言有限状态机代码的小伙伴:在微信公众号【Linux编程用C】后台回复 【fsm】 即可获取!
欢迎大家加小C微信【LinuxCodeUseC】,我们一起交流讨论学习!