OS  v1.7.5
Documentation
Loading...
Searching...
No Matches
Finite State Machines (FSM)

Overview

The state machine is one of the fundamental programming patterns that are most commonly used. This approach breaks down the design into a series of finite steps called "states" that perform some narrowly defined actions. Every state can change to another as a consequence of incoming stimuli also called events or signals. This elemental mechanism allows designers to solve complex engineering problems in a very straightforward way. Knowing the importance of this approach in the development of embedded applications, the OS adopts this design pattern as a kernel extension.

In an effort to maximize efficiency and minimize complexity, the extension implements the basic features of the Harel statecharts to represent hierarchical state machines. These features form a proper subset that approaches in a very minimalist way, some of the specifications of the UML statecharts, including:

  • Nested states with proper handling of group transitions and group reactions.
  • Guaranteed execution of entry/exit actions upon entering/exiting states.
  • Straightforward transitions and guards.

In addition to this, the provided implementation also features a powerful coding abstraction including transition tables and timeout signals, allowing to build scalable solutions from simple flat state-machines to complex statecharts.

The provided approach

In QuarkTS++, a state machine must be instantiated with an object of type qOS::stateMachine. States are represented as instances of the qOS::sm::state object.

fsmarch++
FSM module design

One important attribute of the qOS::sm::state object is the callback function, which is used to describe the behavior specific to the state. Also, there is a pointer to the parent state to define the nesting of the state and its place in the hierarchical topology. As shown in figure above, a state machine consists of a least one state, the "top-level" state. So concrete state machine are built by adding an arbitrary number of states and defining callback functions. The only purpose of the top state is to provide the root of the hierarchy, so that the highest level can return to the top as their parent state.

Setting up a state machine

Like any other OS object, a Finite State Machine (FSM) must be explicitly initialized before it can be used. The qOS::stateMachine::setup() method initializes the instance, sets the callback for the top state, sets the initial state and the surrounding callback function.

Subscribing states and defining callbacks

State machines are constructed by composition, therefore, the topology of a state machine is determined upon construction. In this FSM implementation, there is no distinction between composite states(states containing substates) and leaf states. All states are potentially composite. The method qOS::stateMachine::add() should be used to initialize the state and define its position in the topology.

A state callback-functions takes a qOS::sm::handler_t object as input argument and returns a sm::status value. An example is shown in the following code snippet:

sm::status ExampleState_Callback( sm::handler_t h ) {
/* TODO: State code */
return sm::status::SUCCESS;
}

States can also be defined using the object-oriented programming approach. In this particular case, a class must be defined that inherits from qOS::sm::state and the activities method, where the behavior of the state resides, must be overridden.

class myCustomState : public sm::state {
sm::status activities( sm::handler_t h ) override {
// TODO : State code
return sm::status::SUCCESS;
}
};

The state callback handler: performing transitions and retrieving data

Because callback functions are methods derived from the state-machine object, they have direct access to some attributes via the qOS::sm::handler_t argument. The usage of this object it's required to make the FSM moves between states and additionally get extra data. The provided attributes are:

  • qOS::sm::handler_t::nextState() : Method to set the desired next state. The application writer should use this method passing another state to produce a state transition in the next FSM's cycle. Changing this field will only take effect when the state is executed under user custom-defined signals or in the absence of signals signalID::QSM_SIGNAL_NONE. In additon the user can specify as second argument the history mode. Use this argument option if the transition is to a composite state. This argument defines how the story should be handled. If this argument is ignored, sm::historyMode::NO_HISTORY is assumed. The possible values for this attribute are:
    • sm::historyMode::NO_HISTORY : History is not preserved. Composite states will start according to their default transition.
    • sm::historyMode::SHALLOW_HISTORY : History will be kept to allow the return to only the top-most sub-state of the most recent state configuration, which is entered using the default entry rule.
    • sm::historyMode::DEEP_HISTORY : History will be kept to allow full state configuration of the most recent visit to the containing region.
  • qOS::sm::handler_t::startState() : Desired nested initial state (substate). The application writer should change this field to set the initial transition if the current state is a parent(or composite state). Changing this field attribute only takes effect when the state is executed under the signalID::SIGNAL_START signal.
  • qOS::sm::handler_t::signal() : Method to read the received signal. Can return any of the following values:
    • signalID::SIGNAL_NONE if no signal is available.
    • signalID::SIGNAL_ENTRY if the current state has just entered from another state.
    • signalID::SIGNAL_START to set nested initial transitions by using the qOS::sm::handler_t::startState() method.
    • signalID::SIGNAL_EXIT if the current state has just exited to another state.
    • Any other user-defined signal will reside here, including the qOS::sm::SIGNAL_TIMEOUT() signals.
  • qOS::sm::handler_t::SignalData (read-only) : Pointer to the data associated with the signal. For internal signals, including timeout signals, its value will be nullptr.
  • qOS::sm::handler_t::lastStatus() : Retrieve the exit(or return) status of the last state. Should be used in the Surrounding callback to perform the corresponding actions for every value. On states callback will take the value sm::status::ABSENT
  • qOS::sm::handler_t::thisMachine() : Method to get a reference to the container state machine.
  • qOS::sm::handler_t::thisState() : Method to get a reference to the state being evaluated.
  • qOS::sm::handler_t::Data (read-only) : State-machine associated data. If the FSM is running as a task, the associated event data can be queried through this field. (here, a cast to qOS::event_t is mandatory).
  • qOS::sm::handler_t::StateData (read-only) : State associated data. Storage-pointer.

Within the callback function of every state or the activities method, only one level of dispatching (based on the signal) is necessary. Typically this is archived using a single-level switch statement. Callback functions communicate with the state machine engine through the qOS::sm::handler_t and the return value of type sm::status.

The semantic is simple, if a signal is processed, the callback functions returns the status value sm::status::SIGNAL_HANDLED. Otherwise, it throws the signal for further processing by higher-level states. Also, this returning mechanism can be used to handle exceptions by using the surrounding callback.

Entry/Exit actions and default transitions are also implemented inside the callback function in the response to pre-defined signals. sm::signalID::SIGNAL_ENTRY, sm::signalID::SIGNAL_EXIT and sm::signalID::SIGNAL_START. The state machine generates and dispatches these signals to appropriate handlers upon state transitions.

The example below shows what a status callback should look like including the use of the handler.

sm::status ExampleState_Callback ( sm::handler_t h ) {
switch ( h.signal() ) {
case sm::signalID::SIGNAL_START:
break;
case sm::signalID::SIGNAL_ENTRY:
break;
case sm::signalID::SIGNAL_EXIT:
break;
case USER_DEFINED_SIGNAL :
h.nextState( OtherState ); /*transition*/
break;
default:
break;
}
return sm::status::SUCCESS;
}

As shown above, the return value represents the exit status of the state, and it can be handled with an additional surrounding callback function \( S_u \) established at the moment of the FSM setup. The values allowed to return are listed below.

  • sm::status::SUCCESS
  • sm::status::FAILURE
  • sm::status::SIGNAL_HANDLED
  • Any other integer value between -32762 and 32767

To code initial transitions, the application writer should catch the sm::signalID::SIGNAL_START, perform the required actions and then designate the target sub-state by using the qOS::sm::handler_t::startState() method. Regular transitions are coded in a very similar way, except that here, you catch the custom-defined signal and then use the qOS::sm::handler_t::nextState() method. The developer is free to write and control state transitions. Transitions are only allowed under the availability of user custom-defined signals. Regular transitions are not allowed at an entry point (signalID::SIGNAL_ENTRY), exit point (signalID::SIGNAL_EXIT), or a start point (signalID::SIGNAL_START).

Note
User should not target the top state in a transition and use it as transition source either. The only customizable aspect of the top state is the initial transition.

The surrounding callback

It is a checkpoint before and after each state executes its activities through its state callback. The behavior of this surrounding callback must be defined by the programmer.

surrounding
Surrounding callback invocation after and before the current state

The surrounding callback \( S_u \) invocation occurs after and before the current state \( P \). When the surrounding callback is executed, indicates its own checkpoint through the Status attribute of the sm::handler_t argument. Unlike a state callback, the surrounding callback should not return anything, thus, the callback should be written as:

void SurroundingCallback_Example( sm::handler_t h ) {
switch ( h.lastStatus() ) {
case sm::status::BEFORE_ANY:
/* TODO: before any code */
break;
case sm::status::FAILURE:
/* TODO: failure code */
break;
case sm::status::SUCCESS:
/* TODO: success code */
break;
case sm::status::SIGNAL_HANDLED:
/* TODO: signal handled code */
break;
case 5: /*user-defined return value*/
/* TODO: used defined*/
break;
default:
/*handle the unexpected*/
break
}
}

As you can see in the example below, the surrounding execution case its verified through the FSM handle by reading the Status field.

Adding a state machine as a task

The best strategy to run a FSM is delegating it to a task. For this, an overload of qOS::core::add() that takes an state-machine object as argument is provided. Here, the task does not have a specific callback, instead, it will evaluate the active state of the FSM, and later, all the other possible states in response to events that mark their own transition. The task will be scheduled to run every t seconds in qOS::task::PERIODIC mode.

By using this method, the kernel will take care of the FSM by itself, so the usage of qOS::stateMachine::run() can be omitted.

Now that a task is running a dedicated state machine, the specific task event-information can be obtained in every state callback through the Data field of the qOS::sm::handler_t argument.

Check the example below:

sm::status Example_State( sm::handler_t h ) {
event_t *e = h.Data;
/* Get the event info of the task that owns this state machine*/
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
break;
case sm::signalID::SIGNAL_EXIT:
break;
default:
switch ( e->getTrigger() ) {
case trigger::byTimeElapsed:
/* TODO: Code for this case */
break;
case trigger::byNotificationSimple:
/* TODO: Code for this case */
break;
case trigger::byQueueCount:
/* TODO: Code for this case */
break;
default: break;
}
/* TODO: State code */
break;
}
return sm::status::SUCCESS;
}

A demonstrative example for a FSM

In this example, one press of the button turns on the LED, a second push of the button will make the LED blink and if the button is pressed again, the LED will turn off. Also, our system must turn off the LED after a period of inactivity. If the button hasn't been pressed in the last 10 seconds, the LED will turn off.

ledfsm++
Flat FSM example with three states

To start the implementation, let's define the necessary global variables...

task LED_Task; /*The task node*/
stateMachine LED_FSM; /*The state -machine handle*/
sm::state State_LEDOff, State_LEDOn, State_LEDBlink;

Then, we define our states as the flow diagram shown in the figure above.

sm::status State_LEDOff_Callback( sm::handler_t h ) {
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
BSP_LED_OFF();
break;
case sm::signalID::SIGNAL_EXIT: case sm::signalID::SIGNAL_START: /*Ignore*/
break;
default:
if ( BUTTON_PRESSED ) {
h.nextState( State_LEDOn );
}
break;
}
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status State_LEDOn_Callback( sm::handler_t h ) {
static timer timeout;
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
timeout.set( 10_sec );
BSP_LED_ON();
break;
case sm::signalID::SIGNAL_EXIT: case sm::signalID::SIGNAL_START: /*Ignore*/
break;
default:
if ( timeout.expired() ) {
h.nextState( State_LEDOff );
}
if ( BUTTON_PRESSED ) {
h.nextState( State_LEDBlink );
}
break;
}
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status State_LEDBlink_Callback( sm::handler_t h ) {
static timer timeout;
static timer blinktime;
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
timeout.set( 10_sec );
break;
case sm::signalID::SIGNAL_EXIT: case sm::signalID::SIGNAL_START: /*Ignore*/
break;
default:
if ( timeout.expired() || BUTTON_PRESSED ) {
h.nextState( State_LEDOff );
}
if ( blinktime.freeRun( 500_ms ) ) {
BSP_LED_TOGGLE();
}
break;
}
return sm::status::SUCCESS;
}

Finally, we add the task to the scheduling scheme running the dedicated state machine. Remember that you must set up the scheduler before adding a task to the scheduling scheme.

LED_FSM.setup( nullptr, State_LEDOff );
LED_FSM.add( State_LEDOff, State_LEDOff_Callback );
LED_FSM.add( State_LEDOn, State_LEDOn_Callback );
LED_FSM.add( State_LEDBlink, State_LEDBlink_Callback );
os.add( LED_Task , LED_FSM , core::MEDIUM_PRIORITY, 100_ms );
bool add(task &Task, taskFcn_t callback, const priority_t p, const duration_t t, const iteration_t n, const taskState s=taskState::ENABLED_STATE, void *arg=nullptr) noexcept
Add a task to the scheduling scheme. The task is scheduled to run every t time units,...

Sending signals

To communicate within and between state machines or even other contexts, use signals. A signal is a simple value that can be used to abstract an incoming event. In the receiving state machine, a queue or an exclusion variable receives the signal and holds it until the state machine can evaluate it.

When coding state machines, the application writer can benefit from this simple event-abstraction mechanism. On the one hand, there would be a more uniform programming when writing state callbacks and on the other hand, the communication of the state machine from other contexts becomes easier.

To send a signal to a state machine, use the qOS::stateMachine::sendSignal() method. This method can manage their delivery to one of these possible destinations: an exclusion variable or a signal queue:

  • An exclusion variable its a variable with an important distinction, it can only be written if it is empty. The empty situation only happens, if the engine has already propagated the signal within the state machine. If the signal has not yet propagated, the signal sending cannot be carried out.
  • When a signal queue is used, the signal is put into a FIFO structure and the engine takes care of dispatching the signal in an orderly manner. The only situation where the signal cannot be delivered is if the queue is full. This is the preferred destination, as long as there is a previously installed signal queue.
Note
If the signal-queue its available, the qOS::stateMachine.sendSignal() method will always select it as the default destination.
Warning
If a state machine, a task, or another context sends a signal to a full queue, a queue overflow occurs. The result of the queue overflow it that the state machine drops the new signal.

Installing a signal queue

A state machine can have a FIFO queue to allow the delivery of signals from another contexts. If the signal queue is installed, the state-machine engine constantly monitors the queue for available signals. The engine then propagates the signal through the hierarchy until it is processed. To enable this functionality in your state machine, the queue must be installed by using the qOS::stateMachine::install() method.

The install operation should be performed after both, the queue and the FSM are correctly initialized by using qOS::queue::setup() and qOS::stateMachine::setup() respectively.

Note
Make sure that queues are enabled in the config.h header file
Attention
Use the qOS::sm::signalQueue struct to instantiate a signal queue for FSM. If not, user should setup explicitly the signal queue with qOS::queue::setup(), and size it based on the type qOS::sm::signal_t.
Note
If the state machines its delegated to a task, make sure to install the queue prior to setting up the task. In this way, a kernel connection can be performed between the FSM signal queue and the FSM-task, allowing the OS to catch signals to produce a task event, this prevents the wait of the task for the specified period, resulting in faster handling of incoming signals.

Using a transition table

In this approach, the FSM is coded in tables with the outgoing transitions of every state, where each entry relates signals, actions and the target state. This is an elegant method to translate the FSM to actual implementation as the handling for every state and event combination is encapsulated in the table.

Here, the application writer gets a quick picture of the FSM and the embedded software maintenance is also much more under control. A transition table should be explicitly installed in the target state with the corresponding entries, an n-sized array of sm::transition elements following the layout described below:

The method qOS::sm::state::install(), should be used to perform the transition table installation to a specific state.

General transition table layout for a state
Signal Id Signal action/guard Target state History mode Signal data
sm::signalID signalAction_t sm::state& historyMode void*
Signal1 nullptr StateB 0 nullptr
Signal3 DoOnSignal3 StateD 0 &sig3data
... ... ... ... ...
Signal6 nullptr StateA 0 nullptr
reduced transition table layout for a state
Signal Id Target state
sm::signalID sm::state*
Signal1 StateB
... ...
Signal6 StateA

Caveats:

  • State transitions are not limited to the specification of the transition table. A state callback owns the higher precedence to change a state. The application writer can use both, a transition table and the qOS::sm::handler_t::nextState() method in state callbacks to perform a transition to the FSM.
  • Special care is required when the table grows very large, that is, when there are many invalid state/event combinations, leading to a waste of memory. There is also a memory penalty as the number of states and events grows. The application writer needs to accurately account for this during the initial design. A statechart pattern can be used to improve the design and reduce the number of transition entries.
  • The user is responsible for defining the transitions according to the topology of the state machine. Undefined behaviors can occur if the topology is broken with poorly defined transitions.

Signal actions and guards

Transition tables allow the usage of this feature. When an event-signal is received from the queue, the signal-action, if available, is evaluated before the transition is triggered. This action is user-defined and should be coded as a function that takes a qOS::sm::handler_t object and returns a value of type bool.

bool Signal_Action( sm::handler_t h ) {
/* TODO : Event -signal action*/
return true; /*allow the state transition*/
}

The return value is checked after to allow or reject the state transition. The application writer can code a boolean expression to implement statechart guards or perform some pre-transition procedure.

Remarks
If a signal-action returns false, the event-signal is rejected, preventing the state transition to be performed in the calling FSM.
Note
When a transition entry is defined. the signal-action should be located as the third parameter of the entry. Please see the transition layout. A nullptr value will act as a NOT-defined, always allowing the state-transition.

FSM Timeout specification

A timeout specification is a mechanism to simplify the notion of time passage inside states. The basic usage model of the timeout signals is as follows:

A timeout specification allocates one or more timer objects. The user relates in a table each specific timeout operation within the state where are they going to operate. So, according to the table, when a state needs to arrange for a timeout, the engine can set or reset the given timer. When the FSM engine detects that the appropriate moment has arrived (a timer expiration occurs), it inserts the timeout signal directly into the recipient's event queue. The recipient then processes the timeout signal just like any other signal.

Given the above explanation, it is evident that for its operation, the state machine requires an installed signal queue. A timeout specification is referenced by an object of type qOS::sm::timeoutSpec and must be installed inside the state machine using the method qOS::stateMachine::install(). Then, timeout operations can be defined in a table for each state by using the qOS::sm::state::install().

A timeout specification element is defined as a structure of type qOS::sm::timeoutStateDefinition and should follow this layout:

Timeout specification layout
Timeout value Options

The options for every timeout its a bitwise value that indicates which timeout should be used and the operations than should be performed internally by the state-machine engine. These options can be combined with a bitwise OR and are detailed as follows:

  • sm::TIMEOUT_USE_SIGNAL(index) : To select the timeout signal to be used in the specification. Should be a value between 0 and Q_FSM_MAX_TIMEOUTS-1
  • sm::TIMEOUT_SET_ENTRY : To set the timeout when the specified state its entering.
  • sm::TIMEOUT_RST_ENTRY : To reset the timeout when the specified state its entering.
  • sm::TIMEOUT_SET_EXIT : To set the timeout when the specified state its exiting.
  • sm::TIMEOUT__RST_EXIT : To reset the timeout when the specified state its exiting.
  • sm::TIMEOUT_KEEP_IF_SET : To apply the Set operation only if the timeout its in a reset state.
  • sm::TIMEOUT_PERIODIC : To put the timeout in periodic mode.
Note
Data associated with timeout signals should be set to nullptr. Any other value will be ignored and will be passed as nullptr to the FSM handler.
Attention
The user is responsible for writing timeout specifications correctly. Care must be taken that the specifications do not collide between hierarchical states to avoid overwriting operations.
Note
You can increase the number of available timeouts instances by changing the Q_FSM_MAX_TIMEOUTS configuration macro inside config.h.

Demonstrative example using transition tables

The following example shows the implementation of the led-button FSM presented above by using the transition table approach with signal-queue and a timeout specification.

Before getting started, the required variables should be defined:

/*define the FSM application event-signals*/
enum : sm::signalIDType {
SIGNAL_BUTTON_PRESSED = sm::SIGNAL_USER( 1 ),
SIGNAL_DELAY = sm::SIGNAL_TIMEOUT( 0 ),
SIGNAL_BLINK = sm::SIGNAL_TIMEOUT( 1 ),
};
task LED_Task; /*The task node*/
stateMachine LED_FSM; /*The state-machine handler*/
sm::state State_LEDOff, State_LEDOn, State_LEDBlink;
sm::signalQueue<5> LEDsigqueue;
sm::timeoutSpec tm_spectimeout;
/*create the transition tables for every state*/
sm::transition LEDOff_transitions[] = {
{ SIGNAL_BUTTON_PRESSED, State_LEDOn}
};
sm::transition LEDOn_transitions[] = {
{ SIGNAL_DELAY, State_LEDOff },
{ SIGNAL_BUTTON_PRESSED, State_LEDBlink }
};
sm::transition LEDBlink_transitions[] = {
{ SIGNAL_DELAY, State_LEDOff },
{ SIGNAL_BUTTON_PRESSED, State_LEDOff }
};
/*define the timeout specifications */
sm::timeoutStateDefinition LedOn_Timeouts[] = {
{ 10_sec, sm::TIMEOUT_USE_SIGNAL( 0 ) | sm::TIMEOUT_SET_ENTRY | sm::TIMEOUT_RST_EXIT },
};
sm::timeoutStateDefinition LEDBlink_timeouts[] = {
{ 10_sec, sm::TIMEOUT_USE_SIGNAL( 0 ) | sm::TIMEOUT_SET_ENTRY | sm::TIMEOUT_RST_EXIT },
{ 0.5_sec, sm::TIMEOUT_USE_SIGNAL( 1 ) | sm::TIMEOUT_SET_ENTRY | sm::TIMEOUT_RST_EXIT | sm::TIMEOUT_PERIODIC }
};

Then, we define the callback for the states.

sm::status State_LEDOff_Callback( sm::handler_t h ) {
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
BSP_LED_OFF();
break;
default:
break;
}
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status State_LEDOn_Callback( sm::handler_t h ) {
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
BSP_LED_ON();
break;
default:
break;
}
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status State_LEDBlink_Callback( sm::handler_t h ) {
switch ( h.signal() ) {
case SIGNAL_BLINK:
BSP_LED_TOGGLE();
break;
default:
break;
}
return sm::status::SUCCESS;
}

In the previous code snippet, we assumed that SIGNAL_BUTTON_PRESSED can be delivered from either the interrupt context or another task.

To finish the setup, a task is added to handle the FSM and then, the transition table can be installed with the other required objects.

LED_FSM.setup( nullptr, State_LEDOff );
LED_FSM.add( State_LEDOff, State_LEDOff_Callback );
LED_FSM.add( State_LEDOn, State_LEDOn_Callback );
LED_FSM.add( State_LEDBlink, State_LEDBlink_Callback );
LED_FSM.install( LEDsigqueue );
LED_FSM.install( tm_spectimeout );
State_LEDOff.install( LEDOff_transitions );
State_LEDOn.install( LEDOn_transitions, LedOn_Timeouts );
State_LEDBlink.install( LEDBlink_transitions, LEDBlink_timeouts );
os.add( LED_Task, LED_FSM, core::MEDIUM_PRIORITY, 100_ms );

Using the hierarchical approach

In conventional state machine designs, all states are considered at the same level. The design does not capture the commonality that exists among states. In real life, many states handle most transitions in similar fashion and differ only in a few key components. Even when the actual handling differs, there is still some commonality. It is in these situations where the hierarchical designs make the most sense.

A hierarchical state-machine is characterized by having compound states. A composite state is defined as state that has inner states and can be used as a decomposition mechanism that allows factoring of common behaviors and their reuse. And this is the biggest advantage of this design, because it captures the commonality by organizing the states as a hierarchy. The states at the higher level in the hierarchy perform the common handling, while the lower level states inherit the commonality from higher level ones and perform the state specific functions.

Example using a hierarchical FSM

This example takes the "Cruise Control" study case, a real-time system that manages the speed of an automobile based on inputs from the driver.

hsm
Cruise control FSM example

The behavior of this system is state-dependent in that the executed actions correspond not only to the driver input, but also to the current state of the system and with the status of the engine and the brake. The figure above illustrates the modeling of this system with the "Automated Control" state acting as composite.

Before getting started, the required user-defined signals, variables, and entries of the transition table should be defined:

enum : sm::signalIDType {
SIGNAL_ENGINE_ON = sm::SIGNAL_USER( 1 ),
SIGNAL_ACCEL = sm::SIGNAL_USER( 2 ),
SIGNAL_RESUME = sm::SIGNAL_USER( 3 ),
SIGNAL_OFF = sm::SIGNAL_USER( 4 ),
SIGNAL_BRAKE_PRESSED = sm::SIGNAL_USER( 5 ),
SIGNAL_CRUISE = sm::SIGNAL_USER( 6 ),
SIGNAL_REACHED_CRUISING = sm::SIGNAL_USER( 7 ),
SIGNAL_ENGINE_OFF = sm::SIGNAL_USER( 8 ),
};
task CruiseControlTask;
stateMachine Top_SM;
/*highest level states*/
sm::state state_idle, state_initial, state_cruisingoff, state_automatedcontrol;
/*states inside the state_automatedcontrol*/
sm::state state_accelerating, state_cruising, state_resuming;
sm::signalQueue<10> top_sigqueue;
/*=======================================================================*/
/* TRANSITION TABLES */
/*=======================================================================*/
sm::transition idle_transitions[] = {
{ SIGNAL_ENGINE_ON, SigAct_ClearDesiredSpeed, state_initial }
};
sm::transition initial_transitions[] = {
{ SIGNAL_ACCEL, SigAct_BrakeOff, state_accelerating }
};
sm::transition accel_transitions[] = {
{ SIGNAL_CRUISE, state_cruising }
};
sm::transition cruising_transitions[] = {
{ SIGNAL_OFF, state_cruisingoff },
{ SIGNAL_ACCEL, state_accelerating }
};
sm::transition resuming_transitions[] = {
{ SIGNAL_ACCEL, state_accelerating }
};
sm::transition cruisingoff_transitions[] = {
{ SIGNAL_ACCEL, SigAct_BrakeOff, state_accelerating },
{ SIGNAL_RESUME, SigAct_BrakeOff, state_resuming },
{ SIGNAL_ENGINE_OFF, state_idle }
};
sm::transition automated_transitions[] = {
{ SIGNAL_BRAKE_PRESSED, state_cruisingoff }
};
/*---------------------------------------------------------------------*/

Then, signal-actions and state callbacks are later defined

/*=======================================================================*/
/* EVENT-SIGNAL ACTIONS AND GUARDS */
/*=======================================================================*/
bool SigAct_ClearDesiredSpeed( sm::handler_t h ) {
(void)h;
Speed_ClearDesired();
return true;
}
/*---------------------------------------------------------------------*/
bool SigAct_BrakeOff( sm::handler_t h ) {
(void)h; /*unused*/
return ( BSP_BREAK_READ() == OFF ) ? true : false; /*check guard*/
}
/*=======================================================================*/
/* STATE CALLBACK FOR THE TOP FSM */
/*=======================================================================*/
sm::status state_top_callback( sm::handler_t h ) {
sm::status RetVal = sm::status::SUCCESS;
switch ( h.signal() ) {
case sm::signalID::SIGNAL_ENTRY:
break;
case sm::signalID::SIGNAL_EXIT:
break;
}
return RetVal;
}
/*=======================================================================*/
/* CALLBACKS FOR THE STATES ABOVE TOP */
/*=======================================================================*/
sm::status state_idle_callback( sm::handler_t h ) {
/*TODO : state activities*/
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status state_initial_callback( sm::handler_t h ) {
/*TODO : state activities*/
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status state_cruisingoff_callback( sm::handler_t h ) {
/*TODO : state activities*/
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status state_automatedcontrol_callback( sm::handler_t h ) {
/*TODO : state activities*/
return sm::status::SUCCESS;
}
/*=======================================================================*/
/* STATE CALLBACKS FOR THE AUTOMATED CONTROL FSM */
/*=======================================================================*/
sm::status state_accelerating_callback( sm::handler_t h ) {
switch ( h.signal() ) {
case sm::signalID::SIGNAL_EXIT:
Speed_SelectDesired();
break;
default:
Speed_Increase();
break;
}
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status state_resuming_callback( sm::handler_t h ) {
Cruising_Resume();
return sm::status::SUCCESS;
}
/*---------------------------------------------------------------------*/
sm::status state_cruising_callback( sm::handler_t h ) {
Speed_Maintain();
return sm::status::SUCCESS;
}

Finally, the dedicated task for the FSM and related objects are configured.

Top_SM.setup( state_top_callback, state_idle );
/*subscribe to the highest level states*/
Top_SM.add( state_idle, state_idle_callback );
Top_SM.add( state_initial, state_initial_callback );
Top_SM.add( state_cruisingoff, state_cruisingoff_callback );
Top_SM.add( state_automatedcontrol, state_automatedcontrol_callback );
/*subscribe to the states within the state_automatedcontrol*/
state_automatedcontrol.add( state_accelerating, state_accelerating_callback );
state_automatedcontrol.add( state_resuming, state_resuming_callback );
state_automatedcontrol.add( state_cruising, state_cruising_callback );
Top_SM.install( top_sigqueue );
state_idle.install( idle_transitions );
state_initial.install( initial_transitions );
state_cruisingoff.install( cruisingoff_transitions );
state_automatedcontrol.install( automated_transitions );
state_accelerating.install( accel_transitions );
state_resuming.install( resuming_transitions );
state_cruising.install( cruising_transitions );
os.add( CruiseControlTask, Top_SM, core::MEDIUM_PRIORITY, 100_ms );

Example with history pseudo-states

State transitions defined in high-level composite states often deal with events that require immediate attention; however, after handling them, the system should return to the most recent substate of the given composite state. UML statecharts address this situation with two kinds of history pseudostates: "shallow history" and "deep history"( denoted as the circled H and H* icon respectively in the figure).

fsmhist
Example with history pseudo-states
  • Shallow history : A transition to the shallow history state in a composite state invokes the last state that was active, at the same depth as the history state itself, prior to the most recent exit of the composite state.
  • Deep history : A transition to the deep history state within a composite state invokes the state that was active, immediately before the most recent exit of the composite state. The last active state can be nested at any depth.

Here, the way to specify this type of transition in QuarkTS++ is very straightforward, you only need to assign the history-mode in the last entry of the transition as shown below:

enum : sm::signalIDType {
SIGNAL_A = sm::SIGNAL_USER( 1 ),
SIGNAL_B = sm::SIGNAL_USER( 2 ),
SIGNAL_C = sm::SIGNAL_USER( 3 ),
SIGNAL_D = sm::SIGNAL_USER( 4 ),
SIGNAL_E = sm::SIGNAL_USER( 5 ),
SIGNAL_F = sm::SIGNAL_USER( 6 ),
};
sm::signalQueue<10> sigqueue;
stateMachine super;
sm::state state1 , state2 , state3 , state4 , state5 , state6;
sm::transition state1_transitions [] = {
{ SIGNAL_A , state2 , sm::historyMode::SHALLOW_HISTORY },
{ SIGNAL_B , state2 , sm::historyMode::DEEP_HISTORY },
{ SIGNAL_C , state2 }
};
sm::transition state2_transitions [] = {
{ SIGNAL_D , state1 }
};
sm::transition state3_transitions [] = {
{ SIGNAL_E , state4 }
};
sm::transition state5_transitions [] = {
{ SIGNAL_F , state6 }
};

Next, the configuration and topology of the state-machine is presented, including the default transitions (the small circles filled with black). Please do not forget to define the callbacks for each state.

super.setup( state_top_callback , state1 );
super.add( state1 , state1_callback );
super.add( state2 , state2_callback, state3 );
state2.add( state3, state3_callback );
state2.add( state4, state4_callback, state5 );
state4.add( state5, state5_callback );
state4.add( state6, state6_callback );
super.installSignalQueue( sigqueue );
state1.install( state1_transitions );
state2.install( state2_transitions );
state3.install( state3_transitions );
state5.install( state5_transitions );
os.add( SMTask , super , core::MEDIUM_PRIORITY, 100_ms );