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:
- Too large
- Too complex
- High learning curve
- External libraries
- 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:
- Compact
- Transition tables
- State action
- 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:
- 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:
- ― the motor is not spinning but is at rest.
- Do nothing.
- ― starts the motor from a dead stop.
- Turn on motor power.
- Set motor speed.
- ― adjust the speed of an already moving motor.
- Change motor speed.
- ― 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:
- macros.
- GUARD
- macros.
- 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()
- ST_ - state function prepend characters
- GD_ - guard function prepend characters
- EN_ - entry function prepend characters
- 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
- EVENT_IGNOREDCANNOT_HAPPEN
- 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:
- Idle
- Completed
- Failed
inherits those states and creates new ones:
- Start Test
- Acceleration
- Wait For Acceleration
- Deceleration
- 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