状态机编程实例-嵌套switch-case法

嵌入式软件开发中,状态机编程是一个比较实用的代码实现方式,特别适用于事件驱动的系统。

本篇,以一个炸弹拆除的小游戏为例,介绍状态机编程的思路。

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状态图的画法与介绍,可参考之前的文章UML状态图详解,这里使用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类图的介绍可参考之前的文章:UML简介与类图详解。

子图( 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显示屏。

演示视频:
https://www.bilibili.com/video/BV1Bs4y1v7Gp

状态机编程实例-炸弹拆除小游戏

4 总结

本篇以一个炸弹拆除的小游戏为例,介绍了嵌入式软件开发中,状态机编程的思路:

  • 分析系统需要哪几种状态,哪几种事件
  • 定义这些状态、事件,以及状态机的数据结构
  • 使用UML建模,设计对应的状态图
  • 根据状态图,使用C/C++语言,编程实现对应的功能
  • 结合硬件进行调试,分析

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/30077.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

KW 新闻 | KaiwuDB 亮相数字中国并发布离散制造场景解决方案

4月26-30日&#xff0c;以“加快数字中国建设&#xff0c;推进中国式现代化”为主题的第六届数字中国建设峰会在福州市圆满召开。KaiwuDB 受邀亮相大会参展并发布“离散制造场景解决方案”&#xff0c;旨在以数字化方案驱动生产方式、治理方式变革&#xff0c;推进离散制造业物…

45道SQL题目陆续更新

文章目录 学习视频配置环境第一天内连接 外连接sql执行顺序 第二天group by 的用法 第三天第四天order bycase when窗口函数 第五天第六天第七天limit第八天 45、查询下月过生日的学生信息 学习视频 学习视频 配置环境 四张表 配置四张表的sql语句 #创建发据库 create dat…

TOGAF10®标准中文版--(阶段A — 架构愿景)方法

3.5.1 概述 阶段 A 从收到发起组织向架构组织发出的架构工作请求开始。 在TOGAF 标准 —EA能力和治理中讨论了确保公司管理层的适当认可和确认&#xff0c;以及直线管理层的支持和承诺所涉及的问题。 A阶段还定义了架构工作的范围内和范围外的内容以及必须处理的约束条件。在…

浅析 xml 数据格式文件

浅析 xml 数据格式文件 xml ( Extensible Markup Language ) 全称 -> 可拓展的标记语言&#xff1b; xml文件的主要用途&#xff1a;xml文件主要用于数据的 传输 和 存储&#xff0c;并不是展示&#xff1b; xml标签与html的区别&#xff1a;节点的标签使用方式和 html 十分…

linuxOPS系统服务_linux高级命令

find命令 find 路径 [选项 选项的值] … 选项作用-name根据文件的名称进行-type按文件类型进行搜索&#xff0c;f代表普通文件&#xff0c;d代表文件夹 find命令查找文件 示例1 查找一个文件 案例1 ,在linux整个系统中查找 test.txt文件 find / -name test.txt -type f案例…

算法刷题-字符串-重复的子字符串

KMP算法还能干这个 459.重复的子字符串 力扣题目链接 给定一个非空的字符串&#xff0c;判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母&#xff0c;并且长度不超过10000。 示例 1: 输入: “abab” 输出: True 解释: 可由子字符串 “ab” 重复两…

Ubuntu下载速度过慢解决

今天用Ubuntu下载Roberta文件到本地&#xff0c;速度特别慢&#xff0c;Ubuntu 系统自带的源文件&#xff0c;都是国外的源网址&#xff0c;在国内下载安装升级源或者依赖的时候&#xff0c;都比较慢&#xff0c;更换国内的源地址&#xff0c;轻松搞定此问题。 目录 一、备份…

ChatGPT的未来发展

文章目录 1.什么是ChatGPT2.ChatGPT的基础技术3.ChatGPT工作原理4.ChatGPT应用场景5.ChatGPT局限性6.ChatGPT的未来发展 ✍创作者&#xff1a;全栈弄潮儿 &#x1f3e1; 个人主页&#xff1a; 全栈弄潮儿的个人主页 &#x1f3d9;️ 个人社区&#xff0c;欢迎你的加入&#xff…

一键部署通义千问预体验丨阿里云云原生 5 月动态

云原生月度动态 云原生是企业数字创新的最短路径。 《阿里云云原生每月动态》&#xff0c;从趋势热点、产品新功能、服务客户、开源与开发者动态等方面&#xff0c;为企业提供数字化的路径与指南。 本栏目每月更新。 01 趋势热点 &#x1f947; Apache RocketMQ 入选可信开…

哨兵3号(Sentinel 3)卫星数据处理

李国春 本文介绍使用RSD处理欧空局哨兵3号&#xff08;Sentinel 3&#xff09;卫星数据的具体方法。 气象数据多用NetCDF格式交换和存储&#xff0c;气象卫星数据也是如此。气象业内认为HDF5也是NetCDF的一部分&#xff0c;虽然文件扩展名使用的是.nc&#xff0c;但是实际上就…

【AntDB数据库】AntDB数据库告警管理

告警历史 功能概述 数据库系统的主机、单节点集群的被监测指标达到告警阀值时&#xff0c;AMOPS就会产生告警并展示在告警分类页面上。 告警分类页面提供告警搜索查看功能&#xff0c;用户可以指定监控项、集群、事件级别、时间范围和告警对象对告警进行搜索。 查询的告警数…

【MySQL数据库基础】

MySQL数据库基础 1. 数据库的操作1.1 显示当前的数据库1.2 创建数据库1.3 使用数据库1.4 删除数据库 2. 常用数据类型2.1整数&#xff08;xxxint&#xff09;2.2日期时间类型2.3字符串型 3. 表的操作3.1 查看表结构3.2 创建表3.3 删除表 1. 数据库的操作 1.1 显示当前的数据库…

React学习之路-目录结构

目录结构 node_modules — 存放项目依赖包 public — 存放网站的静态资源文件 favicon.icon — 网站偏爱图标index.html — 主页面&#xff08;重要&#xff09;logo192.png — logo图logo512 — logo图manifest.json — 应用加壳的配置文件robots.txt — 爬…

Elasticsearch:实用 BM25 - 第 1 部分:分片如何影响 Elasticsearch 中的相关性评分

作者&#xff1a;Shane Connelly 背景 在 Elasticsearch 5.0 中&#xff0c;我们切换到 Okapi BM25 作为我们的默认相似度算法&#xff0c;这是用于对与查询相关的结果进行评分的算法。 在本博客中&#xff0c;我不会过多地介绍 BM25 与替代措施&#xff0c;但如果你想了解 B…

【备战秋招】每日一题:4月23日美团春招第一题:题面+题目思路 + C++/python/js/Go/java带注释

为了更好的阅读体检&#xff0c;为了更好的阅读体检&#xff0c;&#xff0c;可以查看我的算法学习博客第一题-申请奖学金 在线评测链接:P1245 题目内容 塔子哥是一个热爱学习的大学生&#xff0c;他的梦想是成为一名优秀的算法竞赛高手。为了实现自己的梦想&#xff0c;他需…

Vue中v-text、v-html、v-on的基本语法(二)

文章目录 前言一、vue中data属性定义对象、数组相关数据二、v-text、v-html指令使用三、v-on基本指令使用(一)四、v-on指令基本使用(二)之在函数中获取vue实例本身this五、v-on指令基本使用(二)之在函数中传递参数六、v-on指令基本使用(二)之简化写法绑定函数和事件定义的两种写…

关于VPN的一些总结和理解

关于VPN的一些总结和理解 前言一、VPN的概述二、VPN的原理2.1 原理概述2.2 虚拟网卡2.3 点对点隧道的建立 三、其他3.1 vpn和vlan的区别&#xff1f;3.2 vpn和web代理的关系&#xff1f; 参考 前言 同样的机缘巧合&#xff0c;最近看了一些关于vpn的内容&#xff0c;总结一下&a…

go+vue自建运维管理平台

文章目录 鲁班运维平台容器管理集群管理namespace管理节点管理工作负载存储管理网络管理配置管理事件中心 kuboard 鲁班运维平台 这个平台和spug很像&#xff0c;感觉就像是spug运维平台的容器版本。 但是如果是容器平台则选择的余地很大&#xff0c;优秀的如kubersphere、kub…

LeetCod刷题笔记

目录 2739.总行驶距离 思路&#xff1a;模拟 代码 6890.找出分区值 思路&#xff1a;急转弯 代码: 1254.统计封闭岛屿的数目​编辑 思路&#xff1a;DFS 代码&#xff1a; 6447.给墙壁刷油漆 思路&#xff1a;动态规划 代码&#xff1a; 思路&#xff1a;状态DP 代码&…

Rust in Action笔记 第四章生命周期、所有权、借用

第四章用了一个行星通信的例子来阐述整个主题&#xff0c;主要角色有地面站&#xff08;Ground station&#xff09;、人造卫星&#xff08;CubeSat&#xff09;&#xff0c;两者有不同的状态并且能互相发消息通信&#xff1b; Rust有类型安全&#xff08;type safety&#xf…