Chapter 4. Hierarchical Event Processor Implementation

… 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.

— John Carmack

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.

Key Features of the QEP Event Processor

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:

  • • Full support for hierarchical state nesting.

  • • Guaranteed entry/exit action execution on arbitrary state transition topology.

  • • Full support of nested initial transitions.

  • • 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.

  • • Support for events with arbitrary parameters.

  • • 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.

QEP Structure

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.

QEP event processor and the calculator HSM derived from it. The $ in front of the Calc operations denotes static member functions (see Appendix B).

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++.

QEP Source Code Organization

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

  •  qpc                         - QP/C root directory (qpcpp for QP/C++)

  •    |

  •    +-include                 - QP platform-independent include files (*.H files)

  •     | +-qassert.h            - QP embedded systems-friendly assertions

  •    | +-qep.h                  - QEP interface

  •    | . . .

  •    |

  •    +-ports                   - QP platform-specific ports

  •    | +-80x88               -ports to the 80x86 CPU

  •    | | +-dos                - ports for DOS (the non-preemptive "vanilla" scheduler)

  •    | | | +-tcpp101   - the Turbo C++ 1.01 compiler

  •    | | | | +-l             - large memory model

  •    | | | | |  +-dbg       - build directory for the Debug configuration

  •    | | | | |  | +-qep.lib   – QEP library

  •    | | | | |  +-qep_port.h  – QEP port header file

  •    | | | | |  +-make.bat     – make script for building the QP libraries

  •    | |

  •    | +-. . .               - QP ports to other CPUs …

  •    |

  •    +-qep                  - QEP event processor component

  •    | |

  •    | +-source          - QEP platform-independent source code (*.C files)

  •    | |  +-qep_pkg.h   - internal, packet-scope interface of QEP

  •    | |  +-qep.c           - contains definition of reserved signals

  •    | |  +-qfsm_ini.c- contains definition of QFsm_init()

  •    | |  +-qfsm_dis.c  - contains definition of QFsm_dispatch()

  •    | |  +-qhsm_ini.c - contains definition of QHsm_init()

  •    | |  +-qhsm_dis.c - contains definition of QHsm_dispatch()

  •    | |  +-qhsm_top.c - contains definition of QHsm_top()

  •    | |  +-qhsm_in.c   - contains definition of QHsm_isIn()

  •    | |

  •    | +-lint

  •    | |  +-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.

Events

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.

Event Signal (QSignal)

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.

  • #ifndef QEP_SIGNAL_SIZE

  •       #define QEP_SIGNAL_SIZE 1

  • #endif

  • #if (QEP_SIGNAL_SIZE == 1)

  •        typedef uint8_t QSignal;

  • #elif (QEP_SIGNAL_SIZE == 2)

  •        typedef uint16_t QSignal;

  • #elif (QEP_SIGNAL_SIZE == 4)

  •        typedef uint32_t QSignal;

  • #else

  •        #error "QEP_SIGNAL_SIZE defined incorrectly, expected 1, 2, or 4"

  • #endif

Note

All components of the QP framework, including QEP, use the following standard exact-width integer types (WG14/N843 C99 Standard, Section 7.18.1.1):

Exact SizeUnsignedSigned
8 bitsuint8_tint8_t
16 bitsuint16_tint16_t
32 bitsuint32_tint32_t

QEvent Structure in C

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.)

Listing 4.2. QEvent structure in C (file <qp>qpcincludeqevent.h)

  • typedef struct QEventTag {

  •        QSignal sig;                                            /* signal of the event */

  •        uint8_t dynamic_; /* attributes of a dynamic event (0 for static event) */

  •         /* add event parameters by derivation from the QEvent structure… */

  •  } QEvent;                                                           /* the QEvent type */

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):

  • typedef struct CalcEvtTag {

  •        QEvent super;                                           /* derives from QEvent */

  •        uint8_t key_code;                                           /* code of the key */

  • } CalcEvt;                                                       /* the CalcEvt type */

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.

QEvent Structure in C++

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.

Listing 4.3. QEvent structure in C++ (file <qp>qpcppincludeqevent.h)

  • struct QEvent {

  •        QSignal sig;                                 // signal of the event instance

  •        uint8_t dynamic_;     // attributes of a dynamic event (0 for static event)

  •        // add event parameters by inheriting from QEvent

  • };

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:

  • struct CalcEvt : public QEvent {

  •         uint8_t key_code;                                                 // code of the key

  • };

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.

Hierarchical State-Handler Functions

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.

Designating the Superstate (Q_SUPER() Macro)

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:

  • #define Q_RET_SUPER           ((QState)3)

  • #define Q_SUPER(super_)  

  •        (((QHsm *)me)->state = (QStateHandler)(super_),  Q_RET_SUPER)

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).

Hierarchical State-Handler Function Example in C

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.

Listing 4.4. Example of a hierarchical state-handler function in C (file calc.c)

  • (1) QState Calc_int1(Calc *me, QEvent const *e) {

  • (2)         switch (e->sig) {

  • (3)                case DIGIT_0_SIG:      /* intentionally fall through */

  • (4)                case DIGIT_1_9_SIG: {

  • (5)                       BSP_insert(((CalcEvt const *)e)->key_code);

  • (6)                       return Q_HANDLED()

  •                }

  •                case POINT_SIG: {

  •                      BSP_insert(((CalcEvt const *)e)->key_code);

  • (7)                      return Q_TRAN(&Calc_frac1);

  •                }

  •         }

  • (8)         return Q_SUPER(&Calc_operand1);

  •      }

  • (1) Each state handler takes two parameters: the state machine pointer “me” and the constant pointer “e” to QEvent. It returns QState, which conveys the status of the event handling to the event processor.

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 as CalcEvt. 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.

Hierarchical State-Handler Function Example in C++

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)

  •      QState Calc::int1(Calc *me, QEvent const *e) {

  •             switch (e->sig) {

  •                   case DIGIT_0_SIG:                        // intentionally fall through

  •                    case DIGIT_1_9_SIG: {

  • (1)                       BSP_insert(((static_cast<CalcEvt const *>(e)))->key_code);

  •                       return Q_HANDLED();

  •                   }

  •                     case POINT_SIG: {

  •                            BSP_insert(((static_cast<CalcEvt const *>(e)))->key_code);

  •                            return Q_TRAN(&Calc::frac1);

  •                    }

  •            }

  •             return Q_SUPER(&Calc::operand1);

  •      }

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 the QHsm subclass, such as the calculator state machine class Calc (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).

Hierarchical State Machine Class

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++.

Hierarchical State Machine in C (Structure QHsm)

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).

Listing 4.6. QHsm structure and related functions (file <qp>qpcincludeqep.h)

  • typedef struct QHsmTag {

  • (1)    QStateHandler state;     /* current active state (state-variable) */

  • } QHsm;

  • (2) #define QHsm_ctor(me_, initial_) ((me_)->state = (initial_))

  • (3) void QHsm_init    (QHsm *me, QEvent const *e);

  • (4) void QHsm_dispatch(QHsm *me, QEvent const *e);

  • (5) uint8_t QHsm_isIn (QHsm *me, QHsmState state);

  • (6) QState QHsm_top   (QHsm *me, QEvent const *e);

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.

Hierarchical State Machine in C++ (Class QHsm)

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)

  •        class QHsm {

  •        protected:

    • (1)           QStateHandler m_state;      // current active state (state-variable)

    •        public:

    • (2)         void init       (QEvent const *e = (QEvent const *)0);

    • (3)         void dispatch(QEvent const *e);

    • (4)         uint8_t isIn  (QHsmState state);

    •        protected:

    • (5)         QHsm(QStateHandler initial) : m_state(initial) {} // protected ctor

    • (6)         static QState top(QHsm *me, QEvent const *e);     };

  • (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 that QHsm::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++”).

The Top State and the Initial Pseudostate

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 top state and the initial pseudostate.

Figure 4.2. The top state and the initial pseudostate.

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:

  • QState QHsm_top(QHsm *me, QEvent const *e) {

  •         (void)me;             /* avoid the compiler warning about unused parameter */

  •         (void)e;               /* avoid the compiler warning about unused parameter */

  •        return Q_IGNORED();                /* the top state ignores all events */

  • }

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:

  • QState Calc_initial(Calc *me, QEvent const *e) {

  •        (void)e;                  /* avoid the compiler warning about unused parameter */

  •        BSP_clear();                       /* clear the calculator display */

  •        return Q_TRAN(&Calc_on);          /* designate the default state */

  • }

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.

Entry/Exit Actions and Nested Initial Transitions

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:

  • enum QReservedSignals {

  •        Q_ENTRY_SIG = 1,                            /* signal for coding entry actions */

  •        Q_EXIT_SIG,                                  /* signal for coding exit actions */

  •        Q_INIT_SIG,                           /* signal for coding initial transitions */

  •        Q_USER_SIG                       /* first signal that can be used in user code */

  • };

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

  • QState Calc_on(Calc *me, QEvent const *e) {

  •         switch (e->sig) {

  •                case Q_ENTRY_SIG: {                                    /* entry action */

  •                       BSP_message("on-ENTRY;");

  •                       return Q_HANDLED();

  •                }

  •                case Q_EXIT_SIG: {                                         /* exit action */

  •                       BSP_message("on-EXIT;");

  •                       return Q_HANDLED();

  •                }

  •                case Q_INIT_SIG: {                    /* nested initial transition */

  •                       BSP_message("on-INIT;");

  •                       return Q_TRAN(&Calc_ready);

  •                }

  •                case C_SIG: {

  •                       BSP_clear();

  •                       return Q_TRAN(&Calc_on);

  •                }

  •                case OFF_SIG: {

  •                       return Q_TRAN(&Calc_final);

  •                }

  •         }

  •         return Q_SUPER(&QHsm_top);

  • }

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.

Listing 4.9. Enumerating signals for the Calc state machine

  • enum CalcSignals {

  •        C_SIG = Q_USER_SIG,

  •        CE_SIG,

  •        DIGIT_0_SIG,

  •        DIGIT_1_9_SIG,

  •        POINT_SIG,

  •        OPER_SIG,

  •        EQUALS_SIG,

  •        OFF_SIG

  • };

Note

The reserved signals Q_ENTRY_SIG, Q_EXIT_SIG, and Q_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 as QEQ_EMPTY_SIG_) is reserved as well and should cause a state-handler function to always return the superstate without any side effects.

Reserved Events and Helper Macros in QEP

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:

  • QEvent const QEP_reservedEvt_[] = {

  •        { (QSignal)QEP_EMPTY_SIG_,   (uint8_t)0 },

  •        { (QSignal)Q_ENTRY_SIG,      (uint8_t)0 },

  •        { (QSignal)Q_EXIT_SIG,        (uint8_t)0 },

  •        { (QSignal)Q_INIT_SIG,        (uint8_t)0 }

  • };

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 interface qep.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):

  •          /** helper macro to trigger reserved event in an HSM */

  • #define QEP_TRIG_(state_, sig_)

  •        ((*(state_))(me, &QEP_reservedEvt_[sig_]))

  • /** helper macro to trigger entry action in an HSM */

  • #define QEP_EXIT_(state_)

  •        if (QEP_TRIG_(state_, Q_EXIT_SIG) == Q_RET_HANDLED) {

  •              /* QS software tracing instrumentation for state entry */

  •        }

  • /** helper macro to trigger exit action in an HSM */

  • #define QEP_ENTER_(state_)

  •        if (QEP_TRIG_(state_, Q_ENTRY_SIG) == Q_RET_HANDLED) {

  •              /* QS software tracing instrumentation for state exit */

  •        }

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. The qassert.h header file provides a number of macros, which include:

  • Q_REQUIRE(), to assert a precondition

  • Q_ENSURE(), to assert a postcondition

  • Q_INVARIANT(), to assert an invariant

  • Q_ASSERT(), to assert a general contract of another type

  • Q_ALLEGE, to assert a general contract and always evaluate the condition, even when assertions are disabled at compile time

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 and qassert.h in more detail.

Topmost Initial Transition (QHsm_init())

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

  • 2 Execution of entry actions to the “on” state

  • 3 Execution of the actions associated with the initial transition defined in the “on” state

  • 4 Execution of the entry actions to the “result” 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 inheritance tree of the calculator HSM with the nested initial transitions.

Figure 4.3. The inheritance tree of the calculator HSM with the nested initial transitions.

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.

The use of the path[] array to enter the target state configuration in the correct order.

Figure 4.4. The use of the path[] array to enter the target state configuration in the correct order.

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)

  • (1) void QHsm_init(QHsm *me, QEvent const *e) {

  •                  QStateHandler t;

  •                                               /* the top-most initial transition must be taken */

  • (2)        Q_ALLEGE((*me->state)(me, e) == Q_RET_TRAN);

  • (3)        t = (QStateHandler)&QHsm_top;                       /* HSM starts in the top state */

  • (4)        do {                                                                           /* drill into the target… */

  • (5)             QStateHandler path [QEP_MAX_NEST_DEPTH_];

  • (6)             int8_t ip = (int8_t)0;                                    /* transition entry path index */

  • (7)             path [0] = me->state;      /* save the target of the initial transition */

  • (8)             (void)QEP_TRIG_(me->state, QEP_EMPTY_SIG_);

  • (9)             while (me->state != t) {

  • (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 */

  • (12)             Q_ASSERT(ip < (int8_t)QEP_MAX_NEST_DEPTH_);

  •             do {                     /* retrace the entry path in reverse (desired) order… */

  • (13)                   QEP_ENTER_(path [ip]);                                          /* enter path [ip] */

  • (14)             } while ((--ip) >= (int8_t)0);

  • (15)             t = path [0];    /* current state becomes the new source */

  • (16)        } while (QEP_TRIG_(t, Q_INIT_SIG) == Q_RET_TRAN);

  • (17)        me->state = t;    }

  • (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.

  • (15) The current state becomes the new source.

  • (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.

  • (17) The current state is set to the final leaf state.

Dispatching Events (QHsm_dispatch(), General Structure)

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)

  • (1) void QHsm_dispatch(QHsm *me, QEvent const *e) {

  • (2)        QStateHandler path [QEP_MAX_NEST_DEPTH_];

  •        QStateHandler t;

  •        QState r;

  • (3)        t = me->state;                                     /* save the current state */

  • (4)        do {                                   /* process the event hierarchically… */

  • (5)           s = me->state;

  • (6)            r = (*s)(me, e);                               /* invoke state handler s */

  • (7)         } while (r == Q_RET_SUPER);

  • (8)        if (r == Q_RET_TRAN) {                                  /* transition taken? */                   int8_t ip = (int8_t)(-1);                     /* transition entry path index */

  •            int8_t iq;                         /* helper transition entry path index */

  • (9)                  path [0] = me->state;         /*save the target of the transition */

  • (10)              path [1] = t;

  • (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 */       }

  • (14)                     t = me->state;        /* me->state holds the superstate */             }

  • (15)              . . .

  •       }

  • (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.

  • (3) The current state is saved temporarily into ‘t’.

  • (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.

Two segments of an inherited state transition.

Figure 4.5. Two segments of an inherited state transition.

  • (12) The exit action is triggered in the state.

  • (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)

    • /* NOTE: continued from Listing 4.11 */

    •                t = path [0];                                          /* target of the transition */

    • (1)   if (s == t) {         /* (a) check source==target (transition to self) */

    •                    QEP_EXIT_(s)                                       /* exit the source */

    •                    ip = (int8_t)0;                                 /* enter the target */

    •             }

    •             else {

    •                   (void)QEP_TRIG_(t, QEP_EMPTY_SIG_);     /* superstate of target */

    •                   t = me->state;

    • (2)             if (s == t) {               /* (b) check source==target->super */

    •                               ip = (int8_t)0;                                     /* enter the target */

    •                   }

    •                   else {

    • (3)                   (void)QEP_TRIG_(s, QEP_EMPTY_SIG_);              /* superstate of src */

    •                                            /* (c) check source->super==target->super */

    • (4)                  if (me->state == t) {

    •                                 QEP_EXIT_(s)                          /* exit the source */

    •                                  ip = (int8_t)0;                     /* enter the target */

    •                           }

    •                           else {

    •                                                              /* (d) check source->super==target */

    • (5)                         if (me->state == path [0]) {

    •                                         QEP_EXIT_(s)                   /* exit the source */

    •                                 }

    •                                 else { /* (e) check rest of source==target->super->super..

    •                                              * and store the entry path along the way

    •                                              */

    •                                       iq = (int8_t)0;         /* indicate that LCA not found */

    •                                       ip = (int8_t)1;           /* enter target and its superstate */

    •                                       path [1] = t;             /* save the superstate of target */

    •                                       t = me->state;           /* save source->super */

    •                                                                       /* find target->super->super */

    •                                       r = QEP_TRIG_(path [1], QEP_EMPTY_SIG_);

    • (6)                               while (r == Q_RET_SUPER) {

    •                                               path [++ip] = me->state;     /* store the entry path */

    •                                               if (me->state == s) {/* is it the source? */

    •                                                                   iq = (int8_t)1;            /* indicate that LCA found */

    •                                                                   /* entry path must not overflow */

    •                                                     Q_ASSERT(ip < (int8_t)QEP_MAX_NEST_DEPTH_);

    •                                                                 ––ip;        /* do not enter the source */

    •                                                     r = Q_RET_HANDLED;         /* terminate the loop */

    •                                              }

    •                                              else {                   /* it is not the source, keep going up */

    •                                                    r = QEP_TRIG_(me->state, QEP_EMPTY_SIG_);

    •                                              }

    •                                       }

    •                                             if (iq == (int8_t)0) {      /* the LCA not found yet? */

    •                                                                   /* entry path must not overflow */

    • (7)                                      Q_ASSERT(ip < (int8_t)QEP_MAX_NEST_DEPTH_);

    • (8)                                              QEP_EXIT_(s)               /* exit the source */

    •                                                     /* (f) check the rest of source->super

    •                                                                            *                                             == target->super->super...

    •                                                     */

    •                                              iq = ip;

    •                                                   r = Q_RET_IGNORED;      /* indicate LCA NOT found */

    •                                              do {

    • (9)                                               if (t == path [iq]) {                /* is this the LCA? */

    •                                                              r = Q_RET_HANDLED;       /* indicate LCA found */

    •                                                    ip = (int8_t)(iq - 1);        /*do not enter LCA*/

    •                                                    iq = (int8_t)(-1);/* terminate the loop */

    •                                    }

    •                                    else {

    •                                             ––iq; /* try lower superstate of target */

    •                                    }

    •                              } while (iq >= (int8_t)0);

    •                              if (r != Q_RET_HANDLED) {   /* LCA not found yet? */

    •                                           /* (g) check each source->super->...

    •                                             * for each target->super...

    •                                             */

    •                                     r = Q_RET_IGNORED;          /* keep looping */

    •                                     do {

    •                                                        /* exit t unhandled? */

    • (10)                               if (QEP_TRIG_(t, Q_EXIT_SIG)

    •                                       == Q_RET_HANDLED)

    •                                    {

    •                                        (void)QEP_TRIG_(t, QEP_EMPTY_SIG_);

    •                                    }

    •                                    t = me->state;    /*  set to super of t */

    •                                    iq = ip;

    •                                     do {

    • (11)                                    if (t == path [iq]) {/* is this LCA? */

    •                                                         /* do not enter LCA */

    •                                            ip = (int8_t)(iq - 1);

    •                                             iq = (int8_t)(-1);/*break inner */

    •                                             r = Q_RET_HANDLED;/*break outer */

    •                                         }

    •                                         else {

    •                                             ––iq;

    •                                         }

    •                                     } while (iq >= (int8_t)0);

    •                                 } while (r != Q_RET_HANDLED);

    •                             }

    •                         }

    •                     }

    •                 }

    •             }

    •         }

    •                     /* retrace the entry path in reverse (desired) order... */

    • (12) for (; ip >= (int8_t)0; ––ip) {

    •                 QEP_ENTER_(path [ip])                          /* enter path [ip] */

    •         }

    •         t = path [0];                 /* stick the target into register */

    •         me->state = t;             /* update the current state */

    •                                                /* drill into the target hierarchy... */

    • (13) while (QEP_TRIG_(t, Q_INIT_SIG) == Q_RET_TRAN) {

    •             ip = (int8_t)0;

    •                 path [0] = me->state;

    •                (void)QEP_TRIG_(me->state, QEP_EMPTY_SIG_);  /* find superstate */

    •                while (me->state != t) {

    •                 path [++ip] = me->state;

    •                    (void)QEP_TRIG_(me->state, QEP_EMPTY_SIG_);/* find superstate */

    •                 }

    •                me->state = path [0];

    •                                               /* entry path must not overflow */

    •                Q_ASSERT(ip < (int8_t)QEP_MAX_NEST_DEPTH_);

    •                do {   /* retrace the entry path in reverse (correct) order... */

    •                   QEP_ENTER_(path [ip])                      /* enter path [ip] */

    •                 } while ((–ip) >= (int8_t)0);

    •                t = path [0];

    •            }

    •         }

  • (16) The current state is restored from the variable ‘t,’ where it has been stored in step (3).

Executing a Transition in the State Machine (QHsm_dispatch(), Transition)

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.

Ordering of transition types from simplest to progressively more complex in the transition algorithm (Listing 4.12).

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.

Summary of Steps for Implementing HSMs with QEP

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.h contains the board support package interface.

  • bsp.cpp contains the implementation of the board-specific functions.

  • main.cpp contains the main() function and the event loop.

  • 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 calculator HSM running in a Windows console.

Figure 4.7. The calculator HSM running in a Windows console.

Step 1: Enumerating Signals

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.

Step 2: Defining Events

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:

  • struct CalcEvt : public QEvent {

  •         uint8_t key_code;                                       // code of the key

  • };

Step 3: Deriving the Specific State Machine

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

  • (1) class Calc : public QHsm {

  •        private:

  • (2)double  m_operand1; // the value of operand 1 (extended state variable)

  • uint8_t m_operator;         // operator key entered (extended state variable)

  •        public:

  • (3)     Calc() : QHsm((QStateHandler)&Calc::initial) {        // ctor

  •          }

  •     protected:

  • (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

  •   };

Note

You should not be confused by the fact that the Ship state machine example in Chapter 1 derived from the QActive base class rather than QHsm. As you will see in Chapter 7, QActive is a subclass of QHsm, so the Ship state machine is in fact derived from QHsm, albeit not directly.

Step 4: Defining the Initial Pseudostate

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.

  • QState Calc::initial(Calc *me, QEvent const * /* e */) {

  •         BSP_clear();

  •         return Q_TRAN(&Calc::on);

  • }

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.

Step 5: Defining the State-Handler Functions

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.

Coding Entry and Exit Actions

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.

Coding Initial Transitions

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.

Coding Internal Transitions

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.

Coding Regular Transitions

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.

Coding Guard Conditions

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.

  • QState Calc::begin(Calc *me, QEvent const *e) {

  •     switch (e->sig) {

  •         case OPER_SIG: {

  •             if ((static_cast<CalcEvt const *>(e))->key_code == KEY_MINUS) {

  •                 return Q_TRAN(&Calc::negated1);

  •             }

  •             break;

  •           }

  •     }

  •     return Q_SUPER(&Calc::ready);

  • }

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().

Pitfalls to Avoid While Coding State Machines with QEP

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.

Incomplete State Handlers

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:

  • QState Calc_on(Calc *me, QEvent const *e) {

  •     switch (e->sig) {

  •         . . .

  •         case C_SIG: {

  •             return Calc_onClear(me);               /* handle the Clear event */

  •         }

  •         . . .

  •     }

  •     return Q_SUPER(&QHsm_top);

  • }

  • . . .

  • QState Calc_onClear(Calc *me) {

  •     BSP_clear();

  •     return Q_TRAN(&Calc_on);                           /* transition to "on" */

  • }

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]).

Ill-Formed State Handlers

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.

State Transition Inside Entry or Exit Action

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.

Incorrect Casting of Event Pointers

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.

Accessing Event Parameters in Entry/Exit Actions or Initial Transitions

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:

  • QState MyHSM_stateA(MyHSM *me, QEvent const *e) {

  •         switch (e->sig) {

  •                case EVTB_SIG: {

  •                       . . .

  •                       /* the only way to transition to stateB */

  •                       return Q_TRAN(&MYHSM_state B);

  •                }

  •         }

  •         return Q_SUPER(&QHsm_top);

  • }

  • QState MyHSM_stateB(MyHSM *me, QEvent const *e) {

  •         switch (e->sig) {

  •                case Q_ENTRY_SIG: {

  •                       EvtB const *evtB = (EvtB const *)e;          /* INCORRECT cast */

  •                       if (evtB->foo != ) . . .                   /* INCORRECT access */

  •                      . . .

  •                }

  •        }

  •        return Q_SUPER(&QHsm_top);

  • }

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.

Targeting a Nonsubstate in the Initial Transition

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.

Correct (A) and incorrect (B) initial transitions.

Figure 4.8. Correct (A) and incorrect (B) initial transitions.

Code Outside the switch Statement

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.

  • QState MyHSM_stateA(MyHSM *me, QEvent const *e) {

  •     for (i = 0; i < N; ++i) { /* PROBLEMATIC: expensive loop outside switch */

  •         doSomethingExpensive();

  •     }

  •     switch (e->sig) {

  •         . . .

  •     }

  •     return Q_SUPER(&QHsm_top);

  • }

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:

  • QState MyHSM_stateB(MyHSM *me, QEvent const *e) {

  •    uint32_t tmp = 0x12345678;       /* initialization occurring every time */

  •    switch (e->sig) {

  •        . . .

  •        case MY_EVT_SIG: {

  •            uint32_t tmp = 0x12345678;  /* initialization only in this case */

  •            . . .

  •        }

  •        . . .

  •    }

  •    return Q_SUPER(&QHsm_top);

  • }

Suboptimal Signal Granularity

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.

Violating the Run-to-Completion Semantics

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.

Inadvertent Corruption of the Current 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.

Porting and Configuring QEP

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

  •      #ifndef qep_port_h

  •      #define qep_port_h

  •                          /* special keyword used for ROM objects (none for 80x86) */

    • (1) #define Q_ROM

    •                    /* mechanism of accessing const objects in ROM (far pointers) */

    • (2) #define Q_ROM_VAR                far                                            /* 1-byte signal space (255 signals) */

    • (3) #define Q_SIGNAL_SIZE     

    • /* 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 */

    •         #endif                                                        /* qep_port_h */

  • (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 and Q_ROM_VAR refer to the different parts of the object declaration. The macro Q_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 macro Q_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. The Q_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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.42.33