… the cost of adding a feature isn't just the time it takes to code it. The cost also includes the addition of an obstacle to future expansion. … The trick is to pick the features that don't fight each other.
Chapter 2 introduced UML statecharts as a very effective way of getting around the state-explosion problem that plagues the traditional “flat” state machines. The particularly valuable innovation of UML state machines in this respect is the concept of state nesting, because it allows reusing behavior across many states instead of repeating the same actions and transitions over and over again. Hierarchical nesting of states lets you get new behavior almost for free by inheriting all of what is common from the superstates. It lets you define new states rapidly by difference from existing states rather than create every state from scratch each time. Needless to say, formalism like this is a godsend to the developers of event-driven software, because only state hierarchy makes the whole state machine approach truly applicable to real-life problems.
That is, the concept of the hierarchical state machine (HSM) is a true blessing only if it is easy enough to implement in a mainstream programming language, which for embedded systems developers means C. As a visual formalism, HSMs have been intended primarily for automatic code generation by specialized CASE tools (see Section 2.3.13). However, direct manual coding of HSMs isn't really any harder than coding the traditional nonhierarchical FSMs, especially when you use a generic hierarchical event processor that transparently handles all the intricacies of the UML state machine execution semantics.
This chapter describes such a generic hierarchical event processor called QEP, which is part of the QP event-driven framework. We already used the QEP event processor in Section 3.6 of Chapter 3 for implementing traditional “flat” FSMs. Here I describe how this technique can be generalized to support HSMs without sacrificing its good characteristics.
I begin with describing the structure of QEP, explaining both the C and C++ versions. Later in the chapter, I summarize the steps required to implement the calculator HSM designed in Chapter 2. I then provide some guidelines for using the QEP event processor in practice to avoid common pitfalls. I conclude with the instructions for porting and configuring QEP for various processors and compilers.
QEP is a generic, efficient, and highly portable hierarchical event processor that you can use in any event-driven environment, such as GUI systems, computer games, or real-time embedded (RTE) systems. QEP makes every effort to be compliant with the UML specification [OMG 07], but it cannot really implement the entire bulky UML state machine package. Instead, the QEP design strategy is to supply just enough (but not more) of truly essential elements to allow building basic UML-compliant state machines directly and support the higher-level UML concepts only as design patterns. The main features of QEP are:
• Guaranteed entry/exit action execution on arbitrary state transition topology.
• Highly maintainable and traceable boilerplate state machine representation in C or C++, in which every state machine element is mapped to code precisely, unambiguously, and exactly once. This is in contrast to many automatic code generation techniques that often “flatten” the state hierarchy, which breaks traceability by repeating the same transitions and actions in many states.
Note
The direct, precise, and unambiguous mapping of every state machine element to code contribute to the excellent traceability of the QEP HSM implementation technique. The traceability from requirements through design to code is essential for mission-critical systems, such as medical devices or avionic systems.
• Extremely small RAM/ROM footprint. A state machine object requires only one function pointer in RAM. On the ARM Cortex-M3 processor, the hierarchical state machine code requires about 600 bytes whereas the simpler “flat” finite state machine takes only about 120 bytes of code space (ROM).
• No RAM required for representing states and transitions—the number of states is limited only by code space (ROM).
• Fully reentrant event processor code with minimal stack requirements.
• Easy to integrate with any event queuing and dispatching mechanism—for example, simple event-loop, a GUI system like Windows, or an event-driven framework like QP.
• Very clean source code passing strict static analysis with PC-Lint.
• Source code 98 percent compliant with the Motor Industry Software Reliability Association (MISRA) Guidelines for the Use of the C Language in Vehicle-Based Software [MISRA 98].
• Documentation, application examples, and ports to various compilers are available online.
• Q-SPY software tracing instrumentation for unprecedented observability, controllability, and testability (see Chapter 11).
Note
In Chapter 5, you will see how to realize event deferral, orthogonal regions, and transitions to history as state design patterns that build on top of the basic QEP implementation described in this chapter.
Figure 4.1 shows the overall structure of QEP and its relation to the application-specific code, such as the calculator HSM from Figure 2.18 QEP consists of the QHsm
class1 The concept of a “class” in C and derivation of classes is explained in the sidebar “Single Inheritance in C” in Chapter 1. for derivation of state machines and the QEvent
class for derivation of events with parameters, or used as is for events without parameters.
Figure 4.1. QEP event processor and the calculator HSM derived from it. The $ in front of the Calc operations denotes static member functions (see Appendix B).
The QHsm
class keeps track of the current state and provides the standard state machine interface2 Section 3.2 in Chapter 3 introduces the standard state machine interface. operations init()
and dispatch()
. This class is abstract, which means that it is not intended for direct instantiation but rather only for derivation of concrete3 Concrete class is the OOP term and denotes a class that has no abstract operations or protected constructors. Concrete class can be instantiated, as opposed to abstract class, which cannot be instantiated. state machine classes, such as Calc
, shown in Figure 4.1. The derived state machine class defines all extended state variables as data members and provides state-handler functions for all states (e.g., see the note attached to the Calc::on()
member function in Figure 4.1). The following sections explain the implementation of all the QEP elements in C and C++.
Listing 4.1 shows the directories and files comprising the QEP event processor in C. The structure of the C++ version is identical, except that the implementation files have the .cpp
extension.
Listing 4.1. QEP event processor source code organization
+-include - QP platform-independent include files (*.H files)
| | +-dos - ports for DOS (the non-preemptive "vanilla" scheduler)
| | | | | +-dbg - build directory for the Debug configuration
| | | | | +-make.bat – make script for building the QP libraries
| +-source - QEP platform-independent source code (*.C files)
| | +-opt_qep.lnt - specific PC-Lint options for linting QEP
The QEP source files contain typically just one function or a data structure definition per file. This design aims at deploying QEP as a fine-granularity library that you statically link with your applications. Fine granularity means that the QEP library consists of several small, loosely coupled modules (object files) rather than a single module that contains all functionality. For example, a separate module qhsm_in.c
implements the QHsm_isIn()
function; therefore, if your application never calls this function, the linker will not pull in the qhsm_in
module. This strategy puts the burden on the linker to do the heavy lifting of eliminating any unused code automatically at link time, rather than on the application programmer to configure the QEP code for each application at compile time.
Note
The QEP code is instrumented with Q-SPY macros to generate software trace output from state machine execution. However, the instrumentation is disabled by default and will not be shown in the listings discussed in this chapter, for better clarity. Refer to Chapter 11 for more information about Q-SPY software tracing.
The event representation for HSMs is in QEP exactly the same as for FSMs (see Section 3.6 in Chapter 3). Event instances are implemented as event objects that combine the signal and the event parameters into one entity. As shown in Figure 4.1, QEP provides the QEvent
base class for direct instantiation of events without parameters or for derivation of events with arbitrary parameters.
A signal in UML is the specification of an asynchronous stimulus that triggers reactions [OMG 07] and as such is an essential part of an event. The signal conveys the type of the occurrence—what happened. Signals are typically enumerated constants. The following fragment of QEvent.h
header file defines the type of the signal QSignal
to be either 8 bits (uint8_t
), 16 bits (uint16_t
), or 32 bits wide (uint32_t
), depending on the configuration macro Q_SIGNAL_SIZE
. If you don't define this macro, a default of 8 bits is assumed.
Listing 4.2 shows the definition of the QEvent
structure in C. The member sig
of type QSignal
represents the signal. The byte-wide member dynamic_
is used by the QP framework to manage dynamically allocated events. You should never need to access this member from the application-level code. (I'll explain dynamic event allocation in Chapter 7.)
The QEvent
structure can be used as is for events without parameters or can serve as the base structure for derivation of events with arbitrary parameters. The following C code snippet shows how to derive the calculator event CalcEvt
that contains the key-code parameter (also see the sidebar “Single Inheritance in C” in Chapter 1):
Having the common base structure QEvent
for all events ensures that every event object contains the signal at the same offset within the event. This allows using a pointer to a derived event as a parameter to any function that expects a generic QEvent *e
pointer to the base structure. Any such function can always access the sig
data member (as e->sig
) to determine what kind of derived event structure is used. The function can then perform an explicit downcast4 Casting from the subclass to the superclass is called in OOP downcasting because the cast goes down a traditionally drawn inheritance relationship
in a class diagram, such as Figure 4.1. to the derived event structure to get the event parameters. For example, to get the key_code
parameter, a generic QEvent *e
pointer needs to be cast to CalcEvt
as follows: ((CalcEvt *)e)->key_code
.
The point here is that the sig
data member has double responsibility. It obviously must convey the occurrence (what happened?). But in addition, the signal must also uniquely identify the derived event structure so that state-handler functions can explicitly downcast to this derived structure based only on the sig
value. This second responsibility will became clearer in the upcoming Section 4.4.2, where I provide examples of state handler functions.
In C++, the QEvent
structure can be defined without the ugly typedef
, as shown in Listing 4.3. Please note that in C++ struct
is exactly equivalent to class
, except in struct
the default protection level is public and in class
it is private.
Event instances are used primarily as “bags” for passing around signals and event parameters. To generate events efficiently, it's often convenient to use statically preallocated, constant event objects initialized with an initializer list. To allow such initialization in C++, a class must be an aggregate; that is, it must not have private or protected members, constructors, base classes, and virtual functions [Stroustrup 00]. For that reason, QEvent
is declared as struct
in Listing 4.3, without any private members or constructors. (An obvious constructor would take one argument to initialize the sig
attribute.)
The following C++ code snippet shows how to derive the calculator event CalcEvt
class that contains the key-code parameter:
When you derive from QEvent
, the subclass is obviously no longer an aggregate. However, I recommend that you still keep your event classes simple and lightweight. I like to define the QEvent
subclasses using the struct
keyword as a reminder that they are lightweight. In particular, I avoid private members, constructors, or virtual functions in the derived event classes. As you will see in Chapter 7, events generally do not go through conventional instantiation (the standard operator new
isn't used to create dynamic events), so the constructors aren't invoked and the virtual pointers aren't set up.
In QEP, states are represented as state-handler functions that handle all events in the state they implement. The hierarchical state-handler functions use exactly the same signature QStateHandler
as nonhierarchical state handler functions, as described in Section 3.6 of Chapter 3. The only extension to the nonhierarchical implementation technique discussed before is that a hierarchical state handler must additionally inform the event processor about the nesting level of the state. When the hierarchical state handler does not handle the event, the handler must provide the superstate so that the event processor can invoke the superstate handler function, per the semantics of state nesting (see Section 2.3.2). The hierarchical state-handler function provides this additional information to the event processor very similarly as it informs the event processor about a state transition. A state handler sets the state variable to the superstate handler and returns a special status information that distinguishes this situation from a state transition.
When a hierarchical state handler function does not handle the current event, it returns the macro Q_SUPER()
to the event processor, which is defined as follows:
The Q_SUPER()
macro is defined using the comma expression. A comma expression is evaluated from left to right, whereas the type and value of the whole expression is the rightmost operand. The rightmost operand is in this case the status of the operation (superstate), which is returned from the state-handler function. The pivotal aspect of this design is that the Q_SUPER()
macro can be used with respect to structures derived (inheriting) from QHsm
, which in C requires explicit casting (upcasting) to the QHsm
base structure (see the sidebar “Single Inheritance in C” in Chapter 1).
Listing 4.4 shows an example of a hierarchical state-handler function that corresponds to the state “int1” in the calculator statechart in Figure 2.18State “int1” controls entering the integer part of the first operand.
Note
The event pointer is declared as
const
to prevent modifying the event inside the state-handler function. In other words, the state handler is granted read-only access to the event.
(2) Generally, every state handler is structured as a single switch
that discriminates based on the signal of the event e->sig
.
(3,4) Each case
is labeled by the event signal. Signals are typically enumerated constants.
(5) To get to the event parameters, a state handler must perform an explicit downcast from the generic QEvent const*
pointer to the specific derived event pointer, such as Calc const*
in this case.
Note.
At this point it becomes apparent that the signature of the state-handler function is really weakly typed with respect to the event parameter. The compiler knows only that every event is passed as the generic
QEvent*
pointer, but the compiler does not know the specific type of the event, such asCalcEvt
. The application programmer is ultimately responsible for performing a correct downcast to the derived event based on the signal (e->sig
). The point to remember is that you need to be careful because the compiler cannot prevent an incorrect downcast.
(6) Returning Q_HANLDED()
from a hierarchical state handler informs the QEP event processor that the particular event has been handled.
(7) A state transition is accomplished by returning the macro Q_TRAN()
that requires the target of the transition as parameter.
(8) If no case
executes, the state handler returns the Q_SUPER()
macro, which designates the superstate and informs the event processor about it.
Listing 4.5 shows an example of a hierarchical state-handler function that corresponds to the state “operand1” in the calculator statechart in Figure 2.18.
Listing 4.5. Example of a hierarchical state-handler function in C++ (file calc.cpp)
Apart from the trivial syntactic differences (such as the “::
” scope resolution operator instead of an underscore), the structure of a hierarchical state-handler function in C++ is identical to the C version from Listing 4.4. The only interesting difference is the downcast of the generic event pointer to the specific subclass in Listing 4.5(1). Here, I've used the new-style static_cast<>
operator because the cast converts between types related by inheritance. Of course, you can also use the C-style cast if your older C++ compiler does not support the new-style casts.
POINTERS TO MEMBER FUNCTIONS IN C++
The C++ state-handler function takes the “
me
” pointer of its own class type, through which it accesses the state machine data members and member functions (e.g.,me->operand1 = …
). This is because the state-handler functions are static members of theQHsm
subclass, such as the calculator state machine classCalc
(see Section 4.6.3).An obvious and more elegant alternative would be to make the state-handler functions regular, nonstatic class members, which would allow them to access the class members much more naturally through the implicit “
this
” pointer.Indeed this much more elegant alternative has been used in the earlier QEP/C++ version published in the first edition of this book. However, this alternative requires using pointers to member functions instead of simple pointers to functions, which turned out to be a problem in practice.
Even though the earlier C++ version of QEP used pointers to member functions in a rather standard way, the embedded developers have filed a number of alarming reports from the trenches, where the elegant approach either had very lousy performance or did not work at all. For example, some embedded C++ compilers used over 30 machine instructions to de-reference a pointer to member function and only three to de-reference a regular pointer to function. Needless to say, three machine instructions should do the job (see also Section 3.7.1 in Chapter 3).
As it turns out, too many C++ compilers simply don't support pointers to member functions well due to interference from other language features, such as multiple inheritance and virtual base classes. As eloquently explained in the online article “Member Function Pointers and the Fastest Possible C++ Delegates” [Clugston 07], even such widespread and important frameworks as the MFC actually use pointers to member functions in a nonstandard way by subverting the normal C++ type checking.
To avoid inefficiencies and portability issues, the current C++ version of QEP does not use pointers to member functions but simply plain pointers to functions to static member functions that don't have the “
this
” pointer and therefore are not affected by polymorphism or multiple inheritance. Note that the explicit “me
” pointer required by static class members plays the same role as the “context
” pointer required by the object-oriented State design pattern (see Section 3.5.1 in Chapter 3).
As shown in Figure 4.1, the QHsm
base class is the central element of the QEP design. The QHsm
class is abstract, which means that it is not intended for direct instantiation but only for derivation of hierarchical state machines, such as the Calc
state machine in Figure 4.1. The main responsibility of the QHsm
class is keeping track of the current active state. In QEP, the state variable is a pointer to the state-handler function QStateHandler
defined previously.
The QHsm
class also provides the standard state machine interface functions init()
and dispatch()
as well as the constructor and the top state handler. The following sections explain these elements, first in the C version and later in C++.
In C, HSMs are derived from the QHsm
base structure, shown in Listing 4.6.
(1) The QHsm
structure stores the state-variable state
, which is a pointer to a state-handler function. Typically, the QHsm
structure requires just 2 or 4 bytes of RAM, depending on the size of the pointer to function for a given CPU and C compiler options.
(2) The QHsm
“constructor” function-like macro initializes the state variable to the initial-pseudostate function that defines the initial transition. Note that the initial transition is not actually executed at this point.
(3) The QHsm_init()
function triggers the initial transition in the state machine. The function takes an initialization event argument. You can use this event to pass any parameters to initialize the state machine.
(4) The QHsm_dispatch()
function dispatches one event to the state machine.
(5) The QHsm_isIn()
function tests whether the HSM “is in” a given state. Note that an HSM simultaneously “is in” all superstates of the currently active state, and QHsm_isIn()
tests for it. The QHsm_isIn()
function returns 1 (TRUE) if the HSM “is in” a given state (in the hierarchical sense). Otherwise the function returns 0 (FALSE).
(6) The QHsm_top()
function is the hierarchical state handler for the top state. The top state is the UML concept that denotes the ultimate root of the state hierarchy. The top state handler “handles” every event by silently ignoring it, which is the default policy in the UML (see also Section 4.5.3).
In C++, HSMs are derived from the QHsm
abstract base class, shown in Listing 4.7.
Listing 4.7. QHsm class (file <qp>qpcppincludeqep.h)
(1) The QHsm
class stores the state-variable state, which is a pointer to the hierarchical state-handler function. The state variable m_state
is protected so that the concrete state machine classes derived from QHsm
can access it through the macro Q_TRAN()
.
(2) The init()
member function triggers the initial transition in the state machine. The function takes an optional initialization event argument. You can use this event to pass any parameters to initialize the state machine.
(3) The dispatch()
member function dispatches one event to the state machine.
(4) The isIn()
member function tests whether the HSM “is in” a given state. Note that if an HSM is in a substate, it recursively also “is in” all the superstates. The isIn()
function returns 1 (TRUE) if the HSM “is in” a given state (in the hierarchical sense). Otherwise the function returns 0 (FALSE).
(5) The constructor is protected to prevent direct instantiation of QHsm
class, as it is abstract. The constructor initializes the state variable to the initial-pseudostate function that defines the initial transition. Note that the initial transition is not actually executed at this point.
(6) The top()
static member function is the hierarchical state handler for the top state. The top state is in UML the ultimate root of the state hierarchy. The top state handler “handles” every event by silently ignoring it, which is the default policy in the UML (see also Section 4.5.3).
Note
The application-level state-handler functions that don't explicitly nest in any other state return the
&QHsm::top
pointer to the event processor. It is crucial in this design thatQHsm::top()
is a static member, because static member functions can be referenced by the simple pointers-to-functions, whereas regular member functions would require pointers to member functions (also see the sidebar “Pointers to Member Functions in C++”).
Every HSM has the (typically implicit) top state, which surrounds all the other elements of the entire state machine, as depicted in Figure 4.2.
The QHsm
class guarantees that the top state is available to every derived state machine by providing the QHsm_top()
hierarchical state handler subsequently inherited by the subclasses. The QHsm_top()
hierarchical state-handler function is defined as follows:
By the UML semantics, the top state has no superstates and silently ignores all events, so it always returns Q_IGNORED()
to the event processor (see Listing 3.6(15) in Chapter 3). The only purpose, and legitimate use, of the top state is to provide the ultimate root of a state hierarchy so that the highest-level state handlers can return &QHsm_top
as their superstate. In particular, you should never target the top state in a state transition.
The state machine initialization is intentionally divided into two steps. The QHsm
constructor merely initializes the state variable to the initial pseudostate. Later, the application code must trigger the initial transition explicitly by invoking QHsm_init()
(described in the upcoming Section 4.5.6). This design separates instantiation of the state machine from initialization, giving the applications full control over the sequence of initializations in the system. The following code shows an example of an initial pseudostate handler for the calculator state machine:
Note that the topmost initial transition can fire only once (actually, exactly once), because after you leave the top state, you cannot transition back. In other words, your state machine cannot reuse the initial pseudostate in its life cycle.
In Chapter 2, you saw that UML state machines support elements of Moore automata such as entry and exit actions as well as nested initial transitions. These elements are sole characteristics of the state in which they are defined and do not depend, in particular, on the transition path through which the state has been reached. As described in Chapter 3, state-handler functions in QEP can (optionally) define state-specific behavior by responding to the following reserved signals defined in the qep.h
header file:
A state handler can handle these signals by using them as case
labels in the usual switch
statement. A state handler is free to execute any actions in response to those signals, but it should not take any state transitions in entry/exit actions. Conversely, the response to the Q_INIT_SIG
signal must always include the Q_TRAN()
macro to designate the default substate of the current state.
Note
The target of a nested initial transition specified in the
Q_TRAN()
macro must be the direct or transitive substate of the composite state in which the initial transition is defined. In other words, the nested initial transition must “drill into” the state hierarchy but cannot “go up” to target superstates or “sideways” to target peer states. The QEP event processor does not check for such incorrect initial transition targets, which really violate the UML semantics and correspond to malformed state machines and could crash the QEP event processor.
Listing 4.8 provides an example of using an entry action, an exit action, and a nested initial transition.
Listing 4.8. Definition of the Calc_on() state-handler function with entry and exit actions and an initial transition
The reserved signals take up the lowest signal values (0..3), which are thus not available for the applications. For convenience, the public HSM interface contains the signal Q_USER_SIG
, which indicates the first signal free for the users. A typical way of defining application-level signals is to use an enumeration. In this case, Q_USER_SIG
can be used to offset the values of the entire enumeration, as shown in Listing 4.9.
Note
The reserved signals
Q_ENTRY_SIG, Q_EXIT_SIG
, andQ_INIT_SIG
should cause no side effects in state-handler functions that do not have entry actions, exit actions, or initial transitions. The signal 0 (defined internally in the QEP asQEQ_EMPTY_SIG_
) is reserved as well and should cause a state-handler function to always return the superstate without any side effects.
To execute entry/exit actions and initial transitions, the QEP event processor needs to invoke the various state-handler functions and pass to them pointers to event objects containing the reserved signals. For example, to trigger an entry action in the Calc_on()
state handler, the event processor calls the Calc_on()
function with a pointer to an event that has the signal equal to Q_ENTRY_SIG
. To do this efficiently, QEP uses the QEP_reservedEvt_[]
constant array of reserved events, defined as follows:
Note
The reserved signal zero, enumerated as
QEP_EMPTY_SIG_
, is used only internally in QEP and therefore is not defined in the public QEP interfaceqep.h
with the rest of the reserved signals.
The array QEP_reservedEvt_[]
is designed to be indexed by the reserved signals. For example, &QEP_reservedEvt_[Q_ENTRY_SIG]
represents a pointer to an event with the reserved signal Q_ENTRY_SIG
.
The following three helper macros are then used extensively inside the QEP implementation (file <qp>qpcqepsourceqep_pkg.h
):
For example, the macro QEP_TRIG_()
calls a given state-handler function state_
with the reserved event pointer argument &QEP_reservedEvt_[sig_]
, where sig_
is one of these: QEP_EMPTY_SIG_, Q_ENTRY_SIG, Q_EXIT_SIG
, or Q_INIT_SIG
. Note the characteristic syntax of the function call based on a pointer to function (*(state_))(…)
.
Design by contract in C and C++
All components of the QP framework, including QEP, apply the elements of the Design by Contract5 Design by Contract is a registered trademark of Interactive Software Engineering (ISE). (DbC) philosophy, which is a method of programming based on precisely defined specifications of the various software components' mutual obligations (contracts). The central idea of this method is to inherently embed the contracts in the code and validate them automatically at runtime [Meyer 97].
In C or C++, you can implement the most important aspects of DbC with assertions (see [Myrphy 01a, 01b, Samek 03d]). Throughout this book, I use customized assertions defined in the header file
qassert.h
, located in directories<qp>qpcinclude
as well as<qp>qpcppinclude
. Theqassert.h
header file provides a number of macros, which include:Each of these macro works similarly as the standard library macro
assert()
, and their different names serve only to document the purpose of the contract. Section 6.7.3 in Chapter 6 covers DbC andqassert.h
in more detail.
State nesting adds a lot of complexity to the topmost initial transition in an HSM compared to a nonhierarchical FSM. The initial transition in an HSM might be complex because UML semantics require “drilling” into the state hierarchy with the nested initial transitions until the leaf state is reached. For example, the topmost initial transition in the calculator example (Figure 2.18 in Chapter 2) involves the following six steps:
1 Execution of actions associated with the topmost initial transition
3 Execution of the actions associated with the initial transition defined in the “on” state
5 Execution of the actions associated with the initial transition defined in the “result” state
6 Execution of the entry actions to the “begin” state; at this point the transition is done because “begin” is a leaf state with no nested initial transition
Figure 4.3 shows the inheritance tree of the states comprising the calculator statechart. The UML specification requires that higher-level states must be entered before entering lower-level states. Unfortunately, this is exactly the opposite order to the natural direction of navigation through the state handlers denoted by the behavioral inheritance arrow in Figure 4.3. As you recall from Section 4.4, a hierarchical state-handler function provides the superstate, so it's easy to traverse the state hierarchy from lower- to higher-level states. Although this order is very convenient for the efficient implementation of the most frequently used QHsm_dispatch()
function, entering states is harder.
The solution implemented in QEP is to use a temporary array path[]
to record the exit path from the target state of the initial transition without executing any actions (see Figure 4.4). This is achieved by calling the state handlers with the reserved QEP_EMPTY_SIG_
signal, which causes every state handler to immediately return the superstate without executing any actions. The returned superstates are saved in the path[]
array. After reaching the current state, the path[]
array is played backward to enter the target state in the exact reversed order in which it was exited.
Listing 4.10 shows the definition of the QHsm_init()
function that executes a generic topmost initial transition according to the UML semantics. The equivalent C++ implementation of the QHsm::init()
member function is identical, except for trivial syntactic differences between C and C++.
Listing 4.10. Definition of the QHsm_init() function (file <qp>qpcqepsourceqep_ini.c)
(3)
t = (QStateHandler)&QHsm_top; /* HSM starts in the top state */
(6)
int8_t ip = (int8_t)0; /* transition entry path index */
(7)
path [0] = me->state; /* save the target of the initial transition */
(10)
path [++ip] = me->state; (void)QEP_TRIG_(me->state, QEP_EMPTY_SIG_); }
(11)
me->state = path [0]; /* restore the target of the initial tran. */ /* entry path must not overflow */
do { /* retrace the entry path in reverse (desired) order… */
(15)
t = path [0]; /* current state becomes the new source */
(1) The QHsm_init()
implements the QHsm
“class operation” and therefore takes the “me
” pointer. The event pointer parameter e
can be used to provide additional initialization parameters to the state machine.
(2) Inside the Q_ALLEGE()
macro (see the sidebar “Design by Contract in C and C++”) the initial pseudostate handler function is called via the pointer to function stored in the state variable me->state
. The initial pseudostate handler returns the Q_TRAN()
macro, in which it designates the target of the initial transition. The Q_ALLEGE()
macro makes sure that the initial pseudostate always takes the initial transition. I use the Q_ALLEGE()
macro because the initial transition must be executed even when assertions are disabled at compile time.
(3) The temporary variable t
holds the source state of the transition. The first source is the top state.
(4) The do
-loop performs the recursive execution of any nested initial transitions until a leaf state is reached.
(5) The temporary array path[]
stores the pointers to state-handler functions in the exit path from the target state of the initial transition (see Figure 4.4).
(6) The temporary variable ip
is used as the index into the path[]
array.
(7) The path[0]
entry is initialized to hold the target state, which is placed in me->state
by the Q_TRAN()
macro called in the state-handler function.
(8) The superstate of the current state is discovered by calling the state handler with the reserved QEP_EMPTY_SIG_
signal. The hierarchical state handler never handles the QEP_EMPTY_SIG_
signal, so it returns Q_SUPER()
macro, which sets me->state
to the superstate of the given state.
(9) The discovery of superstates continues until the current source state is reached.
Note
It is crucial at this point that the target state of the initial transition indeed is a substate of the source.
(10) The exit path from the target is stored in the path[]
array.
(11) The current state me->state
is restored to the original target of the initial transition.
(12) This assertion makes sure that the path[]
array does not overflow (see the sidebar “Design by Contract in C and C++”).
(13) All states stored in the path[]
array are entered in the correct order.
(14) The entry to the target state configuration continues until index 0, which points to the target itself.
(16) The loop continues as long as the current state handler reports that it handled the initial transition. Otherwise, it is a leaf state and the job of the QHsm_init()
function is done.
Dispatching an event to an HSM requires implementing the UML state nesting semantics, that is, propagating the event through all the levels of nesting until it is handled or reaches the top state. This functionality as well as executing of transitions is implemented in the QHsm_dispatch()
function.
The QHsm_dispatch()
function is the most complicated in the QEP event processor.6 QHsm_dispatch()
is not broken up in to smaller functions to conserve the stack space. I will break up the discussion of the implementation into two steps. First, in this section I explain how the function handles hierarchical event processing. In the next section, I take a closer look at the transition execution algorithm.
Listing 4.11 shows the general structure of the QHsm_dispatch()
function. The implementation uses many elements already described for QHsm_init()
. The code carefully optimizes the number and size of temporary stack variables to minimize the stack use. The equivalent C++ implementation of the QHsm::dispatch()
member function is identical, except for trivial syntactic differences between C and C++.
Listing 4.11. General structure of the QHsm_dispatch() function (file <qp>qpcqepsourceqep_dis.c)
(8)
if (r == Q_RET_TRAN) { /* transition taken? */ int8_t ip = (int8_t)(-1); /* transition entry path index */
(9)
path [0] = me->state; /*save the target of the transition */
(11)
while (t != s) { /* exit current state to transition source s… */
(12)
if (QEP_TRIG_(t, Q_EXIT_SIG) == Q_RET_HANDLED){/*exit handled? */
(13)
(void)QEP_TRIG_(t, QEP_EMPTY_SIG_); /* find superstate of t */ }
(16)
me->state = t; /* set new state or restore the current state */}
(1) The QHsm_dispatch()
implements the QHsm
“class operation” and therefore it takes the “me
” pointer. The job of this function is to process the event e
according to the UML semantics.
(2) The temporary array path[]
stores the pointers to state-handler functions in the exit path from the target state of the initial transition. To conserve the stack space, this array is reused for other purposes as well.
(4) This do
-loop processes the events hierarchically starting from the current state.
(5) The current state is saved in the temporary ‘s
’ in case a transition is taken, which overwrites me->state
. In that case, the variable ‘s
’ holds the source of the transition.
(6) At each level of state nesting, the state-handler function is called. The status of the event handling reported from the state handler is stored in to the temporary variable ‘r
.’
(7) The do
-loop continues as long as state handler functions return superstates (via the Q_SUPER()
macro). Please note that the top state handler ignores all events, so the loop must terminate even if the event is not explicitly handled at any level of state nesting.
(8) If the returned status is Q_RET_TRAN
, the last called state handler must have taken a state transition by executing the Q_TRAN()
macro (see Listing 3.6(16) in Chapter 3).
(9) The current state me->state
holds the target of the transition, which is now placed in path[0]
(see Figure 4.4).
(10) The current state before the transition is copied to path[1]
, so that the variable ‘t
’ can be reused.
(11) This while
-loop executes the transition segment from the current state to the explicit source of the transition. This step covers the case of an inherited state transition—that is, the transition defined at a level higher than the currently active state.
For example, assume that the state “result” is the current active state of the calculator state machine in Figure 4.5 while the user presses one of the operator keys (+, –, *, or /). When the function QHsm_dispatch()
receives the OPER
event, it calls the currently active state handler first, which is the Calc_result()
state handler. This state handler doesn't handle the OPER
event, so it returns the superstate Calc_ready()
. The QHsm_dispatch()
function then calls the Calc_ready()
state handler, which handles the OPER
event by taking a state transition Q_TRAN(&Calc_opEntered)
. However, the correct exit of the current state configuration must include exiting “result” before exiting “ready.” This transition segment is shown in grey in Figure 4.5.
(13) If the exit action is handled, I need to discover the superstate by calling the state handler with the empty event. If the exit action is unhandled, the state handler returned the Q_SUPER()
macro, so me->state
already contains the superstate.
(14) The superstate is stored in the temporary variable ‘t
’ to be compared with the transition source.
(15) The omitted part contains the state transition algorithm shown in Listing 4.12
Listing 4.12. Transition algorithm implementation in the QHsm_dispatch() function (file <qp>qpcqepsourceqep_dis.c)
(1)
if (s == t) { /* (a) check source==target (transition to self) */
(void)QEP_TRIG_(t, QEP_EMPTY_SIG_); /* superstate of target */
(3)
(void)QEP_TRIG_(s, QEP_EMPTY_SIG_); /* superstate of src */
(void)QEP_TRIG_(me->state, QEP_EMPTY_SIG_); /* find superstate */
(void)QEP_TRIG_(me->state, QEP_EMPTY_SIG_);/* find superstate */
do { /* retrace the entry path in reverse (correct) order... */
(16) The current state is restored from the variable ‘t
,’ where it has been stored in step (3).
Executing a generic state transition in a HSM is by far the most complex part of the QEP implementation. The challenge is to quickly find the least common ancestor (LCA) state of the source and target states. (The LCA is the lowest-hierarchy state that is simultaneously the superstate of the source and the target states.) The transition sequence involves the exit of all states up to the LCA (but without exiting the LCA itself), followed by the recursive entry into the target state, followed by “drilling” into the target state configuration with the initial transitions until a leaf state is reached.
Listing 4.12 shows the omitted part of the QHsm_dispatch()
function that implements the general case of a state transition. A large part of the complexity of this part of the code results from the optimization of the workload required to efficiently execute the most frequently used types of state transitions. The optimization criterion used in the transition algorithm is to minimize the number of invocations of state-handler functions, in particular the “empty” invocations (with the reserved QEP_EMPTY_SIG_
), which serve only for eliciting the superstate of a given state handler. The strategy is to order the possible source-target state combinations in such a way that the information about the state hierarchy gained from earlier steps can be used in later states. Figure 4.6 shows such ordering of state transition topologies. This ordering is the basis for the transition algorithm in Listing 4.12.
(1) From the point of view of reducing the number of state handler calls, the simplest transition type is transition to self (Figure 4.6(A)) because this transition type can be determined immediately by testing (source == target
), that is, no state-handler invocations are necessary to check for this transition type. The self-transition requires exiting the source and entering the target. The exit from the source can be performed right away by means of the helper macro QEP_EXIT_()
introduced in Section 4.5.5.
(2) The next transition type is the topology shown in Figure 4.6(B). The check for this transition type is (source == super(target)
) and requires determining the superstate of the target. This transition type requires only entry to the target but no exit from the source.
(3) To proceed further, the algorithm checks for the superstate of the source state. The information about superstates of both the target and the source collected so far is subsequently used to determine two transition types shown in Figure 4.6(C) and (D).
(4) The transition topology in Figure 4.6(C) is the peer-to-peer transition shown (perhaps the most common type). This transition topology can be determined by checking the condition (super(source) == super(target)
) and requires exit from the source and entry to the target. The exit is performed right away by means of the helper macro QEP_EXIT_()
.
(5) The topology shown in Figure 4.6(D) requires testing the condition (super(source) == target
) and involves only entry to the target but no exit from the source.
(6) The topology shown in Figure 4.6(E) requires probing all superstates of the target until a match with the source is found or until the top state is reached. The target state hierarchy determined in this part of the algorithm is stored in the temporary array path[]
and is subsequently reused to perform the entry to the target state configuration in the desired order. Note that the entry path[0]
already holds the target itself, and path[1]
holds the superstate of target discovered in the prior steps.
(7) The macro Q_ASSERT()
implements a customizable, embedded-system friendly assertion. I discuss the use of assertions in the QP event-driven platform in Section 6.7 of Chapter 6.
(8) The transition topology from Figure 4.6(E) is the last that might not require exiting the source state, so if the processed transition does not fall into the (E) category, the source state must be exited.
(9) The topology shown in Figure 4.6(F) requires traversal of the target state hierarchy stored in the array path[]
to find the match with the superstate of source still kept in the temporary variable s
.
(10) The topologies shown in Figure 4.6(G) and (H) require traversal of the target state hierarchy stored in the array path[]
to find the match with any of the superstates of the source.
(11) Because every scan for a match with a given superstate of the source exhausts all possible matches for the LCA, the source's superstate can be safely exited.
Figure 4.6. Ordering of transition types from simplest to progressively more complex in the transition algorithm (Listing 4.12).
The transition types shown in Figure 4.6(A) through Figure 4.6(H) represent all valid transition topologies, and every well-formed transition should be recognized as one of the cases (A) through (H). Once QHsm_dispatch()
detects the type of the transition and executes all necessary exit actions up to the LCA, it must enter the target state configuration.
(12) The entry to the target state configuration is straightforward and involves just a simple for
loop that scans through the array path[]
in the reversed order as it was filled.
(13) The target state can be composite and can have an initial transition as well. Therefore, the while
loop performs the “drilling” into the target until it detects the leaf state. This part of the algorithm is very similar as the QHsm_init()
function explained in Listing 4.10.
Implementing state machines with the QEP event processor is quite a mechanical process consisting of just a few simple steps. You already went through the process at least once in Section 1.7 of Chapter 1, where I explained the implementation of the Ship state machine in the “Fly ‘n’ Shoot” game. Here I present the implementation of the calculator state machine, which was designed in Section 2.4 of Chapter 2 and also served as an example throughout this chapter.
When you know how to code one HSM, you know how to code them all. Therefore, this section will necessarily repeat some of the descriptions from Chapter 1. However, I feel that having all the HSM coding steps conveniently summarized in one section will be helpful for daily programming work. In addition, to reduce the repetition, I describe here the state machine implementation in C++. If you are a C programmer, I hope that by now you are getting more familiar with the concepts of a “class” and “inheritance” and know how to code them in C.
The C++ source code for the calculator state machine is located in the directory <qp>qpcppexamples80x86dos cpp101lcalc
and consists of the following files:
• calc.h
contains the declaration of signals, events, and the global pointer to the Calc
state machine.
• calc.cpp
contains declaration of the Calc
state machine structure and the implementation of the state-handler functions.
• bsp.cpp
contains the implementation of the board-specific functions.
• CALC.PRJ
is the Turbo C++ project file for building the application.
As always, the code I provide is executable and I encourage you to try it out. You can run the example on any Windows PC by double-clicking on the executable located in the directory <qp>qpcppexamples80x86dos cpp101lcalcdbgCALC.EXE.
The calculator example is interactive and you can perform computations with it. You use the keyboard to send keypress events to the application; the state of the calculator display is shown at the command prompt. The calculator recognizes keys: 0, 1, …, 9, ., +, –, *, /, =, C, and E (cancel entry, CE). The Esc key terminates the application. All other keys are ignored.
Figure 4.7 shows a screen shot in which you can see how the calculator handles the expression 2, –, –, –, 2, = that has crashed the Visual Basic calculator in Chapter 2. I'd like to challenge you to crash the state machine-based calculator. The calculator starts with displaying zero aligned at the right edge of the display [ 0]. To the right of the display, you can see the key sent to the calculator. For example, the first key is 2. The key event is followed by the sequence of actions that the calculator HSM performs in response to the key event. I recommend that you correlate this output with the calculator state diagram from Figure 2.18.
The first step of the implementation consists of enumerating all signals recognized by the state machine shown in the state diagram (Figure 2.18 in Chapter 2), such as C, CE, DIGIT_0, DIGIT_1_9, and so on.
Listing 4.9 shows the enumeration of all signals recognized by the calculator state machine. Note that the user-level signals do not start from zero but rather are offset by the constant Q_USER_SIG
. Also note that by QEP convention, all signals have the suffix_SIG
to easily distinguish signals from other constants. The suffix _SIG
is omitted in the state diagram to reduce the clutter.
Many events consist only of the signal and don't need any additional parameters. You can represent such events directly as instances of the QEvent
structure provided in the header file <qp>qpcincludeqevent.h
.
However, some events require parameters. For example, the calculator signal DIGIT_1_9_SIG
communicates only that one of the digit keys 1..9 has been depressed, but the signal alone does not inform us as to which digit key this was. The missing information is added to the event in the form of the key_code
parameter that represents the code of the depressed key.
Note
The granularity of signals has been chosen that way because the behavior of the calculator really is independent of exactly which digit key is depressed (only the digit 0 needs to be treated differently from the rest, and that's why it has been represented as a separate signal). Similarly, the calculator reacts identically to all operators (+,– , *, /) and therefore all operators have been represented by only one signal
OPER_SIG
. Section 4.7.8 talks about achieving the optimal signal granularity.
The following fragment of the calc.h
header file demonstrates how you add event parameters. You define a class (CalcEvt
) that inherits from the QEvent
class. You then add arbitrary parameters as data members:
Hierarchical state machines are represented in QEP as subclasses of the QHsm
abstract base class, which is defined in the header file <qp>qpcppincludeqep.h
. Listing 4.13 demonstrates how you derive the Calc
(calculator) class from QHsm
.
(1) You define a class (Calc
) that inherits the QHsm
base class.
(2) You add arbitrary extended-state variables as data members to the derived class.
(3) You typically provide the default constructor (constructor without parameters) that conveniently encapsulates the initial pseudostate pointer passed to the QHsm
constructor.
(4) You provide the initial pseudostate as a static member function with the shown signature.
(5) You provide the all state handlers as a static member functions with the shown signature.
Listing 4.13. Deriving the Calc class from QHsm
(2)double m_operand1; // the value of operand 1 (extended state variable)
uint8_t m_operator; // operator key entered (extended state variable)
(4) static QState initial (Calc *me, QEvent const *e); // initial pseudostate
(5) static QState on (Calc *me, QEvent const *e); // state handler
static QState error(Calc *me, QEvent const *e); // state handler
static QState ready(Calc *me, QEvent const *e); // state handler
static QState result(Calc *me, QEvent const *e); // state handler
static QState begin(Calc *me, QEvent const *e); // state handler
static QState negated1(Calc *me, QEvent const *e); // state handler
static QState operand1(Calc *me, QEvent const *e); // state handler
static QState zero1(Calc *me, QEvent const *e); // state handler
static QState int1(Calc *me, QEvent const *e); // state handler
static QState frac1(Calc *me, QEvent const *e); // state handler
static QState opEntered (Calc *me, QEvent const *e); // state handler
static QState negated2(Calc *me, QEvent const *e); // state handler
static QState operand2(Calc *me, QEvent const *e); // state handler
static QState zero2(Calc *me, QEvent const *e); // state handler
static QState int2(Calc *me, QEvent const *e); // state handler
static QState frac2(Calc *me, QEvent const *e); // state handler
static QState final(Calc *me, QEvent const *e); // state handler
The initial pseudostate Calc::initial()
shown here takes the “me
” pointer to its own class (Calc *
) as the first argument and an event pointer as the second parameter. This particular initial pseudostate ignores the event, but sometimes such an initialization event can be helpful to provide additional information required to initialize extended-state variables of the state machine.
The initial pseudostate can initialize the extended state variables and perform any other actions, but its most important job is to set the default state of the state machine with the Q_TRAN()
macro, as shown.
Earlier in this chapter, in Listing 4.5 you saw an example of the Calc::int1()
state handler function. Typically, every state handler function consists of a switch
statement that discriminates based on the event signal e->sig
. Each case
is labeled by a signal and terminates either with “return Q_HANDLED()
” or “return Q_TRAN(…)
.” Either one of these return statements informs the QEP event processor that the particular event has been handled. On the other hand, if no case
executes, the state handler exits through the final “return Q_SUPER(…)
” statement, which informs the QEP event processor that the event needs to be handled by the designated superstate.
Highest-level states without explicit superstate (e.g., the “on” state in the calculator example) nest implicitly in the top state. Such states disignate &QHsm::top
as the argument to the Q_SUPER()
macro.
Note
The final
return
statement from a state handler function is the only place where you specify the hierarchy of states. Therefore, this one line of code represents the single point of maintenance for changing the nesting level of a given state.
While coding state-handler functions, you need to keep in mind that QEP will invoke them for various reasons: for hierarchical event processing, for execution of entry and exit actions, for triggering initial transitions, or even just to elicit the superstate of a given state handler. Therefore, you should not assume that a state handler would be invoked only for processing events enlisted in the case
statements. You should also avoid any code outside the switch
statement, especially code that would have side effects.
The qep.h
header file provides two reserved signals Q_ENTRY_SIG
and Q_EXIT_SIG
that the QEP event processor passes to the appropriate state-handler function to execute the state entry actions or exit actions, respectively.
Therefore, as shown in Listing 4.8 earlier in this chapter, to code an entry action, you provide a case
statement labeled with signal Q_ENTRY_SIG
, enlist all the actions you want to execute upon the entry to the state, and terminate the lists with “return Q_HANDLED()
,” which informs the QEP that the entry actions have been handled.
Coding the exit actions is identical, except that you provide a case
statement labeled with the signal Q_EXIT_SIG
, call the actions you want to execute upon the exit from the state, and terminate the lists with “return Q_HANDLED()
,” which informs the QEP that the exit action has been handled.
Every composite state (a state with substates) can have its own initial transition, which in the diagram is represented as an arrow originating from a black ball. For example, the calculator state “on” in Figure 2.18 has such a transition to substate “ready.”
The QEP provides a reserved signal Q_INIT_SIG
that the event processor passes to the appropriate state-handler function to execute the initial transition.
Therefore, as shown Listing 4.8 earlier in this chapter, to code an initial transition, you provide a case
statement labeled with signal Q_INIT_SIG
, enlist all the actions you want to execute upon the initial transition, and then designate the target substate with the Q_TRAN()
macro. The status returned from the Q_TRAN()
macro informs QEP that the initial transition has been handled.
The UML specification requires that the target of the initial transition is a direct or indirect substate of the source state. An initial transition to a nonsubstate (e.g., a peer state, or a superstate) corresponds to a malformed state machine and may even crash the event processor. Note that initial transitions cannot have guard conditions.
Internal transitions are simple reactions to events that never lead to change of state and consequently never cause execution of exit actions, entry actions, or initial transitions.
To code an internal transition, you provide a case
statement labeled with the triggering signal, enlist the actions, and terminate the list with “return Q_HANDLED()
” to inform QEP that the event has been handled.
State-handler Calc::int1()
from Listing 4.5 provides two examples of regular state transitions. To code a regular transition, you provide a case
statement labeled with the triggering signal (e.g., POINT_SIG
), enlist the actions, and then designate the target state with the Q_TRAN()
macro. The status returned from the Q_TRAN()
macro informs QEP that a transition has been taken.
The Q_TRAN()
macro can accept any target state at any level of nesting, such as a peer state, a substate, a superstate, or even the same state as the source of the transition (transition to self).
Note
The QEP hierarchical event processor automatically handles execution of appropriate exit and entry actions during arbitrary state transitions (in the
QHsm::dispatch()
function). Consequently, any change in state machine topology (change in state transitions or state nesting) requires only recompiling the state-handler functions. QEP automatically takes care of figuring out the correct sequence of exit/entry actions and initial transitions to execute for every state transition.
Guard conditions (or simply guards) are Boolean expressions evaluated dynamically based on the value of event parameters and/or the variables associated with the state machine (extended-state variables). The following definition of the Cacl::begin()
state-handler function shows an example of a state transition with a guard.
The guard condition maps simply to an if
-statement that conditionally executes actions. Note that only the TRUE branch of the if
contains the “return Q_TRAN()
” statement, meaning that only the TRUE branch reports that the event has been handled. If the TRUE branch is not taken, the break
statement causes a jump to the final return
that informs the QEP that the event has not been handled. This is in compliance with the UML semantics, which require treating an event as unhandled in case the guard evaluates to FALSE. In that case, the event should be propagated up to the higher levels of hierarchy (to the superstate).
Guard conditions are allowed not just for regular state transitions but for the internal transitions as well. In this case a guard maps to the if
statement that contains “return Q_HANDLED()
” only in the TRUE branch. The only difference for the internal transition is that you return the Q_HANDLED()
macro instead of Q_TRAN()
.
The QEP hierarchical event processor enables building efficient and maintainable state machine implementations in C and C++. However, it is also possible to use QEP incorrectly because the direct manual-coding approach leaves you a lot of freedom in structuring your state machine code. This section summarizes the main pitfalls that various QEP users have fallen into over the years and provides some guidelines on how to benefit the most from QEP.
You should construct only complete state handlers, that is, state-handler functions that directly include all state machine elements pertaining to a given state (such as all actions, all transitions, and all guards), so that you or anyone else could at any time unambiguously draw the state in a diagram using only the state-handler function.
The key is the way you break up the code. Instead of thinking in terms of individual C statements, you should think at a higher level of abstraction, in terms of the idioms defined in Sections 4.6.5-4.6.10 for coding states, transitions, entry/exit actions, initial transitions, and guards.
Consider the following problematic implementation of the “on” state handler of the calculator state machine shown before, in Section 4.5.4:
This Calc_on()
state-handler function differs from the original implementation discussed in Section 4.5.4 only in the way it handles the C_SIG
signal. Though the problematic implementation is in principle equivalent to the original and would perform exactly the same way, the problematic state handler is incomplete because it does not follow the idiom for coding state transition from Section 4.6.9. In particular, the state handler hides the state transition to self triggered by C_SIG
and from such an incomplete state handler alone you would not be able to correctly draw the state in the diagram.
In summary, perhaps the most important principle to keep in mind while coding state machines with QEP is that the code is as much an implementation as it is a specification of a state machine. This perspective on coding state machines with QEP will help you (and others) readily see the state machine structure right from the code and easily and unambiguously map the code back to state diagrams. Conversely, state machine code structured arbitrarily, even if working correctly, might be misleading and therefore difficult to maintain (see also [Samek 03f]).
All nontrivial, semantically rich formalisms, including UML state machines, allow building ill-formed constructs. An ill-formed state machine is inherently wrong, not one that just happens to incorrectly represent behavior. For example, you could draw a UML state diagram with initial transitions targeting peer states rather than substates or conflicting transitions with overlapping guards. The specific state machine implementation technique, such as QEP, introduces additional opportunities of “shooting yourself in the foot.” It is possible, for example, to code a state handler that would nest inside itself (the state-handler function would return a pointer to self). Such a state machine cannot be even drawn in a state diagram but is quite easy to code with QEP.
This section examines more of such situations that result with ill-formed state machines. Often, ill-formed state machines cause an assertion violation within the QEP code (see the sidebar “Design by Contract in C and C++”). However, some pathological cases, such as circular state nesting, could crash the QEP event processor.
Novice QEP users sometimes try to code a transition inside the entry action to a state by using the Q_TRAN()
macro. This happens typically when a developer confuses a statechart with a flowchart (see Section 2.2.3) and thinks of the entered state as just a stage of processing that automatically progresses to the next stage upon completion of the entry actions.
The UML does not allow transitions in entry or exit actions. The typical intention in coding a state transition in an entry action is to enter a given state only under some condition and transition to a different state under some other condition. The correct way of handling this situation is to explicitly code two transitions with complementary guards and with different target states.
As described in Section 4.3, event parameters are added to the QEvent
structure in the process of class inheritance. However, events are uniformly passed to state-handler functions as the generic QEvent*
pointer, even though they point to various derived event classes. That's why you need to downcast the generic QEvent*
pointer onto the pointer to the specific event subclass as shown, for instance, in Listing 4.4(5).
However, to perform the downcast correctly, you need to know what derived event to cast to. The only information you have at this point is the signal of the event (e->sig
), and therefore the signal alone must unambiguously identify the derived event structure. The problem arises if you use one signal with multiple event classes, because then you could cast incorrectly on the wrong event class.
A specific case of incorrect event casting is an attempt to access event parameters when handling entry/exit actions or initial transitions. For example, if a state has only one incoming transition that is triggered with an event with parameters, novice QEP users sometimes try to access these parameters in the entry action to this state. Consider the following hypothetical code example:
The transition in “stateA” triggered by EVTB_SIG
is the only way to get to “stateB.” In the entry action to “stateB” the programmer realizes that some parameter foo
in the EvtB
structure, associated with signal EVTB_SIG
, is needed and casts the generic event on (EvtB const *
). This is an incorrect cast, however, because by the time “stateB” is entered the original triggering event EvtB
is no longer accessible. Instead, the entry action is triggered by a reserved signal Q_ENTRY_SIG
, which is not associated with the EvtB
event structure. This is actually logical because a state can be entered in many different transitions, each one triggered by a different event, and none of them are accessible by the time entry action is processed.
The correct way of handling this situation is to perform actions dependent on the event parameters directly on the transition triggered by this event rather than in the entry action. Alternatively, the event parameters can be stored in the extended-state variables (members of the state machine structure that you access through the “me
” pointer). The extended-state variables are accessible all the time, so they can be used also in the entry/exit actions or initial transitions.
All initial transitions must target direct or indirect substates of the state in which the initial transition is defined. Figure 4.8(A) shows several examples of correct initial transitions. Note that an initial transition can span more than one level of state hierarchy, but it must always target a direct or indirect substate of a given state. Figure 4.8(B) shows one example of the highlighted initial transition in “stateA1” that targets “stateA21.” The problem is that “stateA21” is not a substate of “stateA1” and therefore the state machine in Figure 4.8(B) is ill-formed according to the UML semantics. Coding such an initial transition in QEP will crash the event processor.
As the QEP user, you need to understand that each event that dispatched the state machine through the function QHsm_dispatch()
might potentially cause invocation of many state-handler functions and some of them might be called more than once. This is because the event processor needs to call state-handler functions to perform hierarchical event processing, to handle entry/exit actions and initial transitions, or simply to discover the nesting level of a given state. Therefore, you should not assume that a state-handler function would be called exactly once for a given event, so you should avoid any code outside the main switch
statement dedicated to events, especially if the code has side effects.
For example, the following state-handler function is certainly inefficient, and probably incorrect, because the for
loop executes every time the state handler is invoked, which is not just for the events enlisted as cases in the switch
statement.
You should even avoid allocating and initializing any automatic variables outside the main switch
statement. I specifically recommend using braces after each case
statement so that you can allocate and initialize automatic variables locally in each individual case statement. The following code snippet illustrates the situation:
Nothing affects state machine complexity and efficiency as much as the right granularity and semantics of events. The optimal granularity of signals falls somewhere between the two extremes of too fine and too coarse.
The granularity of signals is too fine if you repeatedly find the same groups of signals handled in the same way. For example, recall the calculator example (Section 2.4 in Chapter 2). The calculator HSM handles all numerals 1 through 9 in the same way. Therefore, introducing a separate signal for each numeral would lead to a signal granularity that is too fine, which would unnecessarily bloat the state-handler functions (you would see long lists of cases handled identically). Instead, the calculator statechart represents the whole group of numerals 1 through 9 as just one signal, IDC_1_9_SIG
(see Figure 2.18).
The granularity of signals is too coarse if you find yourself frequently using guard conditions that test event parameters. In this case, event parameters are the de facto signals. Consider the Windows message WM_COMMAND
, frequently used in Windows GUI applications for all buttons and menus of the application. This signal is too coarse because Windows applications typically must test the wParam
parameter associated with the WM_COMMAND
to determine what actually happened. In other words, values of wParam
are the de facto signals. In this case, the too coarse signal granularity results in a suboptimal (and not very elegant) additional switch
statement based on wParam
nested within the WM_COMMAND
case. When you encounter signals that are too coarse, the first thing you should try is to redefine or remap signals to the right level of granularity before dispatching them to the state machine. However, if you cannot do this, you should include all the de facto signals directly in your state handlers. All too often, the additional layer of signal dispatching (such as the switch
based on wParam
) end up in a separate function, which makes state handlers incomplete in the sense discussed in Section 4.7.1.
All state machine formalisms, including UML statecharts, universally assume run-to-completion (RTC) semantics of processing events. RTC means that a state machine must always complete processing of the previous event before it can start processing the next. The RTC restriction comes from the fact that a state machine must always go from one stable state configuration all the way to another stable state configuration in one indivisible step (RTC step). A state machine cannot accept events before reaching a stable state configuration.
The RTC semantics is implicit in the QEP implementation because each invocation of the function QHsm_dispatch()
represents one RTC step. In single-threaded systems, such as all the examples discussed in this chapter, the RTC semantics cannot be violated because each function must return before it can be called again. However, in multitasking environments, even as simple as the superloop (main+ISRs), the RTC semantics can be easily violated by attempts to dispatch an event to a state machine from an ISR while the same state machine in the background loop is still busy processing the previous event.
A very nasty and difficult-to-debug violation of the RTC semantics is an inadvertent corruption of the current event before the RTC step completes. Recall that the state-handler functions in QEP take just pointers to events, not the copies of the entire event objects. It is therefore possible that the memory pointed to by the event pointer will get corrupted before the current RTC step completes.
For example, consider once more the superloop (main+ISRs) architecture. An ISR produces an event and sets a global flag to trigger a state machine running in the background. The background loop starts processing the event, but before it completes, another interrupt preempts it. The ISR produces another event by overwriting the memory used previously for the first event. The RTC semantics are violated even though the ISR merely sets a flag instead of calling the state machine directly.
The general solution to guarantee RTC semantics in multitasking systems is to use event queues to store events while a state machine is busy. The mechanisms for a thread-safe event queuing and dispatching to multiple concurrently executing state machines can be generalized and reused rather than being reinvented from scratch for each application. Virtually all GUI systems (such as Microsoft Windows, X Windows, and others) are examples of such reusable architectures. QEP can be used with virtually any such event-driven infrastructure. In particular, QEP can be combined with the QF event-driven framework design specifically for the domain of real-time embedded systems. I introduce the QF component in Chapter 6.
Adapting the QEP software to a particular CPU and compiler is called porting. You port and configure the QEP event processor by providing the qep_port.h
header file, which is included in all source files comprising QEP (see Listing 4.1). Listing 4.14 shows an example of qep_port.h
for 80×86 CPU.
Listing 4.14. The qep_port.h header file for the 80x86 QEP port located in the directory <qp>qpcports80x86dos cpp101l
/* special keyword used for ROM objects (none for 80x86) */
/* mechanism of accessing const objects in ROM (far pointers) */
(2)
#define Q_ROM_VAR far /* 1-byte signal space (255 signals) */
/* exact-width types. WG14/N843 C99 Standard, Section 7.18.1.1 */
(4)
typedef signed char int8_t; int int16_t; long int32_t; typedef unsigned char uint8_t; typedef unsigned int uint16_t; typedef unsigned long uint32_t;
(5)
#include "qep.h" /* QEP platform-independent public interface */
(1) The Q_ROM
macro allows enforcing placing the constant objects, such as lookup tables, constant strings, and the like, in ROM rather than in the precious RAM. On CPUs with the Harvard architecture (such as 8051 or the Atmel AVR), the code and data spaces are separate and are accessed through different CPU instructions. The compilers often provide specific extended keywords to designate code or data space, such as the “__code
” extended keyword in the IAR 8051 compiler. Here, for the 80x86 CPU, the definition of the Q_ROM
macro is empty.
(2) The macro Q_ROM_VAR
specifies the kind of the pointer to be used to access the ROM objects because many compilers provide different-sized pointers for accessing objects in various memories. Constant objects allocated in ROM often mandate the use of specific-size pointers (e.g., far
pointers) to get access to ROM objects. An example of valid Q_ROM_VAR
macro definition is __far
(Freescale HC(S)08 compiler).
Note
Macros
Q_ROM
andQ_ROM_VAR
refer to the different parts of the object declaration. The macroQ_ROM
specifies the ROM memory type to allocate an object. This allows compilers generating different instructions for accessing such ROM objects for CPUs with the Harvard architecture. On the other hand, the macroQ_ROM_VAR
specifies the size of the pointer (e.g., the “far
” pointer) to access the ROM data, so it refers just to the size of the object's address, not to the object itself. TheQ_ROM_VAR
macro is useful for the von Neumann machines.
If you don't define macros Q_ROM
or Q_ROM_VAR
, the qep.h
header file will provide default empty definitions, which means that no special extended keywords are necessary to correctly allocate and access the constant objects.
(3) The macro Q_SIGNAL_SIZE
configures the QSignal
type (see Section 4.3.1). If the macro is not defined, the default of 1 byte will be chosen in qep.h
. The valid Q_SIGNAL_SIZE
values 1, 2, or 4 correspond to QSignal
of uint8_t, uint16_t
, and uint32_t
, respectively. The QSignal
data type determines the dynamic range of numerical values of signals in your application.
(4) Porting QEP requires providing the C99-standard exact-width integer types that are consistent with the CPU and compiler options used. For newer C and C++ compilers, you simply need to include the standard header file <stdint.h>
provided by the compiler vendor. For prestandard compilers, you need to provide the typedefs
for the six basic exact-width integer types.
(5) The qep_port.h
platform-specific header file must include the qep.h
platform-independent header file.
Almost all real-life state machines can vastly benefit from the reuse of behavior enabled by hierarchical state nesting. Traditionally, state hierarchy has been considered an advanced feature that mandates automatic code synthesis by CASE tools. However, the use of a generic event processor enables very straightforward manual coding of HSMs.
This chapter described the inner workings of a small, generic, hierarchical event processor called QEP. The event processor consists of just two classes: QHsm
for derivation of state machines and QEvent
for derivation of events with parameters. The event-dispatching algorithm implemented in the QHsm
class has been carefully optimized over the years for both speed and space. The most recent QEP version requires only a single pointer to function per state machine in RAM and minimizes the stack usage by very judiciously sizing automatic variables and by avoiding any recursive calls to state handler functions. State-handler functions are an inexpensive commodity, and there are no limits (except for code space) of how many you can use.
Implementing HSMs with QEP is straightforward because the hierarchical event processor does most of the heavy lifting for you. In fact, coding of even the most complex HSM turns out to be a rather simple exercise in applying just a few straightforward rules. As your design evolves, QEP allows easily changing the state machine topology. In particular, no transition chains must be coded manually. To change the target of a transition, you modify the argument of the Q_TRAN()
macro. Similarly, to change the superstate of a given state, you modify the argument of the Q_SUPER()
macro. All these changes are confined to one line of code.
The most important perspective to keep in mind while coding state machines with QEP is that the source code is as much the implementation as it is the executable specification of your state machine. Instead of thinking in terms of individual C or C++ statements, you should think in terms of state machine elements, such as states, transitions, entry/exit actions, initial transitions, and guards. When you make this quantum leap, you will no longer struggle with convoluted if–else
“spaghetti” code. You will start thinking at a higher level of abstraction about the best ways to partition behavior into states, about the events available at any given time, and about the best state hierarchy for your state machine.
3.142.42.33