<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

狀態機編程實例-嵌套switch-case法

碼農愛學習 ? 來源:碼農愛學習 ? 作者:碼農愛學習 ? 2023-06-15 09:01 ? 次閱讀

嵌入式軟件開發中,狀態機編程是一個比較實用的代碼實現方式,特別適用于事件驅動的系統。

本篇,以一個炸彈拆除的小游戲為例,介紹狀態機編程的思路。

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.qd573.com/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.qd573.com/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;
      }
  }
}

2.2 主函數

兩層switch-case狀態機邏輯編寫好之后,還需要將狀態機運行起來。

運行狀態機的本質,就是周期性的調用狀態機(上面實現的兩層switch-case),當有事件觸發時,設置對應的事件,狀態機在運行時,即可處理對應的事件,從而實現狀態的切換,或是其它的邏輯處理 。

2.2.1 狀態機的運行

狀態機運行的整體邏輯如下:

void loop(void)
{
  static TickEvt tick_evt = {TICK_SIG, 0};
  delay(100); /*狀態機以100ms的循環運行*/
?
  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); /*調度處理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)
    {
      //監測按鍵是否按下, 按下則設置對應的事件e
    }
?
    if (e != (Event *)0) /*有指定的按鍵按下*/
    {
      Bomb1_dispatch(&l_bomb, e);  /*調度處理按鍵事件*/
    }
  }
}

2.2.2 事件的觸發

**在狀態機的每個狀態循環執行前,都檢測一下是否有事件觸發,本例中就是UP、DOWN和ARM的按鍵事件,另外Tick事件是周期性的觸發的。UP、DOWN和ARM的按鍵事件的觸發檢測代碼如下,檢測到對應的按鍵事件后,則設置對應的事件給狀態機,狀態機即可在下次狀態循環中進行處理。 **

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 測試

本例程,使用Arduino作為控制器進行測試,外接3個獨立按鍵和一個IIC接口的OLED顯示屏。

演示視頻

4 總結

本篇以一個炸彈拆除的小游戲為例,介紹了嵌入式軟件開發中,狀態機編程的思路:

  • 分析系統需要哪幾種狀態,哪幾種事件
  • 定義這些狀態、事件,以及狀態機的數據結構
  • 使用UML建模,設計對應的狀態圖
  • 根據狀態圖,使用C/C++語言,編程實現對應的功能
  • 結合硬件進行調試,分析

另外,本篇中,還需要體會的是,對事件的表示,通過結構體繼承(嵌套)的方式,實現一個額外的件參數這種用法。

審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 嵌入式
    +關注

    關注

    4993

    文章

    18357

    瀏覽量

    289923
  • Switch
    +關注

    關注

    1

    文章

    525

    瀏覽量

    57634
  • 編程
    +關注

    關注

    88

    文章

    3444

    瀏覽量

    92549
  • UML
    UML
    +關注

    關注

    0

    文章

    122

    瀏覽量

    30762
  • 狀態機
    +關注

    關注

    2

    文章

    486

    瀏覽量

    27225
收藏 人收藏

    評論

    相關推薦

    STM32狀態機編程實例——全自動洗衣機(下)

    本篇在上篇全自動洗衣機的狀態機編程實例的基礎上,增加了OLED來更新直觀的展示洗衣機的工作狀態,并通過3種測試場景來展示洗衣機工作狀態機的執
    的頭像 發表于 09-07 08:47 ?2786次閱讀
    STM32<b class='flag-5'>狀態機</b><b class='flag-5'>編程</b><b class='flag-5'>實例</b>——全自動洗衣機(下)

    狀態機編程實例-狀態表法

    上篇文章,使用嵌套switch-case法的狀態機編程,實現了一個炸彈拆除小游戲。本篇,繼續介紹狀態機
    的頭像 發表于 06-20 09:05 ?1302次閱讀
    <b class='flag-5'>狀態機</b><b class='flag-5'>編程</b><b class='flag-5'>實例</b>-<b class='flag-5'>狀態</b>表法

    STM32按鍵消抖——入門狀態機思維

    本篇介紹了嵌入式軟件開發中常用的狀態機編程實現,并通過按鍵消抖實例,以常用的switch-case形式,實現了對應的狀態機
    的頭像 發表于 09-02 21:54 ?4332次閱讀
    STM32按鍵消抖——入門<b class='flag-5'>狀態機</b>思維

    狀態機編程實例-面向對象的狀態設計模式

    本編介紹了狀態機編程的第3種方法——面向對象的狀態設計模式,通過C++的繼承特性,以及類指針,實現炸彈拆除小游戲中的狀態機功能。
    的頭像 發表于 06-28 09:04 ?955次閱讀
    <b class='flag-5'>狀態機</b><b class='flag-5'>編程</b><b class='flag-5'>實例</b>-面向對象的<b class='flag-5'>狀態</b>設計模式

    raw os 之狀態機編程

    狀態機編程的歷史很可能久于傳統的操作系統, 傳統的一個大while 循環模式普遍用到了狀態機模式編程, 狀態機一般是基于fsm 的有限
    發表于 02-27 14:35

    switch狀態機

    當程序出現多個狀態的時候,不能避免的會用到狀態機這個東西,今天就說一下最簡單的的switch狀態機。通過switch,
    發表于 01-03 22:37

    什么是狀態機? 狀態機是如何編程的?

    什么是狀態機?狀態機是如何編程的?
    發表于 10-20 07:43

    什么是狀態機?狀態機的三種實現方法

    文章目錄1、什么是狀態機?2、狀態機編程的優點(1)提高CPU使用效率(2) 邏輯完備性(3)程序結構清晰3、狀態機的三種實現方法switch
    發表于 12-22 06:51

    狀態機的相關資料下載

    以前寫狀態機,比較常用的方式是用 if-else 或 switch-case,高級的一點是函數指針列表。最近,看了一文章《c語言設計模式–狀態模式(狀態機)》(來源:embed lin
    發表于 02-15 06:01

    利用狀態機狀態機實現層次結構化設計

    練習九.利用狀態機嵌套實現層次結構化設計目的:1.運用主狀態機與子狀態機產生層次化的邏輯設計;
    發表于 02-11 05:52 ?3162次閱讀
    利用<b class='flag-5'>狀態機</b>的<b class='flag-5'>狀態機</b>實現層次結構化設計

    C語言的switch case多分支選擇語句的詳細資料說明

    1、switch-case開關語句是一種多分支選擇語句,用來實現多方向條件分支。雖然采用if-else條件判斷語句也可以實現多方向條件分支,但是當分支較多時,使用if-else條件語句的嵌套層次
    發表于 07-12 17:39 ?1次下載
    C語言的<b class='flag-5'>switch</b> <b class='flag-5'>case</b>多分支選擇語句的詳細資料說明

    狀態模式(狀態機)

    以前寫狀態機,比較常用的方式是用 if-else 或 switch-case,高級的一點是函數指針列表。最近,看了一文章《c語言設計模式–狀態模式(狀態機)》(來源:embed lin
    發表于 12-16 16:53 ?7次下載
    <b class='flag-5'>狀態</b>模式(<b class='flag-5'>狀態機</b>)

    一個應用在單片機上的極簡圖形化狀態機框架NorthFrame

    NorthFrame是基于非UML極簡理念的狀態機框架,配合NF\_FsmDesigner圖形化開發工具,可無負擔替代傳統switch-case狀態機開發。
    發表于 01-25 16:01 ?0次下載
    一個應用在單片機上的極簡圖形化<b class='flag-5'>狀態機</b>框架NorthFrame

    基于單片機的極簡圖形化狀態機框架NorthFrame

    NorthFrame是基于非UML極簡理念的狀態機框架。配合NF_FsmDesigner圖形化開發工具,可無負擔替代傳統switch-case狀態機開發。
    發表于 02-08 15:44 ?3次下載
    基于單片機的極簡圖形化<b class='flag-5'>狀態機</b>框架NorthFrame

    Switch case中的case順序

    Switch case中的case順序 Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鏈/樹。當switch用比較鏈
    的頭像 發表于 11-20 18:16 ?424次閱讀
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>