Architecture 1

Arch 10 – Enhanced Framework

Quantum Platform (QP) provides a higher level of abstraction (events and states) over tradition RTOS primitives such as threads, queues, mutexes, etc. It allows us to focus on actual behaviors rather than lower level interactions such as when to lock a mutex, whether a function call will block or which inter-task communication primitives are to be used in different cases.

QP is designed to be a generic framework which is flexible and extensible. When we use it in our projects, we would need to provide external hooks and adaptation, such as:

  1. When creating an active object, we need to allocate memory for its event queue externally.
  2. To support orthogonal regions (i.e. multiple hierarchical state machines in one active object), we need to delegate events from an active object to each contained HSM explicitly.
  3. Although QP provides a powerful built-in debugging tool called QSPY, we may want to add custom logging support for our own application needs.
  4. To enforce stronger synchronization among HSMs, we need to apply stricter semantics to our event interface and customize the event delivery mechanism. See a discussion on publish-subscribe in the previous episode.

To make it more convenient to provide the above hooks and extensions, we have added our own thin layer of framework on top of QP. It is defined in the namespace FW under the folder Src/framework of our source tree on GitHub.

Common Attributes

When developing applications with hierarchical state machines (HSMs), it would be very useful to have a human readable ID or name associated with each hierarchical state machine instance, rather than just a hexadecimal C++ pointer.

One way to do that would be to add a m_id or m_name field to each concrete HSM class derived from QActive or QHsm. Over time we would have a collection of data members that are common to all of our concrete HSM classes. It is a great chance to apply encapsulation and use a class to contain all these common attributes along with common helper methods.

We call this class Hsm, meaning that it contains attributes and methods common to all hierarchical state machines. It is defined in Src/framework/include/fw_hsm.h. Below is an extract of the class definition:

class Hsm {
...
protected:
    ...
    Hsmn m_hsmn;
    char const *m_name;
    QP::QHsm *m_qhsm;
    char const *m_state;
    DeferEQueue m_deferEQueue;
    QP::QEQueue m_reminderQueue;
    ...
};

HSM Number – HSMN

The member m_hsmn means HSM Number which is a unique identification of each HSM instance in the system. It is analogous to SSN for Social Security Number in the US. While m_hsmn is an enumeration (i.e. a number), the member m_name is the corresponding name string of the HSM. The use of enumeration is very common in embedded systems since it is much more efficient to perform comparison and assignment on numbers than strings. On the other hand, using strings is more convenient during debugging. We would rather see “DEMO received an event” than “HSM 51 received an event”. By including both in our Hsm class, we take advantages of both approaches.

The HSMN of all hierarchical state machine instances of our application are enumerated in Inc/app_hsmn.h. We use macros to help allocate enumeration values for multiple instances of the same hierarchical state machine class automatically.

Supporting multiple instances of an HSM class is crucial for real-life projects. Image we use an HSM class named TcpConnection to model a TCP connection. If a system needs to support 1000 concurrent connections, it would need 1000 instances of the TcpConnection class. It would be quite tedious to explicitly define the HSMNs for all the instances like this:

enum {
    ...
    TCP_CONNECTION_0,
    TCP_CONNECTION_1,
    ...
    TCP_CONNECTION_999,
    ...
};

Using macros allows us to conveniently define a range of enumeration values of all instances of an HSM class by simply specifying the instance count, which would be 1000 in our TcpConnection example. The macro ADD_HSM() automatically define the enumeration for the base instance (first instance) of the HSM, the total number of instances (count) and the last instance of the HSM. We can then calculate the HSMN of any particular instance by adding its instance number (a zero-based index) to the base instance enumeration. If we so prefer, we may use the macro ADD_ALIAS() to associate a unique enumeration to a particular instance for convenience. We will illustrate with real examples below.

#define APP_HSM \
    ...
    ADD_HSM(GPIO_IN, 5) \
    ADD_HSM(DEMO, 1) \
    ADD_HSM(GPIO_OUT, 1) \
    ...
#define ALIAS_HSM \
    ...
    ADD_ALIAS(USER_BTN,        GPIO_IN) \
    ADD_ALIAS(ACCEL_GYRO_INT,  GPIO_IN+1) \
    ADD_ALIAS(MAG_DRDY,        GPIO_IN+2) \
    ADD_ALIAS(HUMID_TEMP_DRDY, GPIO_IN+3) \
    ADD_ALIAS(PRESS_INT,       GPIO_IN+4) \
    ADD_ALIAS(USER_LED,        GPIO_OUT) \
    ...

In the list above, we show the definition of the HSMNs for the classes GpioIn (GPIO Input), Demo and GpioOut (GPIO Output). It specifies that there are 5 instances of the class GpioIn and one instance each for the class Demo and GpioOut.

The HSMN of the base instance of GpioIn is defined as GPIO_IN, whereas that of the instance i can be calculated by GPIO_IN + i. This provides a programmatic way to identify each instance of a hierarchical state machine class, which is essential to support a large number of instances. For GpioIn, however, since we only need a few of them, it makes sense to define a unique enumeration for each instance based on its function (or usage). For example, USER_BTN being the first instance is equivalent to the base value of GPIO_IN, whereas ACCEL_GYRO_INT (interrupt pin for the accelerometer and gyroscope) being the second instance is equivalent to GPIO_IN + 1. For reference, instances of the GpioIn class are configured in GpioIn.cpp here.

For another example, DEMO is the HSMN of the Demo active object we used to demonstrate the statechart example in the PSiCC book. (See Architecture 6 for details). Since there is only one instance of the class Demo, we can fix its HSMN in its constructor by passing DEMO, along with the corresponding name string “DEMO”, to the constructor of its base class Active. Active is our extension to QActive of QP and we will look into it in the next section.

Demo::Demo() :
    Active((QStateHandler)&Demo::InitialPseudoState, DEMO, "DEMO"),
    m_stateTimer(GetHsmn(), STATE_TIMER), m_inEvt(QEvt::STATIC_EVT) {
    SET_EVT_NAME(DEMO);
}

Region and Active

We introduced the class Hsm in the previous section, which is used to encapsulate common attributes and methods for all HSMs. We have also seen how QP uses the class QHsm to represent an HSM and how it derives QActive from QHsm to represent an active object.

Putting them altogether, we first derive a new class Region from QHsm, which allows us to extend the features of QHsm. One of the extensions is to comprise an Hsm object to hold additional attributes and methods. The class name Region comes from the statechart concept of orthogonal regions which can be viewed as concurrent (parallel) hierarchical state machines running in the same thread of an active object. In other words, a Region is a QHsm having an Hsm object as illustrated below:

Similarly we extend QActive by deriving a new class Active from it. Active comprises an Hsm object along with the memory storage for the event queue making it more convenient to create active objects. Note this convenience is made at the expense of the flexibility to customize the event queue size of each concrete active object class. This works very well for our applications but may not suit yours.

Now we have the class Region representing our extended hierarchical state machines and the class Active representing our extended active objects. How about we combine them together? We can have an active object comprising one or more Region objects to support concurrent state machines running in the same thread. This is similar to having orthogonal regions in a statechart, though there are subtle differences between true orthogonal regions and concurrent state machines, as explained in Section 6 of the paper Statecharts in the Making by David Harel. Also see Chapter 5.4 of the PSiCC book by Miro Samek on the Orthogonal Component pattern adopted here. Since an active object is by itself a hierarchical state machine (as QActive is derived from QHsm), we don’t need to add a Region to an active object just to implement a single state machine.

The concept of orthogonal regions is illustrated in the following class diagram and block diagram:

For reference, the class definitions of Region and Active are extracted below:

class Region : public QP::QHsm {
public:
    Region(QP::QStateHandler const initial, Hsmn hsmn, char const *name) :
        QP::QHsm(initial),
        m_hsm(hsmn, name, this),
        m_container(NULL) {}
    void Init(Active *container);
    void Init(XThread *container);
    Hsm &GetHsm() { return m_hsm; }
    virtual void dispatch(QP::QEvt const * const e);
    // Redirection to m_hsm.
    bool Defer(QP::QEvt const *e) { return m_hsm.Defer(e); }
    void Recall() { m_hsm.Recall(); }
    void Send(Evt *e) { m_hsm.Send(e); }
    ...
protected:
    void Raise(Evt *e) { m_hsm.Raise(e); }
    ...
    Hsm m_hsm;
    QP::QActive *m_container;
};

class Active : public QP::QActive {
public:
    Active(QP::QStateHandler const initial, Hsmn hsmn, char const *name) :
        QP::QActive(initial),
        m_hsm(hsmn, name, this),
        m_hsmnRegMap(m_hsmnRegStor, ARRAY_COUNT(m_hsmnRegStor), ...){
    }
    void Start(uint8_t prio);
    void Add(Region *reg);
    Hsm &GetHsm() { return m_hsm; }
    virtual void dispatch(QP::QEvt const * const e);
    // Redirection to m_hsm.
    bool Defer(QP::QEvt const *e) { return m_hsm.Defer(e); }
    void Recall() { m_hsm.Recall(); }
    void Send(Evt *e) { m_hsm.Send(e); }
    ...
protected:
    void Raise(Evt *e) { m_hsm.Raise(e); }
    enum {
        MAX_REGION_COUNT = 8,
        EVT_QUEUE_COUNT = 64
    };
    Hsm m_hsm;
    HsmnReg m_hsmnRegStor[MAX_REGION_COUNT];
    HsmnRegMap m_hsmnRegMap;
    QP::QEvt const *m_evtQueueStor[EVT_QUEUE_COUNT];
};