
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.
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.
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.
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.
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.
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.
Quick Links
Legal Stuff





