目录:
一、有限状态机(FSM)
二、分层状态机(HSM)
三、事件状态机(ESM)
一、有限状态机(FSM)
功能业务代码写多了,回看代码会发现用if-else if-else
用的越来越多,因为有很多场景需要区分,不同场景下的功能不一样,因此需要通过if
做判断,场景一旦多了起来起来,用if-else if-else
就会越来越多。
这里的
if-else if-else
通常是用来区分不同场景下的功能实现,和上篇提到的表驱动编程还不太一样。
为了避免这种情况,本编介绍一种开发方法--状态机编程。
为什么需要状态机编程?相信很多人在实现
if
判断的时候,偶尔会出现不小心漏掉或者写错一些触发条件,导致功能出现异常的问题,特别是一些复杂的逻辑条件,一大堆的&&
/||
/()
等眼花缭乱的复杂逻辑,写完之后恐怕自己都得检查好一会,担心是不是哪个条件不能正常触发。状态机通常采用switch-case
实现。那么状态机的引入解决了哪些问题呢?
当程序有多个状态时,规范了程序的状态转换,避免了一些引入一些复杂的判断逻辑。
规范了程序在不同状态下的实现和所能提供的能力。
在能力上可以进行横向扩展,提供新的状态来完善现有逻辑
逻辑清楚,实现过程会多考虑一些情况,方便定位问题所在
介绍
什么是状态机?
状态机是有限状态自动机(FSM)的简称,是现实事物运行规则抽象而成的一个数学模型。
简单理解就是:现实事物是有不同状态的,比如灯,就有“亮”和“灭”两种状态,再复杂抽象一点,增加一个“损坏”状态,那这属于异常情况了。
概念
现态:当前所处的状态,一个状态机至少要包含两个状态,某一时刻只能是一种状态,比如刚才说到的灯,就有“亮”和“灭”两种状态
条件:又称事件,执行某个操作的触发条件或者口令,比如灯通过开关控制,操作开关就是一个事件
动作:事件发生以后要执行动作,比如开关按下开关,灯亮,松开则灭
次态:条件满足后要迁往的新状态,比如开关按下后灯从当前灭的状态变为亮的状态
用过"RTOS"的朋友应该也知道任务的几种状态,任务的四种状态分别是:就绪态、运行态、阻塞态、挂起态,不会同时出现两种及两种以上的状态存在,OS 根据当前的状态,和任务优先级、滴答时钟、主动睡眠等条件进行任务的状态切换。
状态机的动作类型
进入动作:在进入状态时进行
退出动作:在退出状态时进行
输入动作:依赖于当前状态 和 输入条件进行
转移动作:在进行特定转移时进行
实现
首先,看一个简单的例子,在不同场景下实现控制电机的功能:设备开机启动三次电机、开关按下一次启动一次、关机启动三次电机。采用非状态机的写法,通过各种标志位去判断设备是否需要控制电机,什么条件下退出等。
只是简单的实现一下,可能其中也有一些状态机的思想吧(毕竟状态机编程思想已经在脑海里,不可避免吧),不过我还是尽量还原我初次编程期间的实现这个功能的逻辑思想吧,勿怪。
/* 控制电机函数 */
void MotorCtrlTask(void)
{
if (ctrlCnt)
{
MotorCtrl(ON);
delay(1);
MotorCtrl(OFF);
}
else
{
MotorCtrl(OFF);
}
}
int isPowerOn = true;
int isPowerOff = false;
int ctrlCnt = 0;
void main(void)
{
while (1)
{
if (isPowerOn)
{
isPowerOn = false;
ctrlCnt = 3;
}
if (keyPress)
{
keyPress = false;
ctrlCnt = 1;
}
if (...) // 关机条件
{
if (ctrlCnt == 0 && !isPowerOff && !isPowerOn)
{
isPowerOff = true;
ctrlCnt = 3;
}
}
MotorCtrlTask();
if (ctrlCnt > 0)
ctrlCnt--;
else
{
if (ctrlCnt == 0 && isPowerOff && !isPowerOn)
{
return;
}
}
}
}
通过采用状态机编程的方式,首先考虑的就是有三种状态:开机、关机和工作状态,先理清楚三种状态之间转换的条件和当前状态需要执行的相关功能,然而在实现过程中就会意识到还需要增加一种过渡状态:关机准备中(关机过程中需要执行的一系列操作)。
只有逻辑清晰了,才会下意识的察觉少了一些东西了,特别是一些临界处理等。
int sysState = POWER_OFF; // 默认关机状态
int ctrlCnt = 0;
/* 控制电机函数 */
void MotorCtrlTask(void)
{
if (ctrlCnt)
{
MotorCtrl(ON);
delay(1);
MotorCtrl(OFF);
}
else
{
MotorCtrl(OFF);
}
}
void main(void)
{
while (1)
{
switch (sysState)
{
case POWER_OFF: // 关机状态
sysState = POWER_ON; // 自动切换成开机状态
ctrlCnt = 3;
break;
case POWER_ON: // 开机过程状态
... // 开机过程中的其他功能
if (ctrlCnt == 0) // 控制结束自动切换工作状态
{
sysState = WORKING;
break;
}
break;
case WORKING: // 工作状态
if (...) // 关机条件
{
sysState = POWER_OFF_READY;
ctrlCnt = 3;
break;
}
if (keyPress)
{
keyPress = false;
ctrlCnt = 1;
}
break;
case POWER_OFF_READY: // 关机准备中
... // 关机准备中的其他功能
if (ctrlCnt == 0) // 控制结束自动退出
{
sysState = POWER_OFF;
return; // 退出程序
}
break;
default:
break;
}
MotorCtrlTask();
if (ctrlCnt > 0)
ctrlCnt--;
}
}
总结:从上述两份代码看,你觉得哪一个逻辑更清晰呢?非状态机实现方式还有一些异常处理没有实现,比如开机过程中在启动三次调节电机过程中按下会有什么现象呢,为了避免这种情况又需要加入多少if
条件判断呢?
适用场景
状态机应用范围挺广的,不止是在C语言中,其他都能使用,准确来说这个属于一种编程思想。特别是业务功能,状态机是常用的。
比如常常使用的模块也有状态机的身影:比如按键的按下和松开,按下又包括按下瞬间、多次按下、持续按下、松开瞬间和持续松开等
通常状态机和表驱动可以结合使用,状态机的现态、条件、动作和次态作为数据,执行这些状态切换的作为逻辑。完全可以根据实际情况灵活使用。
代码参考:
菜单:菜单控制,可以这样理解:当前菜单界面为现态、菜单进入和退出为条件、菜单切换时的函数执行为动作、上下级菜单理解为次态,其中菜单选项表就灵活使用了状态机和表驱动的方式。
https://gitee.com/const-zpc/menu.git
ESP8266:AT指令数据表,包含指令、期望响应、时间和函数指针【后续的动作】等,可以这样理解:数组索引当前值为现态、收到响应和超时为条件、收到响应或超时执行的函数为动作、数组索引的未来值为次态
https://gitee.com/const-zpc/esp8266
二、分层状态机(HSM)
上面介绍的状态机是有限状态机(FSM),下面介绍一下分层状态机(HSM)的理解。
分层状态机是一种用于描述系统行为和控制流的模型,它将系统划分为多个层级,并将每个层级表示为一个状态机。每个状态机描述了一个特定的状态集合以及状态之间的转移规则。每个状态机都有自己的输入和输出,可以与其他状态机进行交互,从而形成整个系统的行为。
分层状态机的设计有助于降低系统复杂度,并且能够更好地组织系统的功能和行为。在分层状态机中,每个状态机负责处理一个特定的子任务或功能,这使得系统更易于理解和维护。
在分层状态机中,高层状态机负责协调和控制下层状态机的行为。高层状态机可以通过向下层状态机发送命令或事件来触发状态转换,下层状态机可以将其处理结果返回给上层状态机,以便上层状态机作出相应的决策。
区别
分层状态机(HSM)和有限状态机(FSM)是两种不同的模型,虽然它们都用于描述系统的行为和控制流,但它们之间还是存在一些区别的。
层次结构不同:分层状态机是将系统划分为多个层级,并将每个层级表示为一个状态机,每个状态机有自己的输入和输出,可以与其他状态机进行交互,形成整个系统的行为。而有限状态机则是单层的结构,描述系统在特定条件下的状态和状态之间的转移。
状态数量不同:分层状态机中的状态数量通常比有限状态机更多。由于分层状态机的每个层级都有自己的状态集合,因此整个系统的状态集合是各层状态集合的并集。而有限状态机只有一个状态集合,状态数量相对较少。
状态之间的转移规则不同:在分层状态机中,状态之间的转移规则可以根据不同的层级进行定义,每个状态机有自己的转移规则。而有限状态机的状态之间的转移规则通常是全局统一的。
功能不同:分层状态机通常用于描述复杂系统的行为和控制流,其设计目的是为了降低系统的复杂度并更好地组织系统的功能和行为。而有限状态机通常用于描述简单的控制流程或者算法。
总之,分层状态机和有限状态机虽然都是描述系统行为和控制流的模型,但是它们的层次结构、状态数量、状态之间的转移规则和应用场景等方面存在较大的不同
举例说明
假设有一个自动售货机,它需要根据用户选择的按钮来售出对应的商品。我们可以使用有限状态机和分层状态机分别来实现这个自动售货机。
首先,我们来看一下如何使用有限状态机来实现自动售货机。假设我们有三个商品可以售卖,分别是饮料、糖果和薯片,对应的按钮分别为A、B和C。那么,我们可以定义以下状态:
初始状态:待命状态
状态1:等待用户选择按钮
状态2:出货状态
根据这些状态,我们可以设计状态转移规则如下:
待命状态:如果用户按下按钮A、B或C,则转移到等待用户选择按钮状态。
等待用户选择按钮状态:如果用户按下按钮A,则转移到出货状态1;如果用户按下按钮B,则转移到出货状态2;如果用户按下按钮C,则转移到出货状态3。
出货状态1:出售饮料,并转移到待命状态。
出货状态2:出售糖果,并转移到待命状态。
出货状态3:出售薯片,并转移到待命状态。
流程图:
接下来,我们来看一下如何使用分层状态机来实现自动售货机。首先,我们可以将自动售货机分解成三个层次:选择商品层、支付层和出货层。每个层次都有自己的状态和状态转移规则,如下所示:
选择商品层:等待用户选择按钮状态
支付层:等待用户支付状态
出货层:出货状态
然后,我们可以进一步细化每个层次的状态和状态转移规则,如下所示:
选择商品层:
初始状态:待命状态
状态1:等待用户选择按钮
转移规则:如果用户按下按钮A、B或C,则转移到等待用户支付状态,并保存用户选择的商品。
支付层:
初始状态:待命状态
状态1:等待用户支付
转移规则:如果用户完成支付,则转移到出货状态,并发送出货指令。
出货层:
初始状态:待命状态
状态1:等待出货指令
状态2:正在出货
转移规则:如果接收到出货指令,则转移到正在出货状态,并执行出货操作;如果出货操作完成,则转移到待命状态。
流程图:
需要注意的是,有限状态机和分层状态机都可以用于自动售货机的设计,选择哪种方法取决于具体的需求和复杂度。如果系统相对简单,可以使用有限状态机。如果需要管理多个子系统、处理多个事件和状态,可以考虑使用分层状态机。
最重要的是,在实际应用中,理解分层状态机的关键是正确地设计状态转移逻辑和事件处理函数。如果状态转移逻辑不正确,可能会导致状态机无法正确响应事件;如果事件处理函数不正确,可能会导致状态机无法正确处理事件。因此,在设计分层状态机时,需要认真考虑状态转移和事件处理的逻辑,并进行充分的测试和验证。
代码参考
有限状态机
有限状态机可以使用 switch
/case
或 if
/else
语句实现状态转移,并在每个状态转移时执行相应的操作。
#include <stdio.h>
#include <stdbool.h>
enum State
{
ST_IDLE,
ST_WAIT_SELECT,
ST_WAIT_PAY,
ST_DISPENSE
};
enum Button
{
BTN_NONE,
BTN_A,
BTN_B,
BTN_C
};
enum State current_state = ST_IDLE;
enum Button current_button = BTN_NONE;
void update_fsm(enum Button button)
{
switch (current_state)
{
case ST_IDLE:
if (button != BTN_NONE)
{
current_button = button;
current_state = ST_WAIT_SELECT;
printf("商品选择: %c\n", button);
}
break;
case ST_WAIT_SELECT:
if (button == BTN_NONE)
{
current_state = ST_WAIT_PAY;
printf("等待支付...\n");
}
break;
case ST_WAIT_PAY:
if (button == BTN_NONE)
{
current_state = ST_DISPENSE;
printf("出货中...\n");
}
break;
case ST_DISPENSE:
if (button == BTN_NONE)
{
current_state = ST_IDLE;
printf("出售完成\n");
}
break;
}
}
int main()
{
update_fsm(BTN_NONE);
update_fsm(BTN_A);
update_fsm(BTN_NONE);
update_fsm(BTN_NONE);
update_fsm(BTN_NONE);
update_fsm(BTN_NONE);
return 0;
}
分层状态机
而分层状态机中由于更加复杂,单纯地采用 switch
/case
或 if
/else
语句已经不太适合不同状态机及其子状态的转移,最好的方式是采用表驱动的方式去实现,然后在数组表中定义每个状态机及其子状态,并且有回调函数用来处理每个状态机及其子状态的处理等,提高系统的可维护性和可扩展性。
由于实现较为复杂,以下代码仅供参考,不具体实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 状态机状态枚举
typedef enum {
STATE_IDLE,
STATE_WAITING_FOR_COIN,
STATE_WAITING_FOR_SELECTION,
STATE_DISPENSING,
STATE_COUNT
} state_t;
// 事件枚举
typedef enum {
EVENT_INSERT_COIN,
EVENT_MAKE_SELECTION,
EVENT_DISPENSE,
EVENT_COUNT
} event_t;
// 状态转移条件函数类型定义
typedef bool (*condition_func_t)(void);
// 状态机状态结构体定义
typedef struct {
void (*enter_func)(void); // 进入状态函数
void (*exit_func)(void); // 退出状态函数
void (*poll_func)(void); // 事件轮询处理函数
condition_func_t (*cond_func)(void); // 转移条件函数
} state_info_t;
// 状态转移表
static const int transition_table[STATE_COUNT][EVENT_COUNT] = {
// EVENT_INSERT_COIN EVENT_MAKE_SELECTION EVENT_DISPENSE
{ STATE_WAITING_FOR_COIN, STATE_IDLE, STATE_IDLE }, // STATE_IDLE
{ STATE_WAITING_FOR_COIN, STATE_WAITING_FOR_SELECTION, STATE_IDLE }, // STATE_WAITING_FOR_COIN
{ STATE_WAITING_FOR_COIN, STATE_WAITING_FOR_SELECTION, STATE_DISPENSING }, // STATE_WAITING_FOR_SELECTION
{ STATE_WAITING_FOR_COIN, STATE_WAITING_FOR_SELECTION, STATE_IDLE } // STATE_DISPENSING
};
// 状态机状态数组
static const state_info_t state_table[STATE_COUNT] = {
// 进入状态函数 退出状态函数 事件轮询处理函数 转移条件函数
{ NULL, NULL, NULL, NULL }, // STATE_IDLE
{ wait_for_coin_enter, wait_for_coin_exit, wait_for_coin_poll, wait_for_coin }, // STATE_WAITING_FOR_COIN
{ wait_for_select_enter, wait_for_select_exit, wait_for_select_poll, waiting_for_select }, // STATE_WAITING_FOR_SELECTION
{ dispensing_enter, dispensing_exit, dispensing_poll, dispensing } // STATE_DISPENSING
};
// 当前状态
static state_t current_state = STATE_IDLE;
三、事件状态机(ESM)
前面介绍了有限状态机(FSM)和分层状态机(HSM),下面介绍的主要是事件型状态机(ESM)。
一种在嵌入式系统中广泛应用的设计方法,用于管理和控制系统的状态转换。它将状态的切换与外部事件的发生联系起来,使系统能够更灵活地响应不同的条件和输入。
介绍
事件驱动型状态机将系统划分为一组状态,每个状态表示系统在特定条件下的行为。状态之间的转换是由外部事件的发生触发的,而不是由内部条件或方法调用直接控制。
主要组成部分
状态(States):系统在不同条件下可能处于的状态。每个状态表示一种特定的行为或状态。
事件(Events):触发状态转换的外部事件,可以是传感器数据的变化、用户输入、定时器中断等。
状态转换(Transitions):定义状态之间的转换规则,指明从一个状态切换到另一个状态需要哪个事件的触发。
事件处理程序(Event Handlers):每个状态可以关联一个或多个事件处理程序,用于处理状态转换时触发的事件。事件处理程序定义了状态之间的行为。
工作原理
事件驱动型状态机的工作原理如下:
初始化:系统初始化时处于初始状态。
事件监测:系统不断监测外部事件的发生。
事件匹配:当某个外部事件发生时,状态机检查当前状态的状态转换表,找到与该事件匹配的状态转换。
状态转换:如果找到匹配的状态转换,系统执行状态转换,并调用关联的事件处理程序。这可能导致系统从当前状态切换到另一个状态。
等待事件:一旦事件处理程序执行完毕,系统会等待下一个外部事件的发生。
区别
基于事件驱动的状态机和普通状态机都是用于管理和控制系统状态转换的方法,但它们在设计思想和实现方式上有一些区别。
普通状态机
状态切换方式:在普通状态机中,状态之间的转换通常是基于内部条件或者直接的方法调用。状态转换通常由状态之间的关系和切换条件直接控制。
切换条件:普通状态机的状态切换通常由硬编码的逻辑条件决定,这些条件可能包括输入信号、定时器、内部变量等。
状态管理:普通状态机需要维护一个状态转换表或者逻辑判断来控制状态的切换和行为。状态之间的关系和切换条件可能会使代码变得复杂,尤其在状态较多或者状态之间的关系较复杂时。
应用场景:普通状态机适用于状态转换较为简单、固定的场景,适合用于描述一些基本的系统行为和流程。
事件型驱状态机
状态切换方式:基于事件驱动的状态机依赖于外部事件的发生来触发状态的转换。状态之间的转换是由事件触发的,状态机根据事件的发生来确定下一个状态。
切换条件:在基于事件驱动的状态机中,状态的转换不是直接由内部逻辑条件决定,而是由外部事件的发生来触发。状态之间的关系和切换条件更加松散。
状态管理:基于事件驱动的状态机不需要维护复杂的状态转换表,而是在收到特定事件时,根据事件来触发状态的切换。这种方式使得状态机的设计更加灵活和可扩展。
应用场景:基于事件驱动的状态机适用于复杂的状态转换和流程,特别是当状态之间的关系比较复杂,或者需要根据外部事件来灵活控制状态转换时,这种方法更加合适。
总体而言,基于事件驱动的状态机相对于普通状态机更具灵活性和扩展性,适用于复杂的嵌入式MCU应用场景。它能够更好地处理状态之间的关系,以及根据外部事件来触发状态的转换,从而提供更高效、灵活和可扩展的状态管理和控制。
代码实现
下面是一个基于事件驱动的状态机框架的简单示例代码,用于说明其工作原理。
#include <stdio.h>
#include <stdbool.h>
// 定义状态枚举
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSED,
STATE_STOPPED
} State;
// 定义事件枚举
typedef enum {
EVENT_START,
EVENT_PAUSE,
EVENT_RESUME,
EVENT_STOP
} Event;
// 定义状态转换表
typedef struct {
State current; // 当前状态
Event event; // 事件
State next; // 事件触发后的新状态
void (*action)(void);
} Transition;
// 状态处理函数
void handleIdle(void)
{
printf("System is idle.\n");
}
void handleRunning(void)
{
printf("System is running.\n");
}
void handlePaused(void)
{
printf("System is paused.\n");
}
void handleStopped(void)
{
printf("System is stopped.\n");
}
// 状态转换函数
void stateTransition(State* currentState, Event event)
{
// 定义状态转换表
static Transition transitions[] = {
{STATE_IDLE, EVENT_START, STATE_RUNNING, handleRunning},
{STATE_RUNNING, EVENT_PAUSE, STATE_PAUSED, handlePaused},
{STATE_PAUSED, EVENT_RESUME, STATE_RUNNING, handleRunning},
{STATE_RUNNING, EVENT_STOP, STATE_STOPPED, handleStopped},
{STATE_PAUSED, EVENT_STOP, STATE_STOPPED, handleStopped}
};
int numTransitions = sizeof(transitions) / sizeof(transitions[0]);
// 查找并执行状态转换
for (int i = 0; i < numTransitions; i++)
{
if (transitions[i].current == *currentState
&& transitions[i].event == event)
{
printf("Transition: %d -> %d\n",
*currentState, transitions[i].next);
transitions[i].action();
*currentState = transitions[i].next;
break;
}
}
}
int main()
{
State currentState = STATE_IDLE;
// 模拟状态转换事件
stateTransition(¤tState, EVENT_START);
stateTransition(¤tState, EVENT_PAUSE);
stateTransition(¤tState, EVENT_RESUME);
stateTransition(¤tState, EVENT_STOP);
return 0;
}
运行结果:
Transition: 0 -> 1
System is running.
Transition: 1 -> 2
System is paused.
Transition: 2 -> 1
System is running.
Transition: 1 -> 3
System is stopped.