狀態機程式設計
阿新 • • 發佈:2019-01-03
有限狀態機FSM思想廣泛應用於硬體控制電路設計,也是軟體上常用的一種處理方法(軟體上稱為FMM--有限訊息機)。它把 複雜的控制邏輯分解成有限個穩定狀態,在每個狀態上判斷事件,變連續處理為離散數字處理,符合計算機的工作特點。同 時,因為有限狀態機具有有限個狀態,所以可以在實際的工程上實現。但這並不意味著其只能進行有限次的處理,相反,有限 狀態機是閉環系統,有限無窮,可以用有限的狀態,處理無窮的事務。 有限狀態機的工作原理如圖1所示,發生事件(event)後,根據當前狀態(cur_state),決定執行的動作(action),並設定 下一個狀態號(nxt_state)。 ------------- | |-------->執行動作action 發生事件event ----->| cur_state | | |-------->設定下一狀態號nxt_state ------------- 當前狀態 圖1 有限狀態機工作原理 e0/a0 --->-- | | -------->---------- e0/a0 | | S0 |----- | -<------------ | e1/a1 | | e2/a2 V ---------- ---------- | S2 |-----<-----| S1 | ---------- e2/a2 ---------- 圖2 一個有限狀態機例項 -------------------------------------------- 當前狀態 s0 s1 s2 | 事件 -------------------------------------------- a0/s0 -- a0/s0 | e0 -------------------------------------------- a1/s1 -- -- | e1 -------------------------------------------- a2/s2 a2/s2 -- | e2 -------------------------------------------- 表1 圖2狀態機例項的二維表格表示(動作/下一狀態) 圖2為一個狀態機例項的狀態轉移圖,它的含義是: 在s0狀態,如果發生e0事件,那麼就執行a0動作,並保持狀態不變; 如果發生e1事件,那麼就執行a1動作,並將狀態轉移到s1態; 如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態; 在s1狀態,如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態; 在s2狀態,如果發生e0事件,那麼就執行a0動作,並將狀態轉移到s0態; 有限狀態機不僅能夠用狀態轉移圖表示,還可以用二維的表格代表。一般將當前狀態號寫在橫行上,將事件寫在縱列上, 如表1所示。其中“--”表示空 (不執行動作,也不進行狀態轉移),“an/sn”表示執行動作an,同時將下一狀態設定為sn。表1和 圖2表示的含義是完全相同的。 觀察表1可知,狀態機可以用兩種方法實現:豎著寫(在狀態中判斷事件)和橫著寫(在事件中判斷狀態)。這兩種實現在本 質上是完全等效的,但在實際操作中,效果卻截然不同。 ================================== 豎著寫(在狀態中判斷事件)C程式碼片段 ================================== cur_state = nxt_state; switch(cur_state){ //在當前狀態中判斷事件 case s0: //在s0狀態 if(e0_event){ //如果發生e0事件,那麼就執行a0動作,並保持狀態不變; 執行a0動作; //nxt_state = s0; //因為狀態號是自身,所以可以刪除此句,以提高執行速度。 } else if(e1_event){ //如果發生e1事件,那麼就執行a1動作,並將狀態轉移到s1態; 執行a1動作; nxt_state = s1; } else if(e2_event){ //如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態; 執行a2動作; nxt_state = s2; } break; case s1: //在s1狀態 if(e2_event){ //如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態; 執行a2動作; nxt_state = s2; } break; case s2: //在s2狀態 if(e0_event){ //如果發生e0事件,那麼就執行a0動作,並將狀態轉移到s0態; 執行a0動作; nxt_state = s0; } } ================================== 橫著寫(在事件中判斷狀態)C程式碼片段 ================================== //e0事件發生時,執行的函式 void e0_event_function(int * nxt_state) { int cur_state; cur_state = *nxt_state; switch(cur_state){ case s0: //觀察表1,在e0事件發生時,s1處為空 case s2: 執行a0動作; *nxt_state = s0; } } //e1事件發生時,執行的函式 void e1_event_function(int * nxt_state) { int cur_state; cur_state = *nxt_state; switch(cur_state){ case s0: //觀察表1,在e1事件發生時,s1和s2處為空 執行a1動作; *nxt_state = s1; } } //e2事件發生時,執行的函式 void e2_event_function(int * nxt_state) { int cur_state; cur_state = *nxt_state; switch(cur_state){ case s0: //觀察表1,在e2事件發生時,s2處為空 case s1: 執行a2動作; *nxt_state = s2; } } 上面橫豎兩種寫法的程式碼片段,實現的功能完全相同,但是,橫著寫的效果明顯好於豎著寫的效果。理由如下: 1、豎著寫隱含了優先順序排序(其實各個事件是同優先順序的),排在前面的事件判斷將毫無疑問地優先於排在後面的事件判 斷。這種if/else if寫法上的限制將破壞事件間原有的關係。而橫著寫不存在此問題。 2、由於處在每個狀態時的事件數目不一致,而且事件發生的時間是隨機的,無法預先確定,導致豎著寫淪落為順序查詢 方式,結構上的缺陷使得大量時間被浪費。對於橫著寫,在某個時間點,狀態是唯一確定的,在事件裡查詢狀態只要使用 switch語句,就能一步定位到相應的狀態,延遲時間可以預先準確估算。而且在事件發生時,呼叫事件函式,在函式裡查詢唯 一確定的狀態,並根據其執行動作和狀態轉移的思路清晰簡潔,效率高,富有美感。 總之,我個人認為,在軟體裡寫狀態機,使用橫著寫的方法比較妥帖。 豎著寫的方法也不是完全不能使用,在一些小專案裡,邏輯不太複雜,功能精簡,同時為了節約記憶體耗費,豎著寫的方法 也不失為一種合適的選擇。 在FPGA類硬體設計中,以狀態為中心實現控制電路狀態機(豎著寫)似乎是唯一的選擇,因為硬體不太可能靠事件驅動(橫 著寫)。不過,在FPGA 裡有一個全域性時鐘,在每次上升沿時進行狀態切換,使得豎著寫的效率並不低。雖然在硬體裡豎著寫也 要使用IF/ELSIF這類查詢語句(用VHDL開發),但他們對映到硬體上是組合邏輯,查詢只會引起門級延遲(ns量級),而且硬體是 真正並行工作的,這樣豎著寫在硬體裡就沒有負面影響。因此,在硬體設計裡,使用豎著寫的方式成為必然的選擇。這也是為 什麼很多搞硬體的工程師在設計軟體狀態機時下意識地只使用豎著寫方式的原因,蓋思維定勢使然也。 TCP和PPP框架協議裡都使用了有限狀態機,這類軟體狀態機最好使用橫著寫的方式實現。以某TCP協議為例,見圖3,有三 種類型的事件:上層下達的命令事件;下層到達的標誌和資料的收包事件;超時定時器超時事件。 上層命令(open,close)事件 ----------------------------------- -------------------- | TCP | <----------超時事件timeout -------------------- ----------------------------------- RST/SYN/FIN/ACK/DATA等收包事件 圖3 三大類TCP狀態機事件 由圖3可知,此TCP協議棧採用橫著寫方式實現,有3種事件處理函式,上層命令處理函式(如tcp_close);超時事件處理函 數 (tmr_slow);下層收包事件處理函式(tcp_process)。值得一提的是,在收包事件函式裡,在各個狀態裡判斷 RST/SYN/FIN/ACK/DATA等標誌(這些標誌類似於事件),看起來象豎著寫方式,其實,如果把包頭和資料看成一個整體,那麼, RST/SYN/FIN/ACK/DATA等標誌就不必被看成獨立的事件,而是屬於同一個收包事件裡的細節,這樣,就不會認為在狀態裡查詢 事件,而是總體上看,是在收包事件裡查詢狀態(橫著寫)。 在PPP裡更是到處都能見到橫著寫的現象,有時間的話再細說。我個人感覺在實現PPP框架協議前必須瞭解橫豎兩種寫法, 而且只有使用橫著寫的方式才能比較完美地實現PPP。