Raindrops on roses, and whiskers on kittens. They’re ok, but state machines are absolutely on our short list of favorite things.
There are probably as many ways to implement a state machine as there are programmers. These range from the terribly complex, one-size-fits-all frameworks down to simply writing a single switch...case block. The frameworks end up being a little bit of a black box, especially if you’re just starting out, while the switch...case versions are very easy to grok, but they don’t really help you write clear, structured code.
In this extra-long edition of Embed with Elliot, we’ll try to bridge the middle ground, demonstrating a couple of state machines with an emphasis on practical coding. We’ll work through a couple of examples of the different ways that they can be implemented in code. Along the way, we’ll Goldilocks solution for a particular application I had, controlling a popcorn popper that had been hacked into a coffee roaster. Hope you enjoy.
What’s a State Machine?
State machines are basically a set of constraints that you set on the way you’d like to approach a problem. Specifically, you try to break your problem down to a bunch of states. In each state, the machine has a given output, which can either depend on a list of variable inputs to the state, or not. Each state also has a list of events which can trigger a transition to another state.
Mapping out your application in terms of what can happen in which state, and then when the machine transitions from one state to the next helps you clearly define your problem. When your code has a structure that makes it easy to plug these concepts in, everything is peachy.
For instance, when you fire up your coffee roaster, it may enter an initialization state, take some actions, and then go straight into a waiting-for-input state. In the waiting-for-input state, there may be two event-driven transitions possible: when one button is pressed it might go into a preheating state where it turns on the fan and heater, or when another button is pressed it might go into a configuration state where it lets you set the desired final roast temperature. The point of defining the states is that they limit when certain actions can happen — you don’t need to worry about setting the roast temperature in the preheating state, for instance.
Picking the right set of limitations to apply to your project’s code gives it structure, and once you get used to the state machine structure, you might find it useful for you. Of course, there are other, grander reasons to think about your problem in terms of a state machine.
One is that they are particularly easy to represent either graphically or in a table, which makes documentation (and later bug-hunting) a lot easier. The other is that, for applications like automatic code generation and automatic FPGA synthesis, there are sophisticated tools that transform your description of the state machine into a finished product without you getting your hands dirty with any of the actual coding.
Hackaday’s own Al Williams recently wrote an article covering state machines in these terms, and his article is a great complement to this one. If you’d like more state machine background, go give his excellent article a read. Here, we’re just interested in making your embedded coding life easier by presenting a few possible design patterns that have stood the test of time.
The Coffee Roaster Application
A concrete example is worth a thousand words, so let’s start off with a perfect state machine application: controlling a small home appliance. In this case, an air popcorn popper that was hacked into service as a small-batch coffee roaster.
The former popcorn popper had a fan that blew the popcorn around and a heater that heat up the air before it entered the popping tube. Both fan and heater were on the same circuit, with the voltage drop across the heater coil cleverly chosen so that the remaining voltage could run the fan motor more or less directly. It also had a thermal fuse to keep it from burning up, and an on-off switch. That’s all, and that’s all that’s necessary for the job.
To turn the popper into a coffee roaster, the fan and heater circuits were cut apart and made separately controllable. The fan got its own 16 V DC power supply, switched on an off with a power MOSFET. The heater runs through a solid-state relay to the mains power. Since the roaster was supposed to have temperature control, a thermocouple and amplifier (with SPI interface) were added. This is all tied together by a microcontroller that’s got four user pushbuttons plus reset, two LEDs, a TTL serial in/out, and a potentiometer connected to an ADC channel just for good measure.
Just writing this down gets us a lot of the way toward defining our state machine. For instance, we know what events are going to be driving most of our state transitions: four buttons and a temperature sensor. In practice, the machine also needs to know about elapsed times, so there’s a seconds counter running inside the microcontroller as well.
To come up with the relevant states, it doesn’t take much more than thinking about the desired roast profile. It turns out that roasting coffee is a lot like doing reflow soldering: you warm everything up, then ramp up the temperature at a controlled rate to a desired maximum, and then cool it back down again. From the roast profile, we get the states “preheat”, “soak”, “ramp”, and “cooldown” corresponding to the obvious phases of the process. Before the roast starts, the machine also goes through a “initialize” and “wait-for-input” states.
A further “emergency-shutdown” state was inspired by the real-life event where a wire came loose in the temperature probe and the roaster created charcoal instead of coffee. A nice thing about state-machine code is that you know exactly which sections of the code you have to modify when you’re adding a new state, and more importantly, which sections of code you don’t have to touch.
In each of the states the code controls the heater, the fan, and the LEDs, and potentially logs output over the serial port. These are our outputs. In the “preheat” state, for instance, the fan and heater are maxed out and the temperature rise is monitored to make sure that the thermocouple is working. If the panic button is pressed, the soak temperature is reached, or the thermocouple isn’t working, the machine moves on to another state.
With this background in mind, let’s look at two different code structures that can implement the same state machine.
Switch Statements
The simplest implementation of a state machine can be built with a chain of if...then...else statements. “If in state A, do something, else if in state B, do something else”, etc. The nesting and indentation get crazy after only a couple of states, and that’s what the switch...case statement is for. You’ll see a lot of state machines coded up like so:
int main(void)
{
enum states_e {INIT, WAIT_FOR_INPUT, PREHEAT, SOAK, ...};
enum states_e state = INIT;
uint16_t temperature = 21;
while (1){
switch(state){
case INIT:
doInitStuff();
state = WAIT_FOR_INPUT;
break;
case WAIT_FOR_INPUT:
if (buttonA_is_pressed()){
state = PREHEAT;
}
if (buttonD_is_pressed(){
state = SET_TEMPERATURE;
}
break;
case PREHEAT:
fanOn();
heaterOn();
if (temperature > SOAK_START){
state = SOAK;
}
if (test_thermo_not_working()){
state = EMERGENCY_SHUTDOWN;
}
break;
etc.
}
temperature = measure_temperature();
}
}
The code goes in an endless loop, testing which state it’s in and then running the corresponding code. The transitions between states are taken care of inside the handler functions, and are as simple as setting the state variable to the next state name. Instead of using numbers to enumerate the states, the code makes use of C’s enum statement which associates easy-to-read names with numbers behind the scenes. (Alternatively, this could be done with a bunch of #define statements, but the enum is sleeker and arguably less error-prone.)
The INIT state simply does its work and passes on to the next state. WAIT_FOR_INPUT doesn’t change the state variable unless a button is pressed, so the microcontroller spins around here waiting. The PREHEAT state actually starts the roaster going, and tests for the conditions that will bring it out of the preheating phase, namely the temperature getting hot enough or the temperature probe not being plugged in.
The EMERGENCY_SHUTDOWN state was added late in the firmware’s life, and it’s worth looking at where you need to touch the code to add or remove a state. Here it required a new enum entry, a new case statement in the switch to handle its behavior, and then transition code added into every other state that could possibly lead to a shutdown.
Finally, the temperature updating routine is called outside of the state machine. We’re not at all sure how this jibes with the state machine orthodoxy, but it’s a very handy place to have periodically polled actions take place, the results of which could possibly be relevant for the next pass through the state machine.
For small state machines, this style of writing a state machine is straightforward, decently extensible, and it’s pretty readable. But you can see how this could get out of hand pretty quickly as the number of states or the amount of code per state increases. It’s like a very long laundry list.
One possible solution to the sheer length of the switch...case statement is to break each state’s work off into a separate function, like so:
switch(global_state){
case INIT:
handle_init_state();
break;
case WAIT_FOR_INPUT:
handle_wait_for_input_state();
break;
etc.
}
It’s shorter, for sure, and until you have a lot of states it’s not unmanageable. Notice that the state variable must now be made global (or otherwise accessible to each of the state handler functions) because the transitions occur inside each state handler function. If all the code does is implement the state machine, the state probably makes sense as a global variable anyway, so that’s no big deal. A little more disconcerting is that the temperature variable, and every other argument to a state handler, will need to be declared global as it stands now, but let’s just roll with it for a moment.
Wrapping up the switch...case version, you need to do three things when you add a new state:
- add a new state name to the
enum - write your new handler code in its
casestatement - write transitions for all of the states that can lead to the new state
Function Pointers Version
Let’s see what we can do to streamline and simplify the switch...case version of the state machine. Each state name matches with one and exactly one handler function, and the change in handler function is all that differs across states. In other words, the handler function really _is_ the state. The global_state variable that gives a name to each of the states, that we thought of as being the central mechanism, is actually superfluous. Whoah!
We could get rid of the switch...case statement and the global_state variable if only we could store the function that we’d like to call in a variable. Instead of creating this big lookup table from state name to handler function, we could just run the stored state handler directly. And that’s exactly what function pointers do in C.
Function Pointers Aside
Here’s a quick rundown on pointers in general, and function pointers in particular. (We’ll have to do a pointers edition of “Embed with Elliot” in the future, because they’re a source of exquisite bugs and much confusion among people who are new to C.) When you define a function, the compiler stashes that function’s code somewhere in memory, and then associates the name you’ve given to the function with that memory location. So when your code calls the function do_something(), the compiler looks up a memory address where the code by that name is located and then starts running it. So far, so good?
Pointers are variables that essentially contain memory locations, and function pointers are variables that contain the address of code in memory. Pointers do a little more than just store a memory location, though. In the case of function pointers, they also need to know the type of arguments that the function requires and the type of the return value because the compiler needs to be able to set up for the function call, and then clean up after it returns. So a function pointer contains the memory address of some code to run, and also must be prototyped the same way as the functions it can point to.
If we have a couple functions defined, and we want to use a function pointer as a variable that can run either of the two functions, we can do it like so:
int add_one(int);
int add_two(int);
int (*function_pointer)(int);
int add_one(int x){
return x+1;
}
int add_two(int x){
return x+2;
}
int a = 0;
function_pointer = add_one;
a = function_pointer(7); // a = 8;
function_pointer = add_two;
a = function_pointer(7); // a = 9;
What’s going on here? First we declare the functions, and then declare a function pointer that is able to point to these kind of functions. Notice that the function pointer’s declaration mimics the prototypes of the functions that it is allowed to point to.
After defining two trivial functions and a variable to store the result in, we can finally point the function pointer at a function. (That was fun to type!) Behind the scenes, the statement function_pointer = add_one takes the location in memory at which the function add_one is stored, and saves it in the function pointer. At the same time, it checks that the arguments and return value of the add_one function match those of the function pointer, to keep us honest.
Now comes the cool part: we can call the function pointer variable just as if we were calling the function that it points to. Why is this cool? Because we’ve written the exact same code, a = function_pointer(7), and gotten different answers depending on which function is being pointed to. OK, cool, but maybe a little bit confusing because now to understand the code, you have to know which function is being pointed to. But that’s exactly the secret to streamlining and simplifying our state machine!
Function Pointer State Machine
Now, instead of storing the state’s name in a variable and then calling different functions based on the name, we can just store the functions themselves in the state variable. There’s no need for the case...switch statement anymore, the code simply runs whatever code is pointed to. Transitions to another state are achieved by simply changing the handler function that’s currently stored in the state pointer.
It’s time for an example:
void handle_state_INIT(void);
void handle_state_WAIT_FOR_INPUT(void);
void (*do_state)(void);
uint16_t temperature = 21;
void handle_state_INIT(void){
doInitStuff();
do_state = handle_state_WAIT_FOR_INPUT;
}
void handle_state_WAIT_FOR_INPUT(void){
if (buttonA_is_pressed()){
do_state = handle_state_PREHEAT;
}
if (buttonD_is_pressed(){
do_state = handle_state_SET_TEMPERATURE;
}
}
(etc.)
int main(void)
{
// Do not forget to initialize me!!!
do_state = handle_state_INIT;
while(1){
do_state();
temperature = measure_temperature();
}
}
As you can see, all of the per-state code is handled in its own functions, just as in the streamlined switch...case version, but we’ve gotten rid of the cumbersome switch...case construct and the global_state variable, replacing it with a global function pointer, do_state.
Now, when you want to add another state, all you have to do is write the new handler code and then add the transitions from each other state handler that can lead to our new state. Not having to fool around with the state enum is one less thing to worry about.
And note that nothing in the main() body needs to change at all; it calls whatever function is pointed to by the global do_state function pointer, and that gets updated in the state handlers with the rest of the state code.
Style-wise, it’s nice to prototype the functions that get pointed to and the function pointer that will point to them all in the same place because that makes it easy to spot any inconsistencies, which the compiler will thankfully also warn you about.
You might want to split the state handlers off into a separate module, and that’s particularly easy given the above framework. The function and function pointer prototypes go off into the module header, and the functions into the code file just as you’d expect. All global variables required for the handlers will also have to be declared extern in the module header.
Wrapup
There are still some loose ends to tie up here, like dealing properly with the proliferation of global variables that have been used to pass arguments to the state handler functions. A great solution for that case is to pass structs to the handlers that contain all of the relevant arguments, but that’s a tale for another time.
In the mean time, you’re hopefully sold on the possibility of casting some of your microcontroller projects in terms of state machines. Learning state machines is a great excuse to build a simple robot or domestic appliance, for instance. Give it a try. To really grok state machines, it helps a lot to break down some complex behaviors into states, events, transitions, and outputs. Just thinking your project through this way is half the battle.
But then once you’ve sketched the machine out on paper, take the next step and code something up. Start out with some simple scaffolding, either the switch...case variety or the function pointer version, and expand out your own code on it. You’ll be surprised how quickly the code can flow once you’ve got the right structure, and state machines can provide the right structure for a reasonably broad variety of tasks. They really are one of our favorite things!
Filed under: Hackaday Columns, Microcontrollers
No comments:
Post a Comment