轉自:https://blog.csdn.net/dfsae/article/details/52995034
事件驅動框架(二)
說明
本篇接上一篇事件驅動框架之后,介紹狀態機的原理相關的,以及事件驅動框架下事件處理狀態機的實現。因為代碼大多還是參照QP源碼,所以僅供學習使用。
有限狀態機介紹
有限狀態機,(英語:Finite-state machine, FSM),又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。
狀態機的概念在很多地方都有用到。我在學數字電路的時候和編程的時候都有接觸過這種數學模型:其實說白了就是一種數學模型,它的原理都是相似的。這里主要在嵌入式編程中的引用。
網上應該有狀態機最簡單的點小燈的例子來借助理解狀態機怎么編程,這里就直接放傳送門了。
傳送門空缺中。。。。
當然還有種層次狀態機(HSM),這種直接把有限狀態機(FSM)給包括進去了。結構上具有層次性,而且設計和實現都比較復雜。可以記住UML圖來進行設計。
事件處理狀態機的實現
該部分參考了一部分的QP框架。QP的設計主要實現層次為實現層次狀態機(HSM)的方式:規定每次事件處理完后的狀態返回值(這個主要是根據UML圖的模型,也就是必須開發者之前設計過的來實現的),狀態轉換過程中會自動根據返回值,判斷實現是否需要調用進入和退出的狀態回調。但是該種方式有一個缺點:它需要開發者對QP非常了解這個框架,函數回調是采用列舉事件(switch)的方式,並且需要自己邏輯上使用狀態機的編程思想,並且需要嚴格對照着UML圖的狀態轉換方式進行編程。
一.狀態機的常見處理方式:
下再介紹幾種常用的狀態機處理方式:但這些都不外乎提供2個接口:dispatch和initial。initial為初始化函數,即負責這個狀態機控制的對象的初始化,及其設置狀態機的初始狀態。dispatch接口用來給狀態機派發一個事件。
1>.在dispatch中列出所有的狀態及對應的事件。
其實QP在這個功能上有點隱藏狀態的意思。缺點就是需要把所有狀態和時間都羅列,代碼的重用性較低,內容容易膨脹。
2>.狀態表(State Table)
該種方式是將事件和狀態綁定到一張二維表上,然后根據不同的狀態和時間進行規律的映射到這張表上,進行事件處理的回調。
缺點:這是一張靜態的狀態表,需要列舉出所有的事件和狀態,但狀態和事件的類型增多時,維護就變得比較困難。
3>.面向對象的設計模式
這種方式是把狀態機作為一個對象,將狀態的原型作為一個抽象類,然后子狀態(具體狀態)繼承於抽象類,完成它的具體實現。個人感覺這種方式和上一種差不多,不過上一種相對來說多出了一張狀態表。
缺點:多態的實現用C語言比較麻煩。如果繼承每個狀態還是需要列舉出所有狀態的實現的,如果對應的狀態的事件為空,還是需要建立某個響應,比較浪費內存。如果是繼承dispatch,則也需要在單個函數體內列舉出事件響應。
4>.pt協程
PT協程會在后面專門章中介紹。
二.FSM的策略
由於這個框架主要還是為了來做GUI的。整個框架采用事件驅動的結構來實現,主要形式采用狀態表的形式。
這里討論一點:就是HSM和FSM的關系。HSM是一種覆蓋面很廣的模型,包括了各種狀態機的模型。但他的層次特征主要體現在對相同的狀態類進行抽象,把同樣的動作進行歸並,比如說一個層次包含了它內包含多個具體狀態的進入和退出的相同的操作。這樣就減少了代碼量和邏輯上的重復性。但如果不用抽象的概念,HSM依然是可以展開成拓展型FSM的(對於這種狀態機有個專業的名字,忘了)。而QP的主要的狀態機還是針對於HSM模型的,但HSM不容易理解,這也可能是限制了QP推廣的原因。
因此在形式上,之前使用過的MATLAB的GUIDE和VB甚至是C#的界面設計時,他的動作響應callback回調函數令我印象深刻。比如:我希望鼠標按下圖形化按鈕后進行一個動作。這里我只需要在那個動作響應的函數里編程就可以了,剩下的事整個框架則會幫我自動完成,我無需關心它是如何被調用的。利用這個想法,因此我更偏向於使用狀態表的方式。但是為了兼容一些類型的狀態機,我仿照了QP的一些做法,並可選擇性的保留了entry和exit的回調。當然,如果當前對象如果對此並沒有要求,則可以省去。
除了SGUI以外,對於其他的對象來說,也可以采取這種策略或者其他幾種常見的處理方式。
三.具體實現
1.事件的實現
事件作為驅動整個框架的關鍵,對象之間通過接受事件,進行響應處理,狀態的轉化。事件驅動的結構如下:
typedef struct AEvtTag AEvt; struct AEvtTag { ASignal sig; /* 事件信號量 */ uint16_t poolID; /* 內存池編號,對應的是事件塊編號 */ };
ASignal 為型號量的類型,信號的多少可以在頭文件中定義,選擇不超出范圍的大小。poolID是對應的事件編號。動態事件是從內存池中分配出來的,可以動態生成和釋放,而靜態事件則不能。
2.狀態機的實現(SGUI的狀態機)
狀態機的機構如下:
struct AFsmTag { const AStateHandler * state_table; /* 指向狀態表 */ ASignal n_signals; /* 信號量總數 */ uint8_t n_states; /* 狀態總數 */ uint8_t state; /* 當前狀態編號 */ uint8_t method ; /* 方式: 提供2種:是否省略進入退出動作,默認忽略。 這里設置這個主要是為了省內存,如果找到更好的映射方式時改進*/ AStateHandler initial; /* 初始化 轉換 */ };
其中大部分都是狀態表設計的原型,用來輔助查表的實現的。這里我增加了一個method的變量,用來標志着個是否是基礎的FSM帶進入entry和exit的事件的。如果對於狀態較多的對象,如果不需要用到進入和退出的動作不必要,則可以省去一大筆空間。
dipatch函數主要是實現事件的派送交給響應函數處理,另外如果有entry和exit,則會自動調用。不過這里有個比較麻煩的問題:用戶在每次處理函數過后還是需要返回一個狀態值(改變狀態或者不改變)。
3.初始化
狀態機初始化代碼如下:顯示調用初始化接口
/*! 預留信號狀態 */ enum { A_ENTRY_SIG = 0, /* 進入狀態動作 */ A_EXIT_SIG, /*退出狀態動作 */ A_DEFAULT_SIG /*用戶預留信號 */ }; void AFsm_init(AFsm *me) { AStateHandler t; (me->initial)(me, (AEvt *)0); //調用對象初始化函數 assert((me->state) < (me->n_state)); if ((me->method == FSM_DEFAULT_METHOD)) { t = *(me->state_table + me->state * me->n_signals + A_ENTRY_SIG); //默認進入預留 (*t)(me, (void *)0); } else { t = *(me->state_table + me->state * me->n_signals); //默認進入第一個狀態 (*t)(me, (void *)0); } }
前面的枚舉量為預留進入退出的信號標識。如果設置為FSM_DEFAULT_METHOD模式,則需要在該對象的第一個信號枚舉=A_DEFAULT_SIG。那SGUI的自定義的一個響應舉例:
/*! 自定義按鍵 */ enum KeyCode { TICK_SIG = A_DEFAULT_SIG, UP_KEY_SIG, DOWN_KEY_SIG, LEFT_KEY_SIG, RIGHT_KEY_SIG, CONFIRM_KEY_SIG, GUI_MAX_SIG };
這樣就默認將進入和退出的信號無形中加入到自定義的信號錢
初始化接口會自動調用對象的初始化函數,再將狀態轉換到默認狀態:如果有設置狀態的退出進入則會默認進入entry狀態進行執行,否則進入第一個狀態的第一個響應進行執行。(這里想着是否能改進一下,制定一下第一次進入的狀態和時間)。
4.調度
狀態機調度代碼如下:
void AFsm_dispatch(AFsm * me, AEvt const * e) { AStateHandler t; uint8_t sta; assert(e);//事件合法性 assert(state);//狀態合法性 sta = me->state; t = *(me->state_table + me->state * me->n_signals + e->sig); //獲取狀態表中的函數指針 if ((*t)(me, e) == A_RET_TRANS && (me->method == FSM_DEFAULT_METHOD)) //得到執行結果 { /* 調用退出 */ t = *(me->state_table + sta * me->n_signals + A_EXIT_SIG); (*t)(me, (void *)0); /* 調用進入 */ t = *(me->state_table + me->state * me->n_signals + A_ENTRY_SIG); (*t)(me, (void *)0); } }
如果該對象沒有設置進入或者退出狀態,則不執行進入退出段響應的代碼。如果有設置FSM_DEFAULT_METHOD模式則在狀態發生改變時自動調用進出狀態的函數。
4.狀態空響應
狀態空響應的代碼用來填充到狀態表中對應狀態和事件什么都不做的位置。這部分代碼如下:
void AFsm_empty(AFsm * me, AEvt const * const e) { (void)me; (void)e; }
1
- 2
- 3
- 4
- 5
四.小結
事件處理的狀態機大概就這樣了,原型還是用了狀態表的原理。后來有聽別人說過PT協程的方法來封裝狀態機,大概記得的印象中主要就是把while-case的結構隱藏到范式中,但實際上還是狀態機的結構,但整體上看起來就和順序編程沒什么差別。因為看過很久了,而且正好要思考怎么拓展框架的靈活性,所以會單獨拉一張來介紹下PT協程。
