OS
v1.7.5
Documentation
|
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:
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.
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.
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.
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.
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:
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.
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:
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.signalID::SIGNAL_START
signal.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.nullptr
.sm::status::ABSENT
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.
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
-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
).
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.
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:
As you can see in the example below, the surrounding execution case its verified through the FSM handle by reading the Status
field.
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:
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.
To start the implementation, let's define the necessary global variables...
Then, we define our states as the flow diagram shown in the figure above.
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.
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:
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.
config.h
header file 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.
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 |
Signal Id | Target state |
---|---|
sm::signalID | sm::state* |
Signal1 | StateB |
... | ... |
Signal6 | StateA |
Caveats:
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
.
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.
false
, the event-signal is rejected, preventing the state transition to be performed in the calling FSM. nullptr
value will act as a NOT-defined, always allowing the state-transition.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 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.nullptr
. Any other value will be ignored and will be passed as nullptr
to the FSM handler. Q_FSM_MAX_TIMEOUTS
configuration macro inside config.h
.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:
Then, we define the callback for the states.
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.
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.
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.
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:
Then, signal-actions and state callbacks are later defined
Finally, the dedicated task for the FSM and related objects are configured.
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).
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:
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.