This section describes how to implement hierarchical state machines with the QP/C++ real-time embedded framework, which is quite a mechanical process consisting of just a few simple rules. (In fact, the process of coding state machines in QP/C++ has been automated by the QM model-based design and code-generating tool.)
To focus this discussion, this section uses the Calculator example, located in the directory qpcpp/examples/workstation/calc. This example has been used in the PSiCC2 book (Section 4.6 "Summary of Steps for Implementing HSMs with QEP")
This section explains how to code the following (marked) elements of a hierarchical state machine:
top
state) this
pointer, without the need for the me
pointer. This "no me-> pointer" state machine implementation is supported in the QM modeling tool starting from version 4.5.0.static
members of the state machine class, which don't have the this
pointer and therefore needed to use the specially supplied "me-> pointer" to access the members of the state machine class. That previous "me-> pointer" state machine implementation style is still supported in QP/C++ for backwards compatibility with the existing code, but it is not recommended for new designs.this
pointer), which allowed it to use internally the simple pointers to functions that are very efficient.this
pointer and therefore can access all members of the state machine class naturally (without the need for the "me-> pointer").Hierarchical state machines are represented in QP/C++ as subclasses of the QHsm abstract base class, which is defined in the header file qpcpp\include\qep.h. Please note that abstract classes like QP::QMsm, QP::QActive and QP::QMActive are also subclasses of QP::QHsm, so their subclasses also can have state machines.
Calc
(Calculator) derives from QP::QHsm, so it can have a state machine 2 The class can have data members (typically private), which will be accessible inside the state machine as the "extended-state variables".
initial
pseudo-state (see step [5]), which binds the state-machine to the class. The Q_STATE_DECL() macro declares two functions for every state: the "state-handler" regular member function and the "state-caller" static member function. So, for example, the declaration of the "on" state Q_STATE_DECL(on) expands to the following two declarations within the Calc
class:
The two functions have each a different purpose.
on_h()
is a regular member function used to implement the state behavior. As a regular class member, it has convenient, direct access to the state machine class attributes. The "state-handler" is called by the "state-caller". on()
is a static member function that has a simple job to call the state-handler member function on the specified instance of the class. Internally, the QEP event processor uses "state-callers" as unique "handles" for the states. Specifically, the QEP event processor uses the simple function pointers to these state-callers
, which are simple objects (e.g. 32-bit addresses in ARM Cortex-M CPUs), because they don't use the this
calling convention. These simple function pointers can be stored very efficiently inside the state machine objects and can be compared quickly inside the QEP algorithm that implements the UML semantics of hierarchical state machines. virtual
, which allows them to be overridden in the subclasses of a given state machine class. Such inheritance of entire sate machines is an advanced concept, which should be used only in very special circumstances and with great caution. To declare a virtual
state-handler, you simply prepend virtual
in front of the Q_STATE_DECL() macro, as in the following examples: The definition of the state machine class is the actual code for your state machine. You need to define (i.e., write the code for) all "state-handler" member functions you declared in the state machine class declaration. You don't need to explicitly define the "state-caller" static functions, because they are synthesized implicitly in the macro Q_STATE_DEF()).
One important aspect to realize about coding "state-handler" functions is that they are always called from the QEP event processor. The purpose of the "state-handlers" is to perform your specific actions and then to tell the event processor what needs to be done with the state machine. For example, if your "state-handler" performs a state transition, it executes some actions and then it calls the special tran(<target>) function, where it specifies the <target>
state of this state transition. The state-handler then returns the status from the tran()
function, and through this return value it informs the QEP event processor what needs to be done with the state machine. Based on this information, the event-processor might decide to call this or other state-handler functions to process the same current event. The following code examples should make all this clearer.
Every state that has been declared with the Q_STATE_DECL() macro in the state machine class needs to be defined with the Q_STATE_DEF() macro. For example, the state "ready" in the Calculator state machines, the Q_STATE_DEF(Calc, ready) macro expands into the following code:
Calc::ready()
state-caller function is fully defined to call the "state-handler" function on the provided me
pointer, which is explicitly cast to the class instance. Calc::ready_h()
"state-handler" member function is provided, which needs to be followed by the body ({...}
) of the "state-handler" member function. Every state machine must have exactly one top-most initial pseudo-state, which is assumed when the state machine is instantiated in the constructor of the state machine class. By convention, the initial pseudo-state should be always called initial.
This top-most initial pseudo-state has one transition, which points to the state that will become active after the state machine is initialized (through the QHsm::init() function). The following code the definition of the initial
pseudo-state for the Calc
class:
initial
in this case). this
pointer. e
, which is often not used. If the event is not used, this line of code avoids the compiler warning about unused parameter. tran()
function is the pointer to the target state of the transition. The top-most initial pseudo-state must return the value from the tran()
function. Every regular state (including states nested in other states) is also coded with the Q_STATE_DEF() macro. The function body, following the macro, consists of the switch
statement that discriminates based on the event signal (e->sig
). The following code shows the complete definition of the Calculator "on" state. The explanation section below the code clarifies the main points.
on
in this case). status_
will hold the status of what will happen in the state-handler. This status will be eventually returned from the state-handler to the QEP event processor. e->sig
, which is passed to the state-handler as parameter. 4 The special, reserved event signal Q_ENTRY_SIG is generated by the QEP event processor to let the state-handler process an entry action to the state.
NOTE: By convention, all event signals end with the _SIG
suffix. The _SIG
suffix is omitted in the QM state machine diagrams.
this
pointer) status_
to Q_RET_HANDLED. case
with the break
statement. status_
to Q_RET_HANDLED. status_
to the value returned from tran()
. DIGIT_1_9_SIG
is handled in its own case
statement. e
. The macro Q_EVT_CAST() encapsulates downcasting the event pointer e
to the specific event type (CalcEvt
in this case). DIGIT_1_9_SIG
event triggers a state transition to state "int1", which you code with the tran(&int1) function. 15-16 The state-handler function has direct access to the data members of the Calc
class.
NOTE: The previous implementation would require the use of "me->" pointer to access the data members.
default
case handles the situation when this state does not prescribe how to handle the given event. This is where you define the superstate of the given state. 18 The superstate of the given state is specified by calling the super() function.
NOTE: A state that does not explicitly nest in any state, such as the "on" state in the Calculator, calls super(&top)
switch
statement code and the single return
from the state-handler function is compliant with the MISRA standards.For embedded system applications, it is always interesting to know the overhead of the implementation used. It turns out that the chosen "state-caller"/"state-handler" implementation is very efficient. The following dis-assembly listing shows the code generated for invocation of a state-handler from the QEP code. The compiler used is IAR C/C++ EWARM 8.32 with Cortex-M target CPU and Medium level of optimization.
The machine code instructions [1-3] are the minimum code to call a function with two parameters via a function pointer (in R4). The single branch instruction [4] represents the only overhead of using the "state-caller" indirection layer. This instruction takes about 4 CPU clock cycles, which is minuscule and typically much better than using a pointer to a C++ member function.
Next: API Reference