本文不是状态机状态机的最佳设计分解实践教程(一)

编曲:李逍遥

前言

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

背景

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

switch 语句提供了最容易和最常见的状态机版本之一来实现。在这里c语言 事件驱动状态机,每个 case 都变成了 switch 语句中的一个状态,实现如下:

1switch (currentState) { 2 case ST_IDLE: 3 // do something in the idle state 4 break; 5  6 case ST_STOP: 7 // do something in the stop state 8 break; 9  10 // etc... 11} 12 13

这种方法当然适用于解决许多不同的设计问题。但是,这种形式的状态机在用于事件驱动的多线程项目时可能非常有限。

第一个问题是控制哪些状态转换有效,哪些无效。不能强制执行状态转换规则。任何过渡都可能在任何时候发生,这并不是特别理想的。对于大多数设计,只有少数过渡模式可用。理想情况下,软件设计应该强制执行这些预定义的状态序列并防止不必要的转换。尝试将数据发送到特定状态时会出现另一个问题。由于整个状态机都在一个函数中,因此很难将额外的数据发送到任何给定的状态。最后,这些设计很少适用于多线程系统。设计者必须确保从单个控制线程调用状态机。

为什么要使用国家机器?

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

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

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

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

可以看出,通过将电机控制分解为离散状态,而不是单一功能,我们可以更轻松地管理如何操作电机的规则。

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

为了以图形方式说明状态和事件,我们使用状态图。下面的图 1 显示了电机控制模块的状态转换。方框代表状态,连接箭头代表事件转换。列出事件名称的箭头是外部事件,而未装饰的行被认为是内部事件。(内部事件和外部事件之间的区别将在本文后面介绍。)

图 1:电机状态图

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

简而言之,使用状态机来捕获和执行可能难以传递和实现的复杂交互。

内部和外部事件

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

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

一旦外部事件启动状态机执行,如果使用了锁,则在外部事件和所有内部事件执行完毕之前,它不能被另一个外部事件中断。这种从运行到完成的模型为状态转换提供了一个多线程安全的环境。可以在状态机引擎中使用信号量或互斥锁来阻塞可能同时访问同一状态机实例的其他线程。有关锁的位置,请参见源代码函数 _SM_ExternalEvent() 的注释。

事件数据

生成事件时,它可以选择附加事件数据以供状态函数在执行期间使用。事件数据是指向任何内置或用户定义数据类型的 const 或非 const 指针。

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

状态转换

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

在此实现中,执行验证转换查找不需要内部事件。假设状态转换是有效的。您可以检查有效的内部和外部事件转换,但实际上这只会占用更多存储空间并且不会产生什么好处。验证转换的真正需求在于异步外部事件,其中客户端可能导致事件在不适当的时间发生。状态机一旦执行,就不能中断。它受私有实现的控制,因此不需要进行转换检查。这使设计人员可以自由地通过内部事件更改状态,而无需更新转换表。

状态机模块

状态机源代码包含在 _StateMachine.c_ 和 _StateMachine.h_ 文件中。下面的代码显示了节标题。此 StateMachine 标头包含各种预处理器多行宏,以简化状态机的实现。

1 2enum { EVENT_IGNORED = 0xFE, CANNOT_HAPPEN = 0xFF }; 3  4typedef void NoEventData; 5  6// State machine constant data 7typedef struct 8{ 9 const CHAR* name; 10 const BYTE maxStates; 11 const struct SM_StateStruct* stateMap; 12 const struct SM_StateStructEx* stateMapEx; 13} SM_StateMachineConst; 14  15// State machine instance data 16typedef struct 17{ 18 const CHAR* name; 19 void* pInstance; 20 BYTE newState; 21 BYTE currentState; 22 BOOL eventGenerated; 23 void* pEventData; 24} SM_StateMachine; 25  26// Generic state function signatures 27typedef void (*SM_StateFunc)(SM_StateMachine* self, void* pEventData); 28typedef BOOL (*SM_GuardFunc)(SM_StateMachine* self, void* pEventData); 29typedef void (*SM_EntryFunc)(SM_StateMachine* self, void* pEventData); 30typedef void (*SM_ExitFunc)(SM_StateMachine* self); 31  32typedef struct SM_StateStruct 33{ 34 SM_StateFunc pStateFunc; 35} SM_StateStruct; 36  37typedef struct SM_StateStructEx 38{ 39 SM_StateFunc pStateFunc; 40 SM_GuardFunc pGuardFunc; 41 SM_EntryFunc pEntryFunc; 42 SM_ExitFunc pExitFunc; 43} SM_StateStructEx; 44  45// Public functions 46#define SM_Event(_smName_, _eventFunc_, _eventData_)  47 _eventFunc_(&_smName_##Obj, _eventData_) 48  49// Protected functions 50#define SM_InternalEvent(_newState_, _eventData_)  51 _SM_InternalEvent(self, _newState_, _eventData_) 52#define SM_GetInstance(_instance_)  53 (_instance_*)(self->pInstance); 54  55// Private functions 56void _SM_ExternalEvent(SM_StateMachine* self,  57 const SM_StateMachineConst* selfConst, BYTE newState, void* pEventData); 58void _SM_InternalEvent(SM_StateMachine* self, BYTE newState, void* pEventData); 59void _SM_StateEngine(SM_StateMachine* self, const SM_StateMachineConst* selfConst); 60void _SM_StateEngineEx(SM_StateMachine* self, const SM_StateMachineConst* selfConst); 61  62#define SM_DECLARE(_smName_)  63 extern SM_StateMachine _smName_##Obj;  64  65#define SM_DEFINE(_smName_, _instance_)  66 SM_StateMachine _smName_##Obj = { #_smName_, _instance_,  67 0, 0, 0, 0 };  68  69#define EVENT_DECLARE(_eventFunc_, _eventData_)  70 void _eventFunc_(SM_StateMachine* self, _eventData_* pEventData); 71  72#define EVENT_DEFINE(_eventFunc_, _eventData_)  73 void _eventFunc_(SM_StateMachine* self, _eventData_* pEventData) 74  75#define STATE_DECLARE(_stateFunc_, _eventData_)  76 static void ST_##_stateFunc_(SM_StateMachine* self, _eventData_* pEventData); 77  78#define STATE_DEFINE(_stateFunc_, _eventData_)  79 static void ST_##_stateFunc_(SM_StateMachine* self, _eventData_* pEventData) 80 81

SM_Event() 宏用于生成外部事件,而 SM_InternalEvent() 则在状态函数执行期间生成内部事件。SM_GetInstance() 获取指向当前状态机对象的指针。

SM_DECLARE 和 SM_DEFINE 用于创建状态机实例。EVENT_DECLARE 和 EVENT_DEFINE 创建外部事件函数。最后,STATE_DECLARE 和 STATE_DEFINE 创建状态函数。

电机示例

Motor 实现了我们假设的电机控制状态机,客户端可以在其中以特定速度启动电机并停止电机。Motor 标头接口如下所示:

1#include "StateMachine.h" 2  3// Motor object structure 4typedef struct 5{ 6 INT currentSpeed; 7} Motor; 8  9// Event data structure 10typedef struct 11{ 12 INT speed; 13} MotorData; 14  15// State machine event functions 16EVENT_DECLARE(MTR_SetSpeed, MotorData) 17EVENT_DECLARE(MTR_Halt, NoEventData) 18 19

此 Motor 源文件使用宏通过隐藏所需的状态机来简化使用。

1// State enumeration order must match the order of state 2// method entries in the state map 3enum States 4{ 5 ST_IDLE, 6 ST_STOP, 7 ST_START, 8 ST_CHANGE_SPEED, 9 ST_MAX_STATES 10}; 11  12// State machine state functions 13STATE_DECLARE(Idle, NoEventData) 14STATE_DECLARE(Stop, NoEventData) 15STATE_DECLARE(Start, MotorData) 16STATE_DECLARE(ChangeSpeed, MotorData) 17  18// State map to define state function order 19BEGIN_STATE_MAP(Motor) 20 STATE_MAP_ENTRY(ST_Idle) 21 STATE_MAP_ENTRY(ST_Stop) 22 STATE_MAP_ENTRY(ST_Start) 23 STATE_MAP_ENTRY(ST_ChangeSpeed) 24END_STATE_MAP(Motor) 25  26// Set motor speed external event 27EVENT_DEFINE(MTR_SetSpeed, MotorData) 28{ 29 // Given the SetSpeed event, transition to a new state based upon  30 // the current state of the state machine 31 BEGIN_TRANSITION_MAP  // - Current State - 32 TRANSITION_MAP_ENTRY(ST_START) // ST_Idle  33 TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop  34 TRANSITION_MAP_ENTRY(ST_CHANGE_SPEED) // ST_Start  35 TRANSITION_MAP_ENTRY(ST_CHANGE_SPEED) // ST_ChangeSpeed 36 END_TRANSITION_MAP(Motor, pEventData) 37} 38  39// Halt motor external event 40EVENT_DEFINE(MTR_Halt, NoEventData) 41{ 42 // Given the Halt event, transition to a new state based upon  43 // the current state of the state machine 44 BEGIN_TRANSITION_MAP  // - Current State - 45 TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_Idle 46 TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop 47 TRANSITION_MAP_ENTRY(ST_STOP) // ST_Start 48 TRANSITION_MAP_ENTRY(ST_STOP) // ST_ChangeSpeed 49 END_TRANSITION_MAP(Motor, pEventData) 50} 51 52

外部事件

MTR_SetSpeed 和 MTR_Halt 类中的外部事件。电机状态机。MTR_SetSpeed 获取指向MotorData事件数据的指针,包括电机速度。此数据结构将在状态处理完成后使用 SM_XFree(),并且必须在函数调用之前使用 SM_XAlloc()。

状态数

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

状态函数

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

1// State machine sits here when motor is not running 2STATE_DEFINE(Idle, NoEventData) 3{ 4 printf("%s ST_Idlen", self->name); 5} 6  7// Stop the motor  8STATE_DEFINE(Stop, NoEventData) 9{ 10 // Get pointer to the instance data and update currentSpeed 11 Motor* pInstance = SM_GetInstance(Motor); 12 pInstance->currentSpeed = 0; 13  14 // Perform the stop motor processing here 15 printf("%s ST_Stop: %dn", self->name, pInstance->currentSpeed); 16  17 // Transition to ST_Idle via an internal event 18 SM_InternalEvent(ST_IDLE, NULL); 19} 20  21// Start the motor going 22STATE_DEFINE(Start, MotorData) 23{ 24 ASSERT_TRUE(pEventData); 25  26 // Get pointer to the instance data and update currentSpeed 27 Motor* pInstance = SM_GetInstance(Motor); 28 pInstance->currentSpeed = pEventData->speed; 29  30 // Set initial motor speed processing here 31 printf("%s ST_Start: %dn", self->name, pInstance->currentSpeed); 32} 33  34// Changes the motor speed once the motor is moving 35STATE_DEFINE(ChangeSpeed, MotorData) 36{ 37 ASSERT_TRUE(pEventData); 38  39 // Get pointer to the instance data and update currentSpeed 40 Motor* pInstance = SM_GetInstance(Motor); 41 pInstance->currentSpeed = pEventData->speed; 42  43 // Perform the change motor speed here 44 printf("%s ST_ChangeSpeed: %dn", self->name, pInstance->currentSpeed); 45} 46 47

STATE_DECLARE 和 STATE_DEFINE 采用两个参数。第一个参数是状态函数名。第二个参数是事件数据类型。如果您不需要事件数据,请使用 NoEventData。宏也可用于创建守卫、退出和进入操作,本文稍后将对此进行说明。

SM_GetInstance() 宏获取状态机对象的实例。宏的参数是状态机名称。

在此实现中,所有状态机功能都必须遵守这些签名,如下所示:

1// Generic state function signatures 2typedef void (*SM_StateFunc)(SM_StateMachine* self, void* pEventData); 3typedef BOOL (*SM_GuardFunc)(SM_StateMachine* self, void* pEventData); 4typedef void (*SM_EntryFunc)(SM_StateMachine* self, void* pEventData); 5typedef void (*SM_ExitFunc)(SM_StateMachine* self);  6 7

每个 SM_StateFunc 接受指向 SM_StateMachine 对象和事件数据的指针。如果使用 NoEventData,则 pEventData 参数将为 NULL。否则,pEventData 参数的类型为 STATE_DEFINE。

在电机的启动状态函数 STATE_DEFINE(Start, MotorData) 中,宏扩展为:

1void ST_Start(SM_StateMachine* self, MotorData* pEventData) 2 3

请注意,每个状态函数都有 self 和 pEventData 参数。self 是指向状态机对象的指针,pEventData 是事件数据。另请注意,宏以“ST_”开头以创建函数的状态名称。ST_Start()。

同样,停止状态函数 STATE_DEFINE(Stop, NoEventData)IS 扩展为:

1void ST_Stop(SM_StateMachine* self, void* pEventData) 2 3

Stop 不接受事件数据,因此 pEventData 参数为 void*。

每个状态/保护/进入/退出功能的宏都会自动添加三个字符。例如,如果使用 STATE_DEFINE(Idle, NoEventData),则调用实际的状态函数名称。ST_空闲()。

SM_GuardFunc 和 SM_Entry 函数类型定义也接受事件数据。SM_ExitFunc 是唯一的c语言 事件驱动状态机,因为不允许使用任何事件数据。

状态图

状态机引擎通过使用状态图知道调用哪个状态函数。状态映射将 currentState 变量映射到特定的状态函数。例如,如果 currentState 为 2,则调用第三个状态映射函数指针条目(从零开始计数)。使用以下三个宏创建状态图:

1BEGIN_STATE_MAP 2STATE_MAP_ENTRY 3END_STATE_MAP 4 5

图片[1]-本文不是状态机状态机的最佳设计分解实践教程(一)-老王博客

BEGIN_STATE_MAP 启动状态映射序列。每个 STATE_MAP_ENTRY 都有一个状态函数名称参数。END_STATE_MAP 终止地图。国家地图 Motor 如下所示:

1BEGIN_STATE_MAP(Motor) 2 STATE_MAP_ENTRY(ST_Idle) 3 STATE_MAP_ENTRY(ST_Stop) 4 STATE_MAP_ENTRY(ST_Start) 5 STATE_MAP_ENTRY(ST_ChangeSpeed) 6END_STATE_MAP 7 8

或者,守卫/进入/退出功能需要利用 _EX(扩展)宏的版本。

1BEGIN_STATE_MAP_EX 2STATE_MAP_ENTRY_EX or STATE_MAP_ENTRY_ALL_EX  3END_STATE_MAP_EX 4 5

STATE_MAP_ENTRY_ALL_EX 宏按顺序为状态动作、保护条件、进入动作和退出动作设置四个参数。状态操作是强制性的,但其他操作是可选的。如果状态没有动作,则使用 0 作为参数。如果状态没有任何保护/进入/退出选项,则 STATE_MAP_ENTRY_EX 宏将所有未使用的选项默认为 0。以下宏片段是本文后面描述的高级示例。

1// State map to define state function order 2BEGIN_STATE_MAP_EX(CentrifugeTest) 3 STATE_MAP_ENTRY_ALL_EX(ST_Idle, 0, EN_Idle, 0) 4 STATE_MAP_ENTRY_EX(ST_Completed) 5 STATE_MAP_ENTRY_EX(ST_Failed) 6 STATE_MAP_ENTRY_ALL_EX(ST_StartTest, GD_StartTest, 0, 0) 7 STATE_MAP_ENTRY_EX(ST_Acceleration) 8 STATE_MAP_ENTRY_ALL_EX(ST_WaitForAcceleration, 0, 0, EX_WaitForAcceleration) 9 STATE_MAP_ENTRY_EX(ST_Deceleration) 10 STATE_MAP_ENTRY_ALL_EX(ST_WaitForDeceleration, 0, 0, EX_WaitForDeceleration) 11END_STATE_MAP_EX(CentrifugeTest) 12 13

不要忘记在每个函数中添加前面的字符(ST_、GD_、EN_ 或 EX_)。

状态机对象

在 C++ 中,对象是语言的一个组成部分。使用 C,您必须更加努力地完成类似的行为。此 C 语言状态机支持多个状态机对象(或多个实例),而不是具有单个静态状态机实现。

SM_StateMachine 数据结构存储状态机实例数据;每个状态机实例存储一个对象。SM_StateMachineConst 数据结构存储常量数据;每种状态机类型都有一个常量对象。

状态机使用 SM_DEFINE 宏。第一个参数是状态机名称。第二个参数是指向用户定义的状态机结构的指针,如果没有用户对象,则为 NULL。

1#define SM_DEFINE(_smName_, _instance_)  2 SM_StateMachine _smName_##Obj = { #_smName_, _instance_,  3 0, 0, 0, 0 }; 4 5

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

1// Define motor objects 2static Motor motorObj1; 3static Motor motorObj2; 4  5// Define two public Motor state machine instances 6SM_DEFINE(Motor1SM, &motorObj1) 7SM_DEFINE(Motor2SM, &motorObj2) 8 9

每个电机对象独立处理状态执行。此 Motor 结构用于存储状态机的特定于实例的数据。在状态函数中,使用 SM_GetInstance() 获取指向 Motor 对象的指针以在运行时进行初始化。

1// Get pointer to the instance data and update currentSpeed 2Motor* pInstance = SM_GetInstance(Motor); 3pInstance->currentSpeed = pEventData->speed; 4 5

转换图

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

1BEGIN_TRANSITION_MAP 2TRANSITION_MAP_ENTRY 3END_TRANSITION_MAP 4 5

MTR_Halt 事件函数 Motor 将转换图定义为:

1// Halt motor external event 2EVENT_DEFINE(MTR_Halt, NoEventData) 3{ 4 // Given the Halt event, transition to a new state based upon  5 // the current state of the state machine 6 BEGIN_TRANSITION_MAP   // - Current State - 7 TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_Idle 8 TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Stop 9 TRANSITION_MAP_ENTRY(ST_STOP)  // ST_Start 10 TRANSITION_MAP_ENTRY(ST_STOP)  // ST_ChangeSpeed 11 END_TRANSITION_MAP(Motor, pEventData) 12} 13 14

BEGIN_TRANSITION_MAP 开始地图。每个 TRANSITION_MAP_ENTRY 它指示状态机应该根据当前状态做什么。每个转换映射中的条目数必须与状态函数的数量完全匹配。在我们的例子中,我们有四个状态函数,所以我们需要四个转换映射条目。每个条目的位置与状态图中定义的状态函数的顺序相匹配。因此,MTR_Halt 函数中表示 EVENT_IGNORED 的第一个条目如下所示:

1TRANSITION_MAP_ENTRY (EVENT_IGNORED) // ST_Idle 2 3

这被解释为“如果在当前状态处于空闲状态时发生暂停事件,则忽略该事件”。

同样,地图上的第三个条目是:

1TRANSITION_MAP_ENTRY (ST_STOP) // ST_Start 2 3

这意味着“如果在当前状态开始时发生暂停事件,则转换到状态停止”。

END_TRANSITION_MAP 终止地图。该宏的第一个参数是状态机名称。第二个参数是事件数据。

C_ASSERT() 宏位于 END_TRANSITION_MAP 中。如果状态机状态的数量与转换映射条目的数量不匹配,则会生成编译时错误。

新状态机步骤

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

状态引擎

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

1// The state engine executes the state machine states 2void _SM_StateEngine(SM_StateMachine* self, SM_StateMachineConst* selfConst) 3{ 4 void* pDataTemp = NULL; 5  6 ASSERT_TRUE(self); 7 ASSERT_TRUE(selfConst); 8  9 // While events are being generated keep executing states 10 while (self->eventGenerated) 11 { 12  // Error check that the new state is valid before proceeding 13  ASSERT_TRUE(self->newState < selfConst->maxStates); 14  15  // Get the pointers from the state map 16  SM_StateFunc state = selfConst->stateMap[self->newState].pStateFunc; 17  18  // Copy of event data pointer 19  pDataTemp = self->pEventData; 20  21  // Event data used up, reset the pointer 22  self->pEventData = NULL; 23  24  // Event used up, reset the flag 25  self->eventGenerated = FALSE; 26  27  // Switch to the new current state 28  self->currentState = self->newState; 29  30  // Execute the state action passing in event data 31  ASSERT_TRUE(state != NULL); 32  state(self, pDataTemp); 33  34  // If event data was used, then delete it 35  if (pDataTemp) 36  { 37   SM_XFree(pDataTemp); 38   pDataTemp = NULL; 39  } 40 } 41} 42 43

守卫、进入、状态和退出操作的状态引擎逻辑由以下序列表示。这个 _SM_StateEngine() 引擎只实现了下面的#1 和#5。扩展的 _SM_StateEngineEx() 引擎使用整个逻辑序列。

评估状态转换表。如果是 EVENT_IGNORED,则忽略该事件并且不执行任何转换。如果 CANNOT_HAPPEN 软件失败。否则,继续下一步。如果定义了保护条件,则执行保护条件函数。如果保护条件返回 FALSE,则忽略状态转换并且不调用状态函数。如果守卫返回 TRUE,或者不存在守卫条件,则执行状态函数。如果为当前状态定义了到新状态的转换并定义了退出动作,则调用当前状态退出动作函数。如果为新状态定义了到新状态的转换并且定义了进入动作,则调用新状态进入动作函数。为新状态调用状态操作函数。新状态现在是当前状态。

生成事件

至此,我们有了一个工作状态机。让我们看看如何为它生成事件。通过动态创建事件数据结构来生成外部事件。SM_XAlloc(),分配结构成员变量,使用SM_Event()宏。下面的代码片段显示了如何进行同步调用。

1MotorData* data; 2   3// Create event data 4data = SM_XAlloc(sizeof(MotorData)); 5data->speed = 100; 6  7// Call MTR_SetSpeed event function to start motor 8SM_Event(Motor1SM, MTR_SetSpeed, data); 9 10

此 SM_Event() 的第一个参数是状态机名称。第二个参数是要调用的事件函数。第三个参数是事件数据,如果没有数据则为NULL。

要从状态函数中生成内部事件,请调用 SM_InternalEvent()。如果目标不接受事件数据,则最后一个参数为 NULL。否则,使用 SM_XAlloc()。

1SM_InternalEvent(ST_IDLE, NULL); 2 3

在上面的例子中,状态函数执行完毕后,状态机将转换到 ST_Idle 状态。另一方面,如果需要将事件数据发送到目标状态,则需要在堆上创建数据结构并作为参数传入。

1MotorData* data;   2data = SM_XAlloc(sizeof(MotorData)); 3data->speed = 100; 4SM_InternalEvent(ST_CHANGE_SPEED, data); 5 6

不要使用堆

所有状态机事件数据都必须动态创建。但是,在某些系统上,不建议使用堆。包括 x_allocator 模块是一个固定的块内存分配器,它消除了堆使用。在 _StateMachine.c_ 中定义 USE_SM_ALLOCATOR 以使用固定块分配器。有关 x_allocator 的信息,请参阅下面的参考部分。

离心机测试示例

此 CentrifugeTest 示例演示了如何创建具有保护、进入和退出操作的扩展状态机。

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

1// CentrifugeTest object structure 2typedef struct 3{ 4  INT speed; 5  BOOL pollActive; 6} CentrifugeTest; 7  8// Define private instance of motor state machine 9CentrifugeTest centrifugeTestObj; 10SM_DEFINE(CentrifugeTestSM, &centrifugeTestObj) 11 12

扩展状态机使用 ENTRY_DECLARE、GUARD_DECLARE 和 EXIT_DECLARE 宏。

1// State enumeration order must match the order of state 2// method entries in the state map 3enum States 4{ 5  ST_IDLE, 6  ST_COMPLETED, 7  ST_FAILED, 8  ST_START_TEST, 9  ST_ACCELERATION, 10  ST_WAIT_FOR_ACCELERATION, 11  ST_DECELERATION, 12  ST_WAIT_FOR_DECELERATION, 13  ST_MAX_STATES 14}; 15  16// State machine state functions 17STATE_DECLARE(Idle, NoEventData) 18ENTRY_DECLARE(Idle, NoEventData) 19STATE_DECLARE(Completed, NoEventData) 20STATE_DECLARE(Failed, NoEventData) 21STATE_DECLARE(StartTest, NoEventData) 22GUARD_DECLARE(StartTest, NoEventData) 23STATE_DECLARE(Acceleration, NoEventData) 24STATE_DECLARE(WaitForAcceleration, NoEventData) 25EXIT_DECLARE(WaitForAcceleration) 26STATE_DECLARE(Deceleration, NoEventData) 27STATE_DECLARE(WaitForDeceleration, NoEventData) 28EXIT_DECLARE(WaitForDeceleration) 29  30// State map to define state function order 31BEGIN_STATE_MAP_EX(CentrifugeTest) 32  STATE_MAP_ENTRY_ALL_EX(ST_Idle, 0, EN_Idle, 0) 33  STATE_MAP_ENTRY_EX(ST_Completed) 34  STATE_MAP_ENTRY_EX(ST_Failed) 35  STATE_MAP_ENTRY_ALL_EX(ST_StartTest, GD_StartTest, 0, 0) 36  STATE_MAP_ENTRY_EX(ST_Acceleration) 37  STATE_MAP_ENTRY_ALL_EX(ST_WaitForAcceleration, 0, 0, EX_WaitForAcceleration) 38  STATE_MAP_ENTRY_EX(ST_Deceleration) 39  STATE_MAP_ENTRY_ALL_EX(ST_WaitForDeceleration, 0, 0, EX_WaitForDeceleration) 40END_STATE_MAP_EX(CentrifugeTest) 41 42

请注意,_EX 扩展了状态映射宏以支持保护/进入/退出功能。每个警卫/网关 DECLARE 宏都必须与 DEFINE 结合使用。例如,StartTest 状态函数声明为:

1GUARD_DECLARE(StartTest, NoEventData) 2 3

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

1// Guard condition to determine whether StartTest state is executed. 2GUARD_DEFINE(StartTest, NoEventData) 3{ 4  printf("%s GD_StartTestn", self->name); 5  if (centrifugeTestObj.speed == 0) 6    return TRUE;  // Centrifuge stopped. OK to start test. 7  else 8    return FALSE;  // Centrifuge spinning. Can't start test. 9} 10 11

多线程安全

为了防止状态机在执行过程中被另一个线程抢占,可以在_SM_ExternalEvent()函数中使用StateMachine模块。在允许执行外部事件之前,可以锁定信号量。在处理完外部事件和所有内部事件后,软件锁被释放,允许另一个外部事件进入状态机实例。

如果应用程序是多线程的,注释指示应该在哪里放置锁定和解锁。_and_ 多个线程能够访问单个状态机实例。请注意,每个 StateMachine 对象都应该有自己的软件锁实例。这将防止单个实例锁定并阻止所有其他实例。StateMachine 对象执行。仅当多个控制线程调用 StateMachine 实例时才需要软件锁。如果没有,则不需要锁定。

结语

使用这种方法而不是旧的 switch 语句样式来实现状态机似乎需要额外的努力。然而,回报是更强大的设计,可以在多线程系统中统一使用。让每个状态都有自己的功能比单个巨大状态更容易阅读。switch 语句并允许向每个状态发送唯一的事件数据。此外,验证状态转换通过消除不必要的状态转换引起的副作用来防止客户端滥用。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ 结尾

扫描下方微信,**加作者微信***到技术交流群,**请先自我介绍。

1嵌入式编程专辑Linux 学习专辑C/C++编程专辑 2Qt进阶学习专辑关注微信公众号『技术让梦想更伟大』,后台回复“m”查看更多内容。 3长按前往图中包含的公众号关注 4 5

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论