C/C++教程-C语言的状态机设计

开课吧小一2021-04-02 15:52

介绍

2000年,我为C/C++用户杂志(R.I.P.)写了一篇题为 "C++中的状态机设计 "的文章。有趣的是,那篇老文章现在还在,而且(在写这篇文章的时候),在Google上搜索C++状态机时,它的点击率排名第一。这篇文章写于15年前,但我在众多项目中继续使用这个基本思想。它结构紧凑,易于理解,而且在大多数情况下,它的功能足以完成我需要的东西。

C/C++教程-C语言的状态机设计

有时候,C语言是最合适的工具。本文根据《C++中的状态机设计》一文中提出的思想,提供了一个另类的C语言状态机实现。该设计适用于任何平台,嵌入式或PC,使用任何C编译器。这个状态机具有以下特点。

- C语言 - 用C语言编写的状态机

- 紧凑--消耗的资源最少

- 对象--支持单一状态机类型的多个实例。

- 过渡表--过渡表精确控制状态转换行为

- 事件--每个事件都是一个简单的函数,有任何参数类型。

- 状态动作--每个状态动作都是一个独立的函数,如果需要的话,它有一个单一的、唯一的事件数据参数。

- 守护/进入/退出动作--状态机可以选择使用守护条件和每个状态的单独进入/退出动作功能。

- 宏--可选的多行宏支持,通过自动化代码 "机械 "简化了使用。

- 错误检查--编译时和运行时的检查可以及早发现错误。

- 线程安全--添加软件锁以使代码线程安全很容易。

本文不是软件状态机的最佳设计分解实践教程。我将专注于状态机代码和简单的例子,复杂程度刚好方便理解其功能和用法。

背景

大多数程序员常用的设计技术是古老的有限状态机(FSM)。设计人员使用这种编程结构将复杂的问题分解成可管理的状态和状态转换。有无数种方法可以实现一个状态机。

开关语句提供了一个最容易实现和最常见的状态机版本。在这里,switch语句内的每一个case都会成为一个状态,实现方式类似于。

C++
Copy Code
switch (currentState) {
   case ST_IDLE:
       // do something in the idle state
       break;

    case ST_STOP:
       // do something in the stop state
       break;

    // etc...
}

这种方法当然适合解决许多不同的设计问题。然而,当在一个事件驱动的多线程项目中使用时,这种形式的状态机可能会有很大的局限性。

第一个问题是围绕着控制哪些状态转换是有效的,哪些是无效的。没有办法强制执行状态转换规则。任何时候都允许有任何过渡,这不是特别理想。对于大多数设计来说,只有少数过渡模式是有效的。理想情况下,软件设计应该强制执行这些预定义的状态序列,防止不需要的过渡。另一个问题出现在试图将数据发送到特定状态时。由于整个状态机位于一个函数内,因此向任何给定状态发送额外的数据被证明是困难的。最后,这些设计很少适合在多线程系统中使用。设计者必须确保状态机是从一个控制线程中调用的。

为什么要使用状态机?

使用状态机实现代码是解决复杂工程问题的一种极为方便的设计技术。状态机将设计分解成一系列的步骤,或者在状态机的行话中被称为状态。每个状态都执行一些狭义的任务。另一方面,事件是刺激,它导致状态机在状态之间移动或转换。

举一个简单的例子,我将在本文中使用这个例子,假设我们正在设计电机控制软件。我们要启动和停止电机,以及改变电机的速度。这很简单。要暴露给客户端软件的电机控制事件将如下。

1. 设置速度--设置电机以特定的速度运行; 2.

2. 停止--停止电机

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

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

1. 闲置--电机没有转动,而是处于静止状态; 2.

o 什么也不做

2.

o 打开电机电源

o 设置电机转速

3.

o 改变电机转速

4. 停止--停止移动的电机

o 关闭电机电源

o 进入闲置状态

可见,将电机控制分解成不同的状态,而不是只有一个单一的功能,我们可以更方便地管理电机的运行规则。

每个状态机都有一个 "当前状态 "的概念。这就是状态机当前所处的状态。在任何特定的时刻,状态机只能处于单一的状态。每一个特定的状态机实例在定义时都可以设置初始状态。然而,该初始状态在对象创建过程中并不执行。只有发送到状态机的事件才会引起状态函数的执行。

为了形象地说明状态和事件,我们使用一个状态图。下图1显示了电机控制模块的状态转换。一个方框表示一个状态,一个连接箭头表示事件转换。列有事件名称的箭头为外部事件,而未加装饰的线条则认为是内部事件。(内部事件和外部事件的区别我将在后面的文章中介绍)。

C/C++教程-C语言的状态机设计

图1:电机状态图

A你可以看到,当一个事件发生时,发生的状态转换取决于状态机的当前状态。例如,当一个SetSpeed事件发生时,电机处于Idle状态,它就会过渡到Start状态。然而,当当前状态为Start时产生的同样的SetSpeed事件会使电机过渡到ChangeSpeed状态。您还可以看到,并非所有的状态转换都是有效的。例如,如果不先经过Stop状态,电机就不能从ChangeSpeed状态过渡到Idle状态。

简而言之,使用状态机可以捕捉和执行复杂的交互,否则可能很难传达和实现。

内部和外部活动

正如我前面提到的,事件是导致状态机在状态之间转换的刺激。例如,按下按钮可以是一个事件。事件可以分为两类:外部事件和内部事件。外部事件,在最基本的层面上,是对状态机模块的函数调用。这些函数是公开的,从外部或状态机对象外部的代码中调用。系统内的任何线程或任务都可以产生外部事件。如果外部事件函数调用导致状态转换发生,状态将在调用者的控制线程内同步执行。而内部事件则是状态机在状态执行过程中自己产生的。

一个典型的场景包括一个外部事件被生成,这同样归结为对模块公共接口的函数调用。根据生成的事件和状态机的当前状态,进行查找以确定是否需要转换。如果需要,状态机就会过渡到新的状态,并执行该状态的代码。在状态函数结束时,执行检查以确定是否有内部事件产生。如果是,则执行另一次过渡,新状态得到执行的机会。这个过程一直持续到状态机不再产生内部事件为止,这时原外部事件函数调用返回。外部事件和所有内部事件(如果有的话)在调用者的控制线程内执行。

一旦外部事件启动状态机执行,在外部事件和所有内部事件完成执行之前,如果使用锁,则不能被另一个外部事件打断。这种运行到完成的模式为状态转换提供了一个多线程安全的环境。Semaphores或mutexes可以在状态机引擎中使用,以阻止其他线程可能试图同时访问同一个状态机实例。关于锁的去向,请参见源码函数_SM_ExternalEvent()注释。

事件数据

当一个事件产生时,它可以选择性地附加事件数据,以便在执行过程中被状态函数使用。事件数据是指向任何内置或用户定义的数据类型的单个const或非const指针。

一旦状态完成执行,事件数据就被认为已经用完,必须删除。因此,任何发送到状态机的事件数据必须通过SM_XAlloc()动态创建。状态机引擎使用SM_XFree()自动释放分配的事件数据。

状态转换

当外部事件产生时,会进行查找,以确定状态转换的动作过程。一个事件有三种可能的结果:新状态、事件被忽略或不能发生。新状态会导致过渡到一个允许执行的新状态。也可以过渡到现有状态,这意味着当前状态被重新执行。对于一个被忽略的事件,没有状态执行。但是,事件数据(如果有的话)会被删除。最后一种可能性,不能发生,是保留给状态机当前状态下事件无效的情况。如果发生这种情况,软件就会出现故障。

在本实施例中,内部事件不需要执行验证性过渡查找。状态转换被假定为有效。你可以同时检查有效的内部和外部事件过渡,但实际上,这只是占用了更多的存储空间,并且产生了忙乱的工作,收益甚微。真正需要验证过渡的地方在于异步、外部事件,在这些事件中,客户端可以在不适当的时间导致事件发生。状态机一旦执行,就不能中断。它处于私有实现的控制之下,从而使过渡检查成为不必要。这使得设计者可以自由地通过内部事件来改变状态,而没有更新过渡表的负担。

状态机模块

状态机的源代码包含在StateMachine.c和StateMachine.h文件中。下面的代码显示了部分头。StateMachine头包含了各种预处理器的多行宏,以方便状态机的实现。

C++
Shrink ▲   Copy Code
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创建状态函数。

电机实例

电机实现了我们假设的电机控制状态机,客户端可以启动电机,以特定的速度,停止电机。电机头界面如下图所示。

C++
Copy Code
#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)

电机源文件使用宏来简化使用,隐藏所需的状态机机械。

C++
Shrink ▲   Copy Code
// 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 被认为是进入电机状态机的外部事件。MTR_SetSpeed取一个指向MotorData事件数据的指针,包含电机速度。这个数据结构将在状态处理完成后使用SM_XFree()释放,所以在函数调用前必须使用SM_XAlloc()创建这个结构。

状态举例

每个状态函数必须有一个与之相关的枚举。这些枚举用来存储状态机的当前状态。在Motor中,States提供了这些枚举,这些枚举稍后用于索引到过渡图和状态图查找表中。

状态功能

状态函数实现每个状态--每个状态机状态下有一个状态函数。STATE_DECLARE用于声明状态函数接口,STATE_DEFINE用于定义实现。

C++
Shrink ▲   Copy Code
// 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()宏获取状态机对象的实例。该宏的参数是状态机名称。

在这个实现中,所有的状态机函数都必须遵守这些签名,这些签名如下。

C++
Copy Code
// 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中指定的类型。否则,pEventData参数是STATE_DEFINE中指定的类型。

在Motor的Start状态函数中,STATE_DEFINE(Start,MotorData)宏展开为。

C++
Copy Code
void ST_Start(SM_StateMachine* self, MotorData* pEventData)

注意每个状态函数都有self和pEventData两个参数,self是状态机对象的指针,pEventData是事件数据。还请注意,宏在状态名前加了 "ST_",以创建函数ST_Start()。

同样,Stop状态函数STATE_DEFINE(Stop,NoEventData)也是展开为。

C++
Copy Code
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函数类型定义也接受事件数据。SM_ExitFunc的独特之处在于不允许接受事件数据。

状态图

状态机引擎通过使用状态图知道要调用哪个状态函数。状态图将currentState变量映射到一个特定的状态函数。例如,如果currentState是2,那么第三个状态图函数指针条目将被调用(从零开始计数)。状态图表是用这三个宏创建的。

C++
Copy Code
BEGIN_STATE_MAP
STATE_MAP_ENTRY
END_STATE_MAP

BEGIN_STATE_MAP开始状态图序列。每个STATE_MAP_ENTRY都有一个状态函数名参数。END_STATE_MAP终止状态图。电机的状态图如下所示。

C++
Copy Code
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

另外,guard/entry/exit 等功能需要用 _EX (extended)版本的宏.

C++
Copy Code
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,下面的宏片段是文章后面介绍的一个高级示例。

C++
Copy Code
// 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。

C++
Copy Code
#define SM_DEFINE(_smName_, _instance_) \
    SM_StateMachine _smName_##Obj = { #_smName_, _instance_, \
        0, 0, 0, 0 };

在本例中,状态机名称为Motor,创建了两个对象和两个状态机。

C++
Copy Code
// 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对象的指针。

C++
Copy Code
// Get pointer to the instance data and update currentSpeed
Motor* pInstance = SM_GetInstance(Motor);
pInstance->currentSpeed = pEventData->speed;

过渡图

最后要注意的细节是状态转换规则。状态机如何知道应该发生哪些过渡?答案是过渡图。过渡图是将currentState变量映射到状态枚举常量的查找表。每个外部事件函数都有一个过渡图表,用三个宏创建。

C++
Copy Code
BEGIN_TRANSITION_MAP
TRANSITION_MAP_ENTRY
END_TRANSITION_MAP

Motor中的MTR_Halt事件函数将过渡图定义为。

C++
Copy Code
// 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,如下图所示。

C++
Copy Code
TRANSITION_MAP_ENTRY (EVENT_IGNORED)    // ST_Idle

这解释为:"如果当前状态为状态Idle时发生Halt事件,则忽略该事件即可。"

同样,地图中的第三条是。

Copy Code
TRANSITION_MAP_ENTRY (ST_STOP)         // ST_Start

这表示 "如果当前为状态Start时发生Halt事件,则过渡到状态Stop。"

END_TRANSITION_MAP终止映射。这个宏的第一个参数是状态机名称。第二个参数是事件数据。

C_ASSERT()宏在END_TRANSITION_MAP内使用。如果状态机状态数和过渡地图条目数不匹配,就会产生编译时错误。

新国机步骤

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

1. 创建一个状态枚举,每个状态函数有一个条目。

2. 界定国家职能

3. 定义事件功能

4. 使用STATE_MAP宏创建一个状态图查询表。

5. 使用TRANSITION_MAP宏为每个外部事件函数创建一个过渡图查询表。

国家发动机

状态引擎根据产生的事件执行状态函数。过渡图是一个由currentState变量索引的SM_StateStruct实例数组。当_SM_StateEngine()函数执行时,它在SM_StateStruct数组中查找正确的状态函数。在状态函数有机会执行之后,它释放事件数据(如果有的话),然后再检查是否有任何内部事件通过SM_InternalEvent()产生。

C++
Shrink ▲   Copy Code
// 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()宏调用外部事件函数来生成外部事件。下面的代码片段展示了如何进行同步调用。

C++
Copy Code
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()创建事件数据。

C++
Copy Code
SM_InternalEvent(ST_IDLE, NULL);

在上面的例子中,一旦状态函数完成执行,状态机将过渡到ST_Idle状态。另一方面,如果需要将事件数据发送到目的状态,则需要在堆上创建数据结构,并作为参数传递进来。

C++
Copy Code
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示例展示了如何使用守卫、进入和退出动作创建扩展状态机。状态图如下所示。

C/C++教程-C语言的状态机设计

图2:CentrifugeTest状态图

创建一个CentrifgeTest对象和状态机。这里唯一的区别是状态机是单人的,这意味着对象是私有的,只能创建一个CentrifugeTest实例。这与Motor状态机不同,Motor状态机允许多个实例。

C++
Copy Code
// 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宏。

C++
Shrink ▲   Copy Code
// 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状态函数的守卫条件被声明为。

C++
Copy Code
GUARD_DECLARE(StartTest, NoEventData)

如果状态函数被执行,守护条件函数返回TRUE,否则返回FALSE。

C++
Copy Code
// 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/C++课程,尽在开课吧C/C++教程频道。

有用
分享
全部评论快来秀出你的观点
登录 后可发表观点…
发表
暂无评论,快来抢沙发!
算法刷题核心能力提升营