Low Power Design #3: State Machines: The Backbone of Embedded Systems Design

Introduction

In the intricate world of embedded systems, managing complex system behaviors efficiently is a constant challenge. Enter the state machine - a powerful design pattern that transforms chaotic system logic into a structured, predictable framework. This blog explores state machines, their critical role in embedded development, and the various implementation approaches.

Why State Machines Matter in Embedded Development

Embedded systems are inherently event-driven and complex. Imagine controlling a coffee machine, managing a medical device, or designing an automotive control system. These systems must:

  • React to multiple events
  • Maintain precise system state
  • Handle error conditions
  • Optimize resource utilization

State machines solve these challenges by:

  • Providing a clear, structured approach to system design
  • Ensuring predictable behavior
  • Simplifying complex control logic
  • Improving code maintainability

The Core Concept: State Machine Fundamentals

A state machine consists of:

  • States: Distinct conditions of a system
  • Events: Triggers that cause state transitions
  • Transitions: Rules defining how states change based on events

Implementation Approaches: A Comparative Analysis

1. Nested Switch Approach

void stateMachine(SystemState currentState, SystemEvent event) {
    switch(currentState) {
        case STATE_IDLE:
            switch(event) {
                case EVENT_START:
                    // Initialization logic
                    break;
            }
            break;
        
        case STATE_PROCESSING:
            switch(event) {
                case EVENT_COMPLETE:
                    // Cleanup logic
                    break;
            }
            break;
    }
}

Pros:

  • Simple to implement
  • Easy to understand for beginners
  • Minimal additional infrastructure

Cons:

  • Becomes complex with multiple states
  • Difficult to maintain
  • Poor scalability
  • Tight coupling of logic

2. State Handler Approach

typedef struct {
    void (*enter)(void);
    void (*exit)(void);
    void (*process)(void);
} StateHandler;

void changeState(StateHandler* newState) {
    if (currentState) {
        currentState->exit();
    }
    
    currentState = newState;
    currentState->enter();
}

Pros:

  • More organized
  • Clear state-specific logic
  • Better encapsulation
  • Supports entry and exit actions

Cons:

  • More memory overhead
  • Slightly more complex
  • Requires careful state management

3. Table-Driven State Handler

typedef struct {
    State currentState;
    Event event;
    State nextState;
    void (*action)(void);
} StateTransitionEntry;

State processStateMachine(State currentState, Event event) {
    for (int i = 0; i < TRANSITION_TABLE_SIZE; i++) {
        if (transitionTable[i].currentState == currentState &&
            transitionTable[i].event == event) {
            
            if (transitionTable[i].action) {
                transitionTable[i].action();
            }
            
            return transitionTable[i].nextState;
        }
    }
    
    return currentState;
}

Pros:

  • Highly flexible
  • Easy to modify and extend
  • Minimal computational overhead
  • Works well with limited resources
  • Supports complex state machines

Cons:

  • Requires more initial setup
  • Needs careful table design
  • Can be slightly harder to read

Deep Dive: Coffee Machine State Machine Example

Practical Implementation Using Table-Driven Approach

It's always good to first draw a state diagram to cover all the possible events and states

stateDiagram-v2 [*] --> IDLE : Power On IDLE --> WATER_HEATING : Start Brewing WATER_HEATING --> BREWING : Water Heated BREWING --> READY : Coffee Completed READY --> IDLE : Coffee Served IDLE --> ERROR : Sensor Malfunction WATER_HEATING --> ERROR : Temperature Failure BREWING --> ERROR : Brewing Interruption ERROR --> [*] : System Shutdown

Let's break down a coffee machine state machine to demonstrate the table-driven approach:

🟢 IDLE: Machine waiting for user input
🟡 WATER_HEATING: Heating water
🟠 BREWING: Making coffee
READY: Coffee is ready
🔴 ERROR: Something went wrong

State Definition

typedef enum {
    STATE_IDLE,
    STATE_WATER_HEATING,
    STATE_BREWING,
    STATE_READY,
    STATE_ERROR
} CoffeeMachineState;

typedef enum {
    EVENT_POWER_ON,
    EVENT_WATER_FILLED,
    EVENT_WATER_HEATED,
    EVENT_BREW_COMPLETE,
    EVENT_RESET,
    EVENT_ERROR
} CoffeeMachineEvent;

State Transition Table

This is how you need to place different events and states w.r.t each other:

  • Current State: [State]
  • Triggering Event: [Event]
  • Next State: [Next State]
  • Optional Action to Perform: [Action]
const StateTransitionEntry coffeeMachineTransitions[] = {
    // IDLE State Transitions
    {STATE_IDLE, EVENT_POWER_ON, STATE_WATER_HEATING, startWaterHeating},
    
    // WATER HEATING State Transitions
    {STATE_WATER_HEATING, EVENT_WATER_HEATED, STATE_BREWING, startBrewing},
    {STATE_WATER_HEATING, EVENT_ERROR, STATE_ERROR, displayError},
    
    // BREWING State Transitions
    {STATE_BREWING, EVENT_BREW_COMPLETE, STATE_READY, notifyCoffeeReady},
    {STATE_BREWING, EVENT_ERROR, STATE_ERROR, displayError},
    
    // READY and ERROR State Transitions
    {STATE_READY, EVENT_RESET, STATE_IDLE, resetMachine},
    {STATE_ERROR, EVENT_RESET, STATE_IDLE, resetMachine}
};

Complete Code

#include <stdio.h>
#include <stdbool.h>

// State Enumeration
typedef enum {
    STATE_IDLE,
    STATE_WATER_HEATING,
    STATE_BREWING,
    STATE_READY,
    STATE_ERROR
} CoffeeMachineState;

// Event Enumeration
typedef enum {
    EVENT_POWER_ON,
    EVENT_WATER_FILLED,
    EVENT_WATER_HEATED,
    EVENT_BREW_COMPLETE,
    EVENT_RESET,
    EVENT_ERROR
} CoffeeMachineEvent;

// Forward declaration of state transition function
typedef CoffeeMachineState (*StateTransitionFunc)(CoffeeMachineEvent event);

// State Transition Table Entry
typedef struct {
    CoffeeMachineState currentState;
    CoffeeMachineEvent event;
    CoffeeMachineState nextState;
    void (*action)(void);  // Optional action on transition
} StateTransitionEntry;

// Global Variables
CoffeeMachineState currentState = STATE_IDLE;

// Action Functions
void startWaterHeating(void) {
    printf("Starting to heat water...\n");
}

void startBrewing(void) {
    printf("Brewing coffee...\n");
}

void notifyCoffeeReady(void) {
    printf("Coffee is ready! Enjoy!\n");
}

void displayError(void) {
    printf("Error occurred! Please reset.\n");
}

void resetMachine(void) {
    printf("Resetting coffee machine...\n");
}

// State Transition Table
const StateTransitionEntry coffeeMachineTransitions[] = {
    // IDLE State Transitions
    {STATE_IDLE, EVENT_POWER_ON, STATE_WATER_HEATING, startWaterHeating},
    
    // WATER HEATING State Transitions
    {STATE_WATER_HEATING, EVENT_WATER_FILLED, STATE_WATER_HEATING, NULL},
    {STATE_WATER_HEATING, EVENT_WATER_HEATED, STATE_BREWING, startBrewing},
    {STATE_WATER_HEATING, EVENT_ERROR, STATE_ERROR, displayError},
    
    // BREWING State Transitions
    {STATE_BREWING, EVENT_BREW_COMPLETE, STATE_READY, notifyCoffeeReady},
    {STATE_BREWING, EVENT_ERROR, STATE_ERROR, displayError},
    
    // READY State Transitions
    {STATE_READY, EVENT_RESET, STATE_IDLE, resetMachine},
    
    // ERROR State Transitions
    {STATE_ERROR, EVENT_RESET, STATE_IDLE, resetMachine}
};

// Number of transition entries
#define NUM_TRANSITIONS (sizeof(coffeeMachineTransitions) / sizeof(StateTransitionEntry))

// State Machine Processing Function
CoffeeMachineState processStateMachine(CoffeeMachineState currentState, CoffeeMachineEvent event) {
    for (int i = 0; i < NUM_TRANSITIONS; i++) {
        // Find matching current state and event
        if (coffeeMachineTransitions[i].currentState == currentState && 
            coffeeMachineTransitions[i].event == event) {
            
            // Execute optional action if exists
            if (coffeeMachineTransitions[i].action) {
                coffeeMachineTransitions[i].action();
            }
            
            // Return next state
            return coffeeMachineTransitions[i].nextState;
        }
    }
    
    // If no transition found, return current state
    printf("No valid transition for current state %d and event %d\n", currentState, event);
    return currentState;
}

// Simulation Function
void simulateCoffeeMachine(void) {
    printf("Coffee Machine State Machine Simulation\n");
    
    // Simulate a typical coffee-making sequence
    currentState = processStateMachine(currentState, EVENT_POWER_ON);
    currentState = processStateMachine(currentState, EVENT_WATER_FILLED);
    currentState = processStateMachine(currentState, EVENT_WATER_HEATED);
    currentState = processStateMachine(currentState, EVENT_BREW_COMPLETE);
    currentState = processStateMachine(currentState, EVENT_RESET);
}

// Main function for demonstration
int main(void) {
    simulateCoffeeMachine();
    return 0;
}

Key Implementation Insights

  1. Separation of Concerns: Each state has clear responsibilities
  2. Flexibility: Easy to add or modify states
  3. Predictability: Defined transitions prevent unexpected behavior
  4. Resource Efficiency: Minimal memory and computational overhead

Conclusion

State machines are not just a design pattern - they're a fundamental approach to managing complexity in embedded systems. By understanding and implementing state machines effectively, developers can create more robust, maintainable, and efficient embedded solutions.

Rather than simulating the state machine by calling simulateCoffeeMachine(), Next Task for an Embedded Engineer would be to implement the event producer and event displatcher functions. Event producer function will look for any incoming internal/external timer/adc/button signals and make a meaningfull event out of it. Event dispatcher function will take that event and call the processStateMachine() function to move the state machine. so the execution order would be eventProducer() -->> eventDispatcher() -->> processStateMachine().

Recommendations

  • Start with simple state machines
  • Document state transitions
  • Consider table-driven approach for complex systems
  • Always handle unexpected events
  • Use const for transition tables in embedded environments

About the Author

A passionate embedded systems engineer with a love for clean, efficient code and innovative solutions.

Comments