狀態機可以說是一組狀態的集合,是協調相關信號動作,完成特定操作的控制中心,傳統應用程序的控制流程基本是順序的,遵循事先設定的邏輯,
從頭到尾地執行。很少有事件能改變標准執行流程,而且這些事件主要涉及異常情況
另一類應用程序由外部發生的事件來驅動--換言之,事件在應用程序之外生成,無法由應用程序或程序員來控制。具體需要執行的代碼取決於接收到的事件,
或者它相對於其他事件的抵達時間。所以,控制流程既不能是順序的,也不能是事先設定好的,因為它要依賴外部事件。
狀態機可歸納為4個要素,即當前狀態,條件,動作,下個狀態。這樣的歸納主要出於對狀態機的內在因果關系的考慮,
當前狀態和條件是因,動作和下個狀態是果。對於復雜些的邏輯,用狀態機會有助於代碼比較清晰,容易維護和Debug,而且效率也不錯
有限狀態機FSM思想廣泛應用於硬件控制電路設計,也是軟件上常用的一種處理方法(軟件上稱為FMM消息機)。它把復雜的控制邏輯分解成有限個穩定狀態,在每個狀態上判斷事件,變連續處理為離散數字處理,符合計算機的工作特點。同時,因為有限狀態機具有有限個狀態,所以可以在實際的工程上實現。但這並不意味着其只能進行有限次的處理,相反,有限狀態機是閉環系統,有限但是無窮,可以用有限的狀態,處理無窮的事件。
有限狀態機的工作原理如1所示,發生事件(event)后,根據當前狀態(curState),決定執行的動作(action)並設置下一個狀態號(nextState)。
圖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代碼片段:
1 cur_state = nxt_state; 2 switch(cur_state) //在當前狀態中判斷事件 3 { 4 case s0: //在s0狀態 5 if(e0_event) //如果發生e0事件,那么就執行a0動作,並保持狀態不變; 6 { 7 //執行a0動作; 8 //nxt_state = s0; //因為狀態號是自身,所以可以刪除此句,以提高運行速度。 9 } 10 else if(e1_event) //如果發生e1事件,那么就執行a1動作,並將狀態轉移到s1態; 11 { 12 //執行a1動作; 13 nxt_state = s1; 14 } 15 else if(e2_event) //如果發生e2事件,那么就執行a2動作,並將狀態轉移到s2態; 16 { 17 //執行a2動作; 18 nxt_state = s2; 19 } 20 else 21 { 22 break; 23 } 24 25 case s1: //在s1狀態 26 if(e2_event) //如果發生e2事件,那么就執行a2動作,並將狀態轉移到s2態; 27 { 28 //執行a2動作; 29 nxt_state = s2; 30 } 31 else 32 { 33 break; 34 } 35 36 case s2: //在s2狀態 37 if(e0_event) //如果發生e0事件,那么就執行a0動作,並將狀態轉移到s0態; 38 { 39 //執行a0動作; 40 nxt_state = s0; 41 } 42 }
橫着寫(在事件中判斷狀態)C代碼片段:
1 //e0事件發生時,執行的函數 2 void e0_event_function(int * nxt_state) 3 { 4 int cur_state; 5 cur_state = *nxt_state; 6 switch(cur_state) 7 { 8 case s0: //觀察表1,在e0事件發生時,s1處為空 9 case s2: //執行a0動作; 10 *nxt_state = s0; 11 } 12 } 13 14 //e1事件發生時,執行的函數 15 void e1_event_function(int * nxt_state) 16 { 17 int cur_state; 18 cur_state = *nxt_state; 19 switch(cur_state) 20 { 21 case s0: //觀察表1,在e1事件發生時,s1和s2處為空 22 //執行a1動作; 23 *nxt_state = s1; 24 } 25 } 26 27 //e2事件發生時,執行的函數 28 void e2_event_function(int * nxt_state) 29 { 30 int cur_state; 31 cur_state = *nxt_state; 32 switch(cur_state) 33 { 34 case s0: //觀察表1,在e2事件發生時,s2處為空 35 case s1: 36 //執行a2動作; 37 *nxt_state = s2; 38 } 39 }
上面橫豎兩種寫法的代碼片段,實現的功能完全相同,但是,橫着寫的效果明顯好於豎着寫的效果。理由如下:
1. 豎着寫隱含了優先級排序(其實各個事件是同優先級的),排在前面的事件判斷將毫無疑問地優先於排在后面的事件判斷,這種if/else if寫法上的限制將破壞事件間原有的關系。而橫着寫不存在此問題。
2. 由於處在每個狀態時的事件數目不一致,而且事件發生的時間是隨機的,無法預先確定,導致豎着寫淪落為順序查詢方式,結構上的缺陷使得大量時間被浪費。對於橫着寫,在某個時間點,狀態是唯一確定的,在事件里查找狀態只要使用switch語句,能一步定位到相應的狀態,延遲時間可以預先准確估算。而且在事件發生時,調用事件函數,在函數里查找唯一確定的狀態,並根據其執行動作和狀態轉移的思路清晰簡潔,效率高,富有美感。
總之,我個人認為,在軟件里寫狀態機使用橫着寫的方法比較妥帖。
豎着寫的方法也不是完全不能使用,在一些小項目里,邏輯不太復雜,功能精簡,同時為了節約內存耗費,豎着寫的方法也不失為一種合適的選擇。
在FPGA類硬件設計中,以狀態為中心實現控制電路狀態機(豎着寫)似乎是唯一的選擇,因為硬件不太可能靠事件驅動(橫着寫)。不過在FPGA里有一個全局時鍾,在每次上升沿時進行狀態切換,使得豎着寫的效率並不低。雖然在硬件里豎着寫也要使用if/else if這類查詢語句,但他們映射到硬件上是組合邏輯,查詢只會引起門級延遲(ns量級),而且硬件是真正並行工作的,這樣豎着寫在硬件里就沒有負面影響。因此,在硬件設計里,使用豎着寫的方式成為必然的選擇。這也是為什么很多搞硬件的工程師在設計軟件狀態機時下意識的只使用豎着寫方式的原因,思維定式罷了。