This QP/C 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. Defining Event Signals and Event Parameters
Next: 8. Using the Built-in Real-Time Kernels and Third-Party RTOSes
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 the help of the QF real-time framework and the QEP hierarchical processor, which is also part of the QP event-driven platform. 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.
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 "qp_port.h"
(2) #include "bsp.h"
(3) #include "game.h"
(4) typedef struct ShipTag {
(5) QActive super;
(6) uint8_t x;
(7) uint8_t y;
(8) uint8_t exp_ctr;
(9) uint16_t score;
(10) } Ship;
(11) static QState Ship_active (Ship *me, QEvent const *e);
(12) static QState Ship_parked (Ship *me, QEvent const *e);
(13) static QState Ship_flying (Ship *me, QEvent const *e);
(14) static QState Ship_exploding(Ship *me, QEvent const *e);
(15) static QState Ship_initial (Ship *me, QEvent const *e);
(16) static Ship l_ship;
(17) QActive * const AO_ship = (QActive *)&l_ship;
- (1) Every application-level C-file that uses the QP platform must include the
qp_port.h header file.
- (2) The
bsp.h header file contains the interface to the Board Support Package.
- (3) The
game.h header file contains the declarations of events and other facilities shared among the components of the application
- (see Listing 6-1).
- (4) This structure defines the Ship active object.
- Note:
- I like to keep active objects, and indeed all state machine objects (such as Mines), strictly encapsulated. Therefore, I don't put the state machine structure definitions in header files but rather define them right in the implementation file, such as ship.c. That way, I can be sure that the internal data members of the Ship structure are not known to any other parts of the application.
- (6-7) The x and y data members represent the position of the Ship on the display.
- (8) The exp_ctr member is used for pacing the explosion animation (see also the "exploding" state in the Ship state diagram in Figure 5-2).
- (9) The score member stores the accumulated score in the game.
- (10) I use the typedef to define the shorter name Ship equivalent to struct ShipTag.
- (11-14) These four functions are called state-handler functions because they correspond one-to-one to the states of the Ship state machine shown in Figure 5-2. For example, the
Ship_active() function represents the "active" state. The QEP event processor calls the state handler functions to realize the UML semantics of state machine execution. <qpc>\include\qep.h. All state handler functions have the same signature. A state handler function takes the state machine pointer and the event pointer as arguments, and returns the status of the operation back to the QEP event processor, for example whether the event was handled or not. The return type QState is typedef-ed to uint8_t in the header file <qpc>\include\qep.h.
- Note:
- I use a simple naming convention to strengthen the association between the structures and the functions designed to operate on these structures. First, I name the functions by combining the typedef'ed structure name with the name of the operation (e.g.,
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, ...)).
- (16) In addition to state handler functions, every state machine must declare the initial pseudostate, which QEP invokes to execute the top-most initial transition (see Figure 5-2(1)). The initial pseudostate handler has signature identical to the regular state handler function.
- (17) In this line I statically allocate the storage for the Ship active object. Please note that the object l_ship is defined static, so that it is accessible only locally at the file scope of the ship.c file.
- (18) In this line I define and initialize the global pointer AO_Ship to the Ship active object (see also Listing 6-1(10)). This pointer is "opaque", because it treats the Ship object as the generic QActive base structure, rather than the specific Ship structure. The power of an "opaque" pointer is that it allows me to completely hide the definition of the Ship structure and make it inaccessible to the rest of the application. Still, the other application components can access the Ship object to post events directly to it via the QActive_postFIFO(QActive *me, QEvent const *e) function.
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"; and
- The top-most initial transition.
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) The global function Ship_ctor() is prototyped in game.h and called at the beginning of main().
- (2) The "me" pointer points to the statically allocated Ship object (see Listing 7-1(16)).
- (3) Every derived structure is responsible for initializing the part inherited from the base structure. The "constructor" QActive_ctor() puts the state machine in the initial pseudostate &Ship_initial. (see Listing 6-1(15)).
- (4-5) The Ship position is initialized.
- (6) The Ship_initial() function defines the top-most initial transition in the Ship state machine (see Figure 5-2(1)).
- (7-8) The Ship active object subscribes to signals
TIME_TICK_SIG and PLAYER_TRIGGER_SIG, as specified in the state diagram in Figure 5-2(1).
- (9) The initial state "active" is specified by invoking the QP macro Q_TRAN().
- Note:
- The macro Q_TRAN() must always follow the return statement.
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.
(1) QState Ship_active(Ship *me, QEvent const *e) {
(2) switch (e->sig) {
(3) case Q_INIT_SIG: {
(4)
(5) return Q_TRAN(&Ship_parked);
}
(6) case PLAYER_SHIP_MOVE_SIG: {
(7) me->x = ((ObjectPosEvt const *)e)->x;
(8) me->y = ((ObjectPosEvt const *)e)->y;
(9) return Q_HANDLED();
}
}
(10) return Q_SUPER(&QHsm_top);
}
QState Ship_flying(Ship *me, QEvent const *e) {
switch (e->sig) {
(11) case Q_ENTRY_SIG: {
(12) ScoreEvt *sev;
me->score = 0;
(13) sev = Q_NEW(ScoreEvt, SCORE_SIG);
(14) sev->score = me->score;
(15) QActive_postFIFO(AO_Tunnel, (QEvent *)sev);
(16) return Q_HANDLED();
}
case TIME_TICK_SIG: {
ObjectImageEvt *oie = Q_NEW(ObjectImageEvt, SHIP_IMG_SIG);
oie->x = me->x;
oie->y = me->y;
oie->bmp = SHIP_BMP;
QActive_postFIFO(AO_Tunnel, (QEvent *)oie);
++me->score;
if ((me->score % 10) == 0) {
ScoreEvt *sev = Q_NEW(ScoreEvt, SCORE_SIG);
sev->score = me->score;
QActive_postFIFO(AO_Tunnel, (QEvent *)sev);
}
return Q_HANDLED();
}
case PLAYER_TRIGGER_SIG: {
ObjectPosEvt *ope = Q_NEW(ObjectPosEvt, MISSILE_FIRE_SIG);
ope->x = me->x;
ope->y = me->y + SHIP_HEIGHT - 1;
QActive_postFIFO(AO_Missile, (QEvent *)ope);
return Q_HANDLED();
}
case DESTROYED_MINE_SIG: {
me->score += ((ScoreEvt const *)e)->score;
return Q_HANDLED();
}
(17) case HIT_WALL_SIG:
(18) case HIT_MINE_SIG: {
(19)
(20) return Q_TRAN(&Ship_exploding);
}
}
(21) return Q_SUPER(&Ship_active);
}
- (1) Each state handler must have the same signature, that is, it must take two parameters: the state machine pointer "me" and the pointer to QEvent. The keyword const before the '*' in the event pointer declaration means that the event pointed to by that pointer cannot be changed inside the state handler function (i.e., the event is read-only). A state handler function must return QState, which conveys the status of the event handling to the QEP event processor.
- (2) Typically, every state handler is structured as a switch statement that discriminates based on the signal of the event signal e->sig.
- (3) This line of code pertains to the nested initial transition Figure 5-2(2). QEP provides a reserved signal Q_INIT_SIG that the framework passes to the state handler function when it wants to execute the initial transition.
- (4) You can enlist any actions associated with this initial transition (none in this particular case).
- (5) You designate the target substate with the Q_TRAN() macro. This macro must always follow the return statement, through which the state handler function informs the QEP event processor that the transition has been taken.
- Note:
- The initial transition must necessarily target a direct or transitive substate of a given state. An initial transition cannot target a peer state or go up in state hierarchy to higher-level states, which in the UML would represent a "malformed" state machine.
- (6) This line of code pertains to the internal transition
PLAYER_SHIP_MOVE_SIG(x, y) in Figure 5-2(3).
- (7-8) You access the data members of the Ship state machine via the "me" argument of the state handler function. You access the event parameters via the "e" argument. You need to cast the event pointer from the generic QEvent base class to the specific event structure expected for the PLAYER_SHIP_MOVE_SIG, which is ObjectPosEvt in this case.
- Note:
- The association between the event signal and event structure (event parameters) is established at the time the event is generated. All recipients of that event must know about this association to perform the cast to the correct event structure.
- (9) You terminate the case statement with "<TT>return Q_HANDLED()</TT>", which informs the QEP event processor that the event has been handled (but no transition has been taken).
- (10) The final return from a state handler function designates the superstate of that state by means of the QEP macro Q_SUPER(). The final return statement from a state handler function represents the single point of maintenance for changing the nesting level of a given state. The state "active" in Figure 5-2 has no explicit superstate, which means that it is implicitly nested in the "top" state. The "top" state is a UML concept that denotes the ultimate root of the state hierarchy in a hierarchical state machine. QEP provides the "top" state as a state handler function QHsm_top(), and therefore the Ship_active() state handler returns the pointer &QHsm_top.
- Note:
- In C and C++, a pointer-to-function QHsm_top() can be written either as QHsm_top, or &QHsm_top. Even though the notation QHsm_top is more succinct, I prefer adding the ampersand explicitly, to leave absolutely no doubt that I mean a pointer-to-function &QHsm_top.
- (11) This line of code pertains to the entry action into state "flying" (Figure 5-2(5)). QEP provides a reserved signal Q_ENTRY_SIG that the framework passes to the state handler function when it wants to execute the entry actions.
- (12) The entry action to "flying" posts the SCORE event to the Tunnel active object (Figure 5-2(5)). This line defines a temporary pointer to the event structure ScoreEvt.
- (13) The QF macro
Q_NEW(ScoreEvt, SCORE_SIG) dynamically allocates an instance of the ScoreEvt from an event pool managed by QF. The macro also performs the association between the signal SCORE_SIG and the allocated event. The Q_NEW() macro returns the pointer to the allocated event.
- (14) The score parameter of the ScoreEvt is set from the state machine member me->score.
- (15) The SCORE(me->score) event is posted directly to the Tunnel active object by means of the QP function QActive_postFIFO(). The arguments of this function are the recipient active object (AO_Tunnel in this case) and the pointer to the event (the temporary pointer sev in this case).
- (16) You terminate the case statement with "<TT>return Q_HANDLED()</TT>", which informs QEP that the entry actions have been handled.
- (17-18) These two lines of code pertain to the state transitions from "flying" to "exploding" (Figure 5-2(9, 10)).
- (19) You can enlist any actions associated with the transition (none in this particular case).
- (20) You designate the target of the transition with the Q_TRAN() macro.
- (21) The final return from a state handler function designates the superstate of that state. The state "flying" in Figure 5-2 nests in the state "active", so the state handler Ship_flying() returns the pointer &Ship_active wrapped with the macro Q_SUPER().
When implementing state handler functions you need to keep in mind that the QEP event processor is in charge here rather than your code. QEP 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. Defining Event Signals and Event Parameters
Next: 8. Using the Built-in Real-Time Kernels and Third-Party RTOSes
Copyright © 2002-2008 Quantum Leaps, LLC. All Rights Reserved.
http://www.quantum-leaps.com
Generated on Thu Jul 3 09:45:52 2008 for QP/C by
1.5.4