量子框架簡稱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官網,立刻下載考察其應用價值。那本教程我也給出百度網盤外鏈以作參考(畢竟是盜版,可不敢太張揚)。
官網鏈接:
百度網盤外鏈:
這個系列下一篇將探討一下QFsm,並着重分析它的dispatch機制。