This is post #5 in a series covering the hardware and software design for my work-in-progress GPS-guided rocket recovery project. The main index to the series of posts is here, and an introduction to the project (a PowerPoint presentation) is here.
In this post I’ll cover stateMachine.c, which implements the finite state machine (FSM) that handles most of the functional logic of the system.
When implementing a state machine, I don’t feel an obligation to observe the constraints of a formal FSM of the kind taught in schools, where all functionality happens in state transitions.
Instead, I treat each state as a system mode. There are state entry actions and state exit actions, but while in each state the system also has a distinct behavior. While I could draw a state diagram, I’ve never bothered – the system is simple enough that it’s unnecessary, and the diagram wouldn’t capture much of the important behavior of the system.
The state machine is run one step by a call to StateMachine() from main(), which happens once each time through the main loop.
Each of the six states, IDLE, ARMED, TIMEDLAUNCH, FLIGHT, FULL, and TEST, (all defined in stateMachine.h) has an associated “state function” that implements the system behavior for that state. Each state also has an associated entry function, which can be called by state functions to cause the system to transition to a new state.
The state functions take and return no parameters and have the same names as the state symbols, but in lowercase:
- void idle(void);
- void armed(void);
- void timedlaunch(void);
- void flight(void);
- void full(void);
- void test(void);
The entry functions also take and return no parameters and are named based on the state they enter:
- void enterIDLE(void);
- void enterARMED(void);
The current state and sub-state are stored in global variables State and Substate.
When conditions warrant, a state function will cause a transition to a different state by calling the appropriate entry function, which will change the global variables State and Substate to reflect the new state, as well as performing any entry actions needed to configure the system for the new state.
A flag, stateTransitionInProgress, is reset to FALSE on entry into StateMachine(). Each entry function starts by verifying that this flag is FALSE, and if so, setting it TRUE. If the flag is not FALSE on entry, this indicates that the FSM has already transitioned to a new state, and therefore the call to the entry routine is invalid. In that case, the entry routine is aborted and the state remains unchanged.
This eliminates the possibility of a subtle bug which could occur if, once a state function calls an entry function (to transition to a new state), later logic in the same state function tries to put the system into a different new state. (Naturally, I managed to do this in an earlier version of the code – that’s why this is here.) Once a new state has been entered (by calling the entry function), the system is no longer in the old state, although it may still be executing the state function of the old state (performing exit actions, etc.). The old state function is no longer allowed to control or alter the state of the system (only the new state function can do that). This lets me freely continue processing other inputs in the state function without having to worry about this case. (Perhaps an equally effective solution would be to simply return from the state function after calling each entry function.)
As mentioned before, the state machine is initialized by main() by calling InitStateMachine(). This is a macro in stateMachine.h that calls state entry routine enterIDLE(), to put the state machine into the initial state, IDLE.
Once on each iteration through main loop (at a rate of 25 Hz), StateMachine() is called.
As mentioned, stateTransitionInProgress is set TRUE, then a case statement executes one of the six state functions, corresponding to the current state.
On return, the FSM may or may not be in a new state. If so, the next call to StateMachine() will execute the new state function.
I think the state machine code is straightforward and self-explanatory, so I’ll just touch on the less obvious points.
The IDLE state is entered with enterIDLE(), which, as do all entry functions, starts with:
settingUpNewState() returns TRUE without doing anything if stateTransitionInProgress is TRUE. Otherwise, it sets stateTransitionInProgress to TRUE itself, and then sets variables controlling various parts of the system to default (most commonly used) values.
These include turning all three LEDs off, turning off the piezo output, loud horn, and other output controls.
This is done here only to reduce code size and simplify entry function code by avoiding redundant code and setting these commonly-altered variables to a known state.
If settingUpNewState() returned FALSE (as it should), entryIDLE() turns on the red LED to indicate the system is in the IDLE state (only the red LED is on, remember that settingUpNewState() turned off the others), then sets State to IDLE and Substate to NONE (IDLE has no sub-states).
It then sets:
IDLEexpireRTC24 = RTC24 + AUTO_ARM_PERIOD;
This uses the same technique described for nextRTC8 in main() (see previous post) to set IDLEexpireRTC24 to the value the RTC will have 60 seconds in the future (AUTO_ARM_PERIOD, in tweaks.h).
As will be seen, when this time comes, the FSM will automatically transition from the IDLE state into the ARMED state (even if no button has been pushed). This reduces the pre-flight setup checklist by one step – there is no need to press the yellow button to put the system in the ARMED state, ready for flight. After 60 seconds in IDLE, it will enter ARMED automatically as soon as the SAFE/ARM switch is set to ARM.
The IDLE state function is idle().
If the SAFE/ARM switch (SW_ARMED, in hardware.h) is set to ARM, the system emits a brief “beep” sound via the piezo transducer each time the GPS receives a good fix (PositionFix == TRUE).
If the IDLEexpireRTC24 time period is up, it transitions into the ARMED state by calling enterARMED().
If the SAFE/ARM switch is set to SAFE, the system stays silent (Beep = OFF). As long as the switch is set to SAFE, the system will not automatically transition to ARMED. This is accomplished by adding another 100 milliseconds to IDLEexpireRTC24 each time it expires (remember, the state machine runs every 40 mS).
Next, if the red button is depressed the ejection system continuity check is performed.
Finally, if the yellow button is pressed the system transitions into ARMED, and if the green button is pressed, into TIMEDLAUNCH.
The ARMED state entry function is enterARMED().
As mentioned before, the ARMED state has two sub-states, SAFE and DANGER. On entry into ARMED the system is in ARMED SAFE.
A separate entry function enterSubstateARMED_DANGER() is used to transition from ARMED SAFE to ARMED DANGER.
In armed(), the rocket is meant to be waiting on the pad for launch. If the the ejection system continuity check fails, the loud horn sounds a warning (if connected) by cycling on/off quickly (FAST_HORN as implemented in ServiceHorn(), called in the idle loop).
As in the IDLE state, the piezo buzzer emits a short beep each time the GPS receives a good fix.
In the ARMED SAFE substate, the only other action is to check if the SAFE/ARM switch has been moved to ARM. In that case, the system transitions to ARMED DANGER by calling enterSubstateARMED_DANGER().
That entry function turns on the piezo buzzer continuously (Buzzer = ON), which audibly indicates the ARMED DANGER state. It also initializes timers nextLogRTC16, which will be used to trigger periodic logging of system status, and nextCameraRTC24, which will be used to trigger the camera.
Returning to the ARMED DANGER part of armed(), in this sub-state the camera takes a picture every 20 seconds (SLOW_CAM_PERIOD, in tweaks.h). The image capture rate is kept low to avoid filling the camera’s memory too quickly before flight.
If the switch is set to SAFE, the system transitions to ARMED SAFE.
Most importantly, if launch is detected (LDet), the system transitions to the FLIGHT state.
Finally, the system logs its status and the latest GPS fix every 3 seconds (SLOW_LOG_PERIOD) while in ARMED DANGER.
The FLIGHT state is entered when launch is detected. It has three sub-states, ASCENT, APOGEE, and DESCENT. FLIGHT ASCENT is the sub-state upon entry into FLIGHT state.
enterFLIGHT() calls FlushLaggedRecords() to write the most recent 1 second of status records to Flash memory (as described earlier). The value of SlowTCpressure at launch is recorded in global PadPressure – this will be compared later (in the FULL state) with the pressure at apogee to determine the maximum altitude above the pad reached during the flight.
The camera is made to take an image when launch is detected, then nextCameraRTC24 is set to record additional images every 3.5 seconds (FAST_CAM_PERIOD); this is as fast as the digital camera I’ve been using can take pictures in its landscape mode (used to force focus to infinity).
In FLIGHT ASCENT, each time a new GPS fix is received (AnyFix) it is stored in the flight log (DoLogFix = AnyFix), and each time through the state machine (25 Hz) the system state is logged (DoLogStatus = TRUE).
The loud horn is turned on during ascent (HORN = SW_ARMED). I’ve thought of trying to measure the vertical speed of the rocket by recording the doppler-shifted horn sound from the ground and measuring the shift. I haven’t tried it yet – it may be difficult because of the noisy rocket motor.
If the main loop detects apogee has been reached (ADet), then the system enters FLIGHT APOGEE.
The FLIGHT APOGEE entry routine enterSubstateFLIGHT_APOGEE() turns on the EJ_TRIGGER parachute ejection current. This fires the AG1 flashbulb that ignites the parachute ejection charge. This is done in the entry routine (instead of the state function for FLIGHT APOGEE) to reduce the time from detection of apogee to triggering the parachute ejection by almost 40 mS.
The smoothed FastTCpressure value at apogee is stored in global ApogeePressure. This will be compared later to PadPressure.
FLIGHTendEjectionRTC16 is set 1.5 seconds in the future to turn off the ejection system current.
Once the 1.5 second ejection current pulse (EJECT_DURATION) is complete, the FLIGHT APOGEE state function transitions to FLIGHT DESCENT.
In FLIGHT DESCENT, all GPS fixes are logged, but system status is logged not at the full rate of 25 Hz, but at 5 Hz (MED_LOG_PERIOD, 200 mS). This conserves Flash memory, as the rocket’s descent under the parachute takes much longer than the rocket-powered ascent. The loud horn “honks” for 1/2 second in every 2.5 seconds (SLOW_HORN); this is meant to help me find the rocket after landing, by following the sound.
In all FLIGHT sub-states, if the Flash memory becomes full, the system transitions to the FULL state to avoid over-writing flight log data. So far, flights have been short enough that this only happens after landing.
In the FULL state, the system signals the maximum altitude reached during the flight (the difference between the pad altitude and apogee), via a simple “beep code” generated by BeepOutMaxAltitude() in peripherals.c.
That routine calculates the difference in pressure altitude between the launch pad (stored in PadPressure) and at apogee (stored in ApogeePressure), and signals it by toggling the global Beep flag to emit the digits of the apogee altitude, in feet AGL. For example, a maximum altitude of 3456 feet would be signaled by 3 beeps, pause, 4 beeps, pause, 5 beeps, pause, 6 beeps, and a longer pause (10 beeps signals a zero). This repeats as long as the system is in the FULL state.
Writing to the Flash memory log is suppressed by setting:
LastUARTrxRTC = RTC;
This is a crude trick that fools FlushFlashBufferSafe() in hardware.c into thinking that it can’t (ever) write to flash because a data burst is coming in from the GPS receiver.
Otherwise, the system in FULL state behaves exactly as it did in the previous FLIGHT sub-state (note that FULL can be reached from any of the three FLIGHT sub-states).