Table of Contents
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
enumfor 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

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







