This QP-nano Tutorial is adapted from Chapter 1 of Practical UML Statecharts in C/C++, Second Edition
by Miro Samek, the founder and president of Quantum Leaps, LLC.
Prev: 6. Signals, Events, and Active Objects
Next: 8. Using the Built-in Real-Time Kernels
Contrary to widespread misconceptions, you don't need big design automation tools to translate hierarchical state machines (UML statecharts) into efficient and highly maintainable C or C++. This section explains how to hand-code the Ship state machine from Figure 5-2 with QP-nano. Once you know how to code this state machine, you know how to code them all.
The source code for the Ship state machine is found in the file ship.c located either in the DOS version or the ARM-Cortex version of the "Fly 'n'
Shoot" game. I break the explanation of this file into three steps.
In the first step you define the Ship data structure. Just like in case of events, you use inheritance to derive the Ship structure from the framework structure QActive (see the sidebar Encapsulation and Single Inheritance in C). Creating this inheritance relationship ties the Ship structure to the QF framework. The main responsibility of the QActive base structure is to store the information about the current active state of the state machine, as well as the event queue and priority level of the Ship active object. In fact, QActive itself derives from a simpler QEP structure QHsm that represents just the current active state of a hierarchical state machine. On top of that information, almost every state machine must also store other "extended-state" information. For example, the Ship object is responsible for maintaining the Ship position as well as the score accumulated in the game. You supply this additional information by means of data members enlisted after the base structure member super, as shown in Listing 7-1.
Listing 7-1 Deriving the Ship structure in file ship.c.
(1) #include "qpn_port.h" (2) #include "bsp.h" (3) #include "game.h" /* local objects -----------------------------------------------------------*/ (4) typedef struct ShipTag { (5) QActive super; /* extend the QActive class */ uint8_t x; uint8_t y; uint8_t exp_ctr; uint16_t score; } Ship; /* the Ship active object */ (6) static QState Ship_initial (Ship *me); (7) static QState Ship_active (Ship *me); static QState Ship_parked (Ship *me); static QState Ship_flying (Ship *me); static QState Ship_exploding(Ship *me); /* global objects ----------------------------------------------------------*/ (8) Ship AO_Ship;
qp_port.h header file.bsp.h header file contains the interface to the Board Support Package.game.h header file contains the declarations of events and other facilities shared among the components of the application Ship_initial() function defines the top-most initial transition in the Ship state machine. The initial pseudostate handler has signature identical to the regular state handler function.Ship_active). Second, I always place the pointer to the structure as the first argument of the associated function and I always name this argument me (e.g., Ship_active(Ship *me, ...)).AO_Ship active object. Please note that actual structure definition for the Ship active object is accessible only locally at the file scope of the ship.c file.main().The state machine initialization is divided into the following two steps for increased flexibility and better control of the initialization timeline:
The state machine "constructor", such as Ship_ctor(), intentionally does not execute the top-most initial transition defined in the initial pseudostate because at that time some vital objects can be missing and critical hardware might not be properly initialized yet3. Instead, the state machine "constructor" merely puts the state machine in the initial pseudostate. Later, the user code must trigger the top-most initial transition explicitly, which happens actually inside the function QActive_start() (see Listing 3-11(18-20)). Listing 7-2 shows the instantiation (the "constructor" function) and initialization (the initial pseudostate) of the Ship active object.
Listing 7-2 Instantiation and Initialization of the Ship active object in ship.c.
(1) void Ship_ctor(void) { (2) Ship *me = &AO_Ship; (3) QActive_ctor(&me->super, (QStateHandler)&Ship_initial); (4) me->x = GAME_SHIP_X; (5) me->y = GAME_SHIP_Y; } /* HSM definition ----------------------------------------------------------*/ (6) QState Ship_initial(Ship *me) { (7) return Q_TRAN(&Ship_active); /* top-most initial transition */ }
In the last step, you actually code the Ship state machine by implementing one state at a time as a state handler function. To determine what elements belong the any given state handler function, you follow around the state's boundary in the diagram (Figure 5-2). You need to implement all transitions originating at the boundary, any entry and exit actions defined in the state, as well as all internal transitions enlisted directly in the state. Additionally, if there is an initial transition embedded directly in the state, you need to implement it as well.
Take for example the state "flying" shown in Figure 5-2. This state has an entry action and two transitions originating at its boundary: HIT_WALL and HIT_MINE(type), as well as three internal transitions TIME_TICK, PLAYER_TRIGGER, and DESTROYED_MINE(score). The "flying" state nests inside the "active" superstate. Listing 7-3 shows two state handler functions of the Ship state machine from Figure 5-2. The state handler functions correspond to the states "active" and "flying", respectively. The explanation section immediately following the listing highlights the important implementation techniques.
Listing 7-3 State handler functions for states "active" and "flying" in ship.c.
QState Ship_active(Ship *me) { (1) switch (Q_SIG(me)) { case Q_INIT_SIG: { /* nested initial transition */ (2) return Q_TRAN(&Ship_parked); } case PLAYER_SHIP_MOVE_SIG: { (3) me->x = (uint8_t)Q_PAR(me); (4) me->y = (uint8_t)(Q_PAR(me) >> 8); (5) return Q_HANDLED(); } } (6) return Q_SUPER(&QHsm_top); } /*..........................................................................*/ QState Ship_flying(Ship *me) { switch (Q_SIG(me)) { case Q_ENTRY_SIG: { me->score = 0; /* reset the score */ (7) QActive_post((QActive *)&AO_Tunnel, SCORE_SIG, me->score); return Q_HANDLED(); } case TIME_TICK_SIG: { /* tell the Tunnel to draw the Ship and test for hits */ (8) QActive_post((QActive *)&AO_Tunnel, SHIP_IMG_SIG, ((QParam)SHIP_BMP << 16) | (QParam)me->x | ((QParam)me->y << 8)); ++me->score; /* increment the score for surviving another tick */ if ((me->score % 10) == 0) { /* is the score "round"? */ QActive_post((QActive *)&AO_Tunnel, SCORE_SIG, me->score); } return Q_HANDLED(); } case PLAYER_TRIGGER_SIG: { /* trigger the Missile */ QActive_post((QActive *)&AO_Missile, MISSILE_FIRE_SIG, (QParam)me->x | (((QParam)me->y + SHIP_HEIGHT - 1) << 8)); return Q_HANDLED(); } case DESTROYED_MINE_SIG: { me->score += Q_PAR(me); /* the score will be sent to the Tunnel by the next TIME_TICK */ return Q_HANDLED(); } case HIT_WALL_SIG: case HIT_MINE_SIG: { (9) return Q_TRAN(&Ship_exploding); } } (10) return Q_SUPER(&Ship_active); }
return Q_HANDLED(), which informs QEP-nano that the initial transition has been handled.return from a state handler function designates the superstate of that state, which is exactly the same as in the full-version QP. QEP-nano provides the "top" state as a state handler function QHsm_top(), and therefore the Ship_active() state handler returns the pointer &QHsm_top. (see the Ship state diagram in Figure 5-2)Ship_flying() returns the pointer &Ship_active.When implementing state handler functions you need to keep in mind that the QEP-nano event processor is in charge here rather than your code. QEP-nano will invoke a state handler function 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 signals enlisted in the case statements. You should avoid any code outside the switch statement, especially code that would have side effects.
Prev: 6. Signals, Events, and Active Objects
Next: 8. Using the Built-in Real-Time Kernels
Copyright © 2002-2010 Quantum Leaps, LLC. All Rights Reserved.
http://www.quantum-leaps.com
1.6.3