嵌入式軟件開發中,狀態機編程是一個比較實用的代碼實現方式,特別適用于事件驅動的系統。
本篇,以一個炸彈拆除的小游戲為例,介紹狀態機編程的思路。
C/C++語言實現狀態機編程的方式有很多,本篇先來介紹最簡單最容易理解的switch-case方法。
1 狀態機實例介紹
1.1 炸彈拆除游戲
如下是一個自制的炸彈拆除小游戲的硬件實物,由3個按鍵:
- UP鍵:用于游戲開始前設置增加倒計時時間;用于游戲開始后,輸入拆除密碼“1”
- DOWN鍵:用于游戲開始前設置減小倒計時時間;用于游戲開始后,輸入拆除密碼“0”
- ARM鍵:用于從設置時間切換到開始游戲;用于輸入拆除密碼后,確認拆除
還有一個屏幕,用于顯示倒計時時間,輸入的拆除密碼等
游戲的玩法:
- 游戲開始前,通過UP或DOWN鍵,設置炸彈拆除的倒計時時間;也可以不設置,使用默認的時間
- 按下ARM鍵,進入倒計時狀態;此時再通過UP或DOWN鍵,UP代表1,DOWN代表0,輸入拆除密碼(正確的密碼在程序中設定了,不可修改,如默認是二進制的1101)
- 再按下ARM鍵,確認拆除;若密碼正確,則拆除成功;若密碼錯誤,可以再次嘗試輸入密碼
- 在倒計時狀態,若倒計時到0時,還沒有拆除成功,則顯示拆除失敗
- 拆除成功或失敗后,會再次回到初始狀態,可重新開始玩
1.2 狀態圖
使用狀態機思路進行編程,首先要畫出對應的UML狀態圖,在畫圖之前,需要先明確此狀態機有哪些****狀態 ,以及哪些 事件 。
對于本篇介紹的炸彈拆除小游戲,可以歸納為兩個狀態:
- 設置狀態(SETTING_STATE):游戲開始前,通過UP和DOWN鍵設置此次游戲的超時時間;通過ARM鍵開始游戲
- 倒計時狀態 (TIMING_STATE):游戲開始后,通過UP和DOWN鍵輸入密碼,UP代表1,DOWN代表0;通過ARM鍵確認拆除
對于事件(或稱信號),有3個按鍵事件,還有一個Tick節拍事件:
- UP鍵信號(UP_SIG):游戲開始前設置增加倒計時時間;游戲開始后,輸入拆除密碼“1”
- DOWN鍵信號(DOWN_SIG):游戲開始前設置減小倒計時時間;游戲開始后,輸入拆除密碼“0”
- ARM鍵信號(ARM_SIG):從設置時間切換到開始游戲;輸入拆除密碼后,確認拆除
- Tick節拍信號(TICK_SIG):用于倒計時的時間遞減
相關的結構定義如下
// 炸彈狀態機的所有狀態
enum BombStates
{
SETTING_STATE, // 設置狀態
TIMING_STATE // 倒計時狀態
};
?
// 炸彈狀態機的所有信號(事件)
enum BombSignals
{
UP_SIG, // UP鍵信號
DOWN_SIG, // DOWN鍵信號
ARM_SIG, // ARM鍵信號
TICK_SIG, // Tick節拍信號
SIG_MAX
};
為了便于維護狀態機所需要用到一些變量,可以將其定義為一個數據結構體,如下:
// 超時的初始值
#define INIT_TIMEOUT 10
?
// 炸彈狀態機數據結構
typedef struct Bomb1Tag
{
uint8_t state; // 標量狀態變量
uint8_t timeout; // 爆炸前的秒數
uint8_t code; // 當前輸入的解除炸彈的密碼
uint8_t defuse; // 解除炸彈的拆除密碼
uint8_t errcnt; // 當前拆除失敗的次數
} Bomb1;
數據結構定義好之后,可以設計UML狀態圖了,關于UML狀態圖的畫法與介紹,可參考之前的文章:http://www.1cnz.cn/d/2076524.html,這里使用visio畫圖。
分析這個狀態圖:
- 初始默認進行“設置狀態”
- 進入“設置狀態”后,會先執行****entry的初始化處理:設置默認的超時時間,用戶的輸入錯誤次數清零
- 處于“ 設置狀態 ”時:
- 通過****UP和DOWN鍵設置此次游戲的超時時間,并在屏幕上顯示設置的時間,這里有最大最小時間的限制(1~60s)
- 通過****ARM鍵開始游戲,并清除用戶的拆除密碼
- 處于“ 倒計時狀態 ”時:
- 通過****UP和DOWN鍵輸入密碼,UP代表1,DOWN代表0,并在屏幕上顯示輸入的密碼
- 通過****ARM鍵確認拆除,若密碼正常,屏幕顯示拆除成功,并進入到“設置狀態”;若密碼不正確,則清除輸入的密碼,并顯示已失敗的次數
- Tick節拍事件(每1/10s一次,即100ms)到來,當精細的時間(fine_time)為0時,說明過去了1s,則倒計時時間減1,屏幕顯示當時的倒計時時間;若倒計時為0,則顯示拆除失敗,并進入到“設置狀態”
1.3 事件表示
對于上述的狀態機事件,可以分為兩類,一類是按鍵事件:UP、DOWN和ARM,一類是Tick。對于第一類事件,指需要單一的事件變量即可區分,對于第二類的Tick,由于引入了1/10s的精細時間,所以這個時間還需要一個額外的****事件參數表示此次Tick事件的精細時間(fine_time)。
這里再介紹一個編程技巧,通過結構體的繼承關系(實際就是嵌套),實現對事件數據結構的設計,如下圖:
**子圖(a)**表示TickEvt與Event是繼承關系,這是UML類圖的畫法,關于UML類圖的介紹可參考之前的文章:http://www.1cnz.cn/d/2072902.html。
**子圖(b)**是這兩個結構體的定義,可以看到TickEvt結構體內部的第1個成員,就是Event結構體,第2個成員,用于表示Tick事件的事件參數。
**子圖(c)**是TickEvt數據結構在內存中的存儲示意,先存儲的是基類結構體的super實例,也就是Event這個結構體,然后存儲的是子類結構的自定義成員,也就是Tick事件的事件參數fine_time。
這兩個結構體的定義如下:
typedef struct EventTag
{
uint16_t sig; // 事件的信號
} Event;
?
typedef struct TickEvtTag
{
Event super; // 派生自Event結構
uint8_t fine_time; // 精細的1/10秒計數器
} TickEvt;
**這樣定義的好處是,對于狀態機事件調度函數Bomb1_dispatch的參數形式,可以統一使用(Event *)類型,將TickEvt類型傳入時,可以取其地址,再轉為(Event *)類型,如下面實例代碼中loop函數中的使用;而在Bomb1_dispatch函數內部需要處理TICK_SIG事件時,又可以再將(Event )類型強制轉為(TickEvt )類型,如下面實例代碼中Bomb1_dispatch函數中的使用。
//狀態機事件調度
void Bomb1_dispatch(Bomb1 *me, Event const *e)
{
//省略...
case TICK_SIG: //Tick信號
{
if (((TickEvt const *)e)- >fine_time == 0)
{
--me- >timeout;
bsp_display_remain_time(me- >timeout); //顯示倒計時時間
if (me- >timeout == 0)
{
bsp_display_bomb(); //顯示爆炸效果
Bomb1_init(me);
}
}
break;
}
//省略...
}
?
//狀態機循環
void loop(void)
{
static TickEvt tick_evt = {TICK_SIG, 0};
delay(100); /*狀態機以100ms的循環運行*/
?
if (++tick_evt.fine_time == 10)
{
tick_evt.fine_time = 0;
}
?
Bomb1_dispatch(&l_bomb, (Event *)&tick_evt); /*調度處理tick事件*/
//省略...
}
2 switch-case嵌套法
狀態圖設計好之后,就可以對照著狀態圖,進行編程實現了。
本篇先使用最簡單最容易理解的switch-case方法,來實現狀態機編程。
2.1 狀態機處理
使用switch-case法實現狀態機,一般需要兩層switch結構。
2.1.1 第一層switch處理狀態
void Bomb1_dispatch(Bomb1 *me, Event const *e)
{
//第一層switch處理狀態
switch (me- >state)
{
//設置狀態
case SETTING_STATE:
{
//...
break;
}
//倒計時狀態
case TIMING_STATE:
{
//...
break;
}
}
}
2.1.2 第二層switch處理事件
這里以狀態機處于“設置狀態”時,對事件(信號)的處理為例
//設置狀態
case SETTING_STATE:
{
//第二層switch處理事件(信號)
switch (e- >sig)
{
//UP按鍵信號
case UP_SIG:
{
//...
break;
}
//DOWN按鍵信號
case DOWN_SIG:
{
//...
break;
}
//ARM按鍵信號
case ARM_SIG:
{
//...
break;
}
}
break;
}
2.1.3 兩層switch-case狀態機完整代碼
// 用于進行狀態轉換的宏
#define TRAN(target_) (me- >state = (uint8_t)(target_))
?
//狀態機事件調度
void Bomb1_dispatch(Bomb1 *me, Event const *e)
{
//第一層switch處理狀態
switch (me- >state)
{
//設置狀態
case SETTING_STATE:
{
//第二層switch處理事件(信號)
switch (e- >sig)
{
//UP按鍵信號
case UP_SIG:
{
if (me- >timeout < 60)
{
++me- >timeout; //設置超時時間+1
bsp_display_set_time(me- >timeout); //顯示設置的超時時間
}
break;
}
//DOWN按鍵信號
case DOWN_SIG:
{
if (me- >timeout > 1)
{
--me- >timeout; //設置超時時間-1
bsp_display_set_time(me- >timeout); //顯示設置的超時時間
}
break;
}
//ARM按鍵信號
case ARM_SIG:
{
me- >code = 0;
TRAN(TIMING_STATE); //轉換到倒計時狀態
break;
}
}
break;
}
//倒計時狀態
case TIMING_STATE:
{
switch (e- >sig)
{
case UP_SIG: //UP按鍵信號
{
me- >code < <= 1;
me- >code |= 1; //添加一個1
bsp_display_user_code(me- >code);
break;
}
case DOWN_SIG: //DWON按鍵信號
{
me- >code < <= 1; //添加一個0
bsp_display_user_code(me- >code);
break;
}
case ARM_SIG: //ARM按鍵信號
{
if (me- >code == me- >defuse)
{
TRAN(SETTING_STATE); //轉換到設置狀態
bsp_display_user_success(); //炸彈拆除成功
Bomb1_init(me);
}
else
{
me- >code = 0;
bsp_display_user_code(me- >code);
bsp_display_user_err(++me- >errcnt);
}
break;
}
case TICK_SIG: //Tick信號
{
if (((TickEvt const *)e)- >fine_time == 0)
{
--me- >timeout;
bsp_display_remain_time(me- >timeout); //顯示倒計時時間
if (me- >timeout == 0)
{
bsp_display_bomb(); //顯示爆炸效果
Bomb1_init(me);
}
}
break;
}
}
break;
}
}
}