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状态图的画法与介绍,可参考之前的文章:https://www.elecfans.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类图的介绍可参考之前的文章:https://www.elecfans.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++语言,编程实现对应的功能
  • 结合硬件进行调试,分析

另外,本篇中,还需要体会的是,对事件的表示,通过结构体继承(嵌套)的方式,实现一个额外的件参数这种用法。

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 嵌入式
    +关注

    关注

    5068

    文章

    19008

    浏览量

    302950
  • Switch
    +关注

    关注

    1

    文章

    532

    浏览量

    58150
  • 编程
    +关注

    关注

    88

    文章

    3587

    浏览量

    93578
  • UML
    UML
    +关注

    关注

    0

    文章

    122

    浏览量

    30848
  • 状态机
    +关注

    关注

    2

    文章

    492

    浏览量

    27469
收藏 人收藏

    评论

    相关推荐

    STM32状态机编程实例——全自动洗衣(下)

    本篇在上篇全自动洗衣状态机编程实例的基础上,增加了OLED来更新直观的展示洗衣的工作状态
    的头像 发表于 09-07 08:47 3243次阅读
    STM32<b class='flag-5'>状态机</b><b class='flag-5'>编程</b><b class='flag-5'>实例</b>——全自动洗衣<b class='flag-5'>机</b>(下)

    状态机编程实例-状态

    上篇文章,使用嵌套switch-case状态机编程,实现了一个炸弹拆除小游戏。本篇,继续介绍状态机
    的头像 发表于 06-20 09:05 1963次阅读
    <b class='flag-5'>状态机</b><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 4763次阅读
    STM32按键消抖——入门<b class='flag-5'>状态机</b>思维

    状态机编程实例-面向对象的状态设计模式

    本编介绍了状态机编程的第3种方法——面向对象的状态设计模式,通过C++的继承特性,以及类指针,实现炸弹拆除小游戏中的状态机功能。
    的头像 发表于 06-28 09:04 1409次阅读
    <b class='flag-5'>状态机</b><b class='flag-5'>编程</b><b class='flag-5'>实例</b>-面向对象的<b class='flag-5'>状态</b>设计模式

    Verilog状态机+设计实例

    在verilog中状态机的一种很常用的逻辑结构,学习和理解状态机的运行规律能够帮助我们更好地书写代码,同时作为一种思想方法,在别的代码设计中也会有所帮助。 一、简介 在使用过程中我们常说
    的头像 发表于 02-12 19:07 3813次阅读
    Verilog<b class='flag-5'>状态机</b>+设计<b class='flag-5'>实例</b>

    switch状态机

    当程序出现多个状态的时候,不能避免的会用到状态机这个东西,今天就说一下最简单的的switch状态机。通过switch
    发表于 01-03 22:37

    什么是状态机状态机的三种实现方法

    文章目录1、什么是状态机?2、状态机编程的优点(1)提高CPU使用效率(2) 逻辑完备性(3)程序结构清晰3、状态机的三种实现方法switch
    发表于 12-22 06:51

    状态机的相关资料下载

    以前写状态机,比较常用的方式是用 if-else 或 switch-case,高级的一点是函数指针列表。最近,看了一文章《c语言设计模式–状态模式(状态机)》(来源:embed lin
    发表于 02-15 06:01

    状态机实例(VHDL源代码)

    状态机实例(VHDL源代码):
    发表于 05-27 10:27 59次下载
    <b class='flag-5'>状态机</b><b class='flag-5'>实例</b>(VHDL源代码)

    利用状态机状态机实现层次结构化设计

    练习九.利用状态机嵌套实现层次结构化设计目的:1.运用主状态机与子状态机产生层次化的逻辑设计;
    发表于 02-11 05:52 3280次阅读
    利用<b class='flag-5'>状态机</b>的<b class='flag-5'>状态机</b>实现层次结构化设计

    状态机概述 如何理解状态机

    本篇文章包括状态机的基本概述以及通过简单的实例理解状态机
    的头像 发表于 01-02 18:03 1w次阅读
    <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 9次下载
    <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>的极简图形化<b class='flag-5'>状态机</b>框架NorthFrame