【嵌入式C语言】基于驱动表实现状态机报警功能的模拟

1 篇文章 0 订阅
1 篇文章 0 订阅

概要

关于进行本次项目的功能模拟,主要由于在实际工作中遇到一种复杂情形----多条件下大量使用if-else语句,造成代码的冗余,且不易于后期维护,特此借助于状态机功能实现,有利于后期项目开发维护,更重要的是 状态机对于编程者或者初学者而言,刚接手一个项目时对功能的解读是大有益处的。

状态机简介

状态机通常是有限状态机的简称,重点在于我们对状态的研究必须是有限的,所以我们在此并不需要去探讨无限的繁杂情况。有限状态机(英文为Finite State Machine,简称FSM)/有限自动状态机(Finite State Automaton,简称FSA),表示为有限个状态以及这些状态之间的转换动作。自动两个字体现了状态间的转移行为是不需要外部干预的,对于这一数学模型的研究,感兴趣者可以多去网上寻找资料学习。方便起见,关于本次模型的构建以及代码或文字的描述,我们习惯地的使用FSM/状态机表示有限状态机。

状态机类型

关于状态机类型,百度百科以及学者研究,参考大量资料,普遍将状态机归为两类,即Moore状态机、Mealy状态机。以下引用源于百度百科。

状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作,完成特定操作的控制中心。状态机分为摩尔(Moore)型状态机和米莉(Mealy)型状态机。
第一类,若输出只和状态有关而与输入无关,则称为Moore状态机
第二类,输出不仅和状态有关而且和输入有关系,则称为Mealy状态机

状态机的工作原理

FSM要进行工作,需要满足四要素,初态事件动作次态,当然不同研究叫法不同,但意思基本一样。在代码中,常用cur_staeventevent_actionnext_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周日晚

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值