1. 程式人生 > >嵌入式系統中的狀態機設計心得

嵌入式系統中的狀態機設計心得

在使用iTRON類OS的嵌入式系統中,除了驅動程式以外,大多數模組也就是中介軟體和應用程式是以任務(TASK)的形式設計的。而iTRON類OS大多采用C語言實現,於是用狀態機的方式實現功能模組成為了主要的設計方法。
至於說面向物件,只要是稍微嚴謹一點的嵌入式系統,設計上要求程式完全覆蓋所有的可能情況。程式不可能在緊急情況下丟擲異常等待除錯。同時由於對硬體和其它應用模組的往往具有嚴重的耦合性,程式碼的重用和擴充套件也不是那麼隨心所欲。當然還有基於語言的執行速度之類的考慮。這種情況下C語言往往取代大多數現代語言成為了主角吧。

iTRON類OS的任務間通訊一般通過兩種方法,事件(EVENT)或者訊息(MESSAGE)。
事件處理快捷,但是無法附帶任何引數且不能疊加。
訊息雖然傳遞稍慢,不過卻可以通過記憶體池等方式附帶一定數量的引數。而且多個同樣的訊息可以累積在訊息棧中依次處理。

如果形象得比喻一下:
事件就是一串位元碼,由特定為的0或1狀態來判斷事件是否發生,而任務以它自己的優先級別處理各種事件。
訊息就是一個緩衝區,OS以FIFO的方式把訊息依從舊到新的順序分發給任務進行對應處理。

說到這裡,我想強調一下本文討論的重點是通過狀態機的方式處理訊息的模型。至於事件的對應,可能今後會另外展開討論。

一個由OS管轄的嵌入式系統中的應用模組,在程式角度上是沒有main函式,也不會被退出的(除非切斷電源)。只要做過任何GUI程式,就不難理解這一點。程式被執行的瞬間,OS呼叫程式的初始化部分(Initialization),然後每隔一個固定的時間片程式的執行部分(Execute),當程式被關閉時休止部分(Exit)會被呼叫。
而嵌入式系統與桌面GUI系統的不同之處在於:
系統的電源被載入,OS完成初始化動作之後,往往會啟動一個電源管理模組,而這個模組則會呼叫所有應用模組的初始化部分。
另一方面,OS或者電源管理模組在監測到電源即將被切斷時,則呼叫所有應用模組的休止部分。
在系統正常執行時,OS會依據各個任務的優先順序依次呼叫它們的執行部分,並且向它們分發各自所屬的訊息。

應用模組在收到訊息後並不是立刻進行處理。設計良好的應用模組往往內部劃分為多個狀態,簡單來說可以有種四到五種狀態:睡眠狀態,初始狀態,空閒狀態,繁忙狀態和故障狀態。當然了,根據不同的設計要求可以做相應的修改和擴充。應用模組內部根據不同的狀態和可能接收到的所有訊息編織出一張狀態對應表。在表中填入適當的函式指標以響應不同狀態下對各種訊息的處理過程。這就是嵌入式系統中普遍的狀態機模式。

舉一個狀態機實現的簡單例子:
我們將建立一個假象的小機器人,這個機器人能坐能走還能打架,打壞了還能自己修復。我打算用狀態機的模式來實現這些功能的框架。

/* 機器人能接受的事件 */
enum {
EVENT_POWERON = 0,
EVENT_POWEROFF,
EVENT_WALK,
EVENT_FIGHT,
EVENT_REST,
EVENT_REPAIR
EVENT_SIZE
};

/* 機器人的狀態 */
enum {
STATUS_SLEEP,
STATUS_INITIAL,
STATUS_NORMAL,
STATUS_FIGHTING,
STATUS_BROKEN,
STATUS_MAX
};
/* 狀態機函式指標的原型 */
typedef void (*MATRIX_FP)(const void*)

/* 狀態機函式表格 */
const MATRIX_FP s_Robot_Matrix [EVENT_SIZE][STATUS_SIZE] = {
/* EVENT_POWERON */
{Robot_PowerOn, Robot_NoProcess, Robot_NoProcess, Robot_NoProcess, Robot_NoProcess},
/* EVENT_POWEROFF */
{Robot_NoProcess, Robot_PowerOff, Robot_PowerOff, Robot_PowerOff, Robot_PowerOff},
/* EVENT_WALK */
{Robot_NoProcess, Robot_NoProcess, Robot_Walk, Robot_Walk, Robot_NoProcess},
/* EVENT_FIGHT */
{Robot_NoProcess, Robot_NoProcess, Robot_BattleMode, Robot_Fight, Robot_NoProcess},
/* EVENT_REST */
{Robot_NoProcess, Robot_NoProcess, Robot_Rest, Robot_NormalMode, Robot_NoProcess},
/* EVENT_REPAIR */
{Robot_NoProcess, Robot_NoProcess, Robot_Repair, Robot_NoProcess, Robot_Repair},
};

/* 機器人事件接受分發函式 */
void Api_Robot_Execute(void *pMsgData) {
byEvent = Api_GetRobotEvent(pMsgData);
byStatus = Api_GetRobotStatus();
if (NULL != s_Robot_Matrix[byEvent][byStatus]) {
(s_Robot_Matrix[byEvent][byStatus])((const void*)(pMsgData);
} else {
Robot_NoProcess(pMsgData);
}
return;
}

以上是狀態機的雛形。
我申明瞭機器人能響應的各種事件和機器人內部的各種狀態。
同時用它們編織起一個狀態/事件響應函式指標二維陣列,也就是一直說到現在的狀態機的矩陣。
最後我設計了一個對外來事件進行解釋,對內部狀態進行讀取,並最終確定呼叫矩陣中具體哪個函式的事件分發函式。

下面在簡單列舉一下填寫在矩陣中所有成員函式將實現什麼樣的功能。

/* 沒有任何功能的空函式 */
void Robot_NoProcess(void *pMsgData);
/* 電源載入,初始化操作 */
void Robot_PowerOn(void *pMsgData);
/* 電源關閉,休止操作 */
void Robot_PowerOff(void *pMsgData);
/* 步行動作 */
void Robot_Walk(void *pMsgData);
/* 切換到戰鬥模式 */
void Robot_BattleMode(void *pMsgData);
/* 戰鬥動作 */
void Robot_Fight(void *pMsgData);
/* 休息動作 */
void Robot_Rest(void *pMsgData);
/* 切換到一般模式 */
void Robot_NormalMode(void *pMsgData);
/* 修理動作 */
void Robot_Repair(void *pMsgData);

機器人在啟動和停止時要進行初始化處理和休止處理。
初始化處理完成後,預設為一般狀態。
一般狀態下可以行走,休息和修復戰鬥傷害。
在一般狀態下執行收到攻擊指令可以切換到攻擊狀態。
攻擊狀態下可以行走和攻擊,並且可以通過休息指令切換回一般狀態。
當機器人的對手太強大,自己被打得七零八落的時候,會強制切換到破損狀態。
在破損狀態下就只能進行修復動作了。
具體的實現我就不說了,如果有興趣可以想象一下各個函式該怎麼實現,一定會很有意思的。

另外還有兩個函式沒有交代:

/* 訊息/事件轉換函式 */
int Api_GetRobotEvent(void *pMsgData);
/* 內部狀態取得函式 */
int Api_GetRobotStatus(void);

訊息事件轉換函式把外部的訊息轉換成狀態機矩陣能夠識別的事件程式碼。
而內部狀態取得函式就像它的名字一樣,只管返回內部狀態,供呼叫狀態機矩陣中的函式指標而使用。

在一個狀態機系統的設計過程中,根據我自己的體會,我覺得以下幾點一定要嚴格遵守。
在每一次將訊息轉化成事件後,讀且僅讀一次內部狀態。並由此決定呼叫哪個成員函式。
成員函式中絕對不能再次讀取狀態並以它為分枝條件進行不同的處理。
一個外部模組絕對不能通過本模組的公開API直接修改本模組的內部狀態。
狀態機矩陣的成員函式寧可數量偏多內容類似,而切勿追求統一,盲目精簡程式碼。
必要的時候一個狀態/事件,響應一個成員函式,也比一個函式通過內部無數的分歧判斷條件進行不同的處理來的容易維護。

關於狀態機的內容有很多前人的研究成果。我只是在實時作業系統下的嵌入式環境中得到了一些微不足道的經驗。
本文旨在拋磚引玉,希望有更多的朋友能夠一起參與討論。