概要
关于进行本次项目的功能模拟,主要由于在实际工作中遇到一种复杂情形----多条件下大量使用if-else语句,造成代码的冗余,且不易于后期维护,特此借助于状态机功能实现,有利于后期项目开发维护,更重要的是 状态机对于编程者或者初学者而言,刚接手一个项目时对功能的解读是大有益处的。
状态机简介
状态机通常是有限状态机的简称,重点在于我们对状态的研究必须是有限的,所以我们在此并不需要去探讨无限的繁杂情况。有限状态机(英文为Finite State Machine,简称FSM)/有限自动状态机(Finite State Automaton,简称FSA),表示为有限个状态以及这些状态之间的转换动作。自动两个字体现了状态间的转移行为是不需要外部干预的,对于这一数学模型的研究,感兴趣者可以多去网上寻找资料学习。方便起见,关于本次模型的构建以及代码或文字的描述,我们习惯地的使用FSM/状态机表示有限状态机。
状态机类型
关于状态机类型,百度百科以及学者研究,参考大量资料,普遍将状态机归为两类,即Moore状态机、Mealy状态机。以下引用源于百度百科。
状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作,完成特定操作的控制中心。状态机分为摩尔(Moore)型状态机和米莉(Mealy)型状态机。
第一类,若输出只和状态有关而与输入无关,则称为Moore状态机
第二类,输出不仅和状态有关而且和输入有关系,则称为Mealy状态机
状态机的工作原理
FSM要进行工作,需要满足四要素,初态、事件、动作、次态,当然不同研究叫法不同,但意思基本一样。在代码中,常用cur_sta、event、event_action和next_sta表示。
初态描述的是状态机当前的初始状态;
事件或者条件描述的是当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移;
动作表现为条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。在代码中用函数来执行一个或多个动作;
次态是相对于初态而言的,是当条件满足后要转移到另一新的状态。其一旦被激活,就转变成新的“初态”了。这是一种状态的转移,驱动表定义好后,由程序自动完成。
状态机的工作原理如下图所示,发生事件(event)后,根据当前状态(cur_state) ,决定执行的动作(action),并设置下一个状态号(nxt_state)。 注:前一句话和下面引用图均来源于百度百科。
!
重点对于状态机而言,传统上我们是用if-else语句或者switch 语句进行逻辑控制的,在单片机的裸机开发中很常见,但是对于现在主流的芯片,如S32K144之类的,这些基于RTOS级的单片机,利用状态机开发是很需要的。并且对于状态机而言,它实际是一个闭环,上面提到的四个要素都被封装起来,在c语言中,我们用结构体和函数进行封装,而在c++或者java中则 是类等,所以对于封装好的东西,我们只需要根据触发事件让他自己遍历执行即可。
所以基于上述,我们可以大致清楚,在代码的实现上,状态机大致由哪些东西组成了,基于结构体,我们得出以下状态机的创建。
typedef struct FSM
{
warn_FsmTable *fsmtb;//下文有介绍,这个其实就是执行的动作
State cur_sta;
Event event;
u8_int sta_maxnum;
}FSM;
状态机模型构建与实现
在本次项目中,因为客户需求,将某一功能进行变更,涉及代码工作量巨大,并且逻辑实现复杂,关键在于基础程序是if-else的逻辑,很难理解,也不容易维护。基于对状态机的了解,本次项目进行了逻辑的重构,鉴于项目代码庞大,牵扯内容较多,并且对于量产项目而言,单片机执行情况复杂,为方便且易于理解,在这里就简单以弹窗报警为例进行模拟实现部分功能。
一、背景
假设对于某一液晶显示弹窗而言,比如洗衣机,电视,手机或者汽车等,以汽车为例,输入值为尿素液量,执行条件为当液量比低于某一值时进行报警,此时报警存在两种状态,一种是友情提示需要注意,二是严重威胁到行车状态,需要进行一定操作。并且这两种报警状态存在回差。下面针对这样的报警假设进行模型构建
一、模型构建
在建模前,先确定好状态以及事件,并根据事件和状态进行相应动作的定义。根据本次研究假设,定义四种状态,及五个触发事件。分别用枚举实现如下:
typedef enum{
S_leisure=0,
S_tLng,
S_jLng,
S_negative,
}State;
typedef enum
{
E_leisure,//空闲/无效
E_below10,//液量低于10%
E_bet10_12,//液量0到12%
E_bet10_20,//液量低于20%
E_negative,//液量比无效值
}Event;
基于这样的定义,我们构建如下图模型。
同时对动作进行定义,在状态机中,我们一般采用函数指针方式进行定义,这和JAVA中类的多态是类似的,在C语言中,函数指针能很好的帮助我们实现多态。
typedef void (*event_action) (Event *event,void *);
对于函数指针和指针函数不太了解的读者朋友,可以参考我的另一文章-----“浅析C语言中的指针函数与函数指针”。
基于上述,根据构建好的模型,我们进行状态表的定义,使用结构体进行封装,也即我们之后需要用到的驱动表,一般定义好驱动表后,直接在表中进行状态转移增删改即可。
typedef struct warn_FsmTable // 状态表
{
Event event;
State cur_sta;
void (*event_action) (Event *event,void *);
State next_sta;
}warn_FsmTable;
状态表对应定义的驱动表,以下把本次模拟所可能出现的情况都进行了枚举,这本质上是一种控制逻辑的实现,如同常用的if语句。
warn_FsmTable fsmtb[]={
/* event */ /* current state*/ /* action */ /* next state */
{ E_leisure, S_leisure, leisure, S_leisure},
{ E_below10, S_leisure, lngwarn_t, S_tLng},
{ E_bet10_20, S_tLng, lngwarn_j, S_jLng},
{ E_bet10_12, S_tLng, keeplast_sta, S_tLng},
{ E_below10, S_jLng, lngwarn_t, S_tLng},
{ E_bet10_20, S_leisure, lngwarn_j, S_jLng},
{ E_negative, S_tLng, cancel, S_negative},
{ E_negative, S_jLng, cancel, S_negative},
{ E_bet10_12, S_leisure, lngwarn_j, S_jLng},
{ E_negative, S_leisure, cancel, S_negative},
{ E_below10, S_tLng, keeplast_sta, S_tLng},
{ E_bet10_20, S_jLng, keeplast_sta, S_jLng},
{ E_negative, S_negative, keeplast_sta, S_negative},
{ E_below10, S_negative, lngwarn_t, S_tLng},
{ E_leisure, S_negative, leisure, S_leisure},
{ E_leisure, S_tLng, leisure, S_leisure},
{ E_leisure, S_jLng, leisure, S_leisure},
};
完成驱动表的定义后,对应的执行函数,需要根据实际情况进行实现,这里举例出部分供参考。这个处理函数实现了这样一个逻辑,当我模拟不断输入某一报警有效值时,在该报警范围内,是不需要重复报警的,当然在实际的整车环境中不会这样提示的,这样处理是模拟不再报警,但又在报警有效范围内的情况而已。
void keeplast_sta(Event *event,void *args){
printf("保持上一有效状态...\n");
warn_scanf(event);
}
这个函数是初始状态下对应的执行动作。在驱动表中,在某一状态下,每一事件的触发都必须有对应的动作才能执行处理,在这一动作处理完成后,根据个人处理逻辑,更新下一触发参数,比如更新触发事件。如下面第二段代码。
void leisure(Event *event,void* args){
printf("当前处于空闲状态...\n");
warn_scanf(event);
}
void warn_scanf(Event *event)
{
.......省略部分......
printf("请输入液量值:");
scanf("%d",&temp[0]);
lng_warn(temp[0]);
if(lngsta == S_leisure || lngsta1 == S_leisure)
{
*event = E_leisure;//更新触发事件,分别下次遍历驱动表时匹配到相应执行动作
}
......省略无关代码...
到此,任务量完成一大半,对于状态机的设计难点在于驱动表的实现,构建好模型后,依据模型进行驱动表的定义。接下来实现状态机功能。代码如下:
/****
遍历状态表,实现状态机 处理事件
****/
int run_fsm_action(FSM* fsm,void *args)
{
u8_int maxnum = fsm->sta_maxnum;
u8_int i = 0;
if(!fsm && fsm->fsmtb==null) return -1;
//或者用断言
//assert(fsm != null && fsm->fsmtb!=null)
State cur_sta = fsm ->cur_sta;
warn_FsmTable *fsmtb = fsm->fsmtb;
for(i = 0; i <maxnum;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; //状态转移下一状态
// printf("下一状态是:%d,对应fmtb[%d]\n",fsmtb[i].next_sta,i);//驱动表走位模拟
break;
}
}
return 1;
}
/****
创建结构体指针函数 ,便于对FSM结构体进行初始化
****/
FSM * create_fsm(warn_FsmTable * fsmtb,State state, Event event,u8_int num)
{
FSM* fsm = (FSM*)malloc(sizeof(FSM));
fsm->cur_sta = state;
fsm->event =event;
fsm->fsmtb = fsmtb;
fsm->sta_maxnum = num;
return fsm;
}
以上已经初步完成对状态机的报警功能模拟,最后将对以上设计进行测试验证,定义一个主函数进行操作,如下。
int main()
{
u8_int num = sizeof(fsmtb)/sizeof(fsmtb[0]);
FSM* fsm = create_fsm(fsmtb,S_leisure,E_leisure,num);
printf("创建结构体指针函数成功,并得到如下初始化内容..\n");
printf("%d,\t%d,\t%d",fsm->cur_sta,fsm->event,fsm->sta_maxnum = num);
for(int i = 0; i < num; i++)
{
if(i%5==0)
printf("\n");
printf("fsm->fsmtb[%d]\t",i);
}//以上测试打印出的数据是否符合驱动表的设计
printf("\n");
while (1)
{
//if(条件)
//{
// FSM* fsm = create_fsm(fsmtb,S_leisure,E_leisure,num);
// }
// else
//FSM* fsm = create_fsm(fsmtb,S_leisure,E_leisure,num);
run_fsm_action(fsm,NULL);//进入无限循环,达到不断输入不断输出的效果
//为防止程序进入死循环,跑飞,这里尽量不要进行所有的scanf语句输入。
/* if(run_fsm_action(fsm,NULL)==1)
{
printf("返回值成功,退出无限循环体.\n"); //只是检验函数是否可以成功退出
break;
}
}*/
return 0;
}
验证结果:输入5 (<10)报警,输入8(<10)维持上一有效状态,输入11(>10),进行回差定义,此时未到报警解除条件。
下图输入 11 (>10),因为是第一次输入,在报警类型2触发条件范围内容,所以触发类型2报警,输入23,提示报警解除,输入-1,表示无效值,提示空闲状态,不需要进行报警处理。
输入26(>20)不触发报警事件,提示空闲状态,输入6(<10)触发报警类型1,输入18(>12但<20),提示报警类型1解除,但在报警类型2范围,触发类型2报警。再输入11(在类型2范围内),提示保持上一有效状态。
总结
结果诚如上述验证,本次项目 关于实行状态机的报警功能模拟实验目前还算成功,在此分享,希望读者朋友喜欢,也希望大佬们多提点意见和建议,相互学习,共同进步。
毕。20231217周日晚