嵌入式单片机的有限状态机FSM应用
有限状态机(Finite State Machine,FSM)这种设计模式可以在嵌入式单片机运用。
在《敏捷软件开发(原则模式与实践)》第29章关于有限状态机中介绍了一个地铁旋转十字门,我们用地铁闸机例子来说明这种状态机。
通常闸机默认是关闭的,当闸机检测到地铁卡,则打开闸机;当乘客通过后,则关闭闸机。如果有人非法通过,则闸机就会报警;如果闸机已经打开,而乘客还在刷卡,闸机就会提醒乘客闸机已经打开,请通过。
状态 | 事件 | 状态 | 动作 |
Locked(上锁) | card(放卡) | UnLocked(开锁) | unlock(开) |
UnLocked(开锁) | pass(通过) | Locked(上锁) | lock(关) |
Locked(上锁) | pass(通过) | Locked(上锁) | alarm(报警) |
UnLocked(开锁) | card(放卡) | UnLocked(开锁) | thanks(提示已开) |
实现有限状态机的方式有很多种,我这里举例了三种,仅供大家参考。
1、嵌套的switch/case语言
根据上面例子,我们可以写出两层switch/case,第一层用于判断当前状态,第二层用于各个状态下的事件管理。
switch(当前状态)
{
case Locked 状态:
switch(事件)
{
case Card 事件:
切换到UnLocked状态;
执行开锁动作;
break;
case Pass 事件:
状态保持不变;
执行报警动作;
break;
}
break;
case UnLocked 状态:
switch(事件)
{
case Card 事件:
状态保持不变;
执行提醒乘客门已开动作;
break;
case Pass 事件:
切换到Locked状态;
执行关锁动作;
break;
}
break;
}
上面这种方式将代码分成了四个互不相连的部分,每个部分对应一种状态转移。
对于非常简单的状态机来说,它足够直观,简洁明了。但是它的缺点也很明显,对于一些比较大型的FSM来说,由于存在大量的状态和事件数目,故而一个函数编写的代码量会与状态个数 * 事件数目 成正比。故而存在难以维护与扩展的问题。
2、状态转移表
typedef struct {
状态;
事件;
切换到的状态;
执行动作;
}transition_t;
transition_t transitions[]=
{
{Locked状态,Card事件,UnLocked状态,unlocd动作},
{UnLocked状态,Pass事件,Locked状态,locd动作},
{Locked状态,Pass事件,Locked状态,alarm动作},
{UnLocked状态,Card事件,UnLocked状态,thanks动作},
};
for (int i = 0; i < sizeof(transitions)/sizeof(transitions[0]); i++)
{
if (当前状态 == transitions[i].状态 && 当前事件 == transitions[i].事件)
{
切换状态: transitions[i].切换到的状态;
执行动作: transitions[i].执行动作;
break;
}
}
根据这样的一个表,各个状态与事件的关系一目了然,代码读起来也比较规范,逻辑都在这个表里,维护也比较方便。缺点是对于大型的状态机,遍历需要一些时间。
3、State状态模式
上面两种写法都是 "面向过程的写法",人们习惯将 "当前状态" 视为一种标量,比如数字1代表Unlocked状态,数字0代表Locked状态,闸机在响应事件时,根据标量的值做出响应的处理。所以无论是嵌套的“switch-case”写法,还是状态转移表都要判断 "当前状态" 是Locked状态还是Unlocked状态?由于存在判断当前状态的操作,所以会出现状态与状态之间的 "耦合" 。对于"switch-case"写法,"耦合" 体现在第一层。
如何将这种 "耦合" 分离,我们可以用 "面向对象" 的写法。下面我来说明一下这种写法的方式 。
第一步:先将 "闸机" 看成一个类。再将 "闸机状态"也看成一个类,且该类有两个事件属性。
#include "turnstile.h"
typedef struct _turnstile_t turnstile_t;
//闸机状态类
typedef struct _turnstile_state_t{
void (*card)(turnstile_t * p_turnstile);
void (*pass)(turnstile_t * p_turnstile);
}turnstile_state_t;
//闸机类
typedef struct _turnstile_t{
turnstile_state_t *p_state;
}turnstile_t;
//关锁状态 放卡
static void locked_card(turnstile_t * p_turnstile)
{
//状态切换
turnstile_state_set(p_turnstile,unlocked_state);
//执行开锁动作
printf("The lock is open\r\n");
}
//关锁 通过
static void locked_pass(turnstile_t * p_turnstile)
{
//状态不切换
//执行报警动作
printf("araming\r\n");
}
//开锁 放卡
static void unlocked_card(turnstile_t * p_turnstile)
{
//状态不切换
//执行谢谢动作
printf("thanks\r\n");
}
//开锁 通过
static void unlocked_pass(turnstile_t * p_turnstile)
{
//状态切换
turnstile_state_set(p_turnstile,locked_state);
//执行关锁动作
printf("The lock is not open\r\n");
}
//切换状态
void turnstile_state_set(turnstile_t * p_this,turnstile_state_t * p_new_state)
{
p_this -> p_state = p_new_state;
}
第二步:实列化闸机状态类:一个实例是闸机关锁状态类,一个实例是闸机开锁状态类。两个闸机类的属性值并不一样。
#include "turnstile.h"
...
...
//实例化两个闸机状态类 且 两个闸机状态类的属性值不一样
//关锁闸机状态实例化
turnstile_state_t locked_state = {locked_card, locked_pass};
//开锁闸机状态实例化
turnstile_state_t unlocked_state = {unlocked_card, unlocked_pass};
//初始化闸机类
void turnstile_init(turnstile_t * p_this)
{
p_this->p_state = &locked_state;
}
//放卡事件
void turnstile_card(turnstile_t * p_this)
{
p_this->p_state->card(p_this);
}
//通过事件
void turnstile_pass(turnstile_t * p_this)
{
p_this->p_state->pass(p_this);
}
这样,在主函数里,闸机只需要创建一个闸机类就行。不需要考虑当前状态是关锁还是开锁。
void main()
{
turnstile_state_t turnstile_t;//实例化闸机类
turnstile_init(&turnstile_t);//初始化该闸机类
while(1)
{
if (触发了放卡事件)
{
turnstile_card(&turnstile_t);
}
if (触发了通过事件)
{
turnstile_pass(&turnstile_t);
}
}
}
对于一个闸机而言,其任意时刻只能处于某一种确定状态。由于抽象类的作用,屏蔽了各个具体状态的差异性。在闸机看来,无论何种状态,它们都提供了card()方法和pass()方法。在该方法里再去根据状态去处理事件。我们也可以在card()和pass()方法,将事件转移给 "状态类"来负责。
这样做的好处是如果遇到要新增的状态时,只需增加一个turnstile_state_t子类即可。
作者:小飞_天空