状态机在嵌入式软件中随处可见,可能你会说状态机有什么难的,不就是 switch 吗?
switch仅仅是最基础的一个点,关于状态机的更多操作,或许你都没有见过,下面分享几种实现方法。1. 状态机基本术语
现态:是指当前所处的状态。
条件:又称为“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
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; } }
优点:简单,代码阅读连贯,容易理解
缺点:
当状态或事件增多时,代码状态函数需要经常改动,状态事件处理函数会代码量会不断增加
状态机没有进行封装,移植性差。
没有实现状态的进入和退出的操作。进入和退出在状态机中尤为重要。进入事件:只会在刚进入时触发一次,主要作用是对状态进行必要的初始化。退出事件:只会在状态切换时触发一次 ,主要的作用是清除状态产生的中间参数,为下次进入提供干净环境
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个函数,还需要列出所有的转换关系。
转自公众号:嵌入式大杂烩
版权声明:本文来源网络,版权归原作者所有。版权问题,请联系删除。