C語言中的狀態機設計深入講解
前言
本文不是關于軟件狀態機的最佳設計分解實踐的教程。我將重點關注狀態機代碼和簡單的示例,這些示例具有足夠的復雜性,以便于理解特性和用法。
背景
大多數程序員常用的設計技術是有限狀態機(FSM)。設計人員使用此編程結構將復雜的問題分解為可管理的狀態和狀態轉換。有無數種實現狀態機的方法。
A switch語句提供了狀態機最容易實現和最常見的版本之一。在這里,每個案例在switch語句成為一個狀態,實現如下所示:
- switch (currentState) {
- case ST_IDLE:
- // do something in the idle state
- break;
- case ST_STOP:
- // do something in the stop state
- break;
- // etc...
- }
這種方法當然適合于解決許多不同的設計問題。然而,在事件驅動的多線程項目上使用時,這種形式的狀態機可能是非常有限的。
第一個問題是控制哪些狀態轉換是有效的,哪些是無效的。無法強制執行狀態轉換規則。任何過渡都可以在任何時候進行,這并不是特別可取的。對于大多數設計,只有少數轉換模式是有效的。理想情況下,軟件設計應該強制執行這些預定義的狀態序列,并防止不必要的轉換。當試圖將數據發送到特定狀態時,會出現另一個問題。由于整個狀態機位于單個函數中,因此向任何給定狀態發送額外數據都是困難的。最后,這些設計很少適合在多線程系統中使用。設計器必須確保狀態機是從單個控制線程調用的。
為什么要用國家機器?
使用狀態機實現代碼是解決復雜工程問題的一種非常方便的設計技術。狀態機將設計分解為一系列步驟,或在狀態機術語中稱為狀態。每個狀態都執行一些狹義的任務。另一方面,事件是一種刺激,它導致狀態機在狀態之間移動或過渡。
舉一個簡單的例子,我將在本文中使用它,假設我們正在設計電機控制軟件。我們想啟動和停止電機,以及改變電機的速度。很簡單。向客戶端軟件公開的電機控制事件如下:
- 設定速度-設定電機以特定速度行駛
- 站住-停止馬達
這些事件提供了以任何速度啟動電機的能力,這也意味著改變已經移動的電機的速度?;蛘呶覀兛梢酝耆V柜R達。對于電機控制模塊,這兩個事件或功能被認為是外部事件.然而,對于使用我們的代碼的客戶機來說,這些只是普通的函數。
這些事件不是狀態機狀態。處理這兩個事件所需的步驟是不同的。在這種情況下,各州是:
- 閑散-馬達不是旋轉的,而是靜止的
- 無所事事
- 啟動-從死胡同啟動馬達
- 開啟電動機電源
- 設定電機轉速
- 變速-調整已經移動的馬達的速度
- 改變電機轉速
- 停-停止移動的馬達
- 關閉電動機電源
- 進入閑置狀態
可以看出,將電機控制分解為離散狀態,而不是單一的功能,我們可以更容易地管理如何操作電機的規則。
每個狀態機都有“當前狀態”的概念。這是狀態機當前所處的狀態。在任何給定的時刻,狀態機只能處于單一狀態。特定狀態機實例的每個實例在定義時都可以設置初始狀態。但是,該初始狀態在對象創建期間不執行。只有發送到狀態機的事件才會導致執行狀態函數。
為了圖形化地說明狀態和事件,我們使用狀態圖。下面的圖1顯示了電機控制模塊的狀態轉換??虮硎緺顟B,連接箭頭表示事件轉換。列出事件名稱的箭頭是外部事件,而未裝飾的行被認為是內部事件。(本文后面將介紹內部事件和外部事件之間的差異。)
圖1:電機狀態圖
如您所見,當事件在狀態轉換中出現時,所發生的狀態轉換取決于狀態機的當前狀態。當SetSpeed事件出現,例如,電機在Idle狀態,則轉換為Start狀態。然而,同樣的SetSpeed當前狀態為Start將電機轉換為ChangeSpeed狀態。您還可以看到,并非所有的狀態轉換都是有效的。例如,馬達不能從ChangeSpeed到Idle而不需要先通過Stop狀態。
簡而言之,使用狀態機捕獲和執行復雜的交互,否則可能很難傳遞和實現。
內外事件
正如我前面提到的,事件是導致狀態機在狀態之間轉換的刺激。例如,按下按鈕可能是一個事件。事件可以分為兩類:外部事件和內部事件。外部事件,在其最基本的級別上,是對狀態機模塊的函數調用.這些函數是公共的,從外部調用,或者從外部代碼調用到狀態機對象。系統中的任何線程或任務都可以生成外部事件。如果外部事件函數調用導致狀態轉換發生,則狀態將在調用方的控制線程內同步執行。另一方面,內部事件是由狀態機本身在狀態執行期間自行生成的。
典型的場景由生成的外部事件組成,該事件同樣可以歸結為模塊的公共接口中的函數調用。根據正在生成的事件和狀態機的當前狀態,執行查找以確定是否需要轉換。如果是這樣,狀態機將轉換到新狀態,并執行該狀態的代碼。在狀態函數的末尾,執行檢查以確定是否生成了內部事件。如果是這樣,則執行另一個轉換,并且新的狀態有機會執行。此過程將繼續進行,直到狀態機不再生成內部事件,此時原始外部事件函數調用將返回。外部事件和所有內部事件(如果有的話)在調用者的控制線程中執行。
一旦外部事件啟動狀態機執行,它不能被另一個外部事件中斷,直到外部事件和所有內部事件已經完成執行,如果使用鎖。這個運行到完成模型為狀態轉換提供了一個多線程安全的環境。可以在狀態機引擎中使用信號量或互斥量來阻止可能同時訪問同一狀態機實例的其他線程。見源代碼函數_SM_ExternalEvent()關于鎖的位置的注釋。
事件數據
生成事件時,它可以選擇附加事件數據,以便在執行過程中由狀態函數使用。事件數據是一個const或者不是-const 指向任何內置或用戶定義的數據類型的指針。
一旦狀態完成執行,事件數據就被認為用完了,必須刪除。因此,發送到狀態機的任何事件數據都必須通過SM_XAlloc()。狀態機引擎自動釋放分配的事件數據。SM_XFree().
狀態轉變
當生成外部事件時,執行查找以確定狀態轉換操作過程。事件有三種可能的結果:新狀態、忽略事件或不能發生。新狀態會導致轉換到允許執行的新狀態。轉換到現有狀態也是可能的,這意味著當前狀態被重新執行。對于被忽略的事件,不執行任何狀態。但是,事件數據(如果有的話)將被刪除。最后一種不可能發生的可能性是保留在事件在狀態機的當前狀態下無效的情況下使用的。如果發生這種情況,軟件就會出現故障。
在此實現中,執行驗證轉換查找不需要內部事件。假設狀態轉換是有效的。您可以檢查有效的內部和外部事件轉換,但實際上,這只會占用更多的存儲空間,并且只會產生很少的好處。驗證轉換的真正需要在于異步的外部事件,在這些事件中,客戶端可能導致事件在不適當的時間發生。一旦狀態機執行,它就不能被中斷。它處于私有實現的控制之下,因此沒有必要進行轉換檢查。這使設計人員可以自由地通過內部事件更改狀態,而無需更新轉換表。
狀態機模塊
狀態機源代碼包含在_StateMachine.c_和_StateMachine.h_檔案。下面的代碼顯示了部分標題。這個StateMachine 報頭包含各種預處理器多行宏,以簡化狀態機的實現。
- enum { EVENT_IGNORED = 0xFE, CANNOT_HAPPEN = 0xFF };
- typedef void NoEventData;
- // State machine constant data
- typedef struct
- {
- const CHAR* name;
- const BYTE maxStates;
- const struct SM_StateStruct* stateMap;
- const struct SM_StateStructEx* stateMapEx;
- } SM_StateMachineConst;
- // State machine instance data
- typedef struct
- {
- const CHAR* name;
- void* pInstance;
- BYTE newState;
- BYTE currentState;
- BOOL eventGenerated;
- void* pEventData;
- } SM_StateMachine;
- // Generic state function signatures
- typedef void (*SM_StateFunc)(SM_StateMachine* self, void* pEventData);
- typedef BOOL (*SM_GuardFunc)(SM_StateMachine* self, void* pEventData);
- typedef void (*SM_EntryFunc)(SM_StateMachine* self, void* pEventData);
- typedef void (*SM_ExitFunc)(SM_StateMachine* self);
- typedef struct SM_StateStruct
- {
- SM_StateFunc pStateFunc;
- } SM_StateStruct;
- typedef struct SM_StateStructEx
- {
- SM_StateFunc pStateFunc;
- SM_GuardFunc pGuardFunc;
- SM_EntryFunc pEntryFunc;
- SM_ExitFunc pExitFunc;
- } SM_StateStructEx;
- // Public functions
- #define SM_Event(_smName_, _eventFunc_, _eventData_)
- _eventFunc_(&_smName_##Obj, _eventData_)
- // Protected functions
- #define SM_InternalEvent(_newState_, _eventData_)
- _SM_InternalEvent(self, _newState_, _eventData_)
- #define SM_GetInstance(_instance_)
- (_instance_*)(self->pInstance);
- // Private functions
- void _SM_ExternalEvent(SM_StateMachine* self,
- const SM_StateMachineConst* selfConst, BYTE newState, void* pEventData);
- void _SM_InternalEvent(SM_StateMachine* self, BYTE newState, void* pEventData);
- void _SM_StateEngine(SM_StateMachine* self, const SM_StateMachineConst* selfConst);
- void _SM_StateEngineEx(SM_StateMachine* self, const SM_StateMachineConst* selfConst);
- #define SM_DECLARE(_smName_)
- extern SM_StateMachine _smName_##Obj;
- #define SM_DEFINE(_smName_, _instance_)
- SM_StateMachine _smName_##Obj = { #_smName_, _instance_,
- 0, 0, 0, 0 };
- #define EVENT_DECLARE(_eventFunc_, _eventData_)
- void _eventFunc_(SM_StateMachine* self, _eventData_* pEventData);
- #define EVENT_DEFINE(_eventFunc_, _eventData_)
- void _eventFunc_(SM_StateMachine* self, _eventData_* pEventData)
- #define STATE_DECLARE(_stateFunc_, _eventData_)
- static void ST_##_stateFunc_(SM_StateMachine* self, _eventData_* pEventData);
- #define STATE_DEFINE(_stateFunc_, _eventData_)
- static void ST_##_stateFunc_(SM_StateMachine* self, _eventData_* pEventData)
這個SM_Event()宏用于生成外部事件,而SM_InternalEvent()在執行狀態函數期間生成內部事件。SM_GetInstance()獲取指向當前狀態機對象的指針。
SM_DECLARE 和SM_DEFINE用于創建狀態機實例。EVENT_DECLARE和EVENT_DEFINE創建外部事件函數。最后,STATE_DECLARE和STATE_DEFINE創建狀態函數。
電機實例
Motor 實現我們假設的電機控制狀態機,其中客戶端可以啟動電機,以特定的速度,并停止電機。這個Motor標題接口如下所示:
- #include "StateMachine.h"
- // Motor object structure
- typedef struct
- {
- INT currentSpeed;
- } Motor;
- // Event data structure
- typedef struct
- {
- INT speed;
- } MotorData;
- // State machine event functions
- EVENT_DECLARE(MTR_SetSpeed, MotorData)
- EVENT_DECLARE(MTR_Halt, NoEventData)
這個Motor源文件使用宏通過隱藏所需的狀態機機器來簡化使用。
- // State enumeration order must match the order of state
- // method entries in the state map
- enum States
- {
- ST_IDLE,
- ST_STOP,
- ST_START,
- ST_CHANGE_SPEED,
- ST_MAX_STATES
- };
- // State machine state functions
- STATE_DECLARE(Idle, NoEventData)
- STATE_DECLARE(Stop, NoEventData)
- STATE_DECLARE(Start, MotorData)
- STATE_DECLARE(ChangeSpeed, MotorData)
- // State map to define state function order
- BEGIN_STATE_MAP(Motor)
- STATE_MAP_ENTRY(ST_Idle)
- STATE_MAP_ENTRY(ST_Stop)
- STATE_MAP_ENTRY(ST_Start)
- STATE_MAP_ENTRY(ST_ChangeSpeed)
- END_STATE_MAP(Motor)
- // Set motor speed external event
- EVENT_DEFINE(MTR_SetSpeed, MotorData)
- {
- // Given the SetSpeed event, transition to a new state based upon
- // the current state of the state machine
- BEGIN_TRANSITION_MAP // - Current State -
- TRANSITION_MAP_ENTRY(ST_START) // ST_Idle
- TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop
- TRANSITION_MAP_ENTRY(ST_CHANGE_SPEED) // ST_Start
- TRANSITION_MAP_ENTRY(ST_CHANGE_SPEED) // ST_ChangeSpeed
- END_TRANSITION_MAP(Motor, pEventData)
- }
- // Halt motor external event
- EVENT_DEFINE(MTR_Halt, NoEventData)
- {
- // Given the Halt event, transition to a new state based upon
- // the current state of the state machine
- BEGIN_TRANSITION_MAP // - Current State -
- TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_Idle
- TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop
- TRANSITION_MAP_ENTRY(ST_STOP) // ST_Start
- TRANSITION_MAP_ENTRY(ST_STOP) // ST_ChangeSpeed
- END_TRANSITION_MAP(Motor, pEventData)
- }
外部事件
MTR_SetSpeed 和MTR_Halt類中的外部事件。Motor狀態機。MTR_SetSpeed 獲取指向MotorData事件數據,包含電機速度。此數據結構將使用SM_XFree()在狀態處理完成后,必須使用SM_XAlloc()函數調用之前。
州數
每個狀態函數都必須有一個與其關聯的枚舉。這些枚舉用于存儲狀態機的當前狀態。在……里面Motor, States提供這些枚舉,這些枚舉稍后用于對轉換映射和狀態映射查找表進行索引。
狀態函數
狀態函數實現每個狀態--每個狀態機狀態一個狀態函數。STATE_DECLARE 用于聲明狀態函數接口和STATE_DEFINE 定義實現。
- // State machine sits here when motor is not running
- STATE_DEFINE(Idle, NoEventData)
- {
- printf("%s ST_Idlen", self->name);
- }
- // Stop the motor
- STATE_DEFINE(Stop, NoEventData)
- {
- // Get pointer to the instance data and update currentSpeed
- Motor* pInstance = SM_GetInstance(Motor);
- pInstance->currentSpeed = 0;
- // Perform the stop motor processing here
- printf("%s ST_Stop: %dn", self->name, pInstance->currentSpeed);
- // Transition to ST_Idle via an internal event
- SM_InternalEvent(ST_IDLE, NULL);
- }
- // Start the motor going
- STATE_DEFINE(Start, MotorData)
- {
- ASSERT_TRUE(pEventData);
- // Get pointer to the instance data and update currentSpeed
- Motor* pInstance = SM_GetInstance(Motor);
- pInstance->currentSpeed = pEventData->speed;
- // Set initial motor speed processing here
- printf("%s ST_Start: %dn", self->name, pInstance->currentSpeed);
- }
- // Changes the motor speed once the motor is moving
- STATE_DEFINE(ChangeSpeed, MotorData)
- {
- ASSERT_TRUE(pEventData);
- // Get pointer to the instance data and update currentSpeed
- Motor* pInstance = SM_GetInstance(Motor);
- pInstance->currentSpeed = pEventData->speed;
- // Perform the change motor speed here
- printf("%s ST_ChangeSpeed: %dn", self->name, pInstance->currentSpeed);
- }
STATE_DECLARE和STATE_DEFINE用兩個參數。第一個參數是狀態函數名。第二個參數是事件數據類型。如果不需要事件數據,請使用NoEventData。宏也可用于創建保護、退出和入口操作,本文稍后將對這些操作進行解釋。
這個SM_GetInstance()宏獲取狀態機對象的實例。宏的參數是狀態機名。
在此實現中,所有狀態機函數都必須遵守這些簽名,如下所示:
- // Generic state function signatures
- typedef void (*SM_StateFunc)(SM_StateMachine* self, void* pEventData);
- typedef BOOL (*SM_GuardFunc)(SM_StateMachine* self, void* pEventData);
- typedef void (*SM_EntryFunc)(SM_StateMachine* self, void* pEventData);
- typedef void (*SM_ExitFunc)(SM_StateMachine* self);
各SM_StateFunc 接受指向SM_StateMachine對象和事件數據。如果NoEventData 被使用時,pEventData爭論將是NULL。否則,pEventData參數的類型為STATE_DEFINE.
在……里面Motor氏Start狀態函數STATE_DEFINE(Start, MotorData) 宏擴展到:
- void ST_Start(SM_StateMachine* self, MotorData* pEventData)
注意,每個狀態函數都有self 和pEventData 爭論。self 是指向狀態機對象的指針,并且pEventData 事件數據。還請注意,宏以“ST_“用于創建函數的狀態名稱。ST_Start().
類似地,Stop 狀態函數STATE_DEFINE(Stop, NoEventData)IS擴展到:
- void ST_Stop(SM_StateMachine* self, void* pEventData)
Stop 不接受事件數據,因此pEventData 論點是void*.
每個狀態/保護/入口/退出函數在宏中自動添加三個字符。例如,如果使用STATE_DEFINE(Idle, NoEventData)實際的狀態函數名被調用。ST_Idle().
- ST_-狀態函數前置字符
- GD_-保護功能前置字符
- EN_-入口函數前面的字符
- EX_-退出函數前置字符
SM_GuardFunc 和SM_Entry 功能typedef也接受事件數據。SM_ExitFunc 是唯一的,因為不允許任何事件數據。
狀態圖
狀態機引擎通過使用狀態映射知道要調用哪個狀態函數.狀態圖映射currentState變量設置為特定的狀態函數。例如,如果currentState 是2,則調用第三個狀態映射函數指針項(從零計數)。狀態映射表是使用以下三個宏創建的:
- BEGIN_STATE_MAP
- STATE_MAP_ENTRY
- END_STATE_MAP
BEGIN_STATE_MAP 啟動狀態映射序列。各STATE_MAP_ENTRY 有一個狀態函數名稱參數。END_STATE_MAP終止地圖。國家地圖Motor 如下所示:
- BEGIN_STATE_MAP(Motor)
- STATE_MAP_ENTRY(ST_Idle)
- STATE_MAP_ENTRY(ST_Stop)
- STATE_MAP_ENTRY(ST_Start)
- STATE_MAP_ENTRY(ST_ChangeSpeed)
- END_STATE_MAP
或者,警衛/入口/出口特性需要利用_EX(擴展)宏的版本。
- BEGIN_STATE_MAP_EX
- STATE_MAP_ENTRY_EX or STATE_MAP_ENTRY_ALL_EX
- END_STATE_MAP_EX
這個STATE_MAP_ENTRY_ALL_EX 宏按照該順序為狀態操作、保護條件、入口操作和退出操作設置了四個參數。狀態操作是強制性的,但其他操作是可選的。如果狀態沒有動作,則使用0為了爭論。如果狀態沒有任何保護/進入/退出選項,則STATE_MAP_ENTRY_EX 宏將所有未使用的選項默認為0。下面的宏片段是本文后面介紹的一個高級示例。
- // State map to define state function order
- BEGIN_STATE_MAP_EX(CentrifugeTest)
- STATE_MAP_ENTRY_ALL_EX(ST_Idle, 0, EN_Idle, 0)
- STATE_MAP_ENTRY_EX(ST_Completed)
- STATE_MAP_ENTRY_EX(ST_Failed)
- STATE_MAP_ENTRY_ALL_EX(ST_StartTest, GD_StartTest, 0, 0)
- STATE_MAP_ENTRY_EX(ST_Acceleration)
- STATE_MAP_ENTRY_ALL_EX(ST_WaitForAcceleration, 0, 0, EX_WaitForAcceleration)
- STATE_MAP_ENTRY_EX(ST_Deceleration)
- STATE_MAP_ENTRY_ALL_EX(ST_WaitForDeceleration, 0, 0, EX_WaitForDeceleration)
- END_STATE_MAP_EX(CentrifugeTest)
不要忘記添加前面的字符(ST_, GD_, EN_或EX_)每項功能。
狀態機對象
在C++中,對象是語言的組成部分。使用C,您必須更加努力地完成類似的行為。此C語言狀態機支持多個狀態機對象(或多個實例),而不是具有單個靜態狀態機實現。
這個SM_StateMachine 數據結構存儲狀態機實例數據;每個狀態機實例存儲一個對象。這個SM_StateMachineConst 數據結構存儲常量數據;每個狀態機類型都有一個常量對象。
狀態機使用SM_DEFINE 宏。第一個參數是狀態機名稱。第二個參數是指向用戶定義的狀態機結構的指針,或NULL 如果沒有用戶對象。
- #define SM_DEFINE(_smName_, _instance_)
- SM_StateMachine _smName_##Obj = { #_smName_, _instance_,
- 0, 0, 0, 0 };
在本例中,狀態機名稱為Motor創建了兩個對象和兩個狀態機。
- // Define motor objects
- static Motor motorObj1;
- static Motor motorObj2;
- // Define two public Motor state machine instances
- SM_DEFINE(Motor1SM, &motorObj1)
- SM_DEFINE(Motor2SM, &motorObj2)
每個馬達對象獨立地處理狀態執行。這個Motor 結構用于存儲狀態機特定于實例的數據。在狀態函數中,使用SM_GetInstance()獲取指向Motor 對象在運行時初始化。
- // Get pointer to the instance data and update currentSpeed
- Motor* pInstance = SM_GetInstance(Motor);
- pInstance->currentSpeed = pEventData->speed;
過渡圖
要注意的最后一個細節是狀態轉換規則。狀態機如何知道應該發生什么轉換?答案是過渡圖。轉換映射是映射currentState 變量為狀態枚舉常量。每個外部事件函數都有一個用三個宏創建的轉換映射表:
- BEGIN_TRANSITION_MAP
- TRANSITION_MAP_ENTRY
- END_TRANSITION_MAP
這個MTR_Halt 事件函數Motor 將轉換映射定義為:
- // Halt motor external event
- EVENT_DEFINE(MTR_Halt, NoEventData)
- {
- // Given the Halt event, transition to a new state based upon
- // the current state of the state machine
- BEGIN_TRANSITION_MAP // - Current State -
- TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_Idle
- TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop
- TRANSITION_MAP_ENTRY(ST_STOP) // ST_Start
- TRANSITION_MAP_ENTRY(ST_STOP) // ST_ChangeSpeed
- END_TRANSITION_MAP(Motor, pEventData)
- }
BEGIN_TRANSITION_MAP開始地圖。各TRANSITION_MAP_ENTRY它指示狀態機根據當前狀態應該做什么。每個轉換映射表中的條目數必須與狀態函數的數目完全匹配。在我們的例子中,我們有四個狀態函數,所以我們需要四個轉換映射條目。每個條目的位置與州映射中定義的狀態函數的順序相匹配。因此,第一個條目在MTR_Halt函數表示EVENT_IGNORED 如下所示:
- TRANSITION_MAP_ENTRY (EVENT_IGNORED) // ST_Idle
這被解釋為“如果在當前狀態為狀態空閑時發生了暫停事件,只需忽略該事件”。
同樣,地圖上的第三個條目是:
- TRANSITION_MAP_ENTRY (ST_STOP) // ST_Start
這表示“如果在當前為狀態啟動時發生了暫停事件,則轉換為狀態停止”。
END_TRANSITION_MAP 終止地圖。此宏的第一個參數是狀態機名稱。第二個參數是事件數據。
這個C_ASSERT()宏在END_TRANSITION_MAP。如果狀態機狀態數與轉換映射項的數目不匹配,則生成編譯時錯誤。
新的狀態機步驟
創建一個新的狀態機需要一些基本的高級步驟:
- 創建一個States 每個狀態函數有一個條目的枚舉
- 定義狀態函數
- 定義事件函數
- 創建一個狀態映射查找表。STATE_MAP宏
- 為每個外部事件函數創建一個轉換映射查找表。TRANSITION_MAP 宏
狀態引擎
狀態引擎基于生成的事件執行狀態函數。轉換映射是SM_StateStruct類索引的實例。currentState 變量。當_SM_StateEngine()函數中查找正確的狀態函數。SM_StateStruct陣列。在狀態函數有機會執行之后,它會釋放事件數據(如果有的話),然后再檢查是否有任何內部事件是通過SM_InternalEvent().
- // The state engine executes the state machine states
- void _SM_StateEngine(SM_StateMachine* self, SM_StateMachineConst* selfConst)
- {
- void* pDataTemp = NULL;
- ASSERT_TRUE(self);
- ASSERT_TRUE(selfConst);
- // While events are being generated keep executing states
- while (self->eventGenerated)
- {
- // Error check that the new state is valid before proceeding
- ASSERT_TRUE(self->newState < selfConst->maxStates);
- // Get the pointers from the state map
- SM_StateFunc state = selfConst->stateMap[self->newState].pStateFunc;
- // Copy of event data pointer
- pDataTemp = self->pEventData;
- // Event data used up, reset the pointer
- self->pEventData = NULL;
- // Event used up, reset the flag
- self->eventGenerated = FALSE;
- // Switch to the new current state
- self->currentState = self->newState;
- // Execute the state action passing in event data
- ASSERT_TRUE(state != NULL);
- state(self, pDataTemp);
- // If event data was used, then delete it
- if (pDataTemp)
- {
- SM_XFree(pDataTemp);
- pDataTemp = NULL;
- }
- }
- }
用于保護、入口、狀態和退出操作的狀態引擎邏輯由以下順序表示。這個_SM_StateEngine()引擎只實現下面的#1和#5。擴展_SM_StateEngineEx()引擎使用整個邏輯序列。
評估狀態轉換表。如果EVENT_IGNORED,則忽略事件而不執行轉換。如果CANNOT_HAPPEN軟件故障。否則,繼續下一步。如果定義了保護條件,則執行保護條件函數。如果保護條件返回FALSE,則忽略狀態轉換而不調用狀態函數。如果衛兵回來TRUE,或者如果不存在保護條件,則執行狀態函數。如果為當前狀態定義了轉換到新狀態并定義了退出操作,則調用當前狀態退出操作函數。如果為新狀態定義了轉換到新狀態并定義了條目操作,則調用新的狀態條目操作函數。調用新狀態的狀態動作函數。新的狀態現在是當前的狀態。
生成事件
此時,我們有一個工作狀態機。讓我們看看如何為它生成事件。通過動態創建事件數據結構生成外部事件。SM_XAlloc(),分配結構成員變量,并使用SM_Event()宏。下面的代碼片段顯示了如何進行同步調用。
- MotorData* data;
- // Create event data
- data = SM_XAlloc(sizeof(MotorData));
- data->speed = 100;
- // Call MTR_SetSpeed event function to start motor
- SM_Event(Motor1SM, MTR_SetSpeed, data);
這個SM_Event()第一個參數是狀態機名稱。第二個參數是要調用的事件函數。第三個參數是事件數據,或者NULL 如果沒有數據。
若要從狀態函數內生成內部事件,請調用SM_InternalEvent()。如果目標不接受事件數據,那么最后一個參數是NULL。否則,使用SM_XAlloc().
- SM_InternalEvent(ST_IDLE, NULL);
在上面的示例中,狀態函數完成執行后,狀態機將轉換為ST_Idle狀態。另一方面,如果需要將事件數據發送到目標狀態,則需要在堆上創建數據結構并作為參數傳入。
- MotorData* data;
- data = SM_XAlloc(sizeof(MotorData));
- data->speed = 100;
- SM_InternalEvent(ST_CHANGE_SPEED, data);
不使用堆
必須動態創建所有狀態機事件數據。然而,在某些系統上,使用堆是不可取的。包括x_allocator模塊是一個固定的塊內存分配程序,它消除了堆的使用。定義USE_SM_ALLOCATOR內_StateMachine.c_若要使用固定塊分配器,請執行以下操作。見參考文獻下面一節x_allocator信息。
離心機測試實例
這個CentrifugeTest 示例演示如何使用保護、入口和退出操作創建擴展狀態機。
A CentrifgeTest 對象和狀態機被創建。這里唯一的區別是狀態機是一個單例,意味著對象是private只有一個例子CentrifugeTest 可以被創造出來。這與Motor 允許多個實例的狀態機。
- // CentrifugeTest object structure
- typedef struct
- {
- INT speed;
- BOOL pollActive;
- } CentrifugeTest;
- // Define private instance of motor state machine
- CentrifugeTest centrifugeTestObj;
- SM_DEFINE(CentrifugeTestSM, ¢rifugeTestObj)
擴展狀態機使用ENTRY_DECLARE, GUARD_DECLARE和EXIT_DECLARE 宏。
- // State enumeration order must match the order of state
- // method entries in the state map
- enum States
- {
- ST_IDLE,
- ST_COMPLETED,
- ST_FAILED,
- ST_START_TEST,
- ST_ACCELERATION,
- ST_WAIT_FOR_ACCELERATION,
- ST_DECELERATION,
- ST_WAIT_FOR_DECELERATION,
- ST_MAX_STATES
- };
- // State machine state functions
- STATE_DECLARE(Idle, NoEventData)
- ENTRY_DECLARE(Idle, NoEventData)
- STATE_DECLARE(Completed, NoEventData)
- STATE_DECLARE(Failed, NoEventData)
- STATE_DECLARE(StartTest, NoEventData)
- GUARD_DECLARE(StartTest, NoEventData)
- STATE_DECLARE(Acceleration, NoEventData)
- STATE_DECLARE(WaitForAcceleration, NoEventData)
- EXIT_DECLARE(WaitForAcceleration)
- STATE_DECLARE(Deceleration, NoEventData)
- STATE_DECLARE(WaitForDeceleration, NoEventData)
- EXIT_DECLARE(WaitForDeceleration)
- // State map to define state function order
- BEGIN_STATE_MAP_EX(CentrifugeTest)
- STATE_MAP_ENTRY_ALL_EX(ST_Idle, 0, EN_Idle, 0)
- STATE_MAP_ENTRY_EX(ST_Completed)
- STATE_MAP_ENTRY_EX(ST_Failed)
- STATE_MAP_ENTRY_ALL_EX(ST_StartTest, GD_StartTest, 0, 0)
- STATE_MAP_ENTRY_EX(ST_Acceleration)
- STATE_MAP_ENTRY_ALL_EX(ST_WaitForAcceleration, 0, 0, EX_WaitForAcceleration)
- STATE_MAP_ENTRY_EX(ST_Deceleration)
- STATE_MAP_ENTRY_ALL_EX(ST_WaitForDeceleration, 0, 0, EX_WaitForDeceleration)
- END_STATE_MAP_EX(CentrifugeTest)
注意_EX擴展狀態映射宏,從而支持保護/進入/退出功能。每個警衛/出入口DECLARE 宏必須與DEFINE。例如,StartTest 國家職能聲明為:
- GUARD_DECLARE(StartTest, NoEventData)
保護條件函數返回TRUE 如果要執行狀態函數或FALSE 不然的話。
- // Guard condition to determine whether StartTest state is executed.
- GUARD_DEFINE(StartTest, NoEventData)
- {
- printf("%s GD_StartTestn", self->name);
- if (centrifugeTestObj.speed == 0)
- return TRUE; // Centrifuge stopped. OK to start test.
- else
- return FALSE; // Centrifuge spinning. Can't start test.
- }
多線程安全
若要防止狀態機正在執行過程中由另一個線程搶占,請將StateMachine 模塊可以在_SM_ExternalEvent()功能。在允許執行外部事件之前,可以鎖定信號量。在處理了外部事件和所有內部事件后,釋放了軟件鎖,允許另一個外部事件進入狀態機實例。
注釋指出,如果應用程序是多線程的,則應將鎖和解鎖放在何處。_和_多個線程能夠訪問單個狀態機實例。注意每個StateMachine 對象應該有自己的軟件鎖實例。這將防止單個實例鎖定并阻止所有其他實例。StateMachine對象執行。只有在下列情況下才需要軟件鎖:StateMachine 實例由多個控制線程調用。如果沒有,則不需要鎖。
結語
使用此方法實現狀態機,而不是舊方法switch語句風格似乎是額外的努力。然而,回報在于一個更健壯的設計,能夠在整個多線程系統上統一使用。讓每一種狀態都具有自己的功能,比單個巨大的狀態更容易讀取。switch語句,并允許向每個狀態發送唯一的事件數據。此外,通過消除不必要的狀態轉換所造成的副作用,驗證狀態轉換可以防止客戶端濫用。