vesperW · 3月14日

嵌入式开发必学 | 状态机常用的几种骚操作

状态机在嵌入式软件中随处可见,可能你会说状态机有什么难的,不就是 switch 吗?

switch 仅仅是最基础的一个点,关于状态机的更多操作,或许你都没有见过,下面分享几种实现方法。

1. 状态机基本术语

  • 现态:是指当前所处的状态。
  • 条件:又称为“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
  • 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

image.png

2. 传统有限状态机 FSM

如下图所示,这是一个定时计数器,计数器存在两种状态,一种为设置状态,一种为计时状态。

设置状态:

  • “+” “-” 按键对初始倒计时进行设置
  • 当计数值设置完成,点击确认键启动计时 ,即切换到计时状态

计时状态:

  • 按下“+” “-” 会进行密码的输入“+”表示 1 ,“-”表示输入 0 ,密码共有 4 位
  • 确认键:只有输入的密码等于默认密码,按确认键才能停止计时,否则计时直接到零,并执行相关操作

图片

3. 嵌套 switch

/***************************************
1.列出所有的状态
***************************************/
typedef enum{
  SETTING,
  TIMING
}STATE_TYPE;
/***************************************
2.列出所有的事件
***************************************/
typedef enum{
    UP_EVT,
  DOWN_EVT,
  ARM_EVT,
  TICK_EVT
}EVENT_TYPE;
/***************************************
3.定义和状态机相关结构
***************************************/
struct  bomb
{
  uint8_t state;
  uint8_t timeout;
  uint8_t code;
  uint8_t defuse_code;
}bomb1;
/***************************************
4.初始化状态机
***************************************/
void bomb1_init(void)
{
  bomb1.state = SETTING;
  bomb1.defuse_code = 6;    //0110 
}
/***************************************
5. 状态机事件派发
***************************************/
void bomb1_fsm_dispatch(EVENT_TYPE evt ,void* param)
{
  switch(bomb1.state)
  {
      case SETTING:
      {
          switch(evt)
          {
              case UP_EVT:    // "+"   按键按下事件
                if(bomb1.timeout< 60)  ++bomb1.timeout;
                  bsp_display(bomb1.timeout);
              break;
              case DOWN_EVT:  // "-"   按键按下事件
                  if(bomb1.timeout > 0)  --bomb1.timeout;
                  bsp_display(bomb1.timeout);
              break;
              case ARM_EVT:   // "确认" 按键按下事件
                  bomb1.state = TIMING;
                  bomb1.code  = 0;
              break;
          }
      } break; 
      case TIMING:
      {
          switch(evt)
          {
              case UP_EVT:    // "+"   按键按下事件
                 bomb1.code = (bomb1.code <<1) |0x01;
              break;
              case DOWN_EVT:  // "-"   按键按下事件
                  bomb1.code = (bomb1.code <<1); 
              break;
              case ARM_EVT:   // "确认" 按键按下事件
                  if(bomb1.code == bomb1.defuse_code){
                      bomb1.state = SETTING;
                  }
                  else{
                    bsp_display("bomb!")
                  }
              break;
              case TICK_EVT:
                  if(bomb1.timeout)
                  {
                      --bomb1.timeout;
                      bsp_display(bomb1.timeout);
                  }
                  if(bomb1.timeout == 0)
                  {
                      bsp_display("bomb!")
                  }
              break;
          }   
      }break;
  }
}

图片

优点:简单,代码阅读连贯,容易理解

缺点:

  1. 当状态或事件增多时,代码状态函数需要经常改动,状态事件处理函数会代码量会不断增加
  2. 状态机没有进行封装,移植性差。
  3. 没有实现状态的进入和退出的操作。进入和退出在状态机中尤为重要。进入事件:只会在刚进入时触发一次,主要作用是对状态进行必要的初始化。退出事件:只会在状态切换时触发一次 ,主要的作用是清除状态产生的中间参数,为下次进入提供干净环境

4. 状态表

二维状态转换表

状态机可以分为状态和事件 ,状态的跃迁都是受事件驱动的,因此可以通过一个二维表格来表示状态的跃迁。

图片

仅当(code == defuse_code) 时才发生到 setting 的转换。

/*1.列出所有的状态*/
enum
{
  SETTING,
  TIMING,
  MAX_STATE
};
/*2.列出所有的事件*/
enum
{
  UP_EVT,
  DOWN_EVT,
  ARM_EVT,
  TICK_EVT,
  MAX_EVT
};

/*3.定义状态表*/
typedef void (*fp_state)(EVT_TYPE evt , void* param);
static  const fp_state  bomb2_table[MAX_STATE][MAX_EVENT] =
{
  {setting_UP , setting_DOWN , setting_ARM , null},
  {setting_UP , setting_DOWN , setting_ARM , timing_TICK}
};

struct bomb_t
{
  const fp_state const *state_table; /* the State-Table */
  uint8_t state; /* the current active state */
  
  uint8_t timeout;
  uint8_t code;
  uint8_t defuse_code;
};
struct bomb bomb2=
{
  .state_table = bomb2_table;
}
void bomb2_init(void)
{
  bomb2.defuse_code = 6; // 0110
  bomb2.state = SETTING;
}

void bomb2_dispatch(EVT_TYPE evt , void* param)
{
  fp_state  s = NULL;
  if(evt > MAX_EVT)
  {
      LOG("EVT type error!");
      return;
  }
  s = bomb2.state_table[bomb2.state * MAX_EVT + evt];
  if(s != NULL)
  {
      s(evt , param);
  }
}
/*列出所有的状态对应的事件处理函数*/
void setting_UP(EVT_TYPE evt, void* param)
{
  if(bomb1.timeout< 60)  ++bomb1.timeout;
  bsp_display(bomb1.timeout);
}

缺点:函数粒度太小是最明显的一个缺点,一个状态和一个事件就会产生一个函数,当状态和事件较多时,处理函数将增加很快,在阅读代码时,逻辑分散。没有实现进入退出动作。

一维状态转换表

图片

实现原理:图片

 typedef void (*fp_action)(EVT_TYPE evt,void* param);
    
    /*转换表基础结构*/
    struct tran_evt_t
    {
       EVT_TYPE evt;
        uint8_t next_state;
    };
    /*状态的描述*/
    struct  fsm_state_t
    {
        fp_action  enter_action;      //进入动作
        fp_action  exit_action;   //退出动作
        fp_action  action;           
        
        tran_evt_t* tran;    //转换表
        uint8_t     tran_nb; //转换表的大小
        const char* name;
    }
    /*状态表本体*/
    #define  ARRAY(x)   x,sizeof(x)/sizeof(x[0])
    const struct  fsm_state_t  state_table[]=
    {
        {setting_enter , setting_exit , setting_action , ARRAY(set_tran_evt),"setting" },
        {timing_enter , timing_exit , timing_action , ARRAY(time_tran_evt),"timing" }
    };
    
    /*构建一个状态机*/
    struct fsm
    {
        const struct state_t * state_table; /* the State-Table */
        uint8_t cur_state;                      /* the current active state */
        
        uint8_t timeout;
        uint8_t code;
        uint8_t defuse_code;
    }bomb3;
    
    /*初始化状态机*/
    void  bomb3_init(void)
    {
        bomb3.state_table = state_table;  //指向状态表
        bomb3.cur_state = setting;
        bomb3.defuse_code = 8; //1000
    }
    /*状态机事件派发*/
    void  fsm_dispatch(EVT_TYPE evt , void* param)
    {
        tran_evt_t* p_tran = NULL;
        
        /*获取当前状态的转换表*/
        p_tran = bomb3.state_table[bomb3.cur_state]->tran;
        
        /*判断所有可能的转换是否与当前触发的事件匹配*/
        for(uint8_t i=0;i<x;i++)
        {
            if(p_tran[i]->evt == evt)//事件会触发转换
            {
                if(NULL != bomb3.state_table[bomb3.cur_state].exit_action){
              bomb3.state_table[bomb3.cur_state].exit_action(NULL);  //执行退出动作
             }
                if(bomb3.state_table[_tran[i]->next_state].enter_action){
                   bomb3.state_table[_tran[i]->next_state].enter_action(NULL);//执行进入动作
                }
                /*更新当前状态*/
                bomb3.cur_state = p_tran[i]->next_state;
            }
            else
            {
                 bomb3.state_table[bomb3.cur_state].action(evt,param);
            }
        }
    }
    /*************************************************************************
    setting状态相关
    ************************************************************************/
    void setting_enter(EVT_TYPE evt , void* param)
    {
        
    }
    void setting_exit(EVT_TYPE evt , void* param)
    {
        
    }
    void setting_action(EVT_TYPE evt , void* param)
    {
        
    }
    tran_evt_t set_tran_evt[]=
    {
        {ARM , timing},
    }
    /*timing 状态相关*/

优点:

  • 各个状态面向用户相对独立,增加事件和状态不需要去修改先前已存在的状态事件函数。
  • 实现了状态的进入和退出
  • 容易根据状态跃迁图来设计 (状态跃迁图列出了每个状态的跃迁可能,也就是这里的转换表)
  • 实现灵活,可实现复杂逻辑,如上一次状态,增加监护条件来减少事件的数量。可实现非完全事件驱动

缺点:

  • 函数粒度较小(比二维小且增长慢),可以看到,每一个状态需要至少 3 个函数,还需要列出所有的转换关系。

END

作者:小麦大叔
来源:小麦大叔

推荐阅读

欢迎大家点赞留言,更多 Arm 技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
2914
内容数
341
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息