65.9K
CodeProject 正在变化。 阅读更多。
Home

C 语言中的状态机设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (51投票s)

2019年2月2日

CPOL

20分钟阅读

viewsIcon

251941

downloadIcon

5034

一个紧凑的 C 有限状态机 (FSM) 实现, 易于在嵌入式和 PC 系统中使用

引言

2000年,我为《C/C++ 用户杂志》(R.I.P.)写了一篇题为“C++状态机设计”的文章。有趣的是,那篇旧文章至今仍然可用,并且(在我写这篇文章时),它是谷歌搜索 C++ 状态机的头号结果。这篇文章写于15多年前,但我一直将其基本思想应用到许多项目中。它紧凑、易于理解,并且在大多数情况下,功能刚刚好,可以满足我的需求。

有时,C 是解决问题的合适工具。本文基于“C++状态机设计”一文中的思想,提供了一种替代的 C 语言状态机实现。该设计适用于任何平台,无论是嵌入式还是 PC,以及任何 C 编译器。此状态机具有以下特性:

  • C 语言 – 状态机用 C 语言编写
  • 紧凑 – 占用最少的资源
  • 对象 – 支持单个状态机类型的多个实例化
  • 转移表 – 转移表精确控制状态转移行为
  • 事件 – 每个事件都是一个简单的函数,可接受任何类型的参数
  • 状态动作 – 每个状态动作都是一个单独的函数,如果需要,可以接受一个唯一的事件数据参数
  • 守卫/进入/退出动作 – 可选地,状态机可以为每个状态使用守卫条件和单独的进入/退出动作函数
  • – 可选的多行宏支持通过自动化代码“机械结构”来简化使用
  • 错误检查 – 编译时和运行时检查可及早发现错误
  • 线程安全 – 添加软件锁以使代码线程安全很容易

本文不是关于软件状态机的最佳设计分解实践的教程。我将专注于状态机代码和简单的示例,这些示例的复杂性足以帮助理解其功能和用法。

使用 CMake 创建构建文件。CMake 是免费开源软件。支持 Windows、Linux 和其他工具链。有关更多信息,请参阅 **CMakeLists.txt** 文件。

查看 GitHub 以获取最新源代码

查看其他相关的 GitHub 仓库

背景

有限状态机(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. 设置速度 – 以特定速度启动电机
  2. 停止 – 停止电机

这些事件提供了以任意所需速度启动电机的能力,这也意味着改变已运行电机的速度。或者我们可以完全停止电机。对于电机控制模块,这两个事件或函数被视为外部事件。然而,对于使用我们代码的客户端来说,它们只是普通的函数。

这些事件不是状态机状态。处理这两个事件所需的步骤是不同的。在这种情况下,状态是:

  1. 空闲 — 电机未运转,处于静止状态
    • 什么都不做
  2. 启动 — 从完全停止状态启动电机
    • 开启电机电源
    • 设置电机速度
  3. 改变速度 — 调整已运行电机的速度
    • 改变电机速度
  4. 停止 — 停止正在运行的电机
    • 关闭电机电源
    • 进入空闲状态

可以看出,将电机控制分解为离散的状态,而不是一个庞大的函数,我们可以更容易地管理操作电机的规则。

每个状态机都有一个“当前状态”的概念。这是状态机当前所处的状态。在任何给定时刻,状态机只能处于一个状态。每个特定状态机实例在定义时都可以设置初始状态。然而,该初始状态在对象创建期间不会执行。只有发送到状态机的事件才会导致状态函数执行。

为了以图形方式说明状态和事件,我们使用状态图。下图(图 1)显示了电机控制模块的状态转移。盒子表示一个状态,连接的箭头表示事件转移。带有事件名称的箭头是外部事件,而未加装饰的线被认为是内部事件。(我稍后在文章中会讨论内部事件和外部事件之间的区别。)

图 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()`。

  1. ST_ - 状态函数前缀字符
  2. GD_ - 守卫函数前缀字符
  3. EN_ - 进入函数前缀字符
  4. 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` 中使用。如果状态机状态数量与转移映射条目数量不匹配,将生成编译时错误。

新建状态机步骤

创建新状态机需要几个基本的高级步骤:

  1. 创建一个 `States` 枚举,其中包含每个状态函数的条目
  2. 定义状态函数
  3. 定义事件函数
  4. 使用 `STATE_MAP` 宏创建一个状态映射查找表
  5. 为每个外部事件函数创建转移映射查找表,使用 `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()` 引擎使用整个逻辑序列。

  1. 评估状态转移表。如果为 `EVENT_IGNORED`,则忽略事件,不执行转移。如果为 `CANNOT_HAPPEN`,则软件会出错。否则,继续下一步。
  2. 如果定义了守卫条件,则执行守卫条件函数。如果守卫条件返回 `FALSE`,则忽略状态转移,不调用状态函数。如果守卫返回 `TRUE`,或者不存在守卫条件,则将执行状态函数。
  3. 如果转移到新状态,并且当前状态有退出动作定义,则调用当前状态的退出动作函数。
  4. 如果转移到新状态,并且新状态有进入动作定义,则调用新状态的进入动作函数。
  5. 调用新状态的状态动作函数。新状态现在是当前状态。

生成事件

此时,我们已经有了一个可用的状态机。让我们看看如何向其生成事件。外部事件是通过使用 `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` 示例展示了如何使用守卫、进入和退出动作创建扩展状态机。状态图如下所示:

图 2: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, &centrifugeTestObj)

扩展状态机使用 `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++ 实现。

参考文献

历史

  • 2019年2月2日:初始发布
  • 2021年5月1日:添加了 getter 功能并修复了编译器警告。更新了源代码。
© . All rights reserved.