C语言中的状态机设计

微信扫一扫,分享到朋友圈

C语言中的状态机设计

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

背景

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

A switch 语句提供了状态机最容易实现和最常见的版本之一。在这里,每个案例在 switch 语句成为一个状态,实现如下所示:

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. 站住 -停止马达

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

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

  1. 闲散 -马达不是旋转的,而是静止的
  • 无所事事
  1. 启动 -从死胡同启动马达
  • 开启电动机电源
  • 设定电机转速
  1. 变速 -调整已经移动的马达的速度
  • 改变电机转速
  1. -停止移动的马达
  • 关闭电动机电源
  • 进入闲置状态

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

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

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

图1:电机状态图

如您所见,当事件在状态转换中出现时,所发生的状态转换取决于状态机的当前状态。当 SetSpeed 事件出现,例如,电机在 Idle 状态,则转换为 Start 状态。然而,同样的 SetSpeed 当前状态为 Start 将电机转换为 ChangeSpeed 状态。您还可以看到,并非所有的状态转换都是有效的。例如,马达不能从 ChangeSpeedIdle 而不需要先通过 Stop 状态。

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

内外事件

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

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

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

事件数据

生成事件时,它可以选择附加事件数据,以便在执行过程中由状态函数使用。事件数据是一个 const 或者不是- const 指向任何内置或用户定义的数据类型的指针。

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

状态转变

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

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

状态机模块

状态机源代码包含在_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_DECLARESM_DEFINE 用于创建状态机实例。 EVENT_DECLAREEVENT_DEFINE 创建外部事件函数。最后, STATE_DECLARESTATE_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_SetSpeedMTR_Halt 类中的外部事件。 Motor 状态机。 MTR_SetSpeed 获取指向 MotorData 事件数据,包含电机速度。此数据结构将使用 SM_XFree() 在状态处理完成后,必须使用 SM_XAlloc() 函数调用之前。

州数

每个状态函数都必须有一个与其关联的枚举。这些枚举用于存储状态机的当前状态。在……里面 MotorStates 提供这些枚举,这些枚举稍后用于对转换映射和状态映射查找表进行索引。

状态函数

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

// State machine sits here when motor is not running
STATE_DEFINE(Idle, NoEventData)
{
printf("%s ST_Idlen", 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: %dn", 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: %dn", 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: %dn", self->name, pInstance->currentSpeed);
}

STATE_DECLARESTATE_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 .

在……里面 MotorStart 状态函数 STATE_DEFINE(Start, MotorData) 宏扩展到:

void ST_Start(SM_StateMachine* self, MotorData* pEventData)

注意,每个状态函数都有 selfpEventData 争论。 self 是指向状态机对象的指针,并且 pEventData 事件数据。还请注意,宏以“ ST_ “用于创建函数的状态名称。 ST_Start() .

类似地, Stop 状态函数 STATE_DEFINE(Stop, NoEventData) IS扩展到:

void ST_Stop(SM_StateMachine* self, void* pEventData)

Stop 不接受事件数据,因此 pEventData 论点是 void* .

每个状态/保护/入口/退出函数在宏中自动添加三个字符。例如,如果使用 STATE_DEFINE(Idle, NoEventData) 实际的状态函数名被调用。 ST_Idle() .

ST_
GD_
EN_
EX_

SM_GuardFuncSM_Entry 功能 typedef 也接受事件数据。 SM_ExitFunc 是唯一的,因为不允许任何事件数据。

状态图

状态机引擎通过使用状态映射知道要调用哪个状态函数.状态图映射 currentState 变量设置为特定的状态函数。例如,如果 currentState2 ,则调用第三个状态映射函数指针项(从零计数)。状态映射表是使用以下三个宏创建的:

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

这个 MTR_Halt 事件函数 Motor 将转换映射定义为:

// 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

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

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

TRANSITION_MAP_ENTRY (ST_STOP)         // ST_Start

这表示“如果在当前为状态启动时发生了暂停事件,则转换为状态停止”。

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() 引擎使用整个逻辑序列。

  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 模块是一个固定的块内存分配程序,它消除了堆的使用。定义 USE_SM_ALLOCATOR 内_StateMachine.c_若要使用固定块分配器,请执行以下操作。见 参考文献 下面一节 x_allocator 信息。

离心机测试实例

这个 CentrifugeTest 示例演示如何使用保护、入口和退出操作创建扩展状态机。状态图如下所示:

图2:离心测试状态图

A 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_DECLAREGUARD_DECLAREEXIT_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_StartTestn", 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 对象执行。只有在下列情况下才需要软件锁: StateMachine 实例由多个控制线程调用。如果没有,则不需要锁。

结语

使用此方法实现状态机,而不是旧方法 switch 语句风格似乎是额外的努力。然而,回报在于一个更健壮的设计,能够在整个多线程系统上统一使用。让每一种状态都具有自己的功能,比单个巨大的状态更容易读取。 switch 语句,并允许向每个状态发送唯一的事件数据。此外,通过消除不必要的状态转换所造成的副作用,验证状态转换可以防止客户端滥用。

微信扫一扫,分享到朋友圈

C语言中的状态机设计

苹果电脑全系换上自研芯片,除了不能打电话,比iPhone 12亮眼多了

上一篇

Ubuntu被曝严重漏洞:切换系统语言+输入几行命令,就能获取root权限

下一篇

你也可能喜欢

C语言中的状态机设计

长按储存图像,分享给朋友