[github]有限状态机

20240409 想法

  • 外部接口(输入包括:控制指令,外部硬件信号,定时器、软件信号 等等)—>事件判断(将外部接口的信号转换成事件序号)–>状态转换;
  • 这总思路的问题:每个状态下事件判断的前后顺序可能不一样,无法统一

用什么来表示事件为好?-用序号

事件对于大部分状态机来说都是一次性的,
即时该事件对软件中同时工作的多个状态机有意义,但对单独的装机来说,他是一次性,
对于一次性的易耗品,不需要保存;最方便的形式就是通过函数参数的形式注入

我之前有种写法是 将输入接口函数放在模块中,在模块的状态机中直接判断
这种方式虽然逻辑上是可以,但是存在的情况是接口状态的查询,返回有多种值,这种写法就比较麻烦,case中嵌套Switchcase;

事件什么时候注入

  • 状态的控制字中,有些数据是不需要保存的,则不用放在控制字中,如控制指令,通过一次函数的调用,实现一次控制的下放和状态的切换;

  • 有些状态需要共同判断某些异常状态,该如何进行统一的判断(层次状态机)?

20240408 状态机的设计

参考资料

  • 在CPU眼中外设慢得跟蜗牛一样
  • 状态图才是真正的源代码,而翻译后的C代码则是“汇编”
  • 错误码用负数表示–表示错误状态

一个状态的设计

1)一件事情你要不停的尝试才有可能成功,
2)每次做都可能会产生2个以上的结果。

一个状态就是程序等待某一个“契机”实现自我的改变(改变行为)

一个状态的功能尽可能简单,功能单一原则是指:每个状态的功能要尽可能单一,
要避免将多个功能复合在同一个状态上,从而产生所谓的“超级状态”的情况。

“状态值”的 隐藏设计,但是 若果别的模块需要知道本模块的状态呢?

// 状态复位
#define READ_BYTE_RESET_FSM() \
    do {s_tState = START;} while(0)
    

fsm_rt_t read_byte(uint8_t *pchByte)
{
//只用一次的枚举,没必要定义类型
//这个枚举是 read_byte 的私有财产,应该放到函数内部
    static enum {
        START = 0,
        READ_BYTE,
        IS_TIMEOUT,
    } s_tState = {START};
    ...
}

状态的跃迁

1)一个状态所有的跃迁条件必须是彼此“互斥”的、唯一的;
2)所有的跃迁必须能覆盖一个状态机所有可能的情况——绝不允许出现漏网之鱼
3)跃迁是个瞬间的行为,你只能认为当条件满足时跃迁的行为就像白驹过隙一样一下就做完了(“跃迁是个瞬间行为”,所以这里的动作也只会被执行一次。)

状态机的入口

start 不仅是状态机的起点,由一个跃迁来连接它和第一个状态;==START 不是一个可以保持的状态,它也不能被看作一个特殊的状态;==一定要自动切换到下一个状态而绝对不能在此停留;START所携带的执行动作一般用作状态机的初始化
状态机的入口:

跃迁需要保存状态的上下文

状态机的复位

  • 复位并不是普通的状态跃迁,它表示将状态机“重置”——复位后的第一次执行,状态机会从_START_那里开始,并且完成必要的状态机初始化操作;
  • 应该尽最大可能避免从状态机外部复位状态机,或者说,状态机的生命周期应该掌控在自己手里。1)状态机中可能存在动态分配的资源
  • 应该用“自杀请求”来代替“直接他杀”

状态图的入口(输入 少)和出口(扇出)

过读图理解设计者意图的速度应该远高于直接阅读翻译后代码的速度
在这里插入图片描述
从状态图到代码的“无脑翻译”
在这里插入图片描述

单实例和多实例的状态机

将逻辑和数据分开,状态机只是实现逻辑,数据在状态机的外部;

  • 无论采用何种状态机翻译方式,可重入的状态机一定会包含一个控制块。
  • 统一采用支持多实例的方式来设计其实在上下文的访问效率上是更高的

每一个状态机引入“控制块”,从面向对象开发的视角来看,本质上是将状态机都以类的形式进行了改造,

typedef struct <控制块类型名称> {
    uint8_t chState;      //!< 状态变量
    <上下文列表>
} <控制块类型名称>

控制块的定义就是状态机的类(Class)定义;
状态机函数是类的方法(Method);
初始化函数是类的构造函数(Constructor);
实际上,状态机函数中用 this 来访问上下文,也已经暴露其OO的本质。

如何在“结构清晰”和“性能优化”之间取得平衡

使用goto适用于那些需要“逆流而上”的场合
使用switch的fall-through特性fall-through具有瀑布一般一泻千里不能回头的特性;

fsm_rt_t check_ok(void)
{
    uint8_t chByte;
    ...
    switch (s_tState) {
        case START:
            s_tState = READ_CHAR_0;
            // break;    //!< fall-through实现虚线切换
        case READ_CHAR_0:
            if (!serial_in(&chByte)) {
                break;                
            }
            s_tState = IS_O;
            // break;    //!< fall-through实现虚线切换
        case IS_O:
            if (chByte != 'O') {
                CHECK_OK_RESET_FSM();
                break;
            }
            s_tState = READ_CHAR_1:
            // break;    //!< fall-through实现虚线切换
        case READ_CHAR_1:
            if (!serial_in(&chByte)) {
                break;                
            }
            s_tState = IS_K;
            // break;    //!< fall-through实现虚线切换
        case IS_K:
            if (chByte != 'K') {
                CHECK_OK_RESET_FSM();
                break;
            }
            CHECK_OK_RESET_FSM();
            return fsm_rt_cpl;
    }
    
    return fsm_rt_on_going;
}

20240407-状态机的实现和对比

实现方式优点缺点
switch case简单直观新增状态或者事件时,需要修改的地方多
表格驱动直观、简单面对转换中的条件逻辑无法处理
面向对象的实现同一事件但是需要不同行为的模式,如按键UI对于类似“顺序跳转”的状态,显得没有必要

面向对象-cfsm-20210410

面相对象:同一事件在不同的状态下体现不同行为的模式

案例1

基于C的面向对象的状态机设计
为每一个状态定一个一个对象,具有多个对象实例

/*定义"停止状态"对象实体*/
State_Object STOP = 
{ 
    StartPlay,              //"停止状态"下对应的”播放按键“事件响应
    Ignore,                 //"停止状态"下对应的”停止按键“事件响应
};

/*定义"播放状态“对象实体*/
State_Object PLAY = 
{ 
    PausePlay,            //"播放状态"下对应的”播放按键“事件响应
    StopPlay,              //"播放状态"下对应的”停止按键“事件响应 
};

/*定义"暂停状态“对象实体*/
State_Object PAUSE = 
{ 
    StartPlay,         //"暂停状态"下对应的”播放按键“事件响应
    StopPlay,          //"暂停状态"下对应的”停止按键“事件响应 
};

然后通过一个状态指针实现不同状态的切换,每个状态的上下文都保存在每个状态的实例中;

/*定义状态指针*/
State_Object * pCurrentState = &STOP;//初始化为“停止状态”

案例2(推荐)

而在cfsm中,状态只有一个上下文实例,状态的切换是动态改变状态的方法;每当需要切换状态,就通过修改对应状态的方法;
采用的是一中“接口类”的概念,接口是不需要实例化,至少包含一个纯虚函数的类就是接口类,接口类定一个了方法,至于方法如何实现则有继承的对象实现;

虽然感觉上有点牵强,但是有异曲同工之妙;

方案的对比

上述两个案例中,案例2通过减少了实例数量,但是牺牲了对象的完整性,
但是有种同一个状态机需要多个实例(不是状态实例,比如电机1、电机2等),如果采用案例1的方案,要为每个模块实例生成多个状态实例,比较夸张;所以还是案例2相对来说更好;

20210403-事件

“事件”

1.可能来之外部信号状态的变化,外部信号从0变成1;
2.外置外部的控制指令:
变化的过程是一个事件;

事件产生后需要被“关注他的”状态所处理(消耗掉),处理之后便是无事发生

轮询状态 VS 轮询事件 VS 面相对象

轮询状态- Moore 型状态机(输出只与当前状态有关)

面对事件少的的状态机

void stateloop(){
	switch(当前状态){
		case 状态1if 事件1(信号状态变化?)-处理xxx;
			elseif 事件...-处理xxx
			break;
		case 状态xxxx:
			if 事件1(信号状态变化?)-处理xxx;
			elseif 事件...-处理xxx
			break;
			break;
	
	}
}

这种写法
通过轮询状态,在状态中判断是否有事件发生,从而完成事件的处理和状态切换;
对事件状态的维护不方便;尤其是针对多个事件的情况;

轮询事件-类似Mealy 型状态机

通过轮询判断事件是否发生
然后将发生的时间注入到状态机中实现状态的切换;

void eventloop(){
	if 事件1switch(状态){
			状态1 处理
			}
	else if 事件2
			

}

两者对比

参考
如果 Event 直接由中断引发,不需要 if 语句轮询就能判断,则用 Moore 型状态机(事件中查询状态)执行速度快。这是因为只需执行对应 Event 的 switch(State) 语句,而且 switch 中只需对 State 进行判断就可以输出结果了。
如果 Event 本身就需要轮询才能得出,则使用 Mealy 型状态机(状态中查询事件)的代码要简单。因为状态中查询事件只有一个 switch(State) 语句。

事件注入

同一个信号对不同的模块有不同的含义
信号的判断放在逻辑中,当触发时,则注入到该模块中

记录

对于简单的状态机,表格驱动 是最方便且直观的;
1.定义好所有的事件类型,每种事件类型又有具体的事件代号;
2.每一个状态,对于每一种事件类型,都应该有对应的响应动作;
3.如果一个状态都是“自动切换”(外部事件很少参与)则整个过程就回比较像流程图;

状态

什么样的才能算是一个状态

状态机(state machine)有5个要素

当设备系统处于xx状态,在满足xxx条件的情况下,发生了xx事件,引起系统xx动作,导致系统从xx状态转换到xxx状态的过程

  • 状态:一个系统在某一时刻所存在的稳定的工作情况,系统在整个工作周期中可能有多个状态。例如一部电动机共有正转、反转、停转这 3 种状态。一个状态机需要在状态集合中选取一个状态作为初始状态。

  • 迁移:系统从一个状态转移到另一个状态的过程称作迁移,迁移不是自动发生的,需要外界对系统施加影响。停转的电动机自己不会转起来,让它转起来必须上电。

  • 事件:某一时刻发生的对系统有意义的事情,状态机之所以发生状态迁移,就是因为出现了事件。对电动机来讲,加正电压、加负电压、断电就是事件。

  • 动作:在状态机的迁移过程中,状态机会做出一些其它的行为,这些行为就是动作,动作是状态机对事件的响应。给停转的电动机加正电压,电动机由停转状态迁移到正转状态,同时会启动电机,这个启动过程可以看做是动作,也就是对上电事件的响应。

  • 条件:状态机对事件并不是有求必应的,有了事件,状态机还要满足一定的条件才能发生状态迁移。还是以停转状态的电动机为例,虽然合闸上电了,但是如果供电线路有问题的话,电动机还是不能转起来。

github

资源地址stateMachine

状态转换图

有两种概念,一种是状态组(父状态,可以理解成状态的概念,非实体),一种是状态(最小单位,可以理解成是状态的实体);
一个状态组中包含了若干个状态(子状态)和若干个状态组(状态的嵌套);
只有状态(子状态)这个最小单位才有可转换型,转换的过程从以状态为落脚点;
体现1:状态组中必然有个入口状态(entryState),因为不能以状态组作为入口状态,转换到状态组的时候必定是先进入该状态组的入口状态;
体现2:在状态的转出过程中,如果子状态没有符合的转换条件,则需要在父状态(及状态组)中查看是否有合适的转换
在这里插入图片描述

内容

类定义

事件类
类型
*数据
转换类
触发转换事件类型
*条件数据
把守检查(数据条件,事件)
转换时执行的动作(当前状态数据,事件,下一个状态数据)
*下一个状态非空
状态类
*父状态
*入口状态(entryState,默认进入的状态,对于 状态组,默认进入的状态)
*转换数组
可转换数-数组个数
*数据-可用于进出状态是数据
进入动作(数据,事件)
出去动作(数据,事件)
状态机类
*当前状态
*前一个状态
*错误状态

使用步骤

初始化状态机 stateM_init ,包括了三个状态:初始状态,前一个状体,错误状态;
运行状态机,stateM_handleEvent 输入事件,处理各种事件 内部实现状态的转换
判断是否有错误;
状态转换的实现,从转换列表(数组)中找可以的转换,第一个符合触发事件类型,没有没有门卫(guard)检查直接进行转换,否则门卫检查事件是否符合条件(condition) ;
如果当前状态没有可以转换的,再找他的父状态组 进行查找,直到父状态组为NULL.parentState = NULL

停止状态机,将当前的转换数=0;

状态的转换

状态转化的集中可能性:
1.状态转换了-下一个状态;
2.状态终点-没有下一个状态了
3.状态错误-转到错误状态机;
4.状态转换到原来状态-loop;

/*
有两种概念,一种是状态组(父状态,可以理解成状态的概念,非实体),一种是状态(最小单位,可以理解成是状态的实体);
一个状态组中包含了若干个状态(子状态)和若干个状态组(状态的嵌套);
只有状态(子状态)这个最小单位才有可转换型,转换的过程从以状态为落脚点;
体现1:状态组中必然有个入口状态(entryState),因为不能以状态组作为入口状态,转换到状态组的时候必定是先进入该状态组的入口状态;
体现2:在状态的转出过程中,如果子状态没有符合的转换条件,则需要在父状态(及状态组)中查看是否有合适的转换*/
int stateM_handleEvent( struct stateMachine *fsm,
      struct event *event )
{
   if ( !fsm || !event )
      return stateM_errArg;

   //如果当前状态为NULL,则进入错误状态
   if ( !fsm->currentState )
   {
      goToErrorState( fsm, event );
      return stateM_errorStateReached;
   }

   //如果当前状态没有转换数(调用stop)
   if ( !fsm->currentState->numTransitions )
      return stateM_noStateChange;

   //名字是当前的状态
   struct state *nextState = fsm->currentState;
   do {
      //根据当前的事件,判断当前状态机是否有可以转换的下一个状态
      struct transition *transition = getTransition( fsm, nextState, event );

      //[转出时]如果当前没有转换,则到父状态中寻找,直到最顶层状态组 可用的转换 ;
      /* If there were no transitions for the given event for the current
       * state, check if there are any transitions for any of the parent
       * states (if any): */
      if ( !transition )
      {
         nextState = nextState->parentState;
         continue;
      }

      /* A transition must have a next state defined. If the user has not
       * defined the next state, go to error state: */
      if ( !transition->nextState )
      {
         goToErrorState( fsm, event );
         return stateM_errorStateReached;
      }

      nextState = transition->nextState;

      //[转入时]如果当前转入的是状态组/嵌套的状态组,则需要转到入口状态,
      /* If the new state is a parent state, enter its entry state (if it has
       * one). Step down through the whole family tree until a state without
       * an entry state is found: */
      while ( nextState->entryState )
         nextState = nextState->entryState;
      
      //首先执行出状态动作
      /* Run exit action only if the current state is left (only if it does
       * not return to itself): */
      if ( nextState != fsm->currentState && fsm->currentState->exitAction )
         fsm->currentState->exitAction( fsm->currentState->data, event );

      //接着执行转换时动作
      /* Run transition action (if any): */
      if ( transition->action )
         transition->action( fsm->currentState->data, event, nextState->
               data );

      //接着执行入状态动作
      /* Call the new state's entry action if it has any (only if state does
       * not return to itself): */
      if ( nextState != fsm->currentState && nextState->entryAction )
         nextState->entryAction( nextState->data, event );

      //更新状态机
      fsm->previousState = fsm->currentState;
      fsm->currentState = nextState;
      
      /* If the state returned to itself: */
      if ( fsm->currentState == fsm->previousState )
         return stateM_stateLoopSelf;

      if ( fsm->currentState == fsm->errorState )
         return stateM_errorStateReached;

      /* If the new state is a final state, notify user that the state
       * machine has stopped: */
      if ( !fsm->currentState->numTransitions )
         return stateM_finalStateReached;

      return stateM_stateChanged;
   } while ( nextState );

   return stateM_noStateChange;
}

案例

在这里插入图片描述

idle状态就是 状态组group的入口状态,默认进入子状态idle

组状态

在这里插入图片描述
idle状态处理不了的转换 交由group状态处理;

关于状态组的应用还转换比较难理解。

入口状态

在这里插入图片描述
0----> 表示 状态组的 入口状态 entryState;
状态的嵌套 涉及到 parentState 父状态;

其他

condition is held 条件成立
面对对象 函数操作:函数指针
bool ( *guard )( void *condition, struct event *event );

应用

大事化小,再将小事按照顺序执行;
group1
stata1
state2
group2
subgroup1
state1
state2
subgroup2
state1
state2
state3
大事化小的过程,小事按照顺序执行;

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值