嵌入式软件开发中,状态机编程是一个比较实用的代码实现方式,特别适用于事件驱动的系统。
本篇,以一个炸弹拆除的小游戏为例,介绍状态机编程的思路。
C/C++语言实现状态机编程的方式有很多,本篇先来介绍最简单最容易理解的switch-case方法。
如下是一个自制的炸弹拆除小游戏的硬件实物,由3个按键:
还有一个屏幕,用于显示倒计时时间,输入的拆除密码等
游戏的玩法:
使用状态机思路进行编程,首先要画出对应的UML状态图,在画图之前,需要先明确此状态机有哪些****状态 ,以及哪些 事件 。
对于本篇介绍的炸弹拆除小游戏,可以归纳为两个状态:
对于事件(或称信号),有3个按键事件,还有一个Tick节拍事件:
相关的结构定义如下
// 炸弹状态机的所有状态
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状态图的画法与介绍,可参考之前的文章://www.lene-v.com/d/2076524.html,这里使用visio画图。
分析这个状态图:
对于上述的状态机事件,可以分为两类,一类是按键事件:UP、DOWN和ARM,一类是Tick。对于第一类事件,指需要单一的事件变量即可区分,对于第二类的Tick,由于引入了1/10s的精细时间,所以这个时间还需要一个额外的****事件参数表示此次Tick事件的精细时间(fine_time)。
这里再介绍一个编程技巧,通过结构体的继承关系(实际就是嵌套),实现对事件数据结构的设计,如下图:
**子图(a)**表示TickEvt与Event是继承关系,这是UML类图的画法,关于UML类图的介绍可参考之前的文章://www.lene-v.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事件*/
//省略...
}
状态图设计好之后,就可以对照着状态图,进行编程实现了。
本篇先使用最简单最容易理解的switch-case方法,来实现状态机编程。
使用switch-case法实现状态机,一般需要两层switch结构。
void Bomb1_dispatch(Bomb1 *me, Event const *e)
{
//第一层switch处理状态
switch (me- >state)
{
//设置状态
case SETTING_STATE:
{
//...
break;
}
//倒计时状态
case TIMING_STATE:
{
//...
break;
}
}
}
这里以状态机处于“设置状态”时,对事件(信号)的处理为例
//设置状态
case SETTING_STATE:
{
//第二层switch处理事件(信号)
switch (e- >sig)
{
//UP按键信号
case UP_SIG:
{
//...
break;
}
//DOWN按键信号
case DOWN_SIG:
{
//...
break;
}
//ARM按键信号
case ARM_SIG:
{
//...
break;
}
}
break;
}
// 用于进行状态转换的宏
#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;
}
}
}