C 语言中的状态机设计






4.99/5 (51投票s)
一个紧凑的 C 有限状态机 (FSM) 实现,
引言
2000年,我为《C/C++ 用户杂志》(R.I.P.)写了一篇题为“C++状态机设计”的文章。有趣的是,那篇旧文章至今仍然可用,并且(在我写这篇文章时),它是谷歌搜索 C++ 状态机的头号结果。这篇文章写于15多年前,但我一直将其基本思想应用到许多项目中。它紧凑、易于理解,并且在大多数情况下,功能刚刚好,可以满足我的需求。
有时,C 是解决问题的合适工具。本文基于“C++状态机设计”一文中的思想,提供了一种替代的 C 语言状态机实现。该设计适用于任何平台,无论是嵌入式还是 PC,以及任何 C 编译器。此状态机具有以下特性:
- C 语言 – 状态机用 C 语言编写
- 紧凑 – 占用最少的资源
- 对象 – 支持单个状态机类型的多个实例化
- 转移表 – 转移表精确控制状态转移行为
- 事件 – 每个事件都是一个简单的函数,可接受任何类型的参数
- 状态动作 – 每个状态动作都是一个单独的函数,如果需要,可以接受一个唯一的事件数据参数
- 守卫/进入/退出动作 – 可选地,状态机可以为每个状态使用守卫条件和单独的进入/退出动作函数
- 宏 – 可选的多行宏支持通过自动化代码“机械结构”来简化使用
- 错误检查 – 编译时和运行时检查可及早发现错误
- 线程安全 – 添加软件锁以使代码线程安全很容易
本文不是关于软件状态机的最佳设计分解实践的教程。我将专注于状态机代码和简单的示例,这些示例的复杂性足以帮助理解其功能和用法。
使用 CMake 创建构建文件。CMake 是免费开源软件。支持 Windows、Linux 和其他工具链。有关更多信息,请参阅 **CMakeLists.txt** 文件。
查看 GitHub 以获取最新源代码
- C语言状态机设计 - 作者:David Lafreniere
查看其他相关的 GitHub 仓库
- C++状态机设计 - 作者:David Lafreniere
- C语言带线程的状态机 - 作者:David Lafreniere
背景
有限状态机(FSM)是大多数程序员武器库中常见的技术。设计人员使用这种编程构造将复杂问题分解为可管理的 States 和 State Transitions。实现状态机的途径不计其数。
switch
语句提供了一种最易于实现且最常见的状态机版本。在这里,`switch` 语句中的每个 `case` 都变成一个状态,实现方式如下:
switch (currentState) {
case ST_IDLE:
// do something in the idle state
break;
case ST_STOP:
// do something in the stop state
break;
// etc...
}
这种方法无疑适用于解决许多不同的设计问题。然而,当应用于事件驱动的多线程项目时,这种形式的状态机可能会受到很大限制。
第一个问题在于控制哪些状态转移是有效的,哪些是无效的。无法强制执行状态转移规则。任何转移都可以随时发生,这不太理想。对于大多数设计,只有少数几种转移模式是有效的。理想情况下,软件设计应该强制执行这些预定义的 State 序列,并阻止不必要的转移。当尝试将数据发送到特定状态时,会出现另一个问题。由于整个状态机位于单个函数中,因此向任何给定状态发送额外数据会非常困难。最后,这些设计很少适用于多线程系统。设计人员必须确保状态机是从单一控制线程调用的。
为什么要使用状态机?
使用状态机实现代码是解决复杂工程问题的极其有用的设计技术。状态机将设计分解为一系列步骤,即状态机术语中的“状态”。每个状态执行一项非常狭窄的任务。另一方面,事件是触发状态机在状态之间移动或转移的刺激。
以一个简单的例子来说明,我将在本文中一直使用这个例子,假设我们正在设计电机控制软件。我们想启动和停止电机,以及改变电机的速度。很简单。将暴露给客户端软件的电机控制事件如下:
- 设置速度 – 以特定速度启动电机
- 停止 – 停止电机
这些事件提供了以任意所需速度启动电机的能力,这也意味着改变已运行电机的速度。或者我们可以完全停止电机。对于电机控制模块,这两个事件或函数被视为外部事件。然而,对于使用我们代码的客户端来说,它们只是普通的函数。
这些事件不是状态机状态。处理这两个事件所需的步骤是不同的。在这种情况下,状态是:
- 空闲 — 电机未运转,处于静止状态
- 什么都不做
- 启动 — 从完全停止状态启动电机
- 开启电机电源
- 设置电机速度
- 改变速度 — 调整已运行电机的速度
- 改变电机速度
- 停止 — 停止正在运行的电机
- 关闭电机电源
- 进入空闲状态
可以看出,将电机控制分解为离散的状态,而不是一个庞大的函数,我们可以更容易地管理操作电机的规则。
每个状态机都有一个“当前状态”的概念。这是状态机当前所处的状态。在任何给定时刻,状态机只能处于一个状态。每个特定状态机实例在定义时都可以设置初始状态。然而,该初始状态在对象创建期间不会执行。只有发送到状态机的事件才会导致状态函数执行。
为了以图形方式说明状态和事件,我们使用状态图。下图(图 1)显示了电机控制模块的状态转移。盒子表示一个状态,连接的箭头表示事件转移。带有事件名称的箭头是外部事件,而未加装饰的线被认为是内部事件。(我稍后在文章中会讨论内部事件和外部事件之间的区别。)
正如您所看到的,当事件到来时,发生的状态转移取决于状态机的当前状态。例如,当收到 `SetSpeed` 事件时,如果电机处于 `Idle` 状态,它将转移到 `Start` 状态。但是,在当前状态为 `Start` 时生成的同一个 `SetSpeed` 事件会将电机转移到 `ChangeSpeed` 状态。您还可以看到并非所有状态转移都是有效的。例如,电机在不先经过 `Stop` 状态的情况下,无法从 `ChangeSpeed` 转移到 `Idle`。
简而言之,使用状态机可以捕捉和强制执行复杂的交互,否则这些交互可能难以传达和实现。
内部事件和外部事件
正如我之前提到的,事件是触发状态机在状态之间转移的刺激。例如,按钮按下可能是一个事件。事件可以分为两类:外部事件和内部事件。外部事件,在其最基本层面,是对状态机模块的函数调用。这些函数是公共的,从外部或状态机对象外部的代码调用。系统中的任何线程或任务都可以生成外部事件。如果外部事件函数调用导致状态转移,则状态将在调用者的控制线程中同步执行。另一方面,内部事件是由状态机本身在状态执行期间自行生成的。
典型的场景是生成一个外部事件,这归结为对模块公共接口的函数调用。根据生成的事件和状态机的当前状态,执行查找以确定是否需要转移。如果需要,状态机会转移到新状态,新状态的代码将执行。在状态函数结束时,会执行一个检查,以确定是否生成了内部事件。如果是,则执行另一个转移,新状态将有机会执行。此过程将继续,直到状态机不再生成内部事件,此时原始外部事件函数调用返回。外部事件和所有内部事件(如果有)将在调用者的控制线程中执行。
一旦外部事件启动状态机执行,在外部事件和所有内部事件执行完成之前,它不会被另一个外部事件中断(如果使用了锁)。这种“运行到完成”模型为状态转移提供了多线程安全的运行环境。信号量或互斥量可用于状态机引擎,以阻止可能尝试同时访问同一状态机实例的其他线程。请参阅源代码函数 `_SM_ExternalEvent()` 的注释,了解锁的位置。
事件数据
当生成事件时,它可以选择性地附加事件数据,供状态函数在执行期间使用。事件数据是指向任何内置或用户定义数据类型的单个 `const` 或非 `const` 指针。
一旦状态完成执行,事件数据就被认为已用完,必须删除。因此,发送到状态机的任何事件数据都必须通过 `SM_XAlloc()` 动态创建。状态机引擎自动使用 `SM_XFree()` 释放分配的事件数据。
状态转移
当生成外部事件时,会执行查找以确定状态转移的行动方案。事件有三种可能的后果:新状态、事件被忽略或无法发生。新状态会导致转移到一个允许执行的新状态。转移到现有状态也是可能的,这意味着当前状态将被重新执行。对于被忽略的事件,不执行任何状态。但是,事件数据(如果有)将被删除。最后一种可能性,“无法发生”,保留给给定状态机的当前状态下事件无效的情况。如果发生这种情况,软件将出错。
在此实现中,不需要内部事件来执行验证转移查找。假定状态转移是有效的。您可以检查内部和外部事件转移的有效性,但在实践中,这只会占用更多存储空间并产生不必要的忙碌,收益却很小。验证转移的真正需求在于异步的外部事件,此时客户端可能会在不适当的时间触发事件。一旦状态机开始执行,它就不能被中断。它处于私有实现的控制之下,因此不需要进行转移检查。这为设计人员提供了通过内部事件更改状态的自由,而无需更新转移表的负担。
StateMachine 模块
状态机源代码包含在 `StateMachine.c` 和 `StateMachine.h` 文件中。下面的代码显示了部分头文件。`StateMachine` 头文件包含各种预处理器多行宏,以简化状态机的实现。
enum { EVENT_IGNORED = 0xFE, CANNOT_HAPPEN = 0xFF };
typedef void NoEventData;
// State machine constant data
typedef struct
{
const CHAR* name;
const BYTE maxStates;
const struct SM_StateStruct* stateMap;
const struct SM_StateStructEx* stateMapEx;
} SM_StateMachineConst;
// State machine instance data
typedef struct
{
const CHAR* name;
void* pInstance;
BYTE newState;
BYTE currentState;
BOOL eventGenerated;
void* pEventData;
} SM_StateMachine;
// Generic state function signatures
typedef void (*SM_StateFunc)(SM_StateMachine* self, void* pEventData);
typedef BOOL (*SM_GuardFunc)(SM_StateMachine* self, void* pEventData);
typedef void (*SM_EntryFunc)(SM_StateMachine* self, void* pEventData);
typedef void (*SM_ExitFunc)(SM_StateMachine* self);
typedef struct SM_StateStruct
{
SM_StateFunc pStateFunc;
} SM_StateStruct;
typedef struct SM_StateStructEx
{
SM_StateFunc pStateFunc;
SM_GuardFunc pGuardFunc;
SM_EntryFunc pEntryFunc;
SM_ExitFunc pExitFunc;
} SM_StateStructEx;
// Public functions
#define SM_Event(_smName_, _eventFunc_, _eventData_) \
_eventFunc_(&_smName_##Obj, _eventData_)
// Protected functions
#define SM_InternalEvent(_newState_, _eventData_) \
_SM_InternalEvent(self, _newState_, _eventData_)
#define SM_GetInstance(_instance_) \
(_instance_*)(self->pInstance);
// Private functions
void _SM_ExternalEvent(SM_StateMachine* self,
const SM_StateMachineConst* selfConst, BYTE newState, void* pEventData);
void _SM_InternalEvent(SM_StateMachine* self, BYTE newState, void* pEventData);
void _SM_StateEngine(SM_StateMachine* self, const SM_StateMachineConst* selfConst);
void _SM_StateEngineEx(SM_StateMachine* self, const SM_StateMachineConst* selfConst);
#define SM_DECLARE(_smName_) \
extern SM_StateMachine _smName_##Obj;
#define SM_DEFINE(_smName_, _instance_) \
SM_StateMachine _smName_##Obj = { #_smName_, _instance_, \
0, 0, 0, 0 };
#define EVENT_DECLARE(_eventFunc_, _eventData_) \
void _eventFunc_(SM_StateMachine* self, _eventData_* pEventData);
#define EVENT_DEFINE(_eventFunc_, _eventData_) \
void _eventFunc_(SM_StateMachine* self, _eventData_* pEventData)
#define STATE_DECLARE(_stateFunc_, _eventData_) \
static void ST_##_stateFunc_(SM_StateMachine* self, _eventData_* pEventData);
#define STATE_DEFINE(_stateFunc_, _eventData_) \
static void ST_##_stateFunc_(SM_StateMachine* self, _eventData_* pEventData)
`SM_Event()` 宏用于生成外部事件,而 `SM_InternalEvent()` 在状态函数执行期间生成内部事件。`SM_GetInstance()` 获取指向当前状态机对象的指针。
`SM_DECLARE` 和 `SM_DEFINE` 用于创建状态机实例。`EVENT_DECLARE` 和 `EVENT_DEFINE` 创建外部事件函数。最后,`STATE_DECLARE` 和 `STATE_DEFINE` 创建状态函数。
电机示例
`Motor` 实现我们的假设电机控制状态机,客户端可以启动电机(以特定速度)并停止电机。下面显示了 `Motor` 头接口。
#include "StateMachine.h"
// Motor object structure
typedef struct
{
INT currentSpeed;
} Motor;
// Event data structure
typedef struct
{
INT speed;
} MotorData;
// State machine event functions
EVENT_DECLARE(MTR_SetSpeed, MotorData)
EVENT_DECLARE(MTR_Halt, NoEventData)
`Motor` 源文件使用宏来简化使用,隐藏了所需的状态机机械结构。
// State enumeration order must match the order of state
// method entries in the state map
enum States
{
ST_IDLE,
ST_STOP,
ST_START,
ST_CHANGE_SPEED,
ST_MAX_STATES
};
// State machine state functions
STATE_DECLARE(Idle, NoEventData)
STATE_DECLARE(Stop, NoEventData)
STATE_DECLARE(Start, MotorData)
STATE_DECLARE(ChangeSpeed, MotorData)
// State map to define state function order
BEGIN_STATE_MAP(Motor)
STATE_MAP_ENTRY(ST_Idle)
STATE_MAP_ENTRY(ST_Stop)
STATE_MAP_ENTRY(ST_Start)
STATE_MAP_ENTRY(ST_ChangeSpeed)
END_STATE_MAP(Motor)
// Set motor speed external event
EVENT_DEFINE(MTR_SetSpeed, MotorData)
{
// Given the SetSpeed event, transition to a new state based upon
// the current state of the state machine
BEGIN_TRANSITION_MAP // - Current State -
TRANSITION_MAP_ENTRY(ST_START) // ST_Idle
TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop
TRANSITION_MAP_ENTRY(ST_CHANGE_SPEED) // ST_Start
TRANSITION_MAP_ENTRY(ST_CHANGE_SPEED) // ST_ChangeSpeed
END_TRANSITION_MAP(Motor, pEventData)
}
// Halt motor external event
EVENT_DEFINE(MTR_Halt, NoEventData)
{
// Given the Halt event, transition to a new state based upon
// the current state of the state machine
BEGIN_TRANSITION_MAP // - Current State -
TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_Idle
TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop
TRANSITION_MAP_ENTRY(ST_STOP) // ST_Start
TRANSITION_MAP_ENTRY(ST_STOP) // ST_ChangeSpeed
END_TRANSITION_MAP(Motor, pEventData)
}
外部事件
`MTR_SetSpeed` 和 `MTR_Halt` 被视为进入 `Motor` 状态机的外部事件。`MTR_SetSpeed` 接受指向 `MotorData` 事件数据的指针,其中包含电机速度。此数据结构将在状态处理完成后使用 `SM_XFree()` 释放,因此在调用函数之前,必须使用 `SM_XAlloc()` 创建它。
状态枚举
每个状态函数都必须有一个与之关联的枚举。这些枚举用于存储状态机的当前状态。在 `Motor` 中,`States` 提供了这些枚举,它们稍后用于索引转移映射和状态映射查找表。
状态函数
状态函数实现每个状态 — 每个状态机状态对应一个状态函数。`STATE_DECLARE` 用于声明状态函数接口,`STATE_DEFINE` 定义实现。
// State machine sits here when motor is not running
STATE_DEFINE(Idle, NoEventData)
{
printf("%s ST_Idle\n", self->name);
}
// Stop the motor
STATE_DEFINE(Stop, NoEventData)
{
// Get pointer to the instance data and update currentSpeed
Motor* pInstance = SM_GetInstance(Motor);
pInstance->currentSpeed = 0;
// Perform the stop motor processing here
printf("%s ST_Stop: %d\n", self->name, pInstance->currentSpeed);
// Transition to ST_Idle via an internal event
SM_InternalEvent(ST_IDLE, NULL);
}
// Start the motor going
STATE_DEFINE(Start, MotorData)
{
ASSERT_TRUE(pEventData);
// Get pointer to the instance data and update currentSpeed
Motor* pInstance = SM_GetInstance(Motor);
pInstance->currentSpeed = pEventData->speed;
// Set initial motor speed processing here
printf("%s ST_Start: %d\n", self->name, pInstance->currentSpeed);
}
// Changes the motor speed once the motor is moving
STATE_DEFINE(ChangeSpeed, MotorData)
{
ASSERT_TRUE(pEventData);
// Get pointer to the instance data and update currentSpeed
Motor* pInstance = SM_GetInstance(Motor);
pInstance->currentSpeed = pEventData->speed;
// Perform the change motor speed here
printf("%s ST_ChangeSpeed: %d\n", self->name, pInstance->currentSpeed);
}
`STATE_DECLARE` 和 `STATE_DEFINE` 使用两个参数。第一个参数是状态函数名。第二个参数是事件数据类型。如果不需要事件数据,请使用 `NoEventData`。还提供了用于创建守卫、退出和进入动作的宏,这些宏将在本文后面进行解释。
`SM_GetInstance()` 宏获取状态机对象的实例。宏的参数是状态机名称。
在此实现中,所有状态机函数都必须遵循以下签名:
// Generic state function signatures
typedef void (*SM_StateFunc)(SM_StateMachine* self, void* pEventData);
typedef BOOL (*SM_GuardFunc)(SM_StateMachine* self, void* pEventData);
typedef void (*SM_EntryFunc)(SM_StateMachine* self, void* pEventData);
typedef void (*SM_ExitFunc)(SM_StateMachine* self);
每个 `SM_StateFunc` 接受指向 `SM_StateMachine` 对象和事件数据的指针。如果使用 `NoEventData`,则 `pEventData` 参数将为 `NULL`。否则,`pEventData` 参数的类型是 `STATE_DEFINE` 中指定的类型。
在 `Motor` 的 `Start` 状态函数中,`STATE_DEFINE(Start, MotorData)` 宏展开为:
void ST_Start(SM_StateMachine* self, MotorData* pEventData)
请注意,每个状态函数都有 `self` 和 `pEventData` 参数。`self` 是状态机对象的指针,`pEventData` 是事件数据。另请注意,宏在状态名称前加上“ST_
”以创建函数 `ST_Start()`。
同样,`Stop` 状态函数 `STATE_DEFINE(Stop, NoEventData)` 展开为:
void ST_Stop(SM_StateMachine* self, void* pEventData)
`Stop` 不接受事件数据,因此 `pEventData` 参数为 `void*`。
三个字符会自动添加到每个状态/守卫/进入/退出函数中。例如,如果使用 `STATE_DEFINE(Idle, NoEventData)` 声明一个函数,则实际的状态函数名为 `ST_Idle()`。
ST_
- 状态函数前缀字符GD_
- 守卫函数前缀字符EN_
- 进入函数前缀字符EX_
- 退出函数前缀字符
`SM_GuardFunc` 和 `SM_Entry` 函数的 `typedef` 也接受事件数据。`SM_ExitFunc` 是独特的,因为它不允许任何事件数据。
状态映射
状态机引擎通过使用 `currentState` 变量来知道调用哪个状态函数。状态映射将 `currentState` 变量映射到一个特定的状态函数。例如,如果 `currentState` 是 `2`,则将调用第三个状态映射函数指针条目(从零开始计数)。状态映射表使用以下三个宏创建:
BEGIN_STATE_MAP
STATE_MAP_ENTRY
END_STATE_MAP
`BEGIN_STATE_MAP` 启动状态映射序列。每个 `STATE_MAP_ENTRY` 都有一个状态函数名参数。`END_STATE_MAP` 终止映射。下面显示了 `Motor` 的状态映射:
BEGIN_STATE_MAP(Motor)
STATE_MAP_ENTRY(ST_Idle)
STATE_MAP_ENTRY(ST_Stop)
STATE_MAP_ENTRY(ST_Start)
STATE_MAP_ENTRY(ST_ChangeSpeed)
END_STATE_MAP
或者,守卫/进入/退出特性需要使用宏的 `_EX`(扩展)版本。
BEGIN_STATE_MAP_EX
STATE_MAP_ENTRY_EX or STATE_MAP_ENTRY_ALL_EX
END_STATE_MAP_EX
`STATE_MAP_ENTRY_ALL_EX` 宏有四个参数,依次是状态动作、守卫条件、进入动作和退出动作。状态动作是必需的,而其他动作是可选的。如果状态没有动作,则对该参数使用 `0`。如果一个状态没有任何守卫/进入/退出选项,`STATE_MAP_ENTRY_EX` 宏会将所有未使用的选项默认为 `0`。下面的宏片段是本文后面将介绍的高级示例。
// State map to define state function order
BEGIN_STATE_MAP_EX(CentrifugeTest)
STATE_MAP_ENTRY_ALL_EX(ST_Idle, 0, EN_Idle, 0)
STATE_MAP_ENTRY_EX(ST_Completed)
STATE_MAP_ENTRY_EX(ST_Failed)
STATE_MAP_ENTRY_ALL_EX(ST_StartTest, GD_StartTest, 0, 0)
STATE_MAP_ENTRY_EX(ST_Acceleration)
STATE_MAP_ENTRY_ALL_EX(ST_WaitForAcceleration, 0, 0, EX_WaitForAcceleration)
STATE_MAP_ENTRY_EX(ST_Deceleration)
STATE_MAP_ENTRY_ALL_EX(ST_WaitForDeceleration, 0, 0, EX_WaitForDeceleration)
END_STATE_MAP_EX(CentrifugeTest)
别忘了为每个函数添加前缀字符(ST_
、GD_
、EN_
或 EX_
)。
状态机对象
在 C++ 中,对象是语言不可或缺的一部分。使用 C,你需要更努力地工作才能实现类似的行为。这种 C 语言状态机支持多个状态机对象(或实例),而不是单一的、静态的状态机实现。
`SM_StateMachine` 数据结构存储状态机实例数据;每个状态机实例一个对象。`SM_StateMachineConst` 数据结构存储常量数据;每个状态机类型一个常量对象。
状态机使用 `SM_DEFINE` 宏定义。第一个参数是状态机名称。第二个参数是指向用户定义的状态机结构的指针,如果用户对象不存在则为 `NULL`。
#define SM_DEFINE(_smName_, _instance_) \
SM_StateMachine _smName_##Obj = { #_smName_, _instance_, \
0, 0, 0, 0 };
在本例中,状态机名称是 `Motor`,并且创建了两个对象和两个状态机。
// Define motor objects
static Motor motorObj1;
static Motor motorObj2;
// Define two public Motor state machine instances
SM_DEFINE(Motor1SM, &motorObj1)
SM_DEFINE(Motor2SM, &motorObj2)
每个电机对象独立处理状态执行。`Motor` 结构用于存储状态机实例特定的数据。在状态函数中,使用 `SM_GetInstance()` 在运行时获取指向 `Motor` 对象的指针。
// Get pointer to the instance data and update currentSpeed
Motor* pInstance = SM_GetInstance(Motor);
pInstance->currentSpeed = pEventData->speed;
转移映射
需要处理的最后一个细节是状态转移规则。状态机如何知道应该发生哪些转移?答案是转移映射。转移映射是一个查找表,将 `currentState` 变量映射到状态枚举常量。每个外部事件函数都有一个使用三个宏创建的转移映射表:
BEGIN_TRANSITION_MAP
TRANSITION_MAP_ENTRY
END_TRANSITION_MAP
`Motor` 中的 `MTR_Halt` 事件函数定义转移映射如下:
// Halt motor external event
EVENT_DEFINE(MTR_Halt, NoEventData)
{
// Given the Halt event, transition to a new state based upon
// the current state of the state machine
BEGIN_TRANSITION_MAP // - Current State -
TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_Idle
TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop
TRANSITION_MAP_ENTRY(ST_STOP) // ST_Start
TRANSITION_MAP_ENTRY(ST_STOP) // ST_ChangeSpeed
END_TRANSITION_MAP(Motor, pEventData)
}
`BEGIN_TRANSITION_MAP` 启动映射。其后的每个 `TRANSITION_MAP_ENTRY` 表明状态机应根据当前状态执行的操作。每个转移映射表中的条目数必须与状态函数的确切数量匹配。在本例中,我们有四个状态函数,因此需要四个转移映射条目。每个条目的位置与状态映射中定义的状态函数的顺序相匹配。因此,`MTR_Halt` 函数中的第一个条目表示 `EVENT_IGNORED`,如下所示:
TRANSITION_MAP_ENTRY (EVENT_IGNORED) // ST_Idle
这被解释为“如果当前状态是 `Idle` 状态时发生 `Halt` 事件,则忽略该事件。”
类似地,映射中的第三个条目是:
TRANSITION_MAP_ENTRY (ST_STOP) // ST_Start
这表明“如果当前状态是 `Start` 状态时发生 `Halt` 事件,则转移到 `Stop` 状态。”
`END_TRANSITION_MAP` 终止映射。此宏的第一个参数是状态机名称。第二个参数是事件数据。
`C_ASSERT()` 宏在 `END_TRANSITION_MAP` 中使用。如果状态机状态数量与转移映射条目数量不匹配,将生成编译时错误。
新建状态机步骤
创建新状态机需要几个基本的高级步骤:
- 创建一个 `States` 枚举,其中包含每个状态函数的条目
- 定义状态函数
- 定义事件函数
- 使用 `STATE_MAP` 宏创建一个状态映射查找表
- 为每个外部事件函数创建转移映射查找表,使用 `TRANSITION_MAP` 宏
状态引擎
状态引擎根据生成的事件执行状态函数。转移映射是一个 `SM_StateStruct` 实例的数组,由 `currentState` 变量索引。当 `_SM_StateEngine()` 函数执行时,它会在 `SM_StateStruct` 数组中查找正确的状态函数。在状态函数有机会执行后,它会释放事件数据(如果有),然后再检查是否通过 `SM_InternalEvent()` 生成了任何内部事件。
// The state engine executes the state machine states
void _SM_StateEngine(SM_StateMachine* self, SM_StateMachineConst* selfConst)
{
void* pDataTemp = NULL;
ASSERT_TRUE(self);
ASSERT_TRUE(selfConst);
// While events are being generated keep executing states
while (self->eventGenerated)
{
// Error check that the new state is valid before proceeding
ASSERT_TRUE(self->newState < selfConst->maxStates);
// Get the pointers from the state map
SM_StateFunc state = selfConst->stateMap[self->newState].pStateFunc;
// Copy of event data pointer
pDataTemp = self->pEventData;
// Event data used up, reset the pointer
self->pEventData = NULL;
// Event used up, reset the flag
self->eventGenerated = FALSE;
// Switch to the new current state
self->currentState = self->newState;
// Execute the state action passing in event data
ASSERT_TRUE(state != NULL);
state(self, pDataTemp);
// If event data was used, then delete it
if (pDataTemp)
{
SM_XFree(pDataTemp);
pDataTemp = NULL;
}
}
}
状态引擎对守卫、进入、状态和退出动作的逻辑通过以下顺序表达。`_SM_StateEngine()` 引擎仅实现以下 #1 和 #5。扩展的 `_SM_StateEngineEx()` 引擎使用整个逻辑序列。
- 评估状态转移表。如果为 `EVENT_IGNORED`,则忽略事件,不执行转移。如果为 `CANNOT_HAPPEN`,则软件会出错。否则,继续下一步。
- 如果定义了守卫条件,则执行守卫条件函数。如果守卫条件返回 `FALSE`,则忽略状态转移,不调用状态函数。如果守卫返回 `TRUE`,或者不存在守卫条件,则将执行状态函数。
- 如果转移到新状态,并且当前状态有退出动作定义,则调用当前状态的退出动作函数。
- 如果转移到新状态,并且新状态有进入动作定义,则调用新状态的进入动作函数。
- 调用新状态的状态动作函数。新状态现在是当前状态。
生成事件
此时,我们已经有了一个可用的状态机。让我们看看如何向其生成事件。外部事件是通过使用 `SM_XAlloc()` 动态创建事件数据结构、为结构成员变量赋值,然后使用 `SM_Event()` 宏调用外部事件函数来生成的。以下代码片段演示了如何进行同步调用。
MotorData* data;
// Create event data
data = SM_XAlloc(sizeof(MotorData));
data->speed = 100;
// Call MTR_SetSpeed event function to start motor
SM_Event(Motor1SM, MTR_SetSpeed, data);
`SM_Event()` 的第一个参数是状态机名称。第二个参数是要调用的事件函数。第三个参数是事件数据,如果没有数据则为 `NULL`。
要从状态函数内部生成内部事件,请调用 `SM_InternalEvent()`。如果目标不接受事件数据,则最后一个参数为 `NULL`。否则,使用 `SM_XAlloc()` 创建事件数据。
SM_InternalEvent(ST_IDLE, NULL);
在上例中,一旦状态函数执行完成,状态机将转移到 `ST_Idle` 状态。另一方面,如果需要将事件数据发送到目标状态,则需要动态创建该数据结构并将其作为参数传递。
MotorData* data;
data = SM_XAlloc(sizeof(MotorData));
data->speed = 100;
SM_InternalEvent(ST_CHANGE_SPEED, data);
无堆使用
所有状态机的事件数据都必须动态创建。然而,在某些系统上,使用堆是不受欢迎的。包含的 `x_allocator` 模块是一个固定块内存分配器,可消除堆使用。在 `StateMachine.c` 中定义 `USE_SM_ALLOCATOR` 以使用固定块分配器。有关 `x_allocator` 的信息,请参阅下面的“参考文献”部分。
离心机测试示例
`CentrifugeTest` 示例展示了如何使用守卫、进入和退出动作创建扩展状态机。状态图如下所示:
创建了一个 `CentrifgeTest` 对象和状态机。这里唯一的区别是状态机是一个单例,这意味着对象是 `private` 的,并且只能创建一个 `CentrifugeTest` 实例。这与 `Motor` 状态机不同,后者允许创建多个实例。
// CentrifugeTest object structure
typedef struct
{
INT speed;
BOOL pollActive;
} CentrifugeTest;
// Define private instance of motor state machine
CentrifugeTest centrifugeTestObj;
SM_DEFINE(CentrifugeTestSM, ¢rifugeTestObj)
扩展状态机使用 `ENTRY_DECLARE`、`GUARD_DECLARE` 和 `EXIT_DECLARE` 宏。
// State enumeration order must match the order of state
// method entries in the state map
enum States
{
ST_IDLE,
ST_COMPLETED,
ST_FAILED,
ST_START_TEST,
ST_ACCELERATION,
ST_WAIT_FOR_ACCELERATION,
ST_DECELERATION,
ST_WAIT_FOR_DECELERATION,
ST_MAX_STATES
};
// State machine state functions
STATE_DECLARE(Idle, NoEventData)
ENTRY_DECLARE(Idle, NoEventData)
STATE_DECLARE(Completed, NoEventData)
STATE_DECLARE(Failed, NoEventData)
STATE_DECLARE(StartTest, NoEventData)
GUARD_DECLARE(StartTest, NoEventData)
STATE_DECLARE(Acceleration, NoEventData)
STATE_DECLARE(WaitForAcceleration, NoEventData)
EXIT_DECLARE(WaitForAcceleration)
STATE_DECLARE(Deceleration, NoEventData)
STATE_DECLARE(WaitForDeceleration, NoEventData)
EXIT_DECLARE(WaitForDeceleration)
// State map to define state function order
BEGIN_STATE_MAP_EX(CentrifugeTest)
STATE_MAP_ENTRY_ALL_EX(ST_Idle, 0, EN_Idle, 0)
STATE_MAP_ENTRY_EX(ST_Completed)
STATE_MAP_ENTRY_EX(ST_Failed)
STATE_MAP_ENTRY_ALL_EX(ST_StartTest, GD_StartTest, 0, 0)
STATE_MAP_ENTRY_EX(ST_Acceleration)
STATE_MAP_ENTRY_ALL_EX(ST_WaitForAcceleration, 0, 0, EX_WaitForAcceleration)
STATE_MAP_ENTRY_EX(ST_Deceleration)
STATE_MAP_ENTRY_ALL_EX(ST_WaitForDeceleration, 0, 0, EX_WaitForDeceleration)
END_STATE_MAP_EX(CentrifugeTest)
注意 `_EX` 扩展状态映射宏,以便支持守卫/进入/退出特性。每个守卫/进入/退出 `DECLARE` 宏必须与 `DEFINE` 匹配。例如,`StartTest` 状态函数的守卫条件声明如下:
GUARD_DECLARE(StartTest, NoEventData)
守卫条件函数返回 `TRUE` 表示执行状态函数,否则返回 `FALSE`。
// Guard condition to determine whether StartTest state is executed.
GUARD_DEFINE(StartTest, NoEventData)
{
printf("%s GD_StartTest\n", self->name);
if (centrifugeTestObj.speed == 0)
return TRUE; // Centrifuge stopped. OK to start test.
else
return FALSE; // Centrifuge spinning. Can't start test.
}
多线程安全
为了防止在状态机执行过程中被另一个线程抢占,`StateMachine` 模块可以在 `_SM_ExternalEvent()` 函数中使用锁。在允许外部事件执行之前,可以锁定一个信号量。当外部事件和所有内部事件处理完毕后,软件锁将被释放,从而允许另一个外部事件进入状态机实例。
注释指出了如果应用程序是多线程的*并且*多个线程能够访问单个状态机实例,则锁和解锁应放置的位置。请注意,每个 `StateMachine` 对象都应该有自己的软件锁实例。这可以防止单个实例锁定并阻止所有其他 `StateMachine` 对象执行。只有当状态机实例由多个控制线程调用时,才需要软件锁。如果不是,则不需要锁。
结论
与旧的 `switch` 语句风格相比,使用这种方法实现状态机可能需要额外的努力。然而,其回报是一个更强大的设计,能够一致地应用于整个多线程系统。让每个状态都拥有自己的函数比一个巨大的 `switch` 语句更容易阅读,并且允许将独特的事件数据发送到每个状态。此外,验证状态转移通过消除不希望的状态转移引起的副作用,可以防止客户端误用。
此 C 语言版本是多年来我在不同项目中使用过的 C++ 实现的近乎翻译。如果使用 C++,请考虑参考文献部分中的 C++ 实现。
参考文献
- C++状态机设计 - 作者:David Lafreniere
- C语言固定块内存分配器 - 作者:David Lafreniere
历史
- 2019年2月2日:初始发布
- 2021年5月1日:添加了 getter 功能并修复了编译器警告。更新了源代码。