Architecture 1

Arch 6 – Introduction to QP

In previous episodes, we have looked at conventional finite state machines (FSMs) and common techniques to implement them, such as double-switch, state table and state pattern. We introduced their more sophisticated cousins of hierarchical state machines (HSMs) which are much more expressive and concise to describe real-world behaviors. However, due to their complexity, traditional implementations for FSMs no longer work for HSMs. We need dedicated libraries or frameworks, such as Quantum Platform (QP) or xstate, to help us implement HSMs. In this episode, we will give a quick introduction to QP which will be used on the STM32-based embedded devices.

Basic Ideas

Like the state table method, QP represents states with functions. It implements one function for each state (including all composite and leaf states). It maintains a function pointer to point to the function of the current leaf state. QP is different from the state table method in that it does not implement a separate function for each event a state handles.

Like the state pattern method, QP represents each state machine with a class. However it does not implement each state in its own class. Instead QP implements each state as a member function of the state machine class which automatically provides a context for all the states. The term context refers to all the member objects and methods of the state machine class that are maintained across and accessible to all the states.

Like the double-switch method, it uses a switch-case construct to handle different events in a given state.

QP combines the good parts of existing methods and patterns to form a new one.

State Member Function

The center piece of QP is the use of a member function to implement each state of an HSM. QP provides a base class QHsm as the basis for all state machine classes. Developers derive their own application state machine classes from QHsm and implement their states as member functions of those derived classes.

We will see a complete example in the next section. First we present the basic form of a state member function:

QState MyHsm::MyState(MyHsm * const me, QEvt const * const e) {
    switch (e->sig) {
        case Q_ENTRY_SIG: {                       // Entry actions.
            ...
            return Q_HANDLED();
        }
        case Q_EXIT_SIG: {                        // Exit actions.
            ...
            return Q_HANDLED();
        }
        case Q_INIT_SIG: {                        // Initial transition.
            ...
            return Q_TRAN(&MyHsm::MyDefaultSubState);
        }
        case MY_EVENT_A: {
            ...
            return Q_TRAN(&MyHsm::MyTargetState); // State transition.
        }
        case MY_EVENT_B: {
            ...
            return Q_HANDLED();                   // Internal transition.
        }
        case MY_EVENT_C: {
            if (me->MyGuard()) {
                ...
                return Q_HANDLED();               // Guard condition passes
            }
            break;                                // Ignored in this state.
        }
    }                                             // Event not handled.
    return Q_SUPER(&MyHsm::MySuperState);         // Returns super state.
}

The basic form above contains almost all important concepts of a statechart.

MyState

MyState is a member function of the state machine class MyHsm. Since it is declared as a static member function, the “this” pointer to the object is passed explicitly into the function via the parameter me. This is not necessary but is done this way in QP to support certain compilers that do not support pointer-to-member-functions properly.

You can have multiple instances of a state machine class, which is a very important concept. For example you may have an HSM class named GpioOut to drive a GPIO output pin (e.g. for pattern generation). You can then define one instance of this class for each GPIO output pin on your hardware.

Note: In some other state machine frameworks, the “me” parameter is also known as the context or model of the state machine.

Event ‘e’

The event to be handled is passed in as the second parameter e. It is passed by reference as a pointer to the event base class QEvt. From its member sig the switch-case statement differentiates which event has arrived.

Once it has figured out the event type, it can downcast e to the actual subclass derived from QEvt. It is done this way to avoid the overhead of virtual functions or RTTI. However we must be very careful to cast to the correct type.

QP provides built-in event types, namely Q_ENTRY_SIG, Q_EXIT_SIG and Q_INIT_SIG as annotated above. We can add as many application specific events as we need, such as MY_EVENT_A, MY_EVENT_B and MY_EVENT_C shown above.

Guard Conditions

A guard condition is implemented with an if statement. When a guard condition is evaluated to false, the transition is disabled and the event must be propagated to the super states until a transition is selected (or the event is discarded if none is found). See the next section about event propagation.

Return Value

The return value of a state member function informs QP the result of an event handling by this state. There are three options via macros provided by QP:

Q_HANDLED() – The event is handled by either entry actions, exit actions or an internal transition.

Q_TRAN() – The event is handled by a state transition (including initial transition) to a different or the same state. The target state is specified as a pointer to member function. All exit and entry actions along the path of a transition will be performed automatically by QP which dispatches Q_ENTRY_SIG, Q_EXIT_SIG and Q_INIT_SIG to all involved states in the proper order according to the rules of statecharts. (See previous episode).

Q_SUPER() – The event is not handled by this state. That is, there is no enabled transition to be selected in this state. The immediate super state is specified as a pointer to member function. If this state is already at the highest level, the event will be propagated to the built-in top state (think of it as the paper) which automatically discards the event. You must make sure you return the correct super state via Q_SUPER() especially when you are copying-and-pasting code from another state; otherwise it would cause an assert at run-time as QP detects a malformed state machine.

Example

This example is based on Figure 2.11 on Page 88 of the PSiCC book by Miro Samek (Book: Practical UML Statecharts in C/C++, 2nd Ed. (state machine.com)). This is a classic example exercising most if not all possible types of transition in a statechart.

It is implemented in the Demo active object. We have not formally covered active objects yet, but now we just need to know an active object is an HSM running in its own thread.

See Src/app/Demo/Demo.cpp of our project code base at GitHub for the complete implementation. The state member function for S1 is extracted here for explanation.

QState Demo::S1(Demo * const me, QEvt const * const e) {
    switch (e->sig) {
        case Q_ENTRY_SIG: {
            EVENT(e);
            return Q_HANDLED();
        }
        case Q_EXIT_SIG: {
            EVENT(e);
            return Q_HANDLED();
        }
        case Q_INIT_SIG: {
            return Q_TRAN(&Demo::S11);
        }
        case DEMO_A_REQ: {
            EVENT(e);
            return Q_TRAN(&Demo::S1);
        }
        case DEMO_B_REQ: {
            EVENT(e);
            return Q_TRAN(&Demo::S11);
        }
        case DEMO_C_REQ: {
            EVENT(e);
            return Q_TRAN(&Demo::S2);
        }
        case DEMO_D_REQ: {
            EVENT(e);
            if (!me->m_foo) {
                me->m_foo = 1;
                return Q_TRAN(&Demo::S);
            }
            break;
        }
        case DEMO_F_REQ: {
            EVENT(e);
            return Q_TRAN(&Demo::S211);
        }
        case DEMO_I_REQ: {
            EVENT(e);
            return Q_HANDLED();
        }
    }
    return Q_SUPER(&Demo::S);
}

Do you see the direct mapping between code and statechart? This is a simple yet powerful concept to translate design into code and vice versa. Do you have the experiences of trying to understand code written by someone else or even by yourself a few months back, with lots of variables set, clear and checked all over the place?

Some sample log output is shown here for reference.

2890 CONSOLE_UART2> demo ?
[Commands]
test           Test function
a              A evt
b              B evt
c              C evt
d              D evt
e              E evt
f              F evt
g              G evt
h              H evt
i              I evt
?              List commands
128825 CONSOLE_UART2> demo c
133344 DEMO(29): S2 DEMO_C_REQ from CONSOLE_UART2(2) seq=0
133344 DEMO(29): S211 EXIT
133344 DEMO(29): S21 EXIT
133344 DEMO(29): S2 EXIT
133344 DEMO(29): S1 ENTRY
133344 DEMO(29): S11 ENTRY
138788 CONSOLE_UART2> demo g
140892 DEMO(29): S11 DEMO_G_REQ from CONSOLE_UART2(2) seq=0
140892 DEMO(29): S11 EXIT
140892 DEMO(29): S1 EXIT
140892 DEMO(29): S2 ENTRY
140892 DEMO(29): S21 ENTRY
140892 DEMO(29): S211 ENTRY
141571 CONSOLE_UART2> demo h
143149 DEMO(29): S211 DEMO_H_REQ from CONSOLE_UART2(2) seq=0
143149 DEMO(29): S211 EXIT
143149 DEMO(29): S21 EXIT
143149 DEMO(29): S2 EXIT
143149 DEMO(29): S1 ENTRY
143149 DEMO(29): S11 ENTRY
143562 CONSOLE_UART2> demo d
145183 DEMO(29): S11 DEMO_D_REQ from CONSOLE_UART2(2) seq=0
145183 DEMO(29): S1 DEMO_D_REQ from CONSOLE_UART2(2) seq=0
145183 DEMO(29): S11 EXIT
145183 DEMO(29): S1 EXIT
145183 DEMO(29): S1 ENTRY
145183 DEMO(29): S11 ENTRY
145600 CONSOLE_UART2> demo d
147024 DEMO(29): S11 DEMO_D_REQ from CONSOLE_UART2(2) seq=0
147024 DEMO(29): S11 EXIT
147024 DEMO(29): S11 ENTRY