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

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

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

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

图1:电机状态图

如您所见,当事件在状态转换中出现时,所发生的状态转换取决于状态机的当前状态。当SetSpeed事件出现,例如,电机在Idle状态,则转换为Start状态。然而,同样的SetSpeed当前状态为Start将电机转换为ChangeSpeed状态。您还可以看到,并非所有的状态转换都是有效的。例如,马达不能从ChangeSpeed到Idle而不需要先通过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_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_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_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)IS扩展到:

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

这个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。如果状态机状态数与转换映射项的数目不匹配,则生成编译时错误。

新的状态机步骤

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

  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模块是一个固定的块内存分配程序,它消除了堆的使用。定义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_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_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语言中的状态机设计的文章就介绍到这了,更多相关C语言状态机设计内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言中的状态机设计深入讲解

    前言 本文不是关于软件状态机的最佳设计分解实践的教程.我将重点关注状态机代码和简单的示例,这些示例具有足够的复杂性,以便于理解特性和用法. 背景 大多数程序员常用的设计技术是有限状态机(FSM).设计人员使用此编程结构将复杂的问题分解为可管理的状态和状态转换.有无数种实现状态机的方法. A switch语句提供了状态机最容易实现和最常见的版本之一.在这里,每个案例在switch语句成为一个状态,实现如下所示: switch (currentState) { case ST_IDLE: // do

  • golang 语言中错误处理机制

    与其他主流语言如 Javascript.Java 和 Python 相比,Golang 的错误处理方式可能和这些你熟悉的语言有所不同.所以才有了这个想法根大家聊一聊 golang 的错误处理方式,以及实际开发中应该如何对错误进行处理.因为分享面对 Golang有一个基本的了解 developers, 所以一些简单地方就不做赘述了. 如何定义错误 在 golang 语言中,无论是在类型检查还是编译过程中,都是将错误看做值来对待,和 string 或者 integer 这些类型值并不差别.声明一个

  • C语言中结构体的内存对齐规则讲解

    目录 1.结构体的内存对齐规则 2.例子 3.为什么存在内存对齐 4.如何修改默认对齐数 1.结构体的内存对齐规则 1.第一个成员在与结构体变量偏移量为0的地址处. 2.其他成员变量都放在对齐数(成员的大小和默认对齐数的较小值)的整数倍的地址处. 对齐数=编译器默认的一个对齐数与该成员大小的较小值.(VS中默认的对齐数是8) 3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数 )的整数倍. 4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最

  • C语言中函数指针与软件设计经验总结

    函数指针与软件设计 记得刚开始工作时,一位高手告诉我,说,longjmp和setjmp玩得不熟,就不要自称为C语言高手.当时我半信半疑,为了让自己向高手方向迈进,还是花了一点时间去学习longjmp和setjmp的用法.后来明白那不单是跳来跳去那样简单,而是一种高级的异常处理机制,在某些情况下确实很有用. 为了显示自己的技巧,也在自己的程序中用过几次.渐渐发现这样的技巧带来的好处是有代价的,破坏了程序的结构化设计,程序变得很难读,尤其对新手来说.终于明白这种技巧不过是一种调味料,在少数情况使用几

  • R语言中字符串的拼接操作实例讲解

    在R语言中 paste 是一个很有用的字符串处理函数,可以连接不同类型的变量及常量. 函数paste的一般使用格式为: paste(..., sep = " ", collapse = NULL) 其 中-表示一个或多个R可以被转化为字符型的对象:参数sep表示分隔符,默认为空格:参数collapse可选,如果不指定值,那么函数paste的返回值是自变量之间通过sep指定的分隔符连接后得到的一个字符型向量:如果为其指定了特定的值,那么自变量连接后的字符型向量会再被连接成一个字符串,之间

  • 在C语言中输入中文字符串讲解

    C语言中的字符串是以ASCII码表的形式存入的,输入英文字符是以英文字符对应的ASCII码的形式进入寄存器中的,例如输出hello world!: 将c反汇编得到, 可以看到h的ASCII码是68,e的ASCII码是65等等存放进内存的,那中文字呢?中文字并没有对应的ASCII码,还可以显示出来吗? 答案当然是肯定的,在计算机中存储中文使用的编码规则是GB2312或GB2312-80,规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,这样我们就可以组合

  • 漫画讲解C语言中最近公共祖先的三种类型

    最近公共祖先定义 查找最近公共祖先 三叉链 代码如下: //三叉链 struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode *parent; TreeNode(int x) : val(x), left(NULL), right(NULL), parent(NULL) {} }; class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* ro

  • C语言数据的存储超详细讲解下篇浮点型在内存中的存取

    目录 前言 浮点型在内存中的存储 浮点数存储的例子 浮点数存储规则 IEEE 754规定 IEEE 754对有效数字M的特别规定 IEEE 754对指数E的特别规定 存入内存是E的规定 从内存取出时E的规定 举例 1 举例 2 举例 3 判断两个浮点数是否相等? 总结 前言 本文接着学习数据的存储相关的内容,主要学习浮点型数在内存中的存储与取出. 浮点型在内存中的存储 常见的浮点数:3.14159.1E10 浮点数家族包括: float.double.long double 类型 浮点数表示的范

  • 深入讲解Go语言中函数new与make的使用和区别

    前言 本文主要给大家介绍了Go语言中函数new与make的使用和区别,关于Go语言中new和make是内建的两个函数,主要用来创建分配类型内存.在我们定义生成变量的时候,可能会觉得有点迷惑,其实他们的规则很简单,下面我们就通过一些示例说明他们的区别和使用,话不多说了,来一起看看详细的介绍吧. 变量的声明 var i int var s string 变量的声明我们可以通过var关键字,然后就可以在程序中使用.当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,st

  • C语言中的字符(char)详细讲解

    1.字符型(char)简介 字符型(char)用于储存字符(character),如英文字母或标点. 严格来说,char 其实也是整数类型(integer type),因为 char 类型储存的实际上是整数,而不是字符. 计算机使用特定的整数编码来表示特定的字符. 2. 声明字符型变量 3. 字符常量与初始化 实例: 用 char 类型来专门表示一个字符,例如: char a='1'; char b='$'; char c='X'; char d=' '; // 空格也是一个字符 char e=

随机推荐