C++基础之状态机

匿名 (未验证) 提交于 2019-12-02 22:56:40

Doc:https://download.csdn.net/download/qccz123456/10567668

PDF:https://download.csdn.net/download/qccz123456/10567664

State Machine Design in C++

A compact C++ finite state machine (FSM) implementation that's easy to use on embedded and PC-based systems.

https://www.codeproject.com/Articles/1087619/State-Machine-Design-in-Cplusplus

In 2000, I wrote an article entitled "State Machine Design in C++

Why another state machine design? Certainly by now there's an existing implementation out there that can be used, right? Maybe. On occasion, I'll try a new state machine and find it doesn't fit my needs for one reason or another. For me, the problem usually boils down to one or more of the following:

  1. Too large
  2. Too complex
  3. High learning curve
  4. External libraries
  5. No event data

Don't get me wrong, some implementations are quite impressive and suitable for many different projects. Every design has certain tradeoffs and this one is no different. Only you can decide if this one meets your needs or not. I'll try to get you bootstrapped as quickly as possible through this article and sample code. This state machine has the following features:

  1. Compact
  2. Transition tables
  3. State action
  4. Guards/entry/exit actions

A switch statement provides one of the easiest to implement and most common version of a state machine. Here, each case within the switch statement becomes a state, implemented something like:

Hide

switch (currentState) {

case ST_IDLE:

// do something in the idle state

break;

case ST_STOP:

// do something in the stop state

break;

// etc...

}

This method is certainly appropriate for solving many different design problems. When employed on an event driven, multithreaded project, however, state machines of this form can be quite limiting.

The first problem revolves around controlling what state transitions are valid and which ones are invalid. There is no way to enforce the state transition rules. Any transition is allowed at any time, which is not particularly desirable. For most designs, only a few transition patterns are valid. Ideally, the software design should enforce these predefined state sequences and prevent the unwanted transitions. Another problem arises when trying to send data to a specific state. Since the entire state machine is located within a single function, sending additional data to any given state proves difficult. And lastly these designs are rarely suitable for use in a multithreaded system. The designer must ensure the state machine is called from a single thread of control.

Why use a state machine?

To take a simple example, which I will use throughout this article, let's say we are designing motor-control software. We want to start and stop the motor, as well as change the motor's speed. Simple enough. The motor control events to be exposed to the client software will be as follows:

  1. Set Speed

These events are not state machine states. The steps required to handle these two events are different. In this case the states are:

  1. ― the motor is not spinning but is at rest.
  • Do nothing.
  1. ― starts the motor from a dead stop.
  • Turn on motor power.
  • Set motor speed.
  1. ― adjust the speed of an already moving motor.
  • Change motor speed.
  1. ― stop a moving motor.
  • Turn off motor power.
  • Go to the Idle state.

As can be seen, breaking the motor control into discreet states, as opposed to having one monolithic function, we can more easily manage the rules of how to operate the motor.

Every state machine has the concept of a "current state." This is the state the state machine currently occupies. At any given moment in time, the state machine can be in only a single state. Every instance of a particular state machine class can set the initial state during construction. That initial state, however, does not execute during object creation. Only an event sent to the state machine causes a state function to execute.

To graphically illustrate the states and events, we use a state diagram. Figure 1 below shows the state transitions for the motor control class. A box denotes a state and a connecting arrow indicates the event transitions. Arrows with the event name listed are external events, whereas unadorned lines are considered internal events. (I cover the differences between internal and external events later in the article.)



As you can see, when an event comes in the state transition that occurs depends on state machine's current state. When a SetSpeed event comes in, for instance, and the motor is in the Idle state, it transitions to the Start state. However, that same SetSpeed event generated while the current state is Start transitions the motor to the ChangeSpeed state. You can also see that not all state transitions are valid. For instance, the motor can't transition from ChangeSpeed to Idle without first going through the Stop state.

In short, using a state machine captures and enforces complex interactions, which might otherwise be difficult to convey and implement.

Internal and external events

As I mentioned earlier, an event is the stimulus that causes a state machine to transition between states. For instance, a button press could be an event. Events can be broken out into two categories: external and internal. The external event, at its most basic level, is a function call into a state-machine object. These functions are public and are called from the outside or from code external to the state-machine object. Any thread or task within a system can generate an external event. If the external event function call causes a state transition to occur, the state will execute synchronously within the caller's thread of control. An internal event, on the other hand, is self-generated by the state machine itself during state execution.

A typical scenario consists of an external event being generated, which, again, boils down to a function call into the class's public interface. Based upon the event being generated and the state machine's current state, a lookup is performed to determine if a transition is required. If so, the state machine transitions to the new state and the code for that state executes. At the end of the state function, a check is performed to determine whether an internal event was generated. If so, another transition is performed and the new state gets a chance to execute. This process continues until the state machine is no longer generating internal events, at which time the original external event function call returns. The external event and all internal events, if any, execute within the caller's thread of control.

ExternalEvent()

Event data

base class. This gives the state machine engine a common base class for which to delete all event data.

Hide

class EventData

{

public:

virtual ~EventData() {}

};

External event no heap data

State transitions

When an external event is generated, a lookup is performed to determine the state transition course of action. There are three possible outcomes to an event: new state, event ignored, or cannot happen. A new state causes a transition to a new state where it is allowed to execute. Transitions to the existing state are also possible, which means the current state is re-executed. For an ignored event, no state executes. However, the event data, if any, is deleted. The last possibility, cannot happen, is reserved for situations where the event is not valid given the current state of the state machine. If this occurs, the software faults.

In this implementation, internal events are not required to perform a validating transition lookup. The state transition is assumed to be valid. You could check for both valid internal and external event transitions, but in practice, this just takes more storage space and generates busywork for very little benefit. The real need for validating transitions lies in the asynchronous, external events where a client can cause an event to occur at an inappropriate time. Once the state machine is executing, it cannot be interrupted. It is under the control of the class's private implementation, thereby making transition checks unnecessary. This gives the designer the freedom to change states, via internal events, without the burden of updating transition tables.

StateMachine class

EventDatabase class.

StateMachine.zip). The code below shows the class declaration.

Hide

class StateMachine

{

public:

enum { EVENT_IGNORED = 0xFE, CANNOT_HAPPEN };

0);

virtual ~StateMachine() {}

return m_currentState; }

protected:

void ExternalEvent(BYTE newState, const EventData* pData = NULL);

void InternalEvent(BYTE newState, const EventData* pData = NULL);

private:

const BYTE MAX_STATES;

const EventData* m_pEventData;

virtual const StateMapRow* GetStateMap() = 0;

virtual const StateMapRowEx* GetStateMapEx() = 0;

void SetCurrentState(BYTE newState) { m_currentState = newState; }

void StateEngine(void

void StateEngine(const StateMapRow* const pStateMap);

void StateEngine(const StateMapRowEx* const pStateMapEx);

};

is the base class used for handling events and state transitions. The interface is contained within four functions:

Hide

void ExternalEvent(BYTE newState, const EventData* pData = NULL);

void InternalEvent(BYTE newState, const EventData* pData = NULL);

virtual const StateMapRow* GetStateMap() = 0;

virtual const StateMapRowEx* GetStateMapEx() = 0;

ExternalEvent()

StateMapRowExGetStateMap()GetStateMapEx() iNULL. However, multiline macros are provided to implement these functions for us, as I will demonstrate shortly.

Motor example

StateMachineMotor

class declaration shown below contains no macros:

Hide

class MotorNMData : public EventData

{

public:

};

// Motor class with no macros

class MotorNM : public StateMachine

{

public:

// External events taken by this state machine

void SetSpeed(MotorNMData* data);

void Halt();

private:

// State enumeration order must match the order of state method entries

// in the state map.

enum States

// Define the state machine state functions with event data type

void ST_Idle(const NoEventData*);

void ST_Stop(const NoEventData*);

void ST_Start(const MotorNMData*);

void ST_ChangeSpeed(const MotorNMData*);

// State map to define state object order. Each state map entry defines a

// state object.

private:

virtual const StateMapRowEx* GetStateMapEx() { return NULL; }

virtual const StateMapRow* GetStateMap() {

static const StateMapRow STATE_MAP[] = {

sizeof(STATE_MAP)/sizeof(StateMapRow)) == ST_MAX_STATES);

return &STATE_MAP[0]; }

};

class uses macros for comparison:

Hide

class MotorData : public EventData

{

public:

};

// Motor class using macro support

class Motor : public StateMachine

{

public:

// External events taken by this state machine

void SetSpeed(MotorData* data);

void Halt();

private:

// State enumeration order must match the order of state method entries

// in the state map.

enum States

// Define the state machine state functions with event data type

// State map to define state object order. Each state map entry defines a

// state object.

};

implements our hypothetical motor-control state machine, where clients can start the motor, at a specific speed, and stop the motor. TheSetSpeed()before the function call is made.

SetSpeed()event transitions to Stop, where, during state execution, an internal event is generated to transition back to the Idle state.

Creating a new state machine requires a few basic high-level steps:

  1. macros.
  2. GUARD
  3. macros.
  4. TRANSITION_MAP

State functions

State functions implement each state ― one state function per state-machine state. In this implementation, all state functions must adhere this state-function signature, which is as follows:

Hide

void <class>::<func>(const EventData*)

<class> and <func> are not template parameters but just placeholders for the particular class and function name respectively. For example, you might choose a signature such asEventData*

macro. The macro arguments are the state machine class name, state function name and event data type.

Hide

Expanding the macros above yields:

Hide

void ST_Idle(const NoEventData*);

StateAction<Motor, NoEventData, &Motor::ST_Idle> Idle;

void ST_Stop(const NoEventData*);

StateAction<Motor, NoEventData, &Motor::ST_Stop> Stop;

void ST_Start(const MotorData*);

StateAction<MotorNM, MotorData, &Motor::ST_Start> Start;

void ST_ChangeSpeed(const MotorData*);

StateAction<Motor, MotorData, &Motor::ST_ChangeSpeed> ChangeSpeed;

STATE_DEFINE(Motor, Idle, NoEventData)ST_Idle()

  1. ST_ - state function prepend characters
  2. GD_ - guard function prepend characters
  3. EN_ - entry function prepend characters
  4. EX_ - exit function prepend characters

macro. The arguments are the state machine class name, state function name, and event data type. The code to implement your state behavior goes inside the state function. Note, any state function code may call

Hide

STATE_DEFINE(Motor, Stop, NoEventData)

{

"Motor::ST_Stop" << endl;

0;

// perform the stop motor processing here

// transition to Idle via an internal event

}

Expanding the macro yields this state function definition.

Hide

void Motor::ST_Stop(const NoEventData* data)

{

"Motor::ST_Stop" << endl;

0;

// perform the stop motor processing here

// transition to Idle via an internal event

}

Motor

Hide

enum States

{

};

EVENT_IGNOREDtells the state engine to fault. This abnormal catastrophic failure condition is never supposed to occur.

State map

m_currentStatem_currentState

Hide

BEGIN_STATE_MAP

STATE_MAP_ENTRY

END_STATE_MAP

is shown below.

Hide

BEGIN_STATE_MAP

GetStateMap()StateMapRow

Hide

private:

virtual const StateMapRowEx* GetStateMapEx() { return NULL; }

virtual const StateMapRow* GetStateMap() {

static const StateMapRow STATE_MAP[] = {

sizeof(STATE_MAP)/sizeof(StateMapRow)) == ST_MAX_STATES);

return &STATE_MAP[0]; }

Hide

BEGIN_STATE_MAP_EX

STATE_MAP_ENTRY_EX or STATE_MAP_ENTRY_ALL_EX

END_STATE_MAP_EX

Hide

BEGIN_STATE_MAP_EX

0, &EntryIdle, 0)

0, 0)

0, 0, &ExitWaitForAcceleration)

0, 0, &ExitWaitForDeceleration)

END_STATE_MAP_EX

StateMapRow:

Hide

struct StateMapRow

{

const StateBase* const State;

};

StateEngine().

Hide

class StateBase

{

public:

virtual void InvokeStateAction(StateMachine* sm, const EventData* data) const = 0;

};

InvokeStateAction()dynamic_cast<>.

Hide

template <class SM, class Data, void (SM::*Func)(const Data*)>

class StateAction : public StateBase

{

public:

virtual void InvokeStateAction(StateMachine* sm, const EventData* data) const

// Downcast the state machine and event data to the correct derived type

const Data* derivedData = dynamic_cast<const Data*>(data);

// Call the state function

};

StateAction<>SM), an event data type (Data) and a member function pointer to the state function (Func

GuardCondition<>EntryAction<>BOOLExitAction<>

Transition map

m_currentStatevariable to a state enum constant. Every external event has a transition map table created with three macros:

Hide

BEGIN_TRANSITION_MAP

TRANSITION_MAP_ENTRY

END_TRANSITION_MAP

Halt()defines the transition map as:

Hide

void Motor::Halt()

{

// - Current State -

// ST_IDLE

// ST_STOP

// ST_START

// ST_CHANGE_SPEED

}

C_ASSERT

Hide

void Motor::Halt()

{

static const BYTE TRANSITIONS[] = {

// ST_IDLE

// ST_STOP

// ST_START

// ST_CHANGE_SPEED

sizeof

}

BEGIN_TRANSITION_MAPHalt()EVENT_IGNORED

Hide

// ST_IDLE

This is interpreted as "If a Halt event occurs while the current state is state Idle, just ignore the event."

Similarly, the third entry in the map is:

Hide

// ST_START

Halt()NULL, but

State engine

StateMapRowStateEngine()GetStateMap()GetStateMapEx():

Hide

void StateMachine::StateEngine(void)

{

const StateMapRow* pStateMap = GetStateMap();

if (pStateMap != NULL)

else

const StateMapRowEx* pStateMapEx = GetStateMapEx();

if (pStateMapEx != NULL)

else

}

InvokeStateAction():

Hide

const StateBase* state = pStateMap[m_newState].State;

state->InvokeStateAction(this, pDataTemp);

Hide

void StateMachine::StateEngine(const StateMapRow* const pStateMap)

{

const

// While events are being generated keep executing states

while (m_eventGenerated)

// Error check that the new state is valid before proceeding

// Get the pointer from the state map

const StateBase* state = pStateMap[m_newState].State;

// Copy of event data pointer

// Event data used up, reset the pointer

// Event used up, reset the flag

// Switch to the new current state

// Execute the state action passing in event data

this, pDataTemp);

// If event data was used, then delete it

if (pDataTemp)

delete pDataTemp;

}

StateMapRowStateMapRowEx

  1. EVENT_IGNOREDCANNOT_HAPPEN
  2. FALSETRUE, or if no guard condition exists, the state function will be executed.

Generating events

Hide

MotorData* data = new MotorData();

data->speed = 50;

motor.SetSpeed(data);

InternalEvent(). If the destination doesn't accept event data, then the function is called with only the state you want to transition to:

Hide

InternalEvent(ST_IDLE);

In the example above, once the state function completes execution the state machine will transition to the Idle state. If, on the other hand, event data needs to be sent to the destination state, then the data structure needs to be created on the heap and passed in as an argument:

Hide

MotorData* data = new MotorData();

data->speed = 100;

InternalEvent(ST_CHANGE_SPEED, data);

External Event No Heap Data

EXTERNAL_EVENT_NO_HEAP_DATAExternalEvent()

Hide

MotorData data;

data.speed = 100;

motor.SetSpeed(&data);

InternalEvent()

Hide

MotorData* data = new MotorData();

data->speed = 100;

InternalEvent(ST_CHANGE_SPEED, data);

Hiding and eliminating heap usage

SetSpeed()SetSpeed()

Hide

void Motor::SetSpeed(INT speed)

{

new MotorData;

// - Current State -

// ST_IDLE

// ST_STOP

// ST_START

// ST_CHANGE_SPEED

}

EXTERNAL_EVENT_NO_HEAP_DATA

EventData

Hide

#include "xallocator.h"

class EventData

{

public:

virtual ~EventData() {}

};

For more information on xallocator, see the article "Replace malloc/free with a Fast Fixed Block Memory Allocator

State machine inheritance

StateMachinesupports state machine inheritance with minimal effort. I’ll use an example to illustrate.

SelfTestCentrifugeTest

Figure 2: CentrifugeTest state diagram

defines the following states:

  1. Idle
  2. Completed
  3. Failed

inherits those states and creates new ones:

  1. Start Test
  2. Acceleration
  3. Wait For Acceleration
  4. Deceleration
  5. Wait For Deceleration

Hide

enum States

{

};

// Define the state machine states

class defines its state machine below. Take note of the "ST_START_TEST = SelfTest::ST_MAX_STATES

Hide

enum States

{

// Continue state numbering using the last SelfTest::States enum value

};

// Define the state machine state functions with event data type

// State map to define state object order. Each state map entry defines a

// state object.

BEGIN_STATE_MAP_EX

0, &EntryIdle, 0)

0, 0)

0, 0, &ExitWaitForAcceleration)

0, 0, &ExitWaitForDeceleration)

CentrifugeTest. Notice the use of the _EX extended state map macros so that guard/entry/exit features are supported. For instance, a guard condition for the StartState is declared as:

Hide

GUARD_DECLARE(CentrifugeTest, GuardStartTest, NoEventData)

FALSE

Hide

GUARD_DEFINE(CentrifugeTest, GuardStartTest, NoEventData)

{

"CentrifugeTest::GuardStartTest" << endl;

if (m_speed == 0)

return// Centrifuge stopped. OK to start test.

else

return// Centrifuge spinning. Can't start test.

}

Base Class External Event Functions

partialmacro which is placed directly above the transition map as shown below.

Hide

void SelfTest::Cancel()

{

// - Current State -

// ST_IDLE

// ST_COMPLETED

// ST_FAILED

}

notST_IDLEST_COMPLETE

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!