(圖片來在網(wǎng)絡,侵刪)
【說在前面的話】
也許從12年前我第一次開始分享狀態(tài)機編寫心得開始,“狀態(tài)機”就像標簽一樣緊緊的貼在了“傻孩子”這個網(wǎng)絡昵稱的額頭上——真是摳都扣不下來。不得不坦白的是,從一開始我介紹狀態(tài)機更多只注重狀態(tài)機這一語言的表現(xiàn)形式,而 故意偷懶避開了狀態(tài)機開發(fā)思維的系統(tǒng)性介紹——也許剛開始真的是沒什么自信,覺得自己也沒有能真正領會狀態(tài)機的所謂精髓,所以不敢瞎說;后來慢慢的掌握了所謂的狀態(tài)機思維模式以后,就是真正的懶惰了。在過去的5年間,盡管那些毛遂自薦參加過我免費遠程培訓的人或多或少都學習到了一系列使用狀態(tài)機進行開發(fā)的思維方式,但畢竟人數(shù)太少。由阿莫論壇改為付費訂閱模式為契機,我也有機會仔細回看、思考之前所寫過的內(nèi)容。 說實話,它們中的很多實在談不上“深入淺出”。 作為公眾號 復更的開端,我 覺 得有必要 系統(tǒng)性的整理 我那些所謂的“理論”、 把它們 寫下來—— 讓更多的人得以 獲得一個討論和交流的起點 。雖然 我后面要寫的內(nèi)容既不是什么教條、也不是什么 標準答案更不會是“金科玉律”, 但一定是一個非常好的出發(fā)點——如果能引起大家的思考和討論,真正掌握 狀態(tài)機開發(fā) ,甚至能 發(fā)展出自己的 理論和風格,那就再好不過了。
(圖片來源:https://en.wikipedia.org/wiki/Finite-state_machine)
【正文】
說來你不信,有限自動機(Finite State Machine),又叫狀態(tài)機是整個計算機學科倒數(shù)第二層的基石;倒數(shù)第一層就是大家所熟悉的組合邏輯(Combinational logic)——如果說組合邏輯是沒啥靈魂的細胞的話,有限自動機就是第一種“能夠任意描述思維邏輯”的神獸大烏龜——整個計算機學科都馱在它的背上。然而,如同組合邏輯電路如同“阿米巴”一樣的簡單,掌握狀態(tài)機的難度跟“幼稚園”差不了多少。不相信的話,我們先從幾個簡單的概念說起:
-
怎么理解狀態(tài)?如何才算一個狀態(tài)
很多小伙伴都曾抱怨說“字面意思我都懂”,但“實際中如何理解什么是狀態(tài)”?、 “怎樣才算一個狀態(tài)呢? ”——我不是第一次遇到這種問題了。 答案其實很直接:
【第一種情況】 : 假設我們嘗試去做一件事情,但這件事情不是每次去做就一定會成功,而且每次去嘗試都有可能產(chǎn)生至少2種以上的結(jié)果,那么針對這件事情的嘗試就應該單獨劃分一個狀態(tài) 。舉個例子:
extern bool serial_out(uint8_t chByte);
函數(shù)serial_out()可以用來向某個串行外設發(fā)送一個字符,比如UART。如果成功了就返回true,如果設備正忙導致本次發(fā)送失敗則立即返回false。由于外設的發(fā)送速度相對CPU的運行頻率來說差了好幾個數(shù)量級——在CPU眼中外設慢得跟蝸牛一樣,所以每次通過serial_out() 發(fā)送字符不一定是成功——很可能外設還在努力“消化”上一次的字符。這種情況下,如果我們要在狀態(tài)機中描述發(fā)送字符這樣的行為,就值得為其單獨分配一個狀態(tài),因為它滿足了我們前面說的條件:1)一件事情你要不停的嘗試才有可能成功,而且2)每次做都可能會產(chǎn)生2個以上的結(jié)果。習慣上,我們會用圖示的方法來描述狀態(tài),以發(fā)送字符'H'為例:
從圖中很容易注意到:
-
我們用圓圈來表示一個狀態(tài);
-
圓圈中心我們會寫一些注釋性質(zhì)的內(nèi)容用來幫助人們理解這個狀態(tài)是做什么的;
-
圖中有三個箭頭,最左上角單純“指向”狀態(tài)的箭頭表示從別的什么地方“躍遷”到了當前狀態(tài)——我們稱為“扇入”;下方從當前狀態(tài)指向別的什么地方的箭頭表示從當前狀態(tài)離開;——我們成為“扇出”;右上角從當前狀態(tài)“扇出”后又“返回到”當前狀態(tài)的情況,我們稱之為“自返”——也就是返回自己的意思。是不是特別簡單。
實際使用的時候,如果單憑一個狀態(tài)圓圈里面的注釋文字,我們?nèi)匀徊荒芾斫膺@個狀態(tài)實際做了什么事情;或者說我們非常好奇這個狀態(tài)實際嘗試做了什么動作,就可以通過以下的標注方法追加更多的信息,比如:
你看,是不是更加清晰了?同樣的情況還可以推廣到“調(diào)用一個函數(shù)而函數(shù)有多個不同的返回值”的情況;或者是“我們通過調(diào)用函數(shù)做了一件事情,雖然函數(shù)沒有返回值,但是我們可以通過多種其它手段來獲得這件事情的多個不同結(jié)果”的情況等等——領會精神,以此類推。
【第二種情況】:假設我們只是單純的在等待某一個事情發(fā)生;或者等待某個一結(jié)果——這個結(jié)果由2個以上的返回值組成等等,那么這個等待行為就需要分配一個獨立的狀態(tài)。舉個例子:
int32_t get_sensor_voltage(void);
函數(shù)get_sensor_volatage()可以返回某個傳感器的電壓值;我們設置了上下兩個門限,一旦電壓超過了任何一個門限,我們就切換到其它狀態(tài),對應的狀態(tài)圖示如下:
在這里,HIGH_THRESHOLD和LOW_THRESHOLD是兩個宏表示上下兩個門限??梢钥吹?,這個狀態(tài)表示:如果傳感器的電壓值在兩個門限之間,我們就留在當前狀態(tài)(通過自返回);如果任意門限被超過,我們就相應的跳轉(zhuǎn)到別的狀態(tài)去。
-
所有的神奇都在狀態(tài)躍遷上
在前面的圖示中,所有的箭頭我們都稱之為“ 躍遷”,表示 從當前狀態(tài)跳轉(zhuǎn)到箭頭所指向的目標狀態(tài)(自返的躍遷就是自己跳回自己)。 躍遷不是無條件的,也不允許無條件——換句話說, 每個躍遷都必須有一個條件:例如第一個例子中的 true和 false就是對應躍遷的條件;后面例子中與門限值的比較也是對應的條件。需要特別強調(diào)的是: 1)一個狀態(tài)所有的躍遷條件必須是彼此“互斥”的、唯一的;2)所有的躍遷必須能覆蓋一個狀態(tài)機所有可能的情況——絕不允許出現(xiàn)漏網(wǎng)之魚,否則 一旦沒有被覆蓋的情況出現(xiàn)就有可能導致整個狀態(tài)機的行為存在“不確定性”——如果狀態(tài)機描述的是一個機器人的行為的話,這就是導致機器人邏輯故障的嚴重 Bug; 3)躍遷是個瞬間的行為,你只能認為當條件滿足時躍遷的行為就像白駒過隙一樣一下就做完了——這點很重要,我們馬上就要細說。前面說過,當某個躍遷的條件得到了滿足,我們就要沿著箭頭的方向從當前狀態(tài)調(diào)轉(zhuǎn)到箭頭所指向的目標狀態(tài)。實際上,在躍遷的過程中我們還可以執(zhí)行一些動作。需要注意的是,正如前面 3)說的那樣, “躍遷是個瞬間行為”,所以這里的 動作也只會被執(zhí)行一次。習慣上,如果某個躍遷存在動作,我們就在躍遷的條件下面加一個橫線,并在橫線的下方按順序列舉所有要執(zhí)行的動作。
比如,我們可以通過一個專門的狀態(tài)來實現(xiàn)一個計數(shù)器延時的效果:
在這個例子中,我們注意到:
-
雖然左上角扇入Delay狀態(tài)的躍遷條件我們并不知道,但在此時復位計數(shù)器s_wCounter是再好不過了。所以我們空出了躍遷條件,并在橫線的下方寫下了計數(shù)器的初始化代碼;
-
右上角的躍遷條件是:“如果計數(shù)器的值小于延時1s所需的最大值”,那么對應的動作就是讓計數(shù)器自增;
-
右下角躍遷的條件是:“計數(shù)器的值超過了規(guī)定的最大值”,因此直接跳到目標狀態(tài)而無需做其它動作。
-
狀態(tài)機的起點和終點
一個狀態(tài)機可以沒有終點,但一定有一個起點,我們稱之為 start。圖示上,習慣用一個實心小圓點來表示。 start 不僅是狀態(tài)機的起點,由一個躍遷來連接它和第一個狀態(tài); start 還是“兼任” 這一躍遷的條件,例如:
容易看出,這里 start 不僅是整個狀態(tài)機的起點,還兼任了扇入Delay狀態(tài)的躍遷的條件——從圖上來看,很容易理解成:“當狀態(tài)機開始時復位計數(shù)器s_wCounter”——可謂一目了然。
-
狀態(tài)機有多簡單
至此,借助前面介紹的概念和圖式方法,我們已經(jīng)可以輕松的繪制一個狀態(tài)機(圖)了。其實前面的例子中,我們已經(jīng)看到了一個完整的 Delay狀態(tài)機,盡管它只有一個狀態(tài)但已麻雀雖小五臟俱全。接下來,我們再展示一個更直接的例子——如何使用 serial_out()發(fā)送字符串“ hello”:
還有另外一種更為通用的方法:
- “不要問,問就是子狀態(tài)機”
如果狀態(tài)機不能調(diào)用子狀態(tài)機,那它跟咸魚有什么兩樣?那么如何用圖示表示子狀態(tài)機呢?廢話少說,直接上圖:
如圖所示:
-
子狀態(tài)機是被圓角矩形包裹的
-
子狀態(tài)機的右上角有一個自反的狀態(tài)遷移,條件是“on going”意味子狀態(tài)機正在執(zhí)行,還未得出一個結(jié)果;
-
子狀態(tài)機的右下角(或者別的什么位置)需要有一個標記有cpl條件的狀態(tài)遷移,表示當子狀態(tài)機內(nèi)部達到了終點cpl以后,子狀態(tài)機從這里退出并躍遷到指定的狀態(tài);
-
子狀態(tài)機有一個標題欄,里面分別列舉了狀態(tài)機的名稱以及傳遞給當前子狀態(tài)機的形參列表。(狀態(tài)機的返回值只能是類似cpl, on-going這樣的狀態(tài),所以不需要特別標記)
通過子狀態(tài)機調(diào)用,我們很容易用已有的狀態(tài)機實現(xiàn)搭積木的功能,比如假設我們將此前Delay的狀態(tài)機也做成子狀態(tài)機,配合這個已有的print_hello子狀態(tài)機,就可以輕松實現(xiàn)一個“打印hello然后延時1秒”的狀態(tài)機:
(這里需要注意,當子狀態(tài)機被調(diào)用時,它使用圓角矩形替代了普通狀態(tài)的圓圈。)
考慮到任何一個狀態(tài)機其實都可以在未來被其它狀態(tài)機調(diào)用,我們實際操作上會把每一個狀態(tài)機都按照子狀態(tài)機的格式進行繪制,因此上面的狀態(tài)機正確的畫法應該是:
怎么樣,是不是很簡單?
【后記】
請不要懷疑, 狀態(tài)機本身是一種編程語言;狀態(tài)圖是描述狀態(tài)機的最常見方式之一;繪制狀態(tài)圖的圖例規(guī)范有很多種,比如 UML規(guī)范等等。本文以及后續(xù)其它文章使用的是一種筆者自己結(jié)合狀態(tài)機的常見畫法并針對嵌入式軟件開發(fā)習慣簡化后的圖例規(guī)范, 簡單、明確、有效,并且可以毫無歧義的嚴格且無腦的翻譯成包括switch狀態(tài)機在內(nèi)的多種C語言實現(xiàn)。在下一篇文章里,我們將以 switch狀態(tài)機為例,介紹狀態(tài)圖的無腦翻譯方式,盡情期待。
免責聲明:本文內(nèi)容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!