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 Cortex-M3 version of the "Fly 'n' Shoot" game. I break the explanation of this file into three steps.
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 "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 */ }
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.
Prev: 6. Signals, Events, and Active Objects
Next: 8. Using the Built-in Real-Time Kernels
1.5.4