量子框架简称QP,是一种状态机框架,实现了有限状态机FSM和层次状态机HSM,目前官方仅有C和C++语言的实现。对于PC端和web端的开发,这个框架有种英雄无用武之地的感觉,但在嵌入式领域这个框架彻底颠覆了我的认识。
提到这个框架,不能不提框架作者Miro Samek的书《Pratical UML Statecharts In C/C++》。这本书既可以看作是状态机方面的著作,也能看作QP的教程,强烈推荐对状态机感兴趣的人读之。
闲话不说,直接用一个简单例子来说说这个框架的使用。当然,这个框架在使用之前是需要根据具体平台和系统做一些简单的移植工作的,这部分会在之后描述。
**************************************
任务:基于状态机控制一个LED灯闪烁
(1)采用基于数值变量的状态机
#define LED_SHORT_DELAY 1000 typedef enum { LedState_Off = 0, LedState_On } LedState; typedef enum { LedSignal_TurnOff = 0, LedSignal_TurnOn } LedSignal; LedState led_state = LedState_Off; LedSignal led_sig; void Led_On(void) { printf("Led is ON.\r\n"); } void Led_Off(void) { printf("Led is OFF.\r\n"); } void Led_Control(LedSignal sig) { switch (led_state) { case LedState_Off: if (LedSignal_TurnOn == sig) { led_state = LedState_On; Led_On(); } break; case LedState_On: if (LedSignal_TurnOff == sig) { led_state = LedState_Off; Led_Off(); } break; } } void Led_Blink(void) { int delay = LED_SHORT_DELAY; led_sig = LedSignal_TurnOff; while (1) { int delay = LED_SHORT_DELAY; led_sig = !led_sig; Led_Control(led_sig); while(delay--); } }
状态机的实现集中在Led_Control这个函数中。它包含了状态led_state、外部信号(事件)sig这两个状态机基本元素。
(2)基于QP的状态机实现
定义状态
static QState Led_StateInitial(QFsm* fsm, QEvt* e); static QState Led_StateOn(QFsm* fsm, QEvt* e); static QState Led_StateOff(QFsm* fsm, QEvt* e);
可以看出QP下的状态是函数指针?对,没错,QP下的状态是通过函数指针来表示的。在这里,引入了第一个问题,采用什么形式保存当前状态好?(采用流水账的形式探讨问题)
a. 变量形式
比如(1)中的led_state变量用于保存当前状态,然后通过switch-case分支对各种外部signal进行判断分别处理。这种方式简单易懂,实现起来也是非常easy。但如果系统复杂一些,n个状态变量,每个状态变量分别对应 a_i(i = 1, …, n) 种状态。在最坏情况下,n个状态相互嵌套,你的switch-case将有a_n*a_(n-1)*…*a_1层。在工业现场,随便复杂一点的应用都会有十几层的switch-case和if-else吧。不要说让别人维护你的代码,编这种程序的你也会叫苦不迭。
b. 状态表(state table)
较为流行的是采用二维表,以状态集为行,signal事件为列。如(1)中的Led_Control状态机可表示为:
LedSignal_TurnOff | LedSignal_TurnOn | |
LedState_Off | \ | Led_On(); // action LedState_On; // next state |
LedState_On | Led_Off(); // action LedState_Off; // next state |
\ |
采用状态表的方式使得状态机执行效率得到很大提高(O(1)),避免了分支判断。其缺点也在上表中显而易见,对于一些状态,某些signal是没有意义的,但仍需在状态表中列出。C语言自身不支持hash,构造出来的状态表通常是一个稀疏矩阵,浪费有限的存储空间。当后期由于需求变更需要调整状态表时,很容易造成遗漏或错误。如果想实现层次状态机,可以想像状态表这种实现方式既繁琐且易出错。当然,对于简单的应用,状态表仍是很好的一个选择。
c. object-oriented状态设计模式
这种方式充分利用面向对象设计思想,设计一个抽象类定义所有signal的处理接口,然后由不同state去继承该类,并实现自身关心的事件处理接口以覆盖基类的相应接口(多态)。同样以Led_Control为例,设计一个抽象类(语法不规范)
class Led; class LedState { public: virtual void OnLedSignal_TurnOn(Led* contex) {}; virtual void OnLedSignal_TurnOff(Led* contex) {}; } class LedOnState : public LedState { public: void OnLedSignal_TurnOff(Led* contex) { Led_Off(); contex->Tran(&contex->stateOff) } } class LedOffState: public LedState { public: void OnLedSignal_TurnOn(Led* contex) { Led_On(); contex->Tran(&contex->stateOn) } } class Led: { private: LedState* m_state; static LedOnState stateOn; static LedOffState stateOff; void Tran(LedState* state) { m_state = state; } public: void OnLedSignal_TurnOn(void) { m_state->OnLedSignal_TurnOn(this) } // 响应'开'信号 void OnLedSignal_TurnOff(void) { m_state->OnLedSignal_TurnOff(this) } // 响应'关'信号 void Init(void) { Tran(stateOff) }; // 初始状态 friend class LedOnState; friend class LedOffState; }
LedState是抽象基类,包含了所有外部signal(LedSignal_TurnOff、LedSignal_TurnOn)的默认处理接口。LedOnState继承于LedState,实现了它自身关心的LedSignal_TurnOff信号处理方法。LedOffState同样继承于LedState,实现了LedSignal_TurnOn信号处理方法。
Led这个类是状态机,包含了两个状态,分别是LedOnState和LedOffState的两个实例。这个状态机有一个重要的成员变量m_state(LedState*类型)用于保存当前状态,而Tran成员函数用于切换状态。当这个状态机响应外部signal时,直接调用当前状态相应的信号处理方法即可。例如,Led状态机Init()后其状态为m_state = &stateOff(LedOffState*),当收到LedSignal_TurnOff信号时,调用自身响应函数Led:OnLedSignal_TurnOff()时,有m_state->OnLedSignal_TurnOff() <—> LedOffState::OnLedSignal_TurnOff(Led*)。而LedOffState类的OnLedSignal_TurnOff继承于LedState,函数为空,因此什么也不做。当收到LedSignal_TurnOn信号时,类似的可以知道其相当于调用了LedOffState::OnLedSignal_TurnOn(Led*)。而该函数执行两个动作:Led_TurnOn();同时调用Tran将状态机的当前状态m_state切换至&stateOn(LedOnState*)。
从上面这个Led例子可看出,基于OO思想的状态机极大程度依赖于像C++这种语言的面向对象特性。好处有很多:
- 各自的状态可以独立处理各自感兴趣的外部信号;
- 状态迁移十分高效,只需改变指针所指内容;
- 信号分发性能也非常可观,可保证O(1)复杂度;
尽管有如上优点,但在实际应用时,需要枚举全部外部信号的处理方法且最大化状态基类的接口数量,这个工作量其实不小,由Led代码也可看出一些端倪。
d. QEP有限状态机
顾名思义,将上述3种思想取长补短再加上QP作者个人原创思想提出的一种状态机。在QP框架中,非层次有限状态机定义为QFsm。虽说在QFsm的C语言实现中,不过是用struct封装了其成员变量和函数,没有类的真正概念,但理解起来也就是类,所以就以QFsm类称谓之。
QFsm含有一个成员变量State保存当前状态,这一点与方式c一致。只是这里的QFsm状态机中的状态都是函数指针,具备QState QStateHandler(void* me, QEvent* e)形式。也就是说,当有外部事件e: QEvent产生时,QFsm的处理很简单,即调用函数state(me, e)。依然以本文开始时的LED示例来说,它有二个状态:
static QState Led_StateOn(QFsm* fsm, QEvt* e); static QState Led_StateOff(QFsm* fsm, QEvt* e);
还有一个Led_StateInitial是每个QFsm状态机必须具有的一个初始化状态,它只负责将状态迁移到默认状态。如LED默认为关状态,那么有:
static QState Led_StateInitial(QFsm* fsm, QEvt* e) { Q_TRAN(&Led_StateOff); }
再看Led的两个状态下对外部事件的处理
static QState Led_StateOn(QFsm* fsm, QEvt* e) { switch (e->sig) { case Q_ENTER_SIG: // 进入该状态时执行动作 return Q_HANDLED; case Q_EXIT_SIG: // 退出该状态时执行动作 return Q_HANDLED; case LedSignal_TurnOff: // '关'信号 Led_Off(); return Q_TRAN(&Led_StateOff); } return Q_IGNORED(); } static QState Led_StateOff(QFsm* fsm, QEvt* e) { switch (e->sig) { case Q_ENTER_SIG: // 进入该状态时执行动作 return Q_HANDLED; case Q_EXIT_SIG: // 退出该状态时执行动作 return Q_HANDLED; case LedSignal_TurnOn: // '开'信号 Led_On(); return Q_TRAN(&Led_StateOn); } return Q_IGNORED(); }
是否能感觉出,有了QP框架的支持,状态机的使用显得清晰明了。同时QP下的状态都具有Enter和Exit动作,这与UML规范下的状态机完全一致。由于这系列日志只关注应用,不会对UML规范和QP状态机有何不同做对比,感兴趣的同道请仔细研读QP的那本教程。
虽然QP在Led这个例子上面的应用及其简单,但也算麻雀虽小,五脏俱全。引出了一组不认识的宏:
Q_ENTER_SIG
Q_EXIT_SIG
Q_HANDLED
Q_IGNORED
Q_TRAN
不光如此,还有QFsm状态之间的切换是怎么实现的,有心的同道肯定会想到怎么你没有讲QFsm的dispatch和Init是怎么回事,是不是状态的切换跟dispatch(分发)有关。这些内容将在这个系列的后续日志中逐一记录。
如果QP仅仅做到QFsm这个程度,根本不值得我如此推崇,当涉及到层次状态机,活动对象这样的概念时,QP框架才真正的让人感觉到了什么是强大。调了众人的胃口,实属不该。按捺不住的同道请点击下面的链接进入QP官网,立刻下载考察其应用价值。那本教程我也给出百度网盘外链以作参考(毕竟是盗版,可不敢太张扬)。
原文发布时间为:2017年01月17日
本文作者:robert_cai
本文来源:博客园,如需转载请联系原作者。