QEP_FSM有限状态机框架
目录
1. 有限状态机框架介绍
有限状态机在C语言或者C++中的典型实现方式主要包含以下几种:
-
嵌套的switch语句
-
状态表
-
面向对象的设计模式
-
基于以上方法的结合变体
典型的实现方法可以参考这篇博客,对嵌套switch和状态表的设计做了详细介绍:
C语言_有限状态机FSM: link
本文介绍一种新的有限状态机的实现方法QEP,是一种基于事件的状态处理方式,QEP是QP框架的一个组件,这里简单介绍一下QP框架。
QP(Quantum Platform, 量子平台,简称QP,)是一个用于实时嵌入式系统的软件框架,QP是轻量级的、开源的、基于层次式状态机的、事件驱动的平台。
QP由一个合乎UML规范的事件处理器(QEP),一个可移植的事件驱动实时框架(QF),一个小型化的运行至完成的内核(QK)和一个软件跟踪系统(QS)组成。
其中,QEP(Quantum Event Processor)是一个合乎UML规范的事件处理器。它使得UML状态机的直接编码(使用UML状态图)成为可能,并能生成高度可维护的C/C++代码。每一个状态机元素都精确无歧义地对应到唯一的代码片段。QEP完全支持层次化状态嵌套,这方便了子状态机的复用而无需重复进行编码。
2. QEP实现原理
QEP结合了状态表的设计方法,但是状态表的设计有一个缺陷,状态表通常是一个二维数组,数组的行和列表示状态和触发事件,数组元素则是事件处理的函数指针,当状态和事件比较多是,将需要大量的函数实现事件的处理。QEP则每个状态需要一个函数,函数中采用switch case结构处理事件。
QEP的创新点在于把状态映射为函数指针,QEP状态机提供给了标准接口init()和dispatch()。其中的核心结构是QFsm,是一个抽象类,不需要直接实例化,而是在创建实例状态的时候继承QFsm父类,派生出实际需要的状态,其中可以添加需要的状态变量。例如本例中,Bomb实例继承了QFsm类,但是有自己的状态便令timeout, code等。QEP事件处理器的结构如下:
2.1 QFsm结构
QStateHandler
是QFsm中的核心结构,使用一个函数指针来表示状态,函数指针所指向的函数里面使用一个switch结构处理该状态下所有的事件,处理完事件返回一个状态值,已处理/忽略/需要状态迁移。
Q_TRAN
宏中直接在需要状态迁移时改变了实例状态(myBomb)即状态函数指针,并且返回了一个状态值Q_RET_TRAN,dispatch根据这个返回的值判断是否进行状态切换,或者状态不需要切换继续执行当前状态。
这个宏的定义是有技巧的,是一个逗号表达式,return执行了逗号之前的内容,返回了逗号之后的值
这里定义的基本事件,是每一个状态必须有的
typedef U8 QState;
typedef QState (*QStateHandler)(void *me, QEvent const *e);
typedef struct QFsmTag{
QStateHandler 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)
#define QEP_EMPTY_SIG 0
enum QReservedSignals{
Q_ENTRY_SIG = 1,
Q_EXIT_SIG,
Q_INIT_SIG,
Q_USER_SIG
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
2.2 状态结构
QFsm状态机不包含层次状态机,不需要处理事件中的dynamic_
,这是事件的父类结构,子类事件结构在第一个元素继承这个父类结构
typedef U8 QSignal;
typedef struct QEventTag
{
QSignal sig;
U8 dynamic_;
}QEvent;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
2.3 QFsm结构对外提供的方法
QFsm提供标准接口init()和dispatch()两个接口,QFsm_dispatch接口判断当前状态时保持还是需要迁移,并且执行状态函数指针,检查是否有事件触发。如果状态不迁移,直接执行状态函数,如果迁移,则执行前一个状态的退出动作和当前状态的进入动作。
void QFsm_init(QFsm *me, QEvent const *e)
{
(*me->state)(me, e);
(void)(*me->state)(me, &(QEP_reservedEvt_[Q_ENTRY_SIG]));
}
void QFsm_dispatch(QFsm *me, QEvent const *e)
{
QStateHandler s = me->state;
QState r = (*s)(me, e);
if (r == Q_RET_TRAN)
{
(void)(*s)(me, &QEP_reservedEvt_[Q_EXIT_SIG]);
(void)(*me->state)(me, &QEP_reservedEvt_[Q_ENTRY_SIG]);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
3. QFsm的使用
3.1 继承父类QFsm实例化
这里事件信号从Q_USER_SIG
开始,因为状态默认有进入/退出/初始化,避免覆盖了这些信号。有三个要点:
- 事件继承
QEvent
- 状态继承
QFsm
- 状态指针的定义全部在应用层代码中
enum BombSignals { /* all signals for the Bomb FSM */
UP_SIG = Q_USER_SIG,
DOWN_SIG,
ARM_SIG,
TICK_SIG
};
typedef struct TickEvtTag {
QEvent super; /* derive from the QEvent structure */
U8 fine_time; /* the fine 1/10 s counter */
} TickEvt;
typedef struct BombTag {
QFsm super; /* derive from QFsm */
U8 timeout; /* number of seconds till explosion */
U8 code; /* currently entered code to disarm the bomb */
U8 defuse; /* secret defuse code to disarm the bomb */
} Bomb;
void Bomb_ctor(Bomb *me, U8 defuse);
QState Bomb_initial(Bomb *me, QEvent const *e);
QState Bomb_setting(Bomb *me, QEvent const *e);
QState Bomb_timing(Bomb *me, QEvent const *e);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
初始化状态对象,Bomb_ctor用于构建和父类的状态关系:
void Bomb_ctor(Bomb *me, U8 defuse) {
QFsm_ctor(&me->super, (QStateHandler)&Bomb_initial);
me->defuse = defuse; /* the defuse code is assigned at instantiation */
}
- 1
- 2
- 3
- 4
3.2 状态函数的定义
QFsm
的核心思想即在于使用一个函数指针来表示状态,本例中有三个状态,初始化/设置状态/定时状态
/*....................................................................*/
QState Bomb_initial(Bomb *me, QEvent const *e) {
(void)e;
me->timeout = INIT_TIMEOUT;
return Q_TRAN(&Bomb_setting);
}
/*....................................................................*/
QState Bomb_setting(Bomb *me, QEvent const *e)
{
switch (e->sig)
{
case UP_SIG:
{
if (me->timeout < 60) {
++me->timeout;
printf("timeout is %d:\n", me->timeout);
}
return Q_HANDLED();
}
case DOWN_SIG:
{
if (me->timeout > 1) {
--me->timeout;
printf("timeout is %d:\n", me->timeout);
}
return Q_HANDLED();
}
case ARM_SIG:
{
return Q_TRAN(&Bomb_timing); /* transition to "timing" */
}
default:
{
return Q_IGNORED();
}
}
return Q_IGNORED();
}
/*....................................................................*/
QState Bomb_timing(Bomb *me, QEvent const *e)
{
switch (e->sig)
{
case Q_ENTRY_SIG:
{
me->code = 0; /* clear the defuse code */
return Q_HANDLED();
}
case UP_SIG:
{
me->code <<= 1;
me->code |= 1;
return Q_HANDLED();
}
case DOWN_SIG:
{
me->code <<= 1;
return Q_HANDLED();
}
case ARM_SIG:
{
if (me->code == me->defuse) {
return Q_TRAN(&Bomb_setting);
}
return Q_HANDLED();
}
case TICK_SIG:
{
if (((TickEvt const *)e)->fine_time == 0) {
--me->timeout;
printf("timeout is %d:\n", me->timeout);
if (me->timeout == 0) {
printf("boom boom boom!\n");
}
}
return Q_HANDLED();
}
}
return Q_IGNORED();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
3.3 注意事项
在QFsm_dispatch
接口中使用了QEP_reservedEvt_
, 定义了基本事件,进入/退出/初始化
//如果状态需要进行初始化,状态迁移后执行,需要注意的是s是dispatch中的局部变量,状态更新时更新的是me,此时s是没有更新的
(void)(*s)(me, &QEP_reservedEvt_[Q_EXIT_SIG]);
//这两句合在一起,先执行前一个状态退出时的动作,然后执行状态迁移之后的进入动作
(void)(*s)(me, &QEP_reservedEvt_[Q_EXIT_SIG]);
(void)(*me->state)(me, &QEP_reservedEvt_[Q_ENTRY_SIG]);
- 1
- 2
- 3
- 4
- 5
基本事件的定义
QEvent const QEP_reservedEvt_[4] = {
{ (QSignal)QEP_EMPTY_SIG, (U8)0 },
{ (QSignal)Q_ENTRY_SIG, (U8)0 },
{ (QSignal)Q_EXIT_SIG, (U8)0 },
{ (QSignal)Q_INIT_SIG, (U8)0 }
};
- 1
- 2
- 3
- 4
- 5
- 6
4. 小结
QFsm状态机的设计时十分巧妙的,通过函数指针来表达一个状态。提供了两个非常简单的接口,接口中操作的状态和事件都是父状态下定义的结构,所有的应用层状态和事件在应用层代码中定义,接口中我们只关心状态是否发生迁移,而不用关心状态中的事件处理。
- QFsm简单,容易通过C语言来实现
- 将状态中事件触发的动作放在状态函数中实现,具备合适的函数粒度,不向普通二维状态表一样,函数粒度太小
- 状态的扩展是方便有效的,不需要改变状态结构
- 使用一个函数指针来表示状态,RAM资源占用少
- 需要枚举事件,不需要枚举状态,不是层次式的状态,但是也可以很好的扩展为层次式的状态机
m状态机的设计时十分巧妙的,通过函数指针来表达一个状态。提供了两个非常简单的接口,接口中操作的状态和事件都是父状态下定义的结构,所有的应用层状态和事件在应用层代码中定义,接口中我们只关心状态是否发生迁移,而不用关心状态中的事件处理。