嵌入式軟件開(kāi)發(fā)中,狀態(tài)機(jī)編程是一個(gè)比較實(shí)用的代碼實(shí)現(xiàn)方式,特別適用于事件驅(qū)動(dòng)的系統(tǒng)。
(資料圖片)
本篇,以一個(gè)炸彈拆除的小游戲?yàn)槔?,介紹狀態(tài)機(jī)編程的思路。
C/C++語(yǔ)言實(shí)現(xiàn)狀態(tài)機(jī)編程的方式有很多,本篇先來(lái)介紹最簡(jiǎn)單最容易理解的switch-case方法。
1 狀態(tài)機(jī)實(shí)例介紹
1.1 炸彈拆除游戲
如下是一個(gè)自制的炸彈拆除小游戲的硬件實(shí)物,由3個(gè)按鍵:
UP鍵:用于游戲開(kāi)始前設(shè)置增加倒計(jì)時(shí)時(shí)間;用于游戲開(kāi)始后,輸入拆除密碼“1”DOWN鍵:用于游戲開(kāi)始前設(shè)置減小倒計(jì)時(shí)時(shí)間;用于游戲開(kāi)始后,輸入拆除密碼“0”ARM鍵:用于從設(shè)置時(shí)間切換到開(kāi)始游戲;用于輸入拆除密碼后,確認(rèn)拆除還有一個(gè)屏幕,用于顯示倒計(jì)時(shí)時(shí)間,輸入的拆除密碼等
游戲的玩法:
游戲開(kāi)始前,通過(guò)UP或DOWN鍵,設(shè)置炸彈拆除的倒計(jì)時(shí)時(shí)間;也可以不設(shè)置,使用默認(rèn)的時(shí)間按下ARM鍵,進(jìn)入倒計(jì)時(shí)狀態(tài);此時(shí)再通過(guò)UP或DOWN鍵,UP代表1,DOWN代表0,輸入拆除密碼(正確的密碼在程序中設(shè)定了,不可修改,如默認(rèn)是二進(jìn)制的1101)再按下ARM鍵,確認(rèn)拆除;若密碼正確,則拆除成功;若密碼錯(cuò)誤,可以再次嘗試輸入密碼在倒計(jì)時(shí)狀態(tài),若倒計(jì)時(shí)到0時(shí),還沒(méi)有拆除成功,則顯示拆除失敗拆除成功或失敗后,會(huì)再次回到初始狀態(tài),可重新開(kāi)始玩1.2 狀態(tài)圖
使用狀態(tài)機(jī)思路進(jìn)行編程,首先要畫(huà)出對(duì)應(yīng)的UML狀態(tài)圖,在畫(huà)圖之前,需要先明確此狀態(tài)機(jī)有哪些****狀態(tài),以及哪些 事件。
對(duì)于本篇介紹的炸彈拆除小游戲,可以歸納為兩個(gè)狀態(tài):
設(shè)置狀態(tài)(SETTING_STATE):游戲開(kāi)始前,通過(guò)UP和DOWN鍵設(shè)置此次游戲的超時(shí)時(shí)間;通過(guò)ARM鍵開(kāi)始游戲倒計(jì)時(shí)狀態(tài) (TIMING_STATE):游戲開(kāi)始后,通過(guò)UP和DOWN鍵輸入密碼,UP代表1,DOWN代表0;通過(guò)ARM鍵確認(rèn)拆除對(duì)于事件(或稱(chēng)信號(hào)),有3個(gè)按鍵事件,還有一個(gè)Tick節(jié)拍事件:
UP鍵信號(hào)(UP_SIG):游戲開(kāi)始前設(shè)置增加倒計(jì)時(shí)時(shí)間;游戲開(kāi)始后,輸入拆除密碼“1”DOWN鍵信號(hào)(DOWN_SIG):游戲開(kāi)始前設(shè)置減小倒計(jì)時(shí)時(shí)間;游戲開(kāi)始后,輸入拆除密碼“0”ARM鍵信號(hào)(ARM_SIG):從設(shè)置時(shí)間切換到開(kāi)始游戲;輸入拆除密碼后,確認(rèn)拆除Tick節(jié)拍信號(hào)(TICK_SIG):用于倒計(jì)時(shí)的時(shí)間遞減相關(guān)的結(jié)構(gòu)定義如下
// 炸彈狀態(tài)機(jī)的所有狀態(tài)enum BombStates{ SETTING_STATE, // 設(shè)置狀態(tài) TIMING_STATE // 倒計(jì)時(shí)狀態(tài)};?// 炸彈狀態(tài)機(jī)的所有信號(hào)(事件)enum BombSignals{ UP_SIG, // UP鍵信號(hào) DOWN_SIG, // DOWN鍵信號(hào) ARM_SIG, // ARM鍵信號(hào) TICK_SIG, // Tick節(jié)拍信號(hào) SIG_MAX};
為了便于維護(hù)狀態(tài)機(jī)所需要用到一些變量,可以將其定義為一個(gè)數(shù)據(jù)結(jié)構(gòu)體,如下:
// 超時(shí)的初始值#define INIT_TIMEOUT 10?// 炸彈狀態(tài)機(jī)數(shù)據(jù)結(jié)構(gòu)typedef struct Bomb1Tag{ uint8_t state; // 標(biāo)量狀態(tài)變量 uint8_t timeout; // 爆炸前的秒數(shù) uint8_t code; // 當(dāng)前輸入的解除炸彈的密碼 uint8_t defuse; // 解除炸彈的拆除密碼 uint8_t errcnt; // 當(dāng)前拆除失敗的次數(shù)} Bomb1;
數(shù)據(jù)結(jié)構(gòu)定義好之后,可以設(shè)計(jì)UML狀態(tài)圖了,關(guān)于UML狀態(tài)圖的畫(huà)法與介紹,可參考之前的文章:https://www.elecfans.com/d/2076524.html,這里使用visio畫(huà)圖。
分析這個(gè)狀態(tài)圖:
初始默認(rèn)進(jìn)行“設(shè)置狀態(tài)”進(jìn)入“設(shè)置狀態(tài)”后,會(huì)先執(zhí)行****entry的初始化處理:設(shè)置默認(rèn)的超時(shí)時(shí)間,用戶(hù)的輸入錯(cuò)誤次數(shù)清零處于“設(shè)置狀態(tài)”時(shí):通過(guò)****UP和DOWN鍵設(shè)置此次游戲的超時(shí)時(shí)間,并在屏幕上顯示設(shè)置的時(shí)間,這里有最大最小時(shí)間的限制(1~60s)通過(guò)****ARM鍵開(kāi)始游戲,并清除用戶(hù)的拆除密碼處于“倒計(jì)時(shí)狀態(tài)”時(shí):通過(guò)****UP和DOWN鍵輸入密碼,UP代表1,DOWN代表0,并在屏幕上顯示輸入的密碼通過(guò)****ARM鍵確認(rèn)拆除,若密碼正常,屏幕顯示拆除成功,并進(jìn)入到“設(shè)置狀態(tài)”;若密碼不正確,則清除輸入的密碼,并顯示已失敗的次數(shù)Tick節(jié)拍事件(每1/10s一次,即100ms)到來(lái),當(dāng)精細(xì)的時(shí)間(fine_time)為0時(shí),說(shuō)明過(guò)去了1s,則倒計(jì)時(shí)時(shí)間減1,屏幕顯示當(dāng)時(shí)的倒計(jì)時(shí)時(shí)間;若倒計(jì)時(shí)為0,則顯示拆除失敗,并進(jìn)入到“設(shè)置狀態(tài)”1.3 事件表示
對(duì)于上述的狀態(tài)機(jī)事件,可以分為兩類(lèi),一類(lèi)是按鍵事件:UP、DOWN和ARM,一類(lèi)是Tick。對(duì)于第一類(lèi)事件,指需要單一的事件變量即可區(qū)分,對(duì)于第二類(lèi)的Tick,由于引入了1/10s的精細(xì)時(shí)間,所以這個(gè)時(shí)間還需要一個(gè)額外的****事件參數(shù)表示此次Tick事件的精細(xì)時(shí)間(fine_time)。
這里再介紹一個(gè)編程技巧,通過(guò)結(jié)構(gòu)體的繼承關(guān)系(實(shí)際就是嵌套),實(shí)現(xiàn)對(duì)事件數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì),如下圖:
**子圖(a)**表示TickEvt與Event是繼承關(guān)系,這是UML類(lèi)圖的畫(huà)法,關(guān)于UML類(lèi)圖的介紹可參考之前的文章:https://www.elecfans.com/d/2072902.html。
**子圖(b)**是這兩個(gè)結(jié)構(gòu)體的定義,可以看到TickEvt結(jié)構(gòu)體內(nèi)部的第1個(gè)成員,就是Event結(jié)構(gòu)體,第2個(gè)成員,用于表示Tick事件的事件參數(shù)。
**子圖(c)**是TickEvt數(shù)據(jù)結(jié)構(gòu)在內(nèi)存中的存儲(chǔ)示意,先存儲(chǔ)的是基類(lèi)結(jié)構(gòu)體的super實(shí)例,也就是Event這個(gè)結(jié)構(gòu)體,然后存儲(chǔ)的是子類(lèi)結(jié)構(gòu)的自定義成員,也就是Tick事件的事件參數(shù)fine_time。
這兩個(gè)結(jié)構(gòu)體的定義如下:
typedef struct EventTag{ uint16_t sig; // 事件的信號(hào)} Event;?typedef struct TickEvtTag{ Event super; // 派生自Event結(jié)構(gòu) uint8_t fine_time; // 精細(xì)的1/10秒計(jì)數(shù)器} TickEvt;
**這樣定義的好處是,對(duì)于狀態(tài)機(jī)事件調(diào)度函數(shù)Bomb1_dispatch的參數(shù)形式,可以統(tǒng)一使用(Event *)類(lèi)型,將TickEvt類(lèi)型傳入時(shí),可以取其地址,再轉(zhuǎn)為(Event *)類(lèi)型,如下面實(shí)例代碼中l(wèi)oop函數(shù)中的使用;而在Bomb1_dispatch函數(shù)內(nèi)部需要處理TICK_SIG事件時(shí),又可以再將(Event )類(lèi)型強(qiáng)制轉(zhuǎn)為(TickEvt )類(lèi)型,如下面實(shí)例代碼中Bomb1_dispatch函數(shù)中的使用。
//狀態(tài)機(jī)事件調(diào)度void Bomb1_dispatch(Bomb1 *me, Event const *e){ //省略... case TICK_SIG: //Tick信號(hào) { if (((TickEvt const *)e)- >fine_time == 0) { --me- >timeout; bsp_display_remain_time(me- >timeout); //顯示倒計(jì)時(shí)時(shí)間 if (me- >timeout == 0) { bsp_display_bomb(); //顯示爆炸效果 Bomb1_init(me); } } break; } //省略...}?//狀態(tài)機(jī)循環(huán)void loop(void){ static TickEvt tick_evt = {TICK_SIG, 0}; delay(100); /*狀態(tài)機(jī)以100ms的循環(huán)運(yùn)行*/? if (++tick_evt.fine_time == 10) { tick_evt.fine_time = 0; }? Bomb1_dispatch(&l_bomb, (Event *)&tick_evt); /*調(diào)度處理tick事件*/ //省略...}
2 switch-case嵌套法
狀態(tài)圖設(shè)計(jì)好之后,就可以對(duì)照著狀態(tài)圖,進(jìn)行編程實(shí)現(xiàn)了。
本篇先使用最簡(jiǎn)單最容易理解的switch-case方法,來(lái)實(shí)現(xiàn)狀態(tài)機(jī)編程。
2.1 狀態(tài)機(jī)處理
使用switch-case法實(shí)現(xiàn)狀態(tài)機(jī),一般需要兩層switch結(jié)構(gòu)。
2.1.1 第一層switch處理狀態(tài)
void Bomb1_dispatch(Bomb1 *me, Event const *e){ //第一層switch處理狀態(tài) switch (me- >state) { //設(shè)置狀態(tài) case SETTING_STATE: { //... break; } //倒計(jì)時(shí)狀態(tài) case TIMING_STATE: {//... break; } }}
2.1.2 第二層switch處理事件
這里以狀態(tài)機(jī)處于“設(shè)置狀態(tài)”時(shí),對(duì)事件(信號(hào))的處理為例
//設(shè)置狀態(tài)case SETTING_STATE:{ //第二層switch處理事件(信號(hào)) switch (e- >sig) { //UP按鍵信號(hào) case UP_SIG: { //... break; } //DOWN按鍵信號(hào) case DOWN_SIG: { //... break; } //ARM按鍵信號(hào) case ARM_SIG: { //... break; } } break;}
2.1.3 兩層switch-case狀態(tài)機(jī)完整代碼
// 用于進(jìn)行狀態(tài)轉(zhuǎn)換的宏#define TRAN(target_) (me- >state = (uint8_t)(target_))?//狀態(tài)機(jī)事件調(diào)度void Bomb1_dispatch(Bomb1 *me, Event const *e){ //第一層switch處理狀態(tài) switch (me- >state) { //設(shè)置狀態(tài) case SETTING_STATE: { //第二層switch處理事件(信號(hào)) switch (e- >sig) { //UP按鍵信號(hào) case UP_SIG: { if (me- >timeout < 60) { ++me- >timeout; //設(shè)置超時(shí)時(shí)間+1 bsp_display_set_time(me- >timeout); //顯示設(shè)置的超時(shí)時(shí)間 } break; } //DOWN按鍵信號(hào) case DOWN_SIG: { if (me- >timeout > 1) { --me- >timeout; //設(shè)置超時(shí)時(shí)間-1 bsp_display_set_time(me- >timeout); //顯示設(shè)置的超時(shí)時(shí)間 } break; } //ARM按鍵信號(hào) case ARM_SIG: { me- >code = 0; TRAN(TIMING_STATE); //轉(zhuǎn)換到倒計(jì)時(shí)狀態(tài) break; } } break; } //倒計(jì)時(shí)狀態(tài) case TIMING_STATE: { switch (e- >sig) { case UP_SIG: //UP按鍵信號(hào) { me- >code < <= 1; me- >code |= 1; //添加一個(gè)1 bsp_display_user_code(me- >code); break; } case DOWN_SIG: //DWON按鍵信號(hào) { me- >code < <= 1; //添加一個(gè)0 bsp_display_user_code(me- >code); break; } case ARM_SIG: //ARM按鍵信號(hào) { if (me- >code == me- >defuse) { TRAN(SETTING_STATE); //轉(zhuǎn)換到設(shè)置狀態(tài) 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信號(hào) { if (((TickEvt const *)e)- >fine_time == 0) { --me- >timeout; bsp_display_remain_time(me- >timeout); //顯示倒計(jì)時(shí)時(shí)間 if (me- >timeout == 0) { bsp_display_bomb(); //顯示爆炸效果 Bomb1_init(me); } } break; } } break; } }}
2.2 主函數(shù)
兩層switch-case狀態(tài)機(jī)邏輯編寫(xiě)好之后,還需要將狀態(tài)機(jī)運(yùn)行起來(lái)。
運(yùn)行狀態(tài)機(jī)的本質(zhì),就是周期性的調(diào)用狀態(tài)機(jī)(上面實(shí)現(xiàn)的兩層switch-case),當(dāng)有事件觸發(fā)時(shí),設(shè)置對(duì)應(yīng)的事件,狀態(tài)機(jī)在運(yùn)行時(shí),即可處理對(duì)應(yīng)的事件,從而實(shí)現(xiàn)狀態(tài)的切換,或是其它的邏輯處理。
2.2.1 狀態(tài)機(jī)的運(yùn)行
狀態(tài)機(jī)運(yùn)行的整體邏輯如下:
void loop(void){ static TickEvt tick_evt = {TICK_SIG, 0}; delay(100); /*狀態(tài)機(jī)以100ms的循環(huán)運(yùn)行*/? if (++tick_evt.fine_time == 10) { tick_evt.fine_time = 0; }? char tmp_buffer[256]; sprintf(tmp_buffer, "T(%1d)%c", tick_evt.fine_time, (tick_evt.fine_time == 0) ? "\\n" : " "); Serial.print(tmp_buffer);? Bomb1_dispatch(&l_bomb, (Event *)&tick_evt); /*調(diào)度處理tick事件*/? BombSignals userSignal = bsp_key_check_signal(); if (userSignal != SIG_MAX) { static Event const up_evt = {UP_SIG}; static Event const down_evt = {DOWN_SIG}; static Event const arm_evt = {ARM_SIG}; Event const *e = (Event *)0;? switch (userSignal) { //監(jiān)測(cè)按鍵是否按下, 按下則設(shè)置對(duì)應(yīng)的事件e }? if (e != (Event *)0) /*有指定的按鍵按下*/ { Bomb1_dispatch(&l_bomb, e); /*調(diào)度處理按鍵事件*/ } }}
2.2.2 事件的觸發(fā)
**在狀態(tài)機(jī)的每個(gè)狀態(tài)循環(huán)執(zhí)行前,都檢測(cè)一下是否有事件觸發(fā),本例中就是UP、DOWN和ARM的按鍵事件,另外Tick事件是周期性的觸發(fā)的。UP、DOWN和ARM的按鍵事件的觸發(fā)檢測(cè)代碼如下,檢測(cè)到對(duì)應(yīng)的按鍵事件后,則設(shè)置對(duì)應(yīng)的事件給狀態(tài)機(jī),狀態(tài)機(jī)即可在下次狀態(tài)循環(huán)中進(jìn)行處理。 **
switch (userSignal){ case UP_SIG: //UP鍵事件 { Serial.print("\\nUP : "); e = &up_evt; break; } case DOWN_SIG: //DOWN鍵事件 { Serial.print("\\nDOWN: "); e = &down_evt; break; } case ARM_SIG: //ARM鍵事件 { Serial.print("\\nARM : "); e = &arm_evt; break; } default:break;}
3 測(cè)試
本例程,使用Arduino作為控制器進(jìn)行測(cè)試,外接3個(gè)獨(dú)立按鍵和一個(gè)IIC接口的OLED顯示屏。
演示視頻:
4 總結(jié)
本篇以一個(gè)炸彈拆除的小游戲?yàn)槔?,介紹了嵌入式軟件開(kāi)發(fā)中,狀態(tài)機(jī)編程的思路:
分析系統(tǒng)需要哪幾種狀態(tài),哪幾種事件定義這些狀態(tài)、事件,以及狀態(tài)機(jī)的數(shù)據(jù)結(jié)構(gòu)使用UML建模,設(shè)計(jì)對(duì)應(yīng)的狀態(tài)圖根據(jù)狀態(tài)圖,使用C/C++語(yǔ)言,編程實(shí)現(xiàn)對(duì)應(yīng)的功能結(jié)合硬件進(jìn)行調(diào)試,分析另外,本篇中,還需要體會(huì)的是,對(duì)事件的表示,通過(guò)結(jié)構(gòu)體繼承(嵌套)的方式,實(shí)現(xiàn)一個(gè)額外的件參數(shù)這種用法。審核編輯:湯梓紅
最近更新