Skip to main content

Implementing State Machines in C: Switch, Table, and Function Pointer Models

·671 words·4 mins
C State-Machine Embedded Architecture
Table of Contents

đź§  What Is a State Machine?
#

A State Machine models system behavior using three fundamental components:

  1. State – Where the system currently is
  2. Event – What just happened
  3. Action / Transition – What to do next and where to go

At runtime, a state machine continuously answers:

Given the current state and an incoming event, what action should I take and what is my next state?

State machines are everywhere: protocol stacks, device drivers, UI logic, power management, and real-time embedded systems.


🔀 Method 1: Switch–Case State Machine
#

The switch–case method is the most direct and commonly used approach. States and events are represented as enums, and behavior is encoded using nested switch statements.

Basic Structure
#

typedef enum {
    STATE_IDLE,
    STATE_BUSY,
    STATE_ERROR
} state_t;

typedef enum {
    EVT_START,
    EVT_STOP,
    EVT_FAIL
} event_t;

void fsm_handle_event(event_t evt)
{
    switch (g_state) {
    case STATE_IDLE:
        switch (evt) {
        case EVT_START:
            start_work();
            g_state = STATE_BUSY;
            break;
        default:
            break;
        }
        break;

    case STATE_BUSY:
        switch (evt) {
        case EVT_STOP:
            stop_work();
            g_state = STATE_IDLE;
            break;
        case EVT_FAIL:
            handle_error();
            g_state = STATE_ERROR;
            break;
        default:
            break;
        }
        break;

    case STATE_ERROR:
        reset_system();
        g_state = STATE_IDLE;
        break;
    }
}

Characteristics
#

Pros

  • Extremely intuitive
  • Easy to debug with breakpoints
  • No indirection or memory overhead

Cons

  • Poor scalability as states Ă— events grow
  • High maintenance cost
  • Logic and structure tightly coupled

Practical Tip
#

Because switch statements are evaluated sequentially, place high-frequency states and events first to minimize branch latency.


đź§© Method 2: Table-Driven State Machine
#

The table-driven approach decouples logic from structure by representing the state machine as a lookup table. Each (state, event) pair maps to an action + next state.

Core Data Structure
#

typedef struct {
    void (*action)(void *event);
    uint8_t next_state;
} fsm_node_t;

FSM Table Definition
#

fsm_node_t fsm_table[NUM_STATES][NUM_EVENTS] = {
    [STATE_IDLE] = {
        [EVT_START] = { start_work, STATE_BUSY },
    },
    [STATE_BUSY] = {
        [EVT_STOP] = { stop_work, STATE_IDLE },
        [EVT_FAIL] = { handle_error, STATE_ERROR },
    },
};

Runtime Execution
#

void fsm_dispatch(uint8_t state, uint8_t event, void *evt)
{
    fsm_node_t *node = &fsm_table[state][event];

    if (node->action) {
        node->action(evt);
        g_state = node->next_state;
    }
}

Why It’s Fast
#

  • Constant-time lookup O(1)
  • CPU branch predictor friendly
  • Ideal for high-throughput systems

đź§± Extended State Machines: The Compressed Table
#

Classic tables struggle with Extended State Machines (ESM), where transitions depend on runtime conditions.

Solution: Action Returns the Next State
#

typedef uint8_t (*fsm_action_t)(void *evt);

typedef struct {
    fsm_action_t action;
} fsm_node_t;
uint8_t busy_action(void *evt)
{
    if (error_detected(evt))
        return STATE_ERROR;
    return STATE_IDLE;
}

This hybrid approach keeps table speed while restoring decision flexibility.


đź§­ Method 3: Function Pointer State Machine
#

This technique removes numeric state IDs entirely. The state is the function itself.

Conceptual Model
#

  • Global state = function pointer
  • Each state function handles events
  • Returns pointer to the next state function

Example Implementation
#

typedef void (*state_fn_t)(event_t evt);

void state_idle(event_t evt);
void state_busy(event_t evt);

state_fn_t current_state = state_idle;

void state_idle(event_t evt)
{
    if (evt == EVT_START) {
        start_work();
        current_state = state_busy;
    }
}

void state_busy(event_t evt)
{
    if (evt == EVT_STOP) {
        stop_work();
        current_state = state_idle;
    }
}

Why Experts Use This
#

Pros

  • Extremely flexible
  • No tables, no enums
  • Natural mapping to behavior

Cons

  • Difficult debugging
  • Vulnerable to memory corruption
  • Stack traces are less informative

⚠️ A corrupted function pointer usually results in an immediate crash.


📊 Comparison Summary
#

Method Complexity Performance Flexibility Safety
Switch–Case Low Medium Medium High
Table-Driven Medium High Low Medium
Function Pointer High High Very High Low

🏗 Beyond FSM: Hierarchical State Machines (HSM)
#

For large systems, flat FSMs become unmanageable. Hierarchical State Machines (HSM) introduce nesting:

  • A parent state defines shared behavior
  • Child states override specifics

Example:

  • POWER_ON

    • IDLE
    • ACTIVE
    • LOW_BATTERY

HSMs dramatically reduce duplication and are widely used in:

  • RTOS kernels
  • Communication protocols
  • Automotive and avionics software


âś… Final Takeaways
#

  • Use switch–case for small, simple systems
  • Use table-driven FSMs for speed and scalability
  • Use function-pointer FSMs when flexibility outweighs safety
  • For complex products, consider HSM frameworks

A well-designed state machine is not just code — it is architecture.

Related

Understanding C and C++ Standard Libraries
·615 words·3 mins
C Cpp Standard-Library Libc Operating-Systems
Understanding Temporary Objects in C++: Lifetime, Cost, and Optimization
·671 words·4 mins
C++ Performance Temporaries Move-Semantics
A Practical Guide to Using the {fmt} Library in Modern C++
·549 words·3 mins
C++ Fmt Formatting Modern-Cpp