嵌入式单片机的有限状态机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子类即可。

作者:小飞_天空

物联沃分享整理
物联沃-IOTWORD物联网 » 嵌入式单片机的有限状态机FSM应用

发表回复