上篇文章,使用嵌套switch-case法的狀態(tài)機(jī)編程,實現(xiàn)了一個炸彈拆除小游戲。
本篇,繼續(xù)介紹狀態(tài)機(jī)編程的第二種方法:狀態(tài)表法,來實現(xiàn)炸彈拆除小游戲的狀態(tài)機(jī)編程。
1 狀態(tài)表法
狀態(tài)表法,顧名思義,就是通過一個狀態(tài)表,來實現(xiàn)狀態(tài)機(jī)中的狀態(tài)轉(zhuǎn)換,下面就先介紹下狀態(tài)表的基礎(chǔ)知識。
1.1 狀態(tài)表
狀態(tài)表 ,最常用的是使用一個2維狀態(tài)表:
- 水平方向是各個事件
- 豎直方向是各個狀態(tài)
- 單元的內(nèi)容是通過(執(zhí)行動作,下一狀態(tài))來表示各種轉(zhuǎn)換關(guān)系
結(jié)合上一篇設(shè)計炸彈拆除小游戲的狀態(tài)圖(2個狀態(tài)和4個事件):
可以設(shè)計出對應(yīng)的狀態(tài)表,如下圖:
- 水平方向的4種事件:UP、DOWN和ARM按鍵事件,TICK事件
- 豎直方向的2種狀態(tài):設(shè)置狀態(tài)和倒計時狀態(tài)
- 單元的內(nèi)容表示執(zhí)行指定動作后,下一狀態(tài)是什么。比如設(shè)置狀態(tài)時按下UP鍵,執(zhí)行setting_UP函數(shù)中的動作后,下一狀態(tài)還是留在設(shè)置狀態(tài)
注意:
- (*):僅當(dāng)(me->code == me->defuse),即密碼輸入正確時,才進(jìn)行狀態(tài)轉(zhuǎn)換至“設(shè)置狀態(tài)”
- ( ):僅當(dāng)(me->fine_time == 0)和(me->timeout != 0),即每過一秒且倒計時未減到0時,才進(jìn)行狀態(tài)轉(zhuǎn)換至“倒計時狀態(tài)”**
1.2 事件處理器
由于狀態(tài)表法可以使用一個非常有規(guī)律的數(shù)據(jù)結(jié)構(gòu)(狀態(tài)表)來表現(xiàn)一個狀態(tài)機(jī),因此編程時可以編寫一個通用的“事件處理器”來實現(xiàn)狀態(tài)機(jī)功能。
如下圖,通用的狀態(tài)表事件處理器,包含兩個主要結(jié)構(gòu):
- 一個外部轉(zhuǎn)換的StateTable結(jié)構(gòu)
- 一個帶有事件參數(shù)和沒有事件參數(shù)的Event結(jié)構(gòu)
此外,StateTable結(jié)構(gòu)有兩個相關(guān)的函數(shù):
- init()函數(shù)用于觸發(fā)狀態(tài)機(jī)的初始轉(zhuǎn)換
- dispatch()函數(shù)用于派送一個事件給狀態(tài)機(jī)處理
需體會的是,StateTable結(jié)構(gòu)是一個抽象的結(jié)構(gòu),按照UML類圖的畫法,這是一個抽象類(使用《abstract》或斜體類名表示),需要通過派生出一個實例類,如圖中的Bomb2,來實現(xiàn)具體的業(yè)務(wù)功能。
在狀態(tài)機(jī)的應(yīng)用程序中,狀態(tài)表僅包含執(zhí)行轉(zhuǎn)換函數(shù)的指針,即函數(shù)指針,而不是(執(zhí)行動作,下一狀態(tài))的形式,使用這種方式,實際就是把狀態(tài)改變的邏輯,放到了轉(zhuǎn)換函數(shù)中,這樣做,使得編程更加靈活,因為狀態(tài)函數(shù)能方便地判斷某些監(jiān)護(hù)條件并隨之改變。
2 狀態(tài)表法的實現(xiàn)
上面介紹了狀態(tài)表法的基礎(chǔ)知識,下面就來通過代碼來介紹狀態(tài)表法的具體實現(xiàn)。
2.1 通用狀態(tài)表事件處理器
上面說到,狀態(tài)表法可以使用一個非常有規(guī)律的狀態(tài)表數(shù)據(jù)結(jié)構(gòu)來表現(xiàn)一個狀態(tài)機(jī),因而在程序設(shè)計時,可以編寫一個通用的狀態(tài)表事件處理器。
2.1.1 接口定義
通用的狀態(tài)表事件處理器,先來通過接口定義,看下它的功能。
注意上面提到的它包含兩個主要結(jié)構(gòu):
- 一個外部轉(zhuǎn)換的StateTable結(jié)構(gòu)
- 一個帶有事件參數(shù)和沒有事件參數(shù)的Event結(jié)構(gòu)
以及StateTable結(jié)構(gòu)的兩個相關(guān)的函數(shù):
- init()函數(shù):用于觸發(fā)狀態(tài)機(jī)的初始轉(zhuǎn)換
- dispatch()函數(shù):用于派送一個事件給狀態(tài)機(jī)處理
// 用于進(jìn)行狀態(tài)轉(zhuǎn)換的宏
#define TRAN(target) (((StateTable *)me)- >state = (uint8_t)(target))
?
typedef struct EventTag
{
uint16_t sig; // 事件的信號
} Event;
?
struct StateTableTag; //提前聲明此變量
?
// 函數(shù)指針
typedef void (*Tran)(struct StateTableTag *me, Event const *e);
?
// 狀態(tài)表數(shù)據(jù)結(jié)構(gòu)
typedef struct StateTableTag
{
uint8_t state; //當(dāng)前狀態(tài)
Tran const *state_table; //狀態(tài)表
uint8_t n_states; //狀態(tài)的個數(shù)
uint8_t n_signals; //事件(信號)的個數(shù)
Tran initial; //初始轉(zhuǎn)換
} StateTable;
?
void StateTable_ctor(StateTable *me, Tran const *table, uint8_t n_states, uint8_t n_signals, Tran initial);
void StateTable_init(StateTable *me);
void StateTable_dispatch(StateTable *me, Event const *e);
void StateTable_empty(StateTable *me, Event const *e);
StateTable_ctor是狀態(tài)表的“構(gòu)造函數(shù)”,僅指向一個基本的初始化動作,不會觸發(fā)初始轉(zhuǎn)換。
StateTable_empty是一個默認(rèn)的空動作,用于狀態(tài)表初始化時,某些需要空單元的地方使用。
另外,這里還要體會函數(shù)指針的用法。什么是函數(shù)指針,下面再來復(fù)習(xí)一下。
2.1.2 體會函數(shù)指針的用法
函數(shù)指針,本質(zhì)是一個指針,其指向的一個函數(shù),其類型定義為:
返回值類型 (* 函數(shù)名) ([形參列表]);
注意和指針函數(shù)的區(qū)別:
何為指針函數(shù)?
*指針函數(shù),本質(zhì)是一個函數(shù),例如 int pfun(int, int); 其返回值是指針類型,即返回一個指針(或稱地址),這個指針指向的數(shù)據(jù)是什么類型都可以。
一個記憶小技巧:指針函數(shù),可以類比int函數(shù),它們都是函數(shù),只是返回值不一樣,一個是返回指針,一個返回int。
首先來看函數(shù)指針的定義,以及基礎(chǔ)用法:
//定義一個函數(shù)指針pFUN,它指向一個返回類型為void,有一個參數(shù)類型為int的函數(shù)
void (*pFun)(int);
?
//定義一個返回類型為void,參數(shù)為int的函數(shù)。從指針層面上理解該函數(shù),其函數(shù)名實際上是一個指針,該指針指向函數(shù)在內(nèi)存中的首地址
void glFun(int a)
{
printf("%d
", a);
}
?
int main()
{
pFun = glFun; //將函數(shù)glFun的地址賦值給變量pFun
(*pFun)(2);//“*pFun”是取pFun所指向地址的內(nèi)容,即取出了函數(shù)glFun()的內(nèi)容,然后給定參數(shù)為2
return 0;
}
實際使用時,常常通過typedef的方式讓函數(shù)指針更直觀方便的進(jìn)行使用:
//定義新的類型PTRFUN, 此類型的實際含義為函數(shù)指針,指向的函數(shù)的返回值是void,參數(shù)是int
typedef void (*PTRFUN)(int);
?
//定義一個返回類型為void,參數(shù)為int的函數(shù)
void glFun(int a)
{
printf("%d
", a);
}
?
int main()
{
PTRFUN pFun; //使用定義的(函數(shù)指針)類型,實例化一個函數(shù)指針
pFun = glFun; //把定義的glFun函數(shù),以函數(shù)名(本質(zhì)即指針)的形式為其賦值
(*pFun)(2); //執(zhí)行該函數(shù)指針指向的內(nèi)容,即指向指向的函數(shù),并指定參數(shù)2
return 0;
}
關(guān)于函數(shù)指針的實際應(yīng)用,也可參考我之前的這篇文章: STM32簡易多級菜單(數(shù)組查表法)
2.1.3 具體實現(xiàn)
看完了通用的狀態(tài)表事件處理器的接口定義,下面再來看下具體實現(xiàn)。
//狀態(tài)表的構(gòu)造
void StateTable_ctor(StateTable *me,
Tran const *table, uint8_t n_states, uint8_t n_signals,
Tran initial)
{
//第一個參數(shù)me為StateTable結(jié)構(gòu),由具體業(yè)務(wù)的派生狀態(tài)表的tateTable結(jié)構(gòu)傳入
me- >state_table = table; //狀態(tài)表, 由具體業(yè)務(wù)的二維狀態(tài)表傳入
me- >n_states = n_states; //二維狀態(tài)表的狀態(tài)數(shù)量
me- >n_signals = n_signals; //二維狀態(tài)表的信號(事件)數(shù)量
me- >initial = initial; //狀態(tài)表的初始準(zhǔn)換函數(shù)
}
?
//狀態(tài)表的初始化
void StateTable_init(StateTable *me)
{
me- >state = me- >n_states;
(*me- >initial)(me, (Event *)0); //初始轉(zhuǎn)換
?
assert(me- >state < me- >n_states); //確保事件范圍的合理
}
?
//狀態(tài)表的調(diào)度(派送一個事件給狀態(tài)機(jī)處理)
void StateTable_dispatch(StateTable *me, Event const *e)
{
Tran t;
?
assert(e- >sig < me- >n_signals); //確保信號范圍的合理
?
//通過當(dāng)前狀態(tài)與當(dāng)前的信號,以及信號的總數(shù),計算得到狀態(tài)表中要執(zhí)行的轉(zhuǎn)換函數(shù)在狀態(tài)表(二維的函數(shù)指針數(shù)組)中的位置
t = me- >state_table[me- >state * me- >n_signals + e- >sig];
(*t)(me, e); //然后執(zhí)行轉(zhuǎn)換函數(shù)
?
assert(me- >state < me- >n_states); //確保狀態(tài)范圍的合理
}
?
//狀態(tài)表的空元素
void StateTable_empty(StateTable *me, Event const *e)
{
(void)me; //用于消除參數(shù)未使用的警告
(void)e;
}
這里要體會一下狀態(tài)表的調(diào)度,即派送一個事件給狀態(tài)機(jī)處理的代碼邏輯,StateTable_dispatch的兩個參數(shù),一個是StateTable結(jié)構(gòu)的二維表,一個是Event結(jié)構(gòu)的信號(事件),注意這個二維狀態(tài)表,存儲的函數(shù)指針(各種轉(zhuǎn)換函數(shù)),所以是一個二維的函數(shù)指針數(shù)組,根據(jù)信號,如何知道要執(zhí)行二維數(shù)組中的哪個函數(shù)呢?還要借助當(dāng)前狀態(tài)機(jī)所處的狀態(tài),即可通過簡單的數(shù)學(xué)運算得出,示意如下圖:
2.2 應(yīng)用邏輯(具體業(yè)務(wù)代碼)
看完了通用的狀態(tài)表事件處理器,就可以在此基礎(chǔ)上,編寫具體的狀態(tài)機(jī)業(yè)務(wù)代碼,實現(xiàn)上一篇介紹的炸彈拆除小游戲。
2.2.1 接口定義
還是先看下炸彈拆除小游戲這個具體業(yè)務(wù)邏輯用到的數(shù)據(jù)結(jié)構(gòu)與接口定義,主要包括:
- 炸彈狀態(tài)機(jī)的狀態(tài)與信號(事件)
- 從狀態(tài)表事件處理器的Event結(jié)構(gòu)派生的帶有事件參數(shù)的TickEvt結(jié)構(gòu)
- 從狀態(tài)表事件處理器的StateTable結(jié)構(gòu)派生的具體的炸彈狀態(tài)機(jī)數(shù)據(jù)結(jié)構(gòu)
- 狀態(tài)表中用到的所有的轉(zhuǎn)換函數(shù)
// 炸彈狀態(tài)機(jī)的所有狀態(tài)
enum BombStates
{
SETTING_STATE, // 設(shè)置狀態(tài)
TIMING_STATE, // 倒計時狀態(tài)
STATE_MAX
};
?
// 炸彈狀態(tài)機(jī)的所有信號(事件)
enum BombSignals
{
UP_SIG, // UP鍵信號
DOWN_SIG, // DOWN鍵信號
ARM_SIG, // ARM鍵信號
TICK_SIG, // Tick節(jié)拍信號
SIG_MAX
};
?
typedef struct TickEvtTag
{
Event super; // 派生自Event結(jié)構(gòu)
uint8_t fine_time; // 精細(xì)的1/10秒計數(shù)器
} TickEvt;
?
// 炸彈狀態(tài)機(jī)數(shù)據(jù)結(jié)構(gòu)
typedef struct Bomb2Tag
{
StateTable super; // 派生自StateTable結(jié)構(gòu)
uint8_t timeout; // 爆炸前的秒數(shù)
uint8_t code; // 當(dāng)前輸入的解除炸彈的密碼
uint8_t defuse; // 解除炸彈的拆除密碼
uint8_t errcnt; // 當(dāng)前拆除失敗的次數(shù)
} Bomb2;
?
//炸彈構(gòu)造
void Bomb2_ctor(Bomb2 *me, uint8_t defuse);
//狀態(tài)表中需要用到的轉(zhuǎn)換函數(shù)(函數(shù)指針)
void Bomb2_initial(Bomb2 *me, Event const *e); //初始轉(zhuǎn)換
void Bomb2_setting_UP(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 設(shè)置狀態(tài)時, 處理UP事件
void Bomb2_setting_DOWN(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 設(shè)置狀態(tài)時, 處理DOWN事件
void Bomb2_setting_ARM(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 設(shè)置狀態(tài)時, 處理ARM事件
void Bomb2_timing_UP(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 倒計時狀態(tài)時, 處理UP事件
void Bomb2_timing_DOWN(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 倒計時狀態(tài)時, 處理DOWN事件
void Bomb2_timing_ARM(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 倒計時狀態(tài)時, 處理ARM事件
void Bomb2_timing_TICK(Bomb2 *me, Event const *e); //轉(zhuǎn)換函數(shù), 倒計時狀態(tài)時, 處理Tick事件