MCU State Flow
- State ( Model )
- User/Machine Output ( View )
- User/Machine Interruption ( Events )
- State Changers ( Controller )
- goto 1.
Breakdown of how it works:
#1. State. You start with a known finite state, which is the overall mode of operation for that piece or pieces. For a switch()
-driven machine, it's a number, typically named by an integer constant for better readability.
#2. Output. Based on the state, you make certain things happen. These adjustments can be done in the switch()
, or in the loop()
as they notice state changes. You can also write explicit sub-routines to call from the switch() to keep code tidy. You can read the state from any point in the program, even interrupts if needed.
#3. Events/Interrupts. Lying in wait, these parts spring into action when something happens that the program needs to know. Both human-origin (ex: button press) and machine-origin (ex: SD card error) fall into this group. They can set flags and publish notifications to the controller that something has happened, but they MUST NOT set the state directly. The last point cannot be under-emphasized. If you start "cheating" and updating state outside of the controller, you create another point of failure and another stop along the debug/enhancement tour.
#4. State Changing switch()
(Controller). This is where the actions meet the state. Only here should you change state to a new one, based on outside flags set by events. If your switch()
is at the top of loop()
, and you return
in each case
, you can call loop()
from other points in the code to fire a state change immediately, but i like to name it in a void()
and call it from loop()
. The switch code should be lightweight and never use delay()
. Lastly, most of the time a case
does nothing, because no outside condition has changed that merits reaction, and it break
s or return
s right there. When something important does change, the code below that early-exit conditional performs as a state change event handler.
#Why bother
It can be a bit much at first, and seem like it's not worth the 5-10 mins up-front to name constants and write out all the switch
bolierplate. I'm a lazy coder, and I still use this because it saves me a ton of time at every point in time besides those first 10 minutes.
Since things can only change in one place, you know exactly where to debug. It's simple to dump the outside values a case
examines to the serial (remove before "launch"), rather than finding that chunk of code in the ether. Doing it all in one place makes it trivial to log when and what with each state change. With minor additional code you can even undo state changes, which can be a boon for users. Such actions are not risky because every part of the outside code knows what to do.
It breaks up tedious chains of semi-related action into independent pieces. You won't need lots of nested if()
s if you follow the above guidelines. Even if you're not a long-time developer, simply following this pattern and slopping/pasting all the other code together, your program will be better in the end and faster and easier to build along the way.