Skip to content
Home » Software Design » State Machine Pattern in C — With Practical Examples

State Machine Pattern in C — With Practical Examples

State Machine Pattern featured image with purple background, Design Patterns badge, SM icon, in C With Examples subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 12 of 31View Full Path →

KEY TAKEAWAYS

📘 State machines are where clean C style pays off most in embedded projects. My complete Master C & Embedded C course takes you from zero to hardware-ready code — free on YouTube (53 videos), or guided on Udemy with quizzes, certificate and my Q&A support.

  • State machines replace scattered if/else flags with organized, explicit state management
  • Table-driven FSMs are great for uniform transitions; switch-based FSMs are great for varied logic
  • Adding new states does not require modifying existing state logic
  • State machines are one of the most practical patterns in embedded C

What Is a State Machine?

A state machine (also called a finite state machine or FSM) is a model where a system can be in exactly one of a finite number of states at any time. Transitions between states happen in response to events or inputs. State machines are one of the most common design patterns in embedded C — used in protocol handlers, UI flows, motor controllers, and command parsers.The key idea: instead of scattering if/else conditions everywhere, you organize behavior around states and transitions. This makes complex logic predictable and easy to modify.

Example 1: Traffic Light Controller

Bad — Tangled If/Else Logic

int light = 0; // 0=red, 1=green, 2=yellow
int timer = 0;

void traffic_update(void) {
    timer++;
    if (light == 0 && timer >= 30) {
        light = 1;
        timer = 0;
        set_green();
    } else if (light == 1 && timer >= 25) {
        light = 2;
        timer = 0;
        set_yellow();
    } else if (light == 2 && timer >= 5) {
        light = 0;
        timer = 0;
        set_red();
    }
    // Adding a flashing mode? Emergency override?
    // This grows into a mess quickly.
}
Adding new states (flashing, emergency) means more nested conditions. The logic becomes fragile and hard to follow.

Good — Table-Driven State Machine

typedef enum { ST_RED, ST_GREEN, ST_YELLOW, ST_COUNT } State;

typedef struct {
    State next_state;
    int   duration;
    void  (*on_enter)(void);
} StateRow;

static const StateRow state_table[ST_COUNT] = {
    [ST_RED]    = { ST_GREEN,  30, set_red    },
    [ST_GREEN]  = { ST_YELLOW, 25, set_green  },
    [ST_YELLOW] = { ST_RED,     5, set_yellow },
};

static State current = ST_RED;
static int   timer   = 0;

void traffic_init(void) {
    current = ST_RED;
    timer = 0;
    state_table[current].on_enter();
}

void traffic_update(void) {
    timer++;
    if (timer >= state_table[current].duration) {
        current = state_table[current].next_state;
        timer = 0;
        state_table[current].on_enter();
    }
}
Adding a new state means adding one row to the table. The update logic never changes. Each state’s behavior and transitions are visible at a glance in the table.

Example 2: UART Command Parser

Bad — Flags and Booleans

int in_header = 1;
int in_body = 0;
int in_checksum = 0;
int body_len = 0;
int body_idx = 0;

void parse_byte(uint8_t b) {
    if (in_header) {
        if (b == 0xAA) {
            in_header = 0;
            in_body = 1;
            body_idx = 0;
        }
    } else if (in_body) {
        body[body_idx++] = b;
        if (body_idx >= body_len) {
            in_body = 0;
            in_checksum = 1;
        }
    } else if (in_checksum) {
        verify_checksum(b);
        in_header = 1;
        in_checksum = 0;
    }
}
Three boolean flags track the state — but only one can be true at a time. This is a state machine disguised as spaghetti code.

Good — Explicit State Machine

typedef enum { WAIT_HEADER, WAIT_LENGTH, READ_BODY, READ_CHECKSUM } ParseState;

static ParseState state = WAIT_HEADER;
static uint8_t body[256];
static int body_len = 0;
static int body_idx = 0;

void parse_byte(uint8_t b) {
    switch (state) {
    case WAIT_HEADER:
        if (b == 0xAA) state = WAIT_LENGTH;
        break;

    case WAIT_LENGTH:
        body_len = b;
        body_idx = 0;
        state = READ_BODY;
        break;

    case READ_BODY:
        body[body_idx++] = b;
        if (body_idx >= body_len) state = READ_CHECKSUM;
        break;

    case READ_CHECKSUM:
        if (verify_checksum(body, body_len, b)) {
            process_packet(body, body_len);
        }
        state = WAIT_HEADER;
        break;
    }
}
The state is explicit. Each case handles exactly one state. Adding a new state (like WAIT_ESCAPE for escaped characters) means adding one enum value and one case block — no risk of breaking existing states.

When to Use State Machines

  • Protocol parsing — byte-by-byte or packet-by-packet processing
  • UI flows — menu navigation, wizard-style screens
  • Motor/actuator control — idle, accelerating, running, braking, fault
  • Connection management — disconnected, connecting, connected, error
  • Any system with distinct modes and clear transitions between them

State Machine Design Tips

  • Use an enum for states — never raw integers or booleans
  • Consider a table-driven approach when states share the same transition logic
  • Use a switch-based approach when each state has unique logic
  • Add entry/exit actions to handle setup and cleanup when entering or leaving a state
  • Always handle invalid or unexpected events in each state — do not silently ignore them

Key Takeaways

  • State machines replace scattered if/else flags with organized, explicit state management
  • Table-driven FSMs are great for uniform transitions; switch-based FSMs are great for varied logic
  • Adding new states does not require modifying existing state logic
  • State machines are one of the most practical patterns in embedded C

Leave a Reply

Your email address will not be published. Required fields are marked *