嵌入式状态机编程-QP状态机框架与常见状态机方法
扫描二维码
随时随地手机看文章
状态机基本术语
现态:是指当前所处的状态。条件:又称为“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。传统有限状态机Fsm实现方法
如图,是一个定时计数器,计数器存在两种状态,一种为设置状态,一种为计时状态设置状态
“ ” “-” 按键对初始倒计时进行设置 当计数值设置完成,点击确认键启动计时 ,即切换到计时状态计时状态
按下“ ” “-” 会进行密码的输入。“ ”表示1 ,“-”表示输入0 ,密码共有4位 确认键:只有输入的密码等于默认密码,按确认键才能停止计时,否则计时直接到零,并执行相关操作嵌套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;
}
}
优点:
- 简单,代码阅读连贯,容易理解
缺点
- 当状态或事件增多时,代码状态函数需要经常改动,状态事件处理函数会代码量会不断增加
- 状态机没有进行封装,移植性差。
- 没有实现状态的进入和退出的操作。进入和退出在状态机中尤为重要
- 进入事件:只会在刚进入时触发一次,主要作用是对状态进行必要的初始化
- 退出事件:只会在状态切换时触发一次 ,主要的作用是清除状态产生的中间参数,为下次进入提供干净环境
状态表
二维状态转换表
状态机可以分为状态和事件 ,状态的跃迁都是受事件驱动的,因此可以通过一个二维表格来表示状态的跃迁。(*) 仅当( 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 (*Tran)(struct StateTableTag *me, Event const *e);
void Bomb2_setting_ARM (Bomb2 *me, Event const *e);
typedef struct Bomb
{
struct StateTableTag *me; //必须为第一个成员
uint8_t private;
}
缺点
- 函数粒度太小是最明显的一个缺点,一个状态和一个事件就会产生一个函数,当状态和事件较多时,处理函数将增加很快,在阅读代码时,逻辑分散。
- 没有实现进入退出动作。
一维状态转换表
实现原理: 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 {
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个函数,还需要列出所有的转换关系。
QP嵌入式实时框架
特点
事件驱动型编程
好莱坞原则:和传统的顺序式编程方法例如“超级循环”,或传统的RTOS 的任务不同。绝大多数的现代事件驱动型系统根据好莱坞原则被构造,(Don’t call me; I’ll call you.)面向对象
类和单一继承。工具
QM ,一个通过UML类图来描述状态机的软件,并且可以自动生成C代码:QS软件追踪工具:QEP实现有限状态机Fsm
/* qevent.h ----------------------------------------------------------------*/
typedef struct QEventTag
{
QSignal sig;
uint8_t dynamic_;
} QEvent;
/* qep.h -------------------------------------------------------------------*/
typedef uint8_t QState; /* status returned from a state-handler function */
typedef QState (*QStateHandler) (void *me, QEvent const *e); /* argument list */
typedef struct QFsmTag /* Finite State Machine */
{
QStateHandler state; /* current active state */
}QFsm;
#define QFsm_ctor(me_, initial_) ((me_)->state = (initial_))
void QFsm_init (QFsm *me, QEvent const *e);
void QFsm_dispatch(QFsm *me, QEvent const *e);
#define Q_RET_HANDLED ((QState)0)
#define Q_RET_IGNORED ((QState)1)
#define Q_RET_TRAN ((QState)2)
#define Q_HANDLED() (Q_RET_HANDLED)
#define Q_IGNORED() (Q_RET_IGNORED)
#define Q_TRAN(target_) (((QFsm *)me)->state = (QStateHandler) (target_),Q_RET_TRAN)
enum QReservedSignals
{
Q_ENTRY_SIG = 1,
Q_EXIT_SIG,
Q_INIT_SIG,
Q_USER_SIG
};
/* file qfsm_ini.c ---------------------------------------------------------*/
#include "qep_port.h" /* the port of the QEP event processor */
#include "qassert.h" /* embedded systems-friendly assertions */
void QFsm_init(QFsm *me, QEvent const *e)
{
(*me->state)(me, e); /* execute the top-most initial transition */
/* enter the target */
(void)(*me->state)(me ,