Initializing State Machine Objects
Initialization of hierarchical state machines requires some attention because you need to execute the top-most initial transition, which in general case can be quite involved. For example, the top-most initial transition in the PELICAN crossing statechart (Figure 2) consists of the following steps: (1) entry to operational, (2) initial transition in operational, (3) entry to carsEnabled, (4) initial transition in carsEnabled, (5) entry to carsGreen, (6) initial transition in "carsGreen", and finally (7) entry to carsGreenNoPed. Of course, you want QP-nano to deal with this complexity, which it can actually do, but in order to reuse the already implemented mechanisms, you need to execute the top-most initial transition as a regular transition.
Figure 3 illustrates how you do it. You need to provide a special state initial that handles the top-most initial transition as a regular state transition. The initial state is nested directly in the top state, which is the UML concept that denotes the ultimate root of the state hierarchy. QP-nano defines the top state handler function QHsm_top, which by default "handles" all events by returning NULL pointer. ("Handling" events in the top state means really silently discarding them, per the UML semantics.)
Configuring and Starting the Application
After you've coded all state machines, you need to tell QP-nano about them, so that it can start managing the state machines (or actually active objects) as components of the application.
QP-nano executes all active objects in the system in a run-to-completion (RTC) fashion, meaning that each active object completely handles the current event before processing the next one. After each RTC step, QP-nano engages a simple scheduler to decide which active object to execute next. The scheduler makes this decision based on the priority statically assigned to each active object upon the system startup (priority-based scheduling). The scheduler always executes the highest-priority active object that has some events in its event queue.
Listing Three shows how to configure and start the application. You customize QP-nano in the qpn_port.h header file, which contains extensive comments explaining all the options (1). Next, you statically allocate all active objects (2-3) as well as correctly sized event queues for them (4-5). Please note, that the highest-priority active object in the system, such as Pedestrian, might not need an event queue buffer at all, because the single event stored inside the state machine itself might be sufficient.
(1) #include "qpn_port.h" /* QP-nano port */ #include "bsp.h" /* Board Support Package (BSP) */ #include "pelican.h" /* application header file */ /*................................................................*/ (2) static Pelican l_pelican; /* statically allocate PELICAN object */ (3) static Ped l_ped; /* statically allocate Pedestrian object */ (4) static QEvent l_pelicanQueue[1]; /* PELICAN's event queue */ /* as the highest-priority task, Ped does not need event queue */ /*................................................................*/ /* CAUTION: the QF_active[] array must be initialized consistently * with the priority assignement in pelican.h */ (5) QActiveCB const Q_ROM QF_active[] = { (6) { (QActive *)0, (QEvent *)0, 0 }, (7) { (QActive *)&l_pelican, l_pelicanQueue, Q_DIM(l_pelicanQueue)}, (8) { (QActive *)&l_ped, (QEvent *)0, 0 /* no queue */ } }; (9) uint8_t const Q_ROM QF_activeNum = (sizeof(QF_active)/sizeof(QF_active[0])) - 1; /*................................................................*/ void main (void) { (11) BSP_init(); /* initialize the board */ (12) Pelican_init(&l_pelican); /* take the top-most initial tran. */ (13) Ped_init(&l_ped, 15); /* take the top-most initial tran. */ (14) QF_run(); /* start executing state machines */ }
Next, at label (5) of Listing Three, you define and initialize a constant array QF_active[], in which you configure all the active objects in the system in the order of their relative priority. QP-nano has been carefully designed not to waste the precious RAM for any information available at compile time. The QF_active[] array is an example of such compile-time information and is allocated in the code-space by the Q_ROM modifier. Q_ROM is a macro that for the IAR 8051 compiler is defined as __code in qpn_port.h. Other Harvard-architecture processors can benefit from this scheme as well. Similarly, at label (9) of Listing Three, you define and initialize another compile-time variable QF_activeNum, which is the total number of active objects actually used in the system. Please note that the zero-element of the QF_active[] is unused, so the number of active objects in the application is the dimension of the QF_active[] array minus 1.
The main() function is remarkably simple. You call the board initialization (11), trigger all initial transitions in the active objects (12-13), and finally transfer the control to the QF_run() function, which implements the QP-nano scheduler running in an endless loop.