HomeAbout UsContact Us

Finite State Machines in Embedded C — A Practical Guide

By embeddedSoft
Published in Embedded C/C++
May 25, 2026
3 min read
Finite State Machines in Embedded C — A Practical Guide

Table Of Contents

01
Introduction
02
Pattern 1: Switch-Based State Machine
03
Pattern 2: Table-Driven State Machine
04
Pattern 3: Function Pointer (OO-Style) State Machine
05
Practical Design Tips
06
Summary
07
References

Introduction

Every embedded system has modes — a motor controller transitions through idle, starting, running, and braking states; a protocol parser walks through header, payload, and checksum phases. Implementing these as explicit state machines separates robust firmware from fragile spaghetti code.

Yet many developers still reach for boolean flags and nested conditionals when a clean finite state machine (FSM) would be simpler and more maintainable. This article walks through three practical FSM patterns in embedded C, with code examples you can adapt directly.

Pattern 1: Switch-Based State Machine

The most straightforward approach uses an enum for states and a switch statement to handle each state’s logic. This is ideal when each state has unique, varied behavior.

Consider a UART packet parser that expects frames with a header byte (0xAA), a length byte, a payload, and a checksum:

typedef enum {
PARSE_WAIT_HEADER,
PARSE_WAIT_LENGTH,
PARSE_READ_BODY,
PARSE_READ_CHECKSUM
} ParseState;
static ParseState state = PARSE_WAIT_HEADER;
static uint8_t body_len;
static uint8_t body_idx;
static uint8_t body[64];
void parse_byte(uint8_t b) {
switch (state) {
case PARSE_WAIT_HEADER:
if (b == 0xAA) {
state = PARSE_WAIT_LENGTH;
}
break;
case PARSE_WAIT_LENGTH:
if (b > 0 && b <= sizeof(body)) {
body_len = b;
body_idx = 0;
state = PARSE_READ_BODY;
} else {
state = PARSE_WAIT_HEADER; /* Invalid length, reset */
}
break;
case PARSE_READ_BODY:
body[body_idx++] = b;
if (body_idx >= body_len) {
state = PARSE_READ_CHECKSUM;
}
break;
case PARSE_READ_CHECKSUM:
if (verify_checksum(body, body_len, b)) {
process_packet(body, body_len);
}
state = PARSE_WAIT_HEADER;
break;
}
}

The state is always explicit. Each case block handles exactly one state, and invalid inputs cause a safe reset. Adding a new state — for example, an escape-byte handling mode — requires only a new enum value and a new case block. The parsing function never needs restructuring.

Pattern 2: Table-Driven State Machine

When states share uniform transition logic, a table-driven approach eliminates repetitive boilerplate. Each row in the table defines a state’s output, duration, entry action, and next state.

This pattern is especially natural for control loops such as a traffic light:

#include <stdint.h>
typedef enum { ST_RED, ST_GREEN, ST_YELLOW, ST_COUNT } State;
typedef struct {
uint3_t led_output;
uint16_t duration_ms;
void (*on_enter)(void);
State next;
} StateRow;
static const StateRow state_table[ST_COUNT] = {
[ST_RED] = { 0x21, 3000, set_red, ST_GREEN },
[ST_GREEN] = { 0x0C, 2500, set_green, ST_YELLOW },
[ST_YELLOW] = { 0x14, 500, set_yellow, ST_RED },
};
static State current_state = ST_RED;
static uint16_t tick_count = 0;
void traffic_init(void) {
current_state = ST_RED;
tick_count = 0;
state_table[current_state].on_enter();
}
void traffic_tick(void) {
tick_count++;
if (tick_count >= state_table[current_state].duration_ms / 10) {
current_state = state_table[current_state].next;
tick_count = 0;
state_table[current_state].on_enter();
}
}

Adding an emergency-flashing mode requires appending one entry to state_table — the traffic_tick function never changes. The entire behavior is visible at a glance in the table, which lives in read-only memory on the target.

This pointer-based variant, used by Jonathan Valvano at UT Austin for Cortex-M traffic light examples, replaces function pointers with direct state-pointer links for even tighter control on resource-constrained targets.

Pattern 3: Function Pointer (OO-Style) State Machine

For systems that need multiple instances of the same state machine type — or that benefit from encapsulation — a function-pointer approach provides object-oriented semantics in pure C:

typedef struct Fsm Fsm;
typedef void (*StateFunc)(Fsm *self, uint8_t event);
struct Fsm {
StateFunc state;
uint8_t context_data;
};
/* State handlers */
static void state_idle(Fsm *self, uint8_t event) {
if (event == EVENT_BUTTON_PRESS) {
start_conversion();
self->state = state_converting;
}
}
static void state_converting(Fsm *self, uint8_t event) {
if (event == EVENT_ADC_COMPLETE) {
self->context_data = read_adc_result();
self->state = state_display;
} else if (event == EVENT_TIMEOUT) {
self->state = state_error;
}
}
static void state_display(Fsm *sm, uint8_t event) {
(void)event;
display_value(sm->context_data);
sm->state = state_idle;
}
static void state_error(Fsm *self, uint8_t event) {
(void)event;
indicate_error();
self->state = state_idle;
}
/* Public API */
void fsm_init(Fsm *self) {
self->state = state_idle;
}
void fsm_dispatch(Fsm *self, uint8_t event) {
self->state(self, event);
}

Each Fsm instance carries its own state and context data. You can run multiple independent state machines — for example, one per sensor channel — from the same code base with zero shared global state.

Practical Design Tips

Always use an enum for state variables. Raw integers and boolean flags obscure intent and invite invalid state values. An enum gives you self-documenting code and, with GCC or IAR, compiler warnings for unhandled cases in switch statements.

Guard every transition. Check that inputs and conditions are valid before transitioning. In the UART parser example, the PARSE_WAIT_LENGTH state rejects zero-length and oversized payloads. This prevents buffer overflows and stuck states.

Handle unexpected events. Every state should have a default transition for events it does not recognize. In safety-critical systems, this often means transitioning to a safe error state rather than silently ignoring the event.

Keep ISRs lean, defer to the main loop. In RTOS or super-loop architectures, set a flag or post an event from the ISR, then let the state machine process it in the main context. This avoids reentrancy issues and keeps interrupt latency low, as emphasized in ARM’s Cortex-M design guidelines.

Separate what from how. As Samek recommends, partition your code strictly into state machine elements — states, transitions, guards, entry/exit actions. When someone reads the code, they should be able to reconstruct the state diagram without additional documentation.

Summary

State machines are one of the most powerful and underused patterns in embedded C. Whether you choose a switch-based approach for varied logic, a table-driven approach for uniform transitions, or a function-pointer approach for encapsulation and multiple instances, the key principle is the same: make the state explicit, define every transition, and let the structure of your code mirror the structure of your design.

The patterns shown here have been proven across billions of deployed devices — from automotive ECUs running QP/C to tiny PIC microcontrollers parsing sensor data. Start your next module with an enum and a switch statement, and you will find that complex sequential logic becomes dramatically simpler to write, debug, and extend.

References

  • UT Austin: Chapter 5 — Finite State Machines in Embedded Systems — Jonathan Valvano’s comprehensive FSM chapter with Cortex-M code examples
  • Practical UML Statecharts in C/C++, 2nd Edition (PSiCC2) — Miro Samek’s book on compact, maintainable hierarchical state machines for embedded systems
  • Keil/ARM: Software-Based Finite State Machine Design — ARM’s application note on FSM implementation for Cortex-M processors
  • Nerdyelectronics: State Machine Pattern in C — Practical examples with traffic light and UART parser implementations
  • Microchip: Implementing State Machines Using 8-Bit PIC Microcontrollers (AN5876) — Microchip’s application note covering hardware and software FSM techniques
  • QP/C Framework Software Design Specification — Quantum Leaps’ formal design spec for hierarchical event-driven frameworks (IEEE 1016)

Tags

state-machinesembedded-cdesign-patternsfsmevent-driven

Share


Previous Article
Certification and Learning Resources for Embedded Engineers
embeddedSoft

embeddedSoft

Insightful articles on embedded systems

Related Posts

Memory-Mapped IO and Peripheral Register Access in Embedded C
Memory-Mapped IO and Peripheral Register Access in Embedded C
May 19, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media