Principia Embedded: Still More Musings on State-Time

Principia Embedded: Still More Musings on State-Time

Random Thoughts

I have a few random thoughts before we get started. If you’re here strictly for the technical detail, feel free to jump ahead to “States and the Art of Doing.” Otherwise, I’d like to linger for a moment on the craft itself.

One, I think it’s fair to say I’m not a writer. At least not by trade. I’m just another guy positing on all things embedded, hoping some of it proves useful to someone else.

There was a time in my youth when you entered the craft, got quickly humbled by elder statesmen, and were taught the experiential side of design. The nuance. They were both interesting times, and they were exciting times. They were times with heads so full of knowledge, there was nothing we didn’t know. Until we thought we knew what we didn’t know, ultimately concluding that we don’t even know what we don’t even know.

Before I learned the art, a punch was just a punch, and a kick, just a kick. After I learned the art, a punch was no longer a punch, a kick, no longer a kick. Now that I understand the art, a punch is just a punch and a kick is just a kick.

And therein lies the art.

I’m hopeful that in some small part, these writings might pass on the type of wisdom that was so freely given to me. To play my part in this cycle.

Two, I realize these musings have become a bit philosophical. Maybe it’s the way my brain is wired. I know that it inspires me. Coding to me is a philosophical art. Just try entertaining the classical dining philosophers problem without delving at least a little into the nature of reality.

Again, maybe that’s just me, but at least I got some of that out of my system. Alright, off the soap box and on to the doing.

States and the Art of Doing

Embedded systems do things.

Some software transforms information — compilers translate text into machine code, processors take data files and create reports. Embedded systems act on the world.

I still get no greater joy than that first touch of working code hitting metal and seeing the world change.

“States and the art of doing” — what exactly is a state, and how do we express it? Are we talking about the states of matter: solid, liquid, gas? Are we talking about the state of a person, a machine, or a system? Are we talking about logic states?

There’s the more classical computing definition: the stored information, data, or configuration defining the condition of a system, program, or machine at a specific, instantaneous moment in time.

I kind of like that one, and it may be the closest representation of how I think about state-time. To explore states, let’s consider the classic example of a coffee maker.

Procedurally, we check for water, wait for the pod cover and start button, heat the water, set a timer based on cup size, start the pump, wait until the timer expires, then turn off the pump and heater.

This sequence of steps presents a map in state-time. State is a point on that n-dimensional map at a particular instant in time.

A Tale of Three Architectures

We start with what is inarguably the simplest solution: a naive decision tree.

void make_coffee(void)
{
    while (1)
    {
        if (water_level() >= MIN_WATER_LEVEL)
        {
            if (pod_closed() && start_button_pressed())
            {
                start_heater();
                while(get_water_temp() < MIN_COFFEE_TEMP)
                {
                    /* do nothing */
                }
                start_timer(cup_size_time);
                start_water_pump();
                while(!timer_elapsed())
                {
                    /* do nothing */
                }
                stop_water_pump();
                stop_heater();
                display("enjoy your coffee!\r\n");
            }
        }
        else
        {
            display("add water\r\n");
        }
    }
}

Not bad, right? Looks simple enough. And as long as nothing changes — it is. State and time are inseparable. We can abstract it, we can hide it, but we can’t avoid it.

Then someone opens the pod mid-brew. Simple fix. Until it isn’t.

start_water_pump();
while(!timer_elapsed() && pod_closed())
{
    /* do nothing */
}
stop_water_pump();

It looks like we fixed it. For now. Then we realize we could open the pod while the water was still heating. So we apply a similar fix there too.

We’ve already hit the death trap of the simple loop. If the pod opens while the water is heating, we fall through the heating loop. But falling through is not the same as handling the fault.

The pump starts. The timer starts. The pod-closed check immediately fails. We stop the pump and heater. And then, with all the confidence in the world, we display: “Enjoy your coffee!”

Complexity has not grown because we made bad decisions. It has grown because reality showed up.

So we do what every reasonable embedded developer does next. We add a flag. Then another. Then another.

One to remember that heating started. One to remember that the brew cycle is active. One to remember that we faulted. One to remember that the pump is running.

And before long, the simple decision tree has become something else entirely. Not quite a formal state machine. Not quite the original recipe. Something in between. A swamp of implied state.

Flags are often state machines before we admit they are state machines.

Making State Explicit

So we bite the bullet and we make the state explicit. This is the value of the finite state machine. It doesn’t eliminate complexity, it gives it a map.

typedef enum
{
    COFFEE_IDLE,
    COFFEE_WAIT_FOR_WATER,
    COFFEE_WAIT_FOR_POD,
    COFFEE_HEATING,
    COFFEE_DISPENSING,
    COFFEE_DONE,
    COFFEE_FAULT
} coffee_state_t;

Look familiar? It should. These are the same steps from the original recipe — plus everything reality added. The flags did not disappear. They became states. Explicit. Named. Organized.

static coffee_state_t brew_state = COFFEE_IDLE;

void make_coffee(void)
{
    while (1)
    {
        switch (brew_state)
        {
            case COFFEE_IDLE:
                if (water_level() < MIN_WATER_LEVEL)
                {
                    display("please add water!\r\n");
                    brew_state = COFFEE_WAIT_FOR_WATER;
                }
                else
                {
                    brew_state = COFFEE_WAIT_FOR_POD;
                }
                break;

            case COFFEE_WAIT_FOR_WATER:
                if (water_level() >= MIN_WATER_LEVEL)
                {
                    brew_state = COFFEE_WAIT_FOR_POD;
                }
                break;

            case COFFEE_WAIT_FOR_POD:
                if (pod_closed() && start_button_pressed())
                {
                    brew_state = COFFEE_HEATING;
                    start_heater();
                }
                break;

            case COFFEE_HEATING:
                if (pod_open())
                {
                    brew_state = COFFEE_FAULT;
                }
                else if (get_water_temp() >= MIN_COFFEE_TEMP)
                {
                    brew_state = COFFEE_DISPENSING;
                    start_timer(cup_size_time);
                    start_water_pump();
                }
                break;

            case COFFEE_DISPENSING:
                if (pod_open())
                {
                    brew_state = COFFEE_FAULT;
                }
                else if (timer_elapsed())
                {
                    brew_state = COFFEE_DONE;
                    stop_heater();
                    stop_water_pump();
                    display("enjoy your coffee!\r\n");
                }
                break;

            case COFFEE_DONE:
                if (pod_open())
                {
                    brew_state = COFFEE_IDLE;
                }
                break;

            case COFFEE_FAULT:
                stop_heater();
                stop_water_pump();
                display("wanna try again?\r\n");
                if (pod_open())
                {
                    brew_state = COFFEE_IDLE;
                }
                break;

            default:
                break;
        }
    }
}

Marketing’s happy that it works. Maybe we are too — except we notice that COFFEE_FAULT hammers the display. We can fix that by updating the display before entering the fault state, or we can shift the complexity again by creating an enter_fault() function.

We notice the same when stopping the heater and the pump between the DONE and FAULT states. So maybe we make another function for that too.

Complexity cannot be destroyed. We just watched it move twice.

Making Time Explicit

All’s going well until it isn’t. Again. Marketing wants a light to blink while brewing, and we’re trapped inside make_coffee().

The problem isn’t the blinking light. The problem is time, and make_coffee() isn’t sharing it. It owns the processor, never returning, never allowing anything else to run.

If we want a blinky light, we have two choices: weave the light into the fabric of the state machine, or make time explicit.

Making time explicit means eliminating the inner while(1) and calling the state machine periodically.

void make_coffee(void)
{
    switch (brew_state)
    {
        /* same state machine, but no while(1) */
    }
}

int main(void)
{
    while (1)
    {
        make_coffee();
        blink_brew_led();
        scan_buttons();
        update_display();
    }
}

We didn’t eliminate time. We externalized it. And now it’s no longer hidden.

make_coffee() no longer makes coffee from beginning to end. It advances the coffee maker one step at a time, whenever the larger system gives it time.

The LED does the same. The buttons do the same. The display does the same. And now we’ve arrived, almost accidentally, at scheduling.

Where Protothreads Begin

Standing at this precipice, another question appears: what if we could keep the clean, linear feel of the original recipe without falling back into the blocking trap?

What if the code could still read like: wait for water → wait for pod → heat → wait for temperature → pump → wait → stop → return home, while still sharing time with the rest of the system?

That question is where protothreads begin to make sense. And that’s where I’ll go next.

Originally published on LinkedIn as “Principia Embedded: Still More Musings on State-Time”.

Need help structuring embedded firmware around state, time, and real-world behavior?
Mesa Technologies provides senior embedded systems consulting for firmware architecture, board bring-up, debugging, project recovery, and risk reduction.

Schedule a technical call