Design and Implementation of State Machines in Embedded Systems: From Complex Logic to Clear Architecture

Introduction

Have you ever written code like this: layers of nested if-else statements, a multitude of condition checks, making maintenance a nightmare?

// Troublesome embedded control logic
<section class="mp_profile_iframe_wrp custom_select_card_wrp" nodeleaf="">
  <mp-common-profile class="mpprofile js_uneditable custom_select_card mp_profile_iframe" data-pluginname="mpprofile" data-id="gh_acd22af287b7" data-nickname="An Embedded Programmer" data-headimg="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/mp-logo.png" data-service_type="1" data-verify_status="1"></mp-common-profile>
  <br class="ProseMirror-trailingBreak">
</section>

void device_control() {
    if (is_power_on) {
        if (is_initialized) {
            if (has_error) {
                if (error_type == CRITICAL) {
                    // Handle critical error
                } else if (error_type == WARNING) {
                    // Handle warning
                }
            } else {
                if (user_input == BUTTON_PRESSED) {
                    if (current_mode == NORMAL) {
                        // Switch to setup mode
                    } else if (current_mode == SETTING) {
                        // Save settings
                    }
                }
            }
        } else {
            // Initialization logic
        }
    } else {
        // Startup logic
    }
}

This code is not only difficult to understand and maintain, but it is also prone to logical errors. Today, we will learn a more elegant solution: state machines. They can make your embedded code clear, maintainable, and extensible.

What is a State Machine?

A state machine is a mathematical model used to describe the behavior of an object at different times. In embedded systems, state machines are particularly suitable for handling control logic with clear state transitions.


Startup Event
Pause Event
Resume Event
Stop Event
Stop Event
Idle
Working
Paused

Why are State Machines Suitable for Embedded Systems?

  • Clear Logic: Each state has a clear responsibility, and state transitions are regular.
  • Easy to Debug: It is clear to know which state the system is currently in.
  • Easy to Extend: Adding new states does not affect existing logic.
  • Resource Friendly: Simple implementation with low memory and CPU overhead.

Basic Components of a State Machine

A complete state machine consists of four core elements:

State

The working mode or condition of the system at a certain point in time. For example: Idle, Working, Error, Sleep, etc.

Event

External inputs that trigger state changes, which can be:

  • • User actions (button presses, touch)
  • • External signals (sensor data, communication messages)
  • • Timer timeouts
  • • Internal conditions being met

Transition

The rules for jumping between states, defining how to transition from one state to another under specific events.

Action

Operations executed during state transitions, such as:

  • • Update display
  • • Send commands
  • • Start timers
  • • Log data

Yes
No
Condition Met
Condition Not Met


Current State
Received Event?
Check Transition Conditions
Execute Transition Action
Switch to New State
Execute Entry Action

Four Classic Application Scenarios

Scenario 1: Device State Management

Application Description: The startup and operation management of embedded devices is a classic application of state machines. From power-on to normal operation, devices need to go through a series of orderly state transitions.


Initialization Complete
Self-Test Passed
Self-Test Failed
Start Command
Stop Command
Run Exception
Reset Command
Initialization
Self-Test
Ready
Error
Running

Code Implementation:

typedef enum {
    STATE_INIT,
    STATE_SELFTEST,
    STATE_READY,
    STATE_RUNNING,
    STATE_ERROR
} DeviceState_t;

typedef enum {
    EVENT_POWER_ON,
    EVENT_INIT_DONE,
    EVENT_TEST_PASS,
    EVENT_TEST_FAIL,
    EVENT_START_CMD,
    EVENT_STOP_CMD,
    EVENT_ERROR_OCCUR,
    EVENT_RESET_CMD
} DeviceEvent_t;

DeviceState_t current_state = STATE_INIT;

void device_state_machine(DeviceEvent_t event) {
    switch (current_state) {
        case STATE_INIT:
            if (event == EVENT_INIT_DONE) {
                printf("Device initialization complete, starting self-test\n");
                start_selftest();
                current_state = STATE_SELFTEST;
            }
            break;

        case STATE_SELFTEST:
            if (event == EVENT_TEST_PASS) {
                printf("Self-test passed, device ready\n");
                current_state = STATE_READY;
            } else if (event == EVENT_TEST_FAIL) {
                printf("Self-test failed, entering error state\n");
                current_state = STATE_ERROR;
            }
            break;

        case STATE_READY:
            if (event == EVENT_START_CMD) {
                printf("Starting work\n");
                start_working();
                current_state = STATE_RUNNING;
            }
            break;

        case STATE_RUNNING:
            if (event == EVENT_STOP_CMD) {
                printf("Stopping work, returning to ready state\n");
                stop_working();
                current_state = STATE_READY;
            } else if (event == EVENT_ERROR_OCCUR) {
                printf("Run exception, entering error state\n");
                current_state = STATE_ERROR;
            }
            break;

        case STATE_ERROR:
            if (event == EVENT_RESET_CMD) {
                printf("Resetting device, reinitializing\n");
                reset_device();
                current_state = STATE_INIT;
            }
            break;
    }
}

Scenario 2: Protocol Parsing

Application Description: In communication protocols such as UART, SPI, and I2C, the received data needs to be parsed according to a specific protocol format, and state machines can elegantly handle this process.


Received Start Byte
Length Valid
Length Invalid
Data Reception Complete
Checksum Correct
Checksum Error
Processing Complete
Waiting for Start
Receiving Length
Receiving Data
Checking Data
Processing Complete

Code Implementation:

typedef enum {
    PARSE_WAIT_HEADER,
    PARSE_GET_LENGTH,
    PARSE_GET_DATA,
    PARSE_CHECKSUM,
    PARSE_COMPLETE
} ParseState_t;

typedef struct {
    ParseState_t state;
    uint8_t buffer[256];
    uint8_t length;
    uint8_t index;
    uint8_t checksum;
} ProtocolParser_t;

ProtocolParser_t parser = {PARSE_WAIT_HEADER, {0}, 0, 0, 0};

void protocol_parse(uint8_t data) {
    switch (parser.state) {
        case PARSE_WAIT_HEADER:
            if (data == 0xAA) {  // Protocol start byte
                parser.index = 0;
                parser.checksum = 0;
                parser.state = PARSE_GET_LENGTH;
            }
            break;

        case PARSE_GET_LENGTH:
            if (data > 0 && data <= 200) {  // Length valid
                parser.length = data;
                parser.state = PARSE_GET_DATA;
            } else {
                parser.state = PARSE_WAIT_HEADER;  // Length invalid, restart
            }
            break;

        case PARSE_GET_DATA:
            parser.buffer[parser.index++] = data;
            parser.checksum += data;

            if (parser.index >= parser.length) {
                parser.state = PARSE_CHECKSUM;
            }
            break;

        case PARSE_CHECKSUM:
            if (data == parser.checksum) {
                printf("Data reception complete, length: %d\n", parser.length);
                process_received_data(parser.buffer, parser.length);
                parser.state = PARSE_COMPLETE;
            } else {
                printf("Checksum error, discarding data\n");
                parser.state = PARSE_WAIT_HEADER;
            }
            break;

        case PARSE_COMPLETE:
            parser.state = PARSE_WAIT_HEADER;  // Ready to receive the next frame
            break;
    }
}

Scenario 3: User Interface Control

Application Description: The menu system, button responses, and other user interaction logic of embedded devices can be implemented very clearly using state machines.


Setting Key
Information Key
Confirm Key
Back Key
Save/Return
Any Key
Main Menu
Settings Menu
Information Display
Time Setting

Code Implementation:

typedef enum {
    UI_MAIN_MENU,
    UI_SETTING_MENU,
    UI_TIME_SETTING,
    UI_INFO_DISPLAY
} UIState_t;

typedef enum {
    KEY_UP,
    KEY_DOWN,
    KEY_ENTER,
    KEY_BACK,
    KEY_SETTING
} KeyEvent_t;

UIState_t ui_state = UI_MAIN_MENU;

void ui_state_machine(KeyEvent_t key) {
    switch (ui_state) {
        case UI_MAIN_MENU:
            if (key == KEY_SETTING) {
                display_setting_menu();
                ui_state = UI_SETTING_MENU;
            } else if (key == KEY_ENTER) {
                display_device_info();
                ui_state = UI_INFO_DISPLAY;
            }
            break;

        case UI_SETTING_MENU:
            if (key == KEY_ENTER) {
                display_time_setting();
                ui_state = UI_TIME_SETTING;
            } else if (key == KEY_BACK) {
                display_main_menu();
                ui_state = UI_MAIN_MENU;
            }
            break;

        case UI_TIME_SETTING:
            if (key == KEY_ENTER) {
                save_time_setting();
                display_setting_menu();
                ui_state = UI_SETTING_MENU;
            } else if (key == KEY_BACK) {
                display_setting_menu();
                ui_state = UI_SETTING_MENU;
            }
            break;

        case UI_INFO_DISPLAY:
            // Any key returns to the main menu
            display_main_menu();
            ui_state = UI_MAIN_MENU;
            break;
    }
}

Scenario 4: Control Systems

Application Description: Control systems such as LED blinking, motor control, and temperature regulation can implement complex control logic using state machines.


Start
Mode Switch
Mode Switch
Mode Switch
Mode Switch
Off
Off
Off
Off
Off
Slow Blink
Fast Blink
Always On
Breathing

Code Implementation:

typedef enum {
    LED_OFF,
    LED_SLOW_BLINK,
    LED_FAST_BLINK,
    LED_ON,
    LED_BREATH
} LEDState_t;

LEDState_t led_state = LED_OFF;
uint32_t led_timer = 0;
uint8_t led_brightness = 0;

void led_control_task() {
    static uint32_t last_time = 0;
    uint32_t current_time = get_system_tick();

    switch (led_state) {
        case LED_OFF:
            set_led(0);  // LED off
            break;

        case LED_SLOW_BLINK:
            if (current_time - last_time > 1000) {  // Switch every second
                toggle_led();
                last_time = current_time;
            }
            break;

        case LED_FAST_BLINK:
            if (current_time - last_time > 200) {   // Switch every 200ms
                toggle_led();
                last_time = current_time;
            }
            break;

        case LED_ON:
            set_led(255);  // LED always on
            break;

        case LED_BREATH:
            // Breathing light effect
            if (current_time - last_time > 10) {
                led_brightness += (led_brightness < 255) ? 1 : -255;
                set_led(led_brightness);
                last_time = current_time;
            }
            break;
    }
}

void led_mode_switch() {
    switch (led_state) {
        case LED_OFF:        led_state = LED_SLOW_BLINK; break;
        case LED_SLOW_BLINK: led_state = LED_FAST_BLINK; break;
        case LED_FAST_BLINK: led_state = LED_ON;         break;
        case LED_ON:         led_state = LED_BREATH;     break;
        case LED_BREATH:     led_state = LED_SLOW_BLINK; break;
    }
}

C Language Implementation Techniques

Basic Template Structure

// Standard state machine template
typedef enum {
    STATE_IDLE,
    STATE_WORKING,
    STATE_ERROR
} state_t;

typedef enum {
    EVENT_START,
    EVENT_STOP,
    EVENT_ERROR
} event_t;

state_t current_state = STATE_IDLE;

void state_machine(event_t event) {
    switch (current_state) {
        case STATE_IDLE:
            // Handle events in idle state
            break;
        case STATE_WORKING:
            // Handle events in working state
            break;
        case STATE_ERROR:
            // Handle events in error state
            break;
        default:
            // Handle unknown state
            break;
    }
}

State Transition Table Method

For more complex state machines, a data-driven approach can be used:

typedef struct {
    state_t current_state;
    event_t event;
    state_t next_state;
    void (*action)(void);
} state_transition_t;

const state_transition_t transitions[] = {
    {STATE_IDLE,    EVENT_START, STATE_WORKING, start_work},
    {STATE_WORKING, EVENT_STOP,  STATE_IDLE,    stop_work},
    {STATE_WORKING, EVENT_ERROR, STATE_ERROR,   handle_error},
    // More transition rules...
};

void execute_state_machine(event_t event) {
    for (int i = 0; i < sizeof(transitions)/sizeof(transitions[0]); i++) {
        if (transitions[i].current_state == current_state &&
            transitions[i].event == event) {

            // Execute transition action
            if (transitions[i].action != NULL) {
                transitions[i].action();
            }

            // State transition
            current_state = transitions[i].next_state;
            break;
        }
    }
}

Design Points and Considerations

Design Principles

  1. 1. State Minimization: Avoid state explosion by merging similar states.
  2. 2. Single Responsibility: Each state should have a clear responsibility.
  3. 3. Clear Transitions: Transition conditions should be clear to avoid ambiguity.

Common Pitfalls

  1. 1. State Omission: Ensure all possible states are considered.
  2. 2. Event Omission: Handle possible events in each state.
  3. 3. Deadlock States: Avoid entering states that cannot be exited.

Debugging Techniques

void log_state_transition(state_t from, state_t to, event_t event) {
    printf("State: %s -> %s (Event: %s)\n",
           state_name[from], state_name[to], event_name[event]);
}

Conclusion

State machines are a powerful tool in embedded development, capable of:

  • Simplifying Complex Logic: Transforming complex condition checks into clear state transitions.
  • Improving Maintainability: Clear logical structure that is easy to understand and modify.
  • Enhancing Reliability: State transitions have clear rules, reducing logical errors.
  • Facilitating Debugging: Clearly track changes in system states.

Learning Suggestions:

  1. 1. Start practicing with simple two or three states.
  2. 2. Draw state transition diagrams before writing code.
  3. 3. Try using them in actual projects.

Advanced Directions:

  • • Hierarchical State Machines
  • • UML State Diagram Modeling
  • • State Machine Code Generation Tools

Master state machines to make your embedded code more elegant and professional!

Follow me for more practical tips on embedded development!

Leave a Comment