文章目錄
- 1、什么是狀態機?
- 2、狀態機編程的優點
- (1)提高CPU使用效率
- (2) 邏輯完備性
- (3)程式結構清晰
- 3、狀態機的三種實作方法
- switch—case 法
- 表格驅動法
- 函式指標法
- 小節
摘要:不知道大家有沒有這樣一種感覺,就是感覺自己玩單片機還可以,各個功能模塊也都會驅動,但是如果讓你完整的寫一套代碼,卻無邏輯與框架可言,上來就是開始寫!東抄抄寫抄抄,說明編程還處于比較低的水平,那么如何才能提高自己的編程水平呢?學會一種好的編程框架或者一種編程思想,可能會受用終生!比如模塊化編程,框架式編程,狀態機編程等等,都是一種好的框架,
,
今天說的就是狀態機編程,由于篇幅較長,大家慢慢欣賞,那么狀態機是一個這樣的東東?狀態機(state machine)有5個要素,分別是狀態(state)、遷移(transition)、事件(event)、動作(action)、條件(guard),
1、什么是狀態機?
狀態機是一個這樣的東東:狀態機(state machine)有 5 個要素,分別是狀態(state)、遷移(transition)、事件(event)、動作(action)、條件(guard),
狀態:一個系統在某一時刻所存在的穩定的作業情況,系統在整個作業周期中可能有多個狀態,例如一部電動機共有正轉、反轉、停轉這 3 種狀態,
一個狀態機需要在狀態集合中選取一個狀態作為初始狀態,
遷移:系統從一個狀態轉移到另一個狀態的程序稱作遷移,遷移不是自動發生的,需要外界對系統施加影響,停轉的電動機自己不會轉起來,讓它轉起來必須上電,
事件:某一時刻發生的對系統有意義的事情,狀態機之所以發生狀態遷移,就是因為出現了事件,對電動機來講,加正電壓、加負電壓、斷電就是事件,
動作:在狀態機的遷移程序中,狀態機會做出一些其它的行為,這些行為就是動作,動作是狀態機對事件的回應,給停轉的電動機加正電壓,電動機由停轉狀態遷移到正轉狀態,同時會啟動電機,這個啟動程序可以看做是動作,也就是對上電事件的回應,
條件:狀態機對事件并不是有求必應的,有了事件,狀態機還要滿足一定的條件才能發生狀態遷移,還是以停轉狀態的電動機為例,雖然合閘上電了,但是如果供電線路有問題的話,電動機還是不能轉起來,
只談概念太空洞了,上一個小例子:一單片機、一按鍵、倆 LED 燈(記為L1和L2)、一人, 足矣!
規則描述:
1、L1L2狀態轉換順序OFF/OFF--->ON/OFF--->ON/ON--->OFF/ON--->OFF/OFF
2、通過按鍵控制L1L2的狀態,每次狀態轉換需連續按鍵5次
3、L1L2的初始狀態OFF/OFF
圖1
下面這段程式是根據功能要求寫成的代碼,
程式清單List1:
void main(void)
{
sys_init();
led_off(LED1);
led_off(LED2);
g_stFSM.u8LedStat = LS_OFFOFF;
g_stFSM.u8KeyCnt = 0;
while(1)
{
if(test_key()==TRUE)
{
fsm_active();
}
else
{
; /*idle code*/
}
}
}
void fsm_active(void)
{
if(g_stFSM.u8KeyCnt > 3) /*擊鍵是否滿 5 次*/
{
switch(g_stFSM.u8LedStat)
{
case LS_OFFOFF:
led_on(LED1); /*輸出動作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_ONOFF; /*狀態遷移*/
break;
case LS_ONOFF:
led_on(LED2); /*輸出動作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_ONON; /*狀態遷移*/
break;
case LS_ONON:
led_off(LED1); /*輸出動作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFON; /*狀態遷移*/
break;
case LS_OFFON:
led_off(LED2); /*輸出動作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFOFF; /*狀態遷移*/
break;
default: /*非法狀態*/
led_off(LED1);
led_off(LED2);
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFOFF; /*恢復初始狀態*/
break;
}
}
else
{
g_stFSM.u8KeyCnt++; /*狀態不遷移,僅記錄擊鍵次數*/
}
}
實際上在狀態機編程中,正確的順序應該是先有狀態轉換圖,后有程式,程式應該是根據設計好的狀態圖寫出來的,不過考慮到有些童鞋會覺得代碼要比轉換圖來得親切,我就先把程式放在前頭了,
這張狀態轉換圖是用UML(統一建模語言)的語法元素畫出來的,語法不是很標準,但拿來解釋問題足夠了,
圖2按鍵控制流水燈狀態轉換圖
圓角矩形代表狀態機的各個狀態,里面標注著狀態的名稱,
帶箭頭的直線或弧線代表狀態遷移,起于初態,止于次態,
圖中的文字內容是對遷移的說明,格式是:事件[條件]/動作串列(后兩項可選),
“事件[條件]/動作串列”要說明的意思是:如果在某個狀態下發生了“事件”,并且狀態機
滿足“[條件]”,那么就要執行此次狀態轉移,同時要產生一系列“動作”,以回應事件,在這個例子里,我用“KEY”表示擊鍵事件,
圖中有一個黑色實心圓點,表示狀態機在作業之前所處的一種不可知的狀態,在運行之前狀態機必須強制地由這個狀態遷移到初始狀態,這個遷移可以有動作串列(如圖1所示),但不需要事件觸發,
圖中還有一個包含黑色實心圓點的圓圈,表示狀態機生命周期的結束,這個例子中的狀態機生生不息,所以沒有狀態指向該圓圈,
關于這個狀態轉換圖就不多說了,相信大家結合著上面的代碼能很容易看明白,現在我們再聊一聊程式清單List1,
先看一下fsm_active()這個函式,g_stFSM.u8KeyCnt = 0;這個陳述句在switch—case里共出現了 5 次,前 4 次是作為各個狀態遷移的動作出現的,從代碼簡化提高效率的角度來看,我們完全可以把這 5 次合并為 1 次放在 switch—case 陳述句之前,兩者的效果是完全一樣的,代碼里之所以這樣啰嗦,是為了清晰地表明每次狀態遷移中所有的動作細節,這種方式和圖2的狀態轉換圖所要表達的意圖是完全一致的,
再看一下g_stFSM這個狀態機結構體變數,它有兩個成員:u8LedStat和 u8KeyCnt,用這個結構體來做狀態機好像有點兒啰嗦,我們能不能只用一個像 u8LedStat 這樣的整型變數來做狀態機呢?
當然可以!我們把圖 2中的這 4 個狀態各自拆分成 5 個小狀態,這樣用 20 個狀態同樣能實作這個狀態機,而且只需要一個 unsigned char 型的變數就足夠了,每次擊鍵都會引發狀態遷移, 每遷移 5 次就能改變一次 LED 燈的狀態,從外面看兩種方法的效果完全一樣,

假設我把功能要求改一下,把連續擊鍵5次改變L1L2的狀態改為連續擊鍵100次才能改變L1L2的狀態,這樣的話第二種方法需要4X100=400個狀態!而且函式fsm_active()中的switch—case陳述句里要有400個case,這樣的程式還有法兒寫么?!
同樣的功能改動,如果用g_stFSM這個結構體來實作狀態機的話,函式fsm_active()只需要將if(g_stFSM.u8KeyCnt>3)改為if(g_stFSM.u8KeyCnt > 98)就可以了!
g_stFSM結構體的兩個成員中,u8LedStat可以看作是質變因子,相當于主變數;u8KeyCnt可以看作是量變因子,相當于輔助變數,量變因子的逐步積累會引發質變因子的變化,
像g_stFSM這樣的狀態機被稱作Extended State Machine,我不知道業內正規的中文術語怎么講,只好把英文詞組搬過來了,
2、狀態機編程的優點
說了這么多,大家大概明白狀態機到底是個什么東西了,也知道狀態機化的程式大體怎么寫了,那么單片機的程式用狀態機的方法來寫有什么好處呢?
(1)提高CPU使用效率

話說我只要見到滿篇都是delay_ms()的程式就會蛋疼,動輒十幾個ms幾十個ms的軟體延時是對CPU資源的巨大浪費,寶貴的CPU機時都浪費在了NOP指令上,那種為了等待一個管腳電平跳變或者一個串口資料而巋然不動的程式也讓我非常糾結,如果事件一直不發生,你要等到世界末日么?
把程式狀態機化,這種情況就會明顯改觀,程式只需要用全域變數記錄下作業狀態,就可以轉頭去干別的作業了,當然忙完那些活兒之后要再看看作業狀態有沒有變化,只要目標事件(定時未到、電平沒跳變、串口資料沒收完)還沒發生,作業狀態就不會改變,程式就一直重復著“查詢—干別的—查詢—干別的”這樣的回圈,這樣CPU就閑不下來了,在程式清單 List3 中,if{}else{}陳述句里else下的內容(代碼中沒有添加,只是加了一條/*idle code*/的注釋示意)就是上文所說的“別的作業” ,
這種處理方法的實質就是在程式等待事件的程序中間隔性地插入一些有意義的作業,好讓CPU不是一直無謂地等待,
(2) 邏輯完備性
我覺得邏輯完備性是狀態機編程最大的優點,

不知道大家有沒有用C語言寫過計算器的小程式,我很早以前寫過,寫出來一測驗,那個慘不忍睹啊!當我規規矩矩的輸入算式的時候,程式可以得到正確的計算結果,但要是故意輸入數字和運算子號的隨意組合,程式總是得出莫名其妙的結果,

后來我試著思維模擬一下程式的作業程序,正確的算式思路清晰,流程順暢,可要碰上了不規矩的式子,走著走著我就暈菜了,那么多的標志位,那么多的變數,變來變去,最后直接分析不下去了,
很久之后我認識了狀態機,才恍然明白,當時的程式是有邏輯漏洞的,如果把這個計算器程式當做是一個反應式系統,那么一個數字或者運算子就可以看做一個事件,一個算式就是一組事件組合,對于一個邏輯完備的反應式系統,不管什么樣的事件組合,系統都能正確處理事件,而且系統自身的作業狀態也一直處在可知可控的狀態中,反過來,如果一個系統的邏輯功能不完備,在某些特定事件組合的驅動下,系統就會進入一個不可知不可控的狀態,與設計者的意圖相悖,
狀態機就能解決邏輯完備性的問題,
狀態機是一種以系統狀態為中心,以事件為變數的設計方法,它專注于各個狀態的特點以及狀態之間相互轉換的關系,狀態的轉換恰恰是事件引起的,那么在研究某個具體狀態的時候,我們自然而然地會考慮任何一個事件對這個狀態有什么樣的影響,這樣,每一個狀態中發生的每一個事件都會在我們的考慮之中,也就不會留下邏輯漏洞,
這樣說也許大家會覺得太空洞,實踐出真知,某天如果你真的要設計一個邏輯復雜的程式,
我保證你會說:哇!狀態機真的很好用哎!
(3)程式結構清晰
用狀態機寫出來的程式的結構是非常清晰的,

程式員最痛苦的事兒莫過于讀別人寫的代碼,如果代碼不是很規范,而且手里還沒有流程圖,讀代碼會讓人暈了又暈,只有順著程式一遍又一遍的看,很多遍之后才能隱約地明白程式大體的作業程序,有流程圖會好一點,但是如果程式比較大,流程圖也不會畫得多詳細,很多細節上的程序還是要從代碼中理解,
相比之下,用狀態機寫的程式要好很多,拿一張標準的UML狀態轉換圖,再配上一些簡明的文字說明,程式中的各個要素一覽無余,程式中有哪些狀態,會發生哪些事件,狀態機如何回應,回應之后跳轉到哪個狀態,這些都十分明朗,甚至許多動作細節都能從狀態轉換圖中找到,可以毫不夸張的說,有了UML狀態轉換圖,程式流程圖寫都不用寫,
套用一句廣告詞:誰用誰知道!
3、狀態機的三種實作方法
狀態機的實作無非就是 3 個要素:狀態、事件、回應,轉換成具體的行為就 3 句話,
- 發生了什么事?
- 現在系統處在什么狀態?
- 在這樣的狀態下發生了這樣的事,系統要干什么?
用 C 語言實作狀態機主要有 3 種方法:switch—case 法、表格驅動法、函式指標法,
switch—case 法
狀態用 switch—case 組織起來, 將事件也用switch—case 組織起來, 然后讓其中一個 switch—case 整體插入到另一個 switch—case 的每一個 case 項中 ,
「程式清單 List4 :」
switch(StateVal)
{
case S0:
switch(EvntID)
{
case E1:
action_S0_E1(); /*S0 狀態下 E1 事件的回應*/
StateVal = new state value;/*狀態遷移,不遷移則沒有此行*/
break;
case E2:
action_S0_E2(); /*S0 狀態下 E2 事件的回應*/
StateVal = new state value;
break;
......
case Em:
action_S0_Em(); /*S0 狀態下 Em 事件的回應*/
StateVal = new state value;
break;
default:
break;
}
break;
case S1:
......
break;
......
case Sn:
......
break;
default:
break;
}
上面的偽代碼示例只是通用的情況,實際應用遠沒有這么復雜,雖然一個系統中事件可能有很多種,但在實際應用中,許多事件可能對某個狀態是沒有意義的,
例如在程式清單 List4中,如果 E2、······ Em 對處在 S0 狀態下的系統沒有意義,那么在 S0 的 case 下有關事件E2、······ Em 的代碼根本沒有必要寫,狀態 S0 只需要考慮事件 E1 的處理就行了,
既然是兩個 switch—case 之間的嵌套, 那么就有一個誰嵌套誰的問題, 所以說 switch—case法有兩種寫法:狀態嵌套事件和事件嵌套狀態,這兩種寫法都可以, 各有利弊, 至于到底選用哪種方式就留給設計人員根據具體情況自行決斷吧,
關于 switch—case 法還有最后一點要說明, 因為 switch—case 的原理是從上到下挨個比較,越靠后,查找耗費的時間就越長,所以要注意狀態和事件在各自的 switch 陳述句中的安排順序,不推薦程式清單 List4 那樣按順序號排布的方式,出現頻率高或者實時性要求高的狀態和事件的位置應該盡量靠前,
表格驅動法
如果說 switch—case 法是線性的,那么表格驅動法則是平面的,表格驅動法的實質就是將狀態和事件之間的關系固化到一張二維表格里, 把事件當做縱軸,把狀態當做橫軸,交點[Sn , Em]則是系統在 Sn 狀態下對事件 Em 的回應 ,

如圖 4, 我把表格中的 Node_SnEm 叫做狀態機節點, 狀態機節點 Node_SnEm 是系統在 Sn狀態下對事件 Em 的回應,這里所說的回應包含兩個方面:輸出動作和狀態遷移,狀態機節點一般是一個類似程式清單 List5 中的結構體變數 ,
struct fsm_node
{
void (*fpAction)(void* pEvnt);
INT8U u8NxtStat;
};
程式清單 List5 中的這個結構體有兩個成員:fpAction 和 u8NxtStat,fpAction 是一個函式指標, 指向一個形式為 void func(void * pEvnt)的函式, func 這個函式是對狀態轉移中動作序列的標準化封裝,
也就是說, 狀態機在狀態遷移的時候, 不管輸出多少個動作、操作多少個變數、呼叫多少個函式,這些行為統統放到函式 func 中去做,
把動作封裝好了之后,再把封裝函式 func 的地址交給函式指標 fpAction,這樣,想要輸出動作,只需要呼叫函式指標 fpAction 就行了,
再看看上面的 func 函式,會發現函式有一個形參 pEvnt,這是一個型別為 void * 的指標, 在程式實際運行時指向一個能存盤事件的變數,通過這個指標我們就能獲知關于事件的全部資訊,這個形參是很有必要的,事件一般包括兩個屬性:事件的型別和事件的內容,
例如一次按鍵事件,我們不僅要知道這是一個按鍵事件,還要知道按下的到底是哪個鍵,事件的型別和狀態機當前的狀態可以讓我們在圖 4 的表格中迅速定位,確定該呼叫哪個動作封裝函式, 但是動作封裝函式要正確回應事件還需要知道事件的內容是什么, 這也就是形參pEvnt 的意義,
由于事件的多樣性,存盤事件內容的資料格式不一定一樣,所以就把 pEvnt 定義成了 void * 型,以增加靈活性,有關 fpAction 的最后一個問題:如果事件 Em 對狀態 Sn 沒有意義,那么狀態機節點Node_SnEm 中的 fpAction 該怎么辦?我的答案是:那就讓它指向一個空函式唄!前面不是說過么,什么也不干也叫回應,
u8NxtStat 存盤的是狀態機的一個狀態值,我們知道, 狀態機回應事件要輸出動作, 也就是呼叫函式指標 fpAction 所指向的那個封裝函式, 函式呼叫完畢后程式回傳主調函式, 狀態機對事件的回應就算結束了, 下一步就要考慮狀態遷移的問題了,
可能要保持本狀態不變, 也可能要遷移到一個新的狀態,該如何抉擇呢?u8NxtStat 存盤的狀態就是狀態機想要的答案!
圖 4 的這張表格反映在 C 語言代碼里就是一個二維陣列,第 1 維就是狀態機的狀態,第 2維就是統一分類的事件,而陣列的元素則是程式清單 List5 中的結構體常量,如果程式中使用表格驅動法,還需要注意一些特別的事項,要將狀態當做表格的橫軸,那么就要求狀態值集合必須滿足以下條件:
(1) 該集合是一個遞增的等差整數數列
(2) 該數列初值為 0
(3) 該數列等差值為 1
“事件” 作為縱軸,其特點和要求與用來做橫軸的“狀態” 完全一致,在 C 語言提供的資料型別中, 沒有比列舉更符合以上要求的可選項了, 極力推薦將狀態集合和事件型別集合做成列舉常量,**表格驅動法的優點:**呼叫介面統一 ,定位快速,
表格驅動法屏蔽了不同狀態下處理各個事件的差異性,因此可以將處理程序中的共性部分提煉出來,做成標準統一的框架式代碼,形成統一的呼叫介面,根據程式清單 List5 中的狀態機節點結構體,做成的框架代碼如程式清單 List6 所示,
表格驅動法查找目標實際上就是一次二維陣列的尋址操作,所以它的平均效率要遠高于switch—case 法,
「程式清單 List6 :」
extern struct fsm_node g_arFsmDrvTbl[][]; /*狀態機驅動表格*/
INT8U u8CurStat = 0; /*狀態暫存*/
INT8U u8EvntTyp = 0; /*事件型別暫存*/
void* pEvnt = NULL; /*事件變數地址暫存*/
struct fsm_node stNodeTmp = {NULL, 0}; /*狀態機節點暫存*/
u8CurStat = get_cur_state(); /*讀取當前狀態*/
u8EvntTyp = get_cur_evnt_typ(); /*讀取當前觸發事件型別*/
pEvnt = (void*)get_cur_evnt_ptr(); /*讀取事件變數地址*/
stNodeTmp = g_arFsmDrvTbl[u8CurStat ][u8EvntTyp ];/*定位狀態機節點*/
stNodeTmp.fpAction(pEvnt ); /*動作回應*/
set_cur_state(stNodeTmp.u8NxtStat); /*狀態遷移*/
.....
表格驅動法好則好矣,但用它寫出來的程式還有點兒小問題,我們先來看看按照表格驅動法寫出來的程式有什么特點 ,
前面說過,表格驅動法可以把狀態機調度的部分做成標準統一的框架代碼,這個框架適用性極強, 不管用狀態機來實作什么樣的應用, 框架代碼都不需要做改動, 我們只需要根據實際應用場合規劃好狀態轉換圖,然后將圖中的各個要素(狀態、事件、動作、遷移,有關“條件”要素一會兒再說)用代碼實作就行了,我把這部分代碼稱作應用代碼,
在應用代碼的.c 檔案中, 你會看到一個宣告為 const 的二維陣列, 也就是圖 4 所示的狀態驅動表格, 還會看到許多彼此之間毫無關聯的函式, 也就是前面提到的動作封裝函式,這樣的一份代碼, 如果手頭上沒有一張狀態轉換圖, 讓誰看了也會一頭霧水, 這樣的格式直接帶來了代碼可讀性差的問題,
如果我們想給狀態機再添加一個狀態,反映到代碼上就是給驅動表格再加一列內容,同時也要新添加若干個動作封裝函式,如果驅動表格很大, 做這些作業是很費事兒的, 而且容易出錯,如果不小心在陣列中填錯了位置, 那么程式跑起來就和設計者的意圖南轅北轍了,
遠沒有在 switch—case 法中改動來得方便、安全,上面說的只是小瑕疵, 其實最讓我不爽的是表格驅動法不能使用干貨 | 嵌入式之狀態機編程 中 g_stFSM那樣的 Extended State Machine(對這個詞組還有印象吧?)!Extended State Machine 的最大特點就是狀態機回應事件之前先判斷條件,根據判定結果選擇執行哪些動作,轉向哪個狀態,
也就是說,系統在狀態 Sn 下發生了事件 Em 后,轉向的狀態不一定是唯一的,這種靈活性是 Extended State Machine 的最有價值的優點,
回過頭來看看程式清單 List5 中給出的狀態機節點結構體,如果系統在狀態 Sn 下發生了事件 Em, 狀態機執行完 fpAction 所給出的動作回應之后, 必須轉到 u8NxtStat 指定的狀態,
表格驅動法的這個特性直接杜絕了 Extended State Machine 在表格驅動法中應用的可能性, 所以表格驅動法的代碼實作中不存在“條件” 這個狀態機要素,ESM,你是如此的優秀,我怎么舍得拋棄你 ?!
再看圖 4 所示的表格驅動法示例圖,如果我們把表格中的代表事件的縱軸去掉,只留下代表狀態的橫軸,將一列合并成一格,前文提到的問題是不是能得到解決呢?不錯!這就是失傳江湖多年的《葵花寶典》 ——閹割版表格驅動法 !!
閹割版表格驅動法,又名壓縮表格驅動法,一維狀態表格與事件 switch—case 的合體,壓縮表格驅動法使用了一維陣列作為驅動表格,陣列的下標即是狀態機的各個狀態,
表格中的元素叫做壓縮狀態機節點, 節點的主要內容還是一個指向動作封裝函式的函式指標, 只不過這個動作封裝函式不是為某個特定事件準備的, 而是對所有的事件都有效的,
節點中不再強制指定狀態機輸出動作完畢后所轉向的狀態, 而是讓動作封裝函式回傳一個狀態, 并把這個狀態作為狀態機新的狀態,
壓縮表格驅動法的這個特點, 完美的解決了 Extended State Machine 不能在表格驅動法中使用的問題 ,
程式清單 List7 中的示例代碼包含了壓縮狀態機節點結構體和狀態機呼叫的框架代碼,
「程式清單 List7:」
struct fsm_node /*壓縮狀態機節點結構體*/
{
INT8U (*fpAction)(void* pEvnt); /*事件處理函式指標*/
INT8U u8StatChk; /*狀態校驗*/
};
......
u8CurStat = get_cur_state(); /*讀取當前狀態*/
......
if(stNodeTmp.u8StatChk == u8CurStat )
{
u8CurStat = stNodeTmp.fpAction(pEvnt ); /*事件處理*/
set_cur_state(u8CurStat ); /*狀態遷移*/
}
else
{
state_crash(u8CurStat ); /*非法狀態處理*/
}
.....
對照程式清單 List5,就會發現程式清單 List7 中 struct fsm_node 結構體的改動之處,首先, fpAction 所指向函式的函式形式變了,動作封裝函式 func 的模樣成了這樣的了:
INT8U func(void * pEvnt);
現在的動作封裝函式 func 是要回傳型別為 INT8U 的回傳值的,這個回傳值就是狀態機要轉向的狀態, 也就是說, 壓縮表格驅動法中的狀態機節點不負責狀態機新狀態的確定, 而把這項任務交給了動作封裝函式 func, func 回傳哪個狀態, 狀態機就轉向哪個狀態,
新狀態由原來的常量變成了現在的變數,自然要靈活許多,上面說到現在的動作封裝函式 func 要對當前發生的所有的事件都要負責, 那么 func 怎么會知道到底是哪個事件觸發了它呢?看一下 func 的形參 void * pEvnt ,
在程式清單 List5 中我們提到過,這個形參是用來向動作封裝函式傳遞事件內容的,但是從前文的敘述中我們知道, pEvnt 所指向的記憶體包含了事件的所有資訊, 包括事件型別和事件內容 , 所以通過形參 pEvnt , 動作封裝函式 func 照樣可以知道事件的型別,
程式清單 List7 中 struct fsm_node 結構體還有一個成員 u8StatChk , 這里面存盤的是狀態機 的一個狀態,干什么用的呢?玩 C 語言陣列的人都知道,要嚴防陣列尋址越界,
要知道,壓縮表格驅動法的驅動表格是一個以狀態值為下標的一維陣列, 陣列元素里面最重要的部分就是一個個動作封裝函式的地址,
函式地址在單片機看來無非就是一段二進制資料, 和記憶體中其它的二進制資料沒什么兩樣,不管程式往單片機 PC 暫存器里塞什么值,單片機都沒意見,假設程式由于某種意外而改動了存盤狀態機當前狀態的變數,使變數值變成了一個非法狀態,
再發生事件時, 程式就會用這個非法的狀態值在驅動表格中尋址, 這時候就會發生記憶體泄露,程式拿泄露記憶體中的未知資料當函式地址跳轉,不跑飛才怪!
為了防止這種現象的發生, 壓縮狀態機節點結構體中又添加了成員 u8StatChk ,u8StatChk中存盤的是壓縮狀態機節點在一維驅動表格的位置, 例如某節點是表格中的第 7 個元素, 那么這個節點的成員 u8StatChk 值就是 6,
看一下程式清單 List7 中的框架代碼示例, 程式在參考函式指標 fpAction 之前, 先檢查當前狀態和當前節點成員 u8CurStat 的值是否一致,一致則認為狀態合法,事件正常回應,如果不一致,則認為當前狀態非法,轉至意外處理,最大限度保證程式運行的安全,當然,如果泄露記憶體中的資料恰好和 u8CurStat 一致,那么這種方法真的就回天乏力了,
還有一個方法也可以防止狀態機跑飛,如果狀態變數是列舉,那么框架代碼就可以獲知狀態值的最大值, 在呼叫動作封裝函式之前判斷一下當前狀態值是否在合法的范圍之內, 同樣能保證狀態機的安全運行,
壓縮表格驅動法中動作封裝函式的定義形式我們已經知道了,函式里面到底是什么樣子的呢?程式清單 List8 是一個標準的示例,
「程式清單List8:」
INT8U action_S0(void* pEvnt)
{
INT8U u8NxtStat = 0;
INT8U u8EvntTyp = get_evnt_typ(pEvnt);
switch(u8EvntTyp )
{
case E1:
action_S0_E1(); /*事件 E1 的動作回應*/
u8NxtStat = new state value; /*狀態遷移,不遷移也必須有本行*/
break;
......
case Em:
action_S0_Em(); /*事件 Em 的動作回應*/
u8NxtStat = new state value; /*狀態遷移,不遷移也必須有本行*/
break;
default:
; /*不相關事件處理*/
break;
}
return u8NxtStat ; /*回傳新狀態*/
}
從程式清單 List8 可以看出, 動作封裝函式其實就是事件 switch—case 的具體實作,函式根據形參 pEvnt 獲知事件型別, 并根據事件型別選擇動作回應, 確定狀態機遷移狀態, 最后將新的狀態作為執行結果回傳給框架代碼,
有了這樣的動作封裝函式, Extended State Machine 的應用就可以完全不受限制了!到此,有關壓縮表格驅動法的介紹就結束了,
個人認為壓縮表格驅動法是相當優秀的**,它既有表格驅動法的簡潔、高效、標準,又有 switch—case 法的直白、靈活、多變**,相互取長補短,相得益彰,
函式指標法
上面說過,用 C 語言實作狀態機主要有 3 種方法(switch—case 法、表格驅動法、函式指標法), 其中函式指標法是最難理解的, 它的實質就是把動作封裝函式的函式地址作為狀態來看待,不過,有了之前壓縮表格驅動法的鋪墊,函式指標法就變得好理解了,因為兩者本質上是相同的,
壓縮表格驅動法的實質就是一個整數值(狀態機的一個狀態)到一個函式地址(動作封裝函式)的一對一映射, 壓縮表格驅動法的驅動表格就是全部映射關系的直接載體,在驅動表格中通過狀態值就能找到函式地址,通過函式地址同樣能反向找到狀態值,
我們用一個全域的整型變數來記錄狀態值,然后再查驅動表格找函式地址,那干脆直接用一個全域的函式指標來記錄狀態得了,還費那勞什子勁干嗎?!這就是函式指標法的前世今生,
用函式指標法寫出來的動作封裝函式和程式清單 List8 的示例函式是很相近的, 只不過函式的回傳值不再是整型的狀態值, 而是下一個動作封裝函式的函式地址, 函式回傳后, 框架代碼再把這個函式地址存盤到全域函式指標變數中,
相比壓縮表格驅動法,在**函式指標法中狀態機的安全運行是個大問題,我們很難找出一種機制來檢查全域函式指標變數中的函式地址是不是合法值,**如果放任不管, 一旦函式指標變數中的資料被篡改,程式跑飛幾乎就不可避免了,
小節
有關狀態機的東西說了那么多,相信大家都已經感受到了這種工具的優越性,狀態機真的是太好用了!其實我們至始至終講的都是有限狀態機(Finite State Machine 現在知道為什么前面的代碼中老是有 fsm 這個縮寫了吧!), 還有一種比有限狀態機更 NB 更復雜的狀態機, 那就是層次狀態機(Hierarchical State Machine 一般簡寫為 HSM),
通俗的說,系統中只存在一個狀態機的叫做有限狀態機,同時存在多個狀態機的叫做層次狀態機(其實這樣解釋層次狀態機有些不嚴謹, 并行狀態機也有多個狀態機, 但層次狀態機各個狀態機之間是上下級關系,而并行狀態機各個狀態機之間是平級關系),
層次狀態機是一種父狀態機包含子狀態機的多狀態機結構,里面包含了許多與面向物件相似的思想, 所以它的功能也要比有限狀態機更加強大, 當一個問題用有限狀態機解決起來有些吃力的時候, 就需要層次狀態機出馬了,
層次狀態機理論我理解得也不透徹, 就不在這里班門弄斧了,大家可以找一些有關狀態機理論的專業書籍來讀一讀,要掌握狀態機編程,理解狀態機(主要指有限狀態機)只是第一步,也是最簡單的一步,更重要的技能是能用狀態機這個工具去分析解剖實際問題:劃分狀態、 提取事件、 確定轉換關系、規定動作等等,形成一張完整的狀態轉換圖,最后還要對轉換圖進行優化,達到最佳,
把實際問題變成了狀態轉換圖, 作業的一大半就算完成了, 這個是具有架構師氣質的任務,剩下的問題就是按照狀態圖編程寫代碼了,這個是具有代碼工特色的作業,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/345698.html
標籤:其他
上一篇:Nginx快取機制和性能優化
