For friends engaged in embedded or C language development, have you often encountered this headache: when handling key presses, serial protocols, or device state transitions, you end up writing a bunch of nested if-else or switch-case statements, with logic tangled like a maze, making subsequent code modifications and bug tracking a nightmare?
In fact, there is a super practical programming approach to solve this “multi-state, multi-event” problem — the state machine. Its core concept is particularly simple: let the program be in only one “state” at any given time, such as the device’s “idle state” or “receiving data state”, and then trigger state transitions through external events (like receiving data, key presses, or timeouts), with each state only handling its own responsibilities.
Implementing a state machine in C not only makes it lightweight and efficient but also clarifies chaotic logic, making the code more readable and maintainable, whether for protocol parsing, key handling, or device control. Next, I will guide you step by step from core concepts to practical steps to master C language state machine programming.
A state machine (Finite State Machine, FSM) is a programming paradigm that manages program behavior through “states”, with the core idea being: the program is in only one “state” at any given time, triggering state transitions by receiving “events” and executing corresponding “actions” for each state. Implementing a state machine in C is lightweight and efficient, widely used in embedded systems, protocol parsing, UI interactions, and more.
1. Core Concepts (Clarifying 3 Key Elements)
| Element | Definition | Example (e.g., Serial Data Parsing) |
|---|---|---|
| State | The behavior mode of the program (stable state), with fixed response logic to different events within the same state | Idle state, waiting for header state, receiving data state, checking CRC state |
| Event | External inputs or internal signals that trigger state changes (e.g., data received, timeout, key pressed) | Received 0xAA (header), received data byte, timeout |
| Action | Logic executed when entering a state, exiting a state, or processing events within a state (optional) | Initializing buffer, storing data, checking CRC |
Core Principles: Unique states, event-driven, predictable transitions
2. Classification of State Machines (Choosing the Right Scenario)
1. Moore Machine
- Characteristics: Output (action) depends only on the current state, independent of triggering events.
- Applicable Scenarios: Behavior determined by the state itself (e.g., device operating modes: standby, running, sleep).
- Example: After entering the “running state”, regardless of which event is triggered, the “start motor” action is executed.
2. Mealy Machine
- Characteristics: Output (action) depends on the current state + triggering event.
- Applicable Scenarios: Different events require different logic to be executed in the same state (e.g., protocol parsing, key handling).
- Example: In the “idle state”, receiving a “header event” transitions to the “receiving data state”, while receiving “other data events” discards the data.
3. Simple Type (Single State Variable)
- Characteristics: Uses a single variable to store the current state, with
<span>switch-case</span>or function pointer jumps. - Applicable Scenarios: Few states (<10), simple transition logic (e.g., short/long key presses, LED blinking control).
3. Steps to Implement State Machines in C (General Process)
Step 1: Define State and Event Enumerations (Clear Classification)
Use<span>enum</span> to manage states and events uniformly, avoiding magic numbers and improving readability.
// 1. Define state enumeration (expand based on business scenario)
typedef enum {
STATE_IDLE, // Idle state (initial state)
STATE_WAIT_HEADER, // Waiting for header state
STATE_RECV_DATA, // Receiving data state
STATE_CHECK_CRC, // Checking CRC state
STATE_ERROR // Error state
} StateType;
// 2. Define event enumeration (signals that trigger state transitions)
typedef enum {
EVENT_NONE, // No event
EVENT_RECV_BYTE, // Received a byte
EVENT_TIMEOUT, // Timeout event
EVENT_CRC_OK, // CRC check passed
EVENT_CRC_ERR // CRC check failed
} EventType;
Step 2: Define State Machine Context (Store Global Information)
Encapsulate the state machine’s “current state”, “temporary data”, “configuration parameters”, etc., in a structure to avoid the proliferation of global variables and support multiple state machine instances.
#define MAX_DATA_LEN 32
typedef struct {
StateType current_state; // Current state
uint8_t data_buf[MAX_DATA_LEN]; // Data buffer
uint16_t data_len; // Length of received data
uint8_t target_header; // Target header (e.g., 0xAA)
uint32_t timeout_cnt; // Timeout counter
} FsmContext;
Step 3: Implement State Handling Functions (Core Logic)
Each state corresponds to a function, with parameters being “context” and “current event”, and the return value being the “next state” (recommended to use function pointers for flexibility and easy expansion).
3.1 Declare State Handling Function Type
// State handling function: input (context, event), output (next state)
typedef StateType (*StateHandler)(FsmContext* ctx, EventType event);
3.2 Implement Specific State Functions
// 1. Idle state handling function
StateType handle_idle(FsmContext* ctx, EventType event) {
switch (event) {
case EVENT_RECV_BYTE:
// Received byte, check if it is the header
if (ctx->data_buf[0] == ctx->target_header) {
ctx->data_len = 1; // Record header length
return STATE_WAIT_HEADER; // Transition to "waiting for header state"
} else {
return STATE_IDLE; // Not a header, remain in idle state
}
case EVENT_TIMEOUT:
// Idle state timeout, no action, remain in state
return STATE_IDLE;
default:
return STATE_IDLE;
}
}
// 2. Waiting for header state handling function (assuming header occupies 2 bytes, here receiving the second byte)
StateType handle_wait_header(FsmContext* ctx, EventType event) {
switch (event) {
case EVENT_RECV_BYTE:
ctx->data_len++;
if (ctx->data_len == 2) { // Header reception complete
return STATE_RECV_DATA; // Transition to "receiving data state"
}
return STATE_WAIT_HEADER; // Not complete, remain in state
case EVENT_TIMEOUT:
// Timeout, reset buffer, return to idle state
ctx->data_len = 0;
return STATE_IDLE;
default:
return STATE_WAIT_HEADER;
}
}
// 3. Receiving data state handling function (assuming data length is parsed from header, simplified to fixed 8 bytes)
StateType handle_recv_data(FsmContext* ctx, EventType event) {
switch (event) {
case EVENT_RECV_BYTE:
ctx->data_buf[ctx->data_len++] = /* received byte */;
if (ctx->data_len == 2 + 8) { // Header + data total 10 bytes
return STATE_CHECK_CRC; // Transition to "checking state"
}
return STATE_RECV_DATA;
case EVENT_TIMEOUT:
ctx->data_len = 0;
return STATE_IDLE;
default:
return STATE_RECV_DATA;
}
}
// 4. Checking CRC state handling function
StateType handle_check_crc(FsmContext* ctx, EventType event) {
switch (event) {
case EVENT_CRC_OK:
// CRC passed, process data (e.g., report, parse)
process_data(ctx->data_buf, ctx->data_len);
ctx->data_len = 0;
return STATE_IDLE;
case EVENT_CRC_ERR:
// CRC failed, enter error state
return STATE_ERROR;
default:
return STATE_CHECK_CRC;
}
}
// 5. Error state handling function
StateType handle_error(FsmContext* ctx, EventType event) {
switch (event) {
case EVENT_NONE:
// Error state resets after delay
if (/* delay ends */) {
ctx->data_len = 0;
return STATE_IDLE;
}
return STATE_ERROR;
default:
return STATE_ERROR;
}
}
Step 4: Create State Transition Table (Optional, Recommended)
Use an array to store the mapping of “state → handling function” to replace nested<span>switch-case</span>, making the code clearer and easier to maintain (especially when there are many states).
// State transition table: state → corresponding handling function
const StateHandler state_table[] = {
[STATE_IDLE] = handle_idle,
[STATE_WAIT_HEADER] = handle_wait_header,
[STATE_RECV_DATA] = handle_recv_data,
[STATE_CHECK_CRC] = handle_check_crc,
[STATE_ERROR] = handle_error
};
// Check that the state enumeration matches the length of the transition table (compile-time check)
_Static_assert(sizeof(state_table)/sizeof(StateHandler) == STATE_ERROR + 1,
"State table size mismatch with StateType");
Step 5: State Machine Scheduler (Drive State Machine Operation)
Provide “initialization” and “event dispatch” interfaces, allowing external calls to trigger state machine operation.
// 1. Initialize state machine
void fsm_init(FsmContext* ctx, uint8_t target_header) {
if (ctx == NULL) return;
ctx->current_state = STATE_IDLE; // Initial state is idle state
ctx->data_len = 0; // Reset buffer
ctx->target_header = target_header; // Set target header
ctx->timeout_cnt = 0; // Reset timeout counter
}
// 2. Dispatch events (core scheduling function)
void fsm_dispatch(FsmContext* ctx, EventType event) {
if (ctx == NULL || event >= EVENT_CRC_ERR + 1) return;
// 1. Get the handling function for the current state
StateHandler current_handler = state_table[ctx->current_state];
if (current_handler == NULL) return;
// 2. Execute handling function, get next state
StateType next_state = current_handler(ctx, event);
// 3. State transition (optional: add hooks for state switch actions, such as exiting current state actions, entering next state actions)
if (next_state != ctx->current_state) {
// Exit action for current state (e.g., log, release resources)
exit_state_action(ctx->current_state);
// Update current state
ctx->current_state = next_state;
// Enter action for next state (e.g., initialize parameters, start timer)
enter_state_action(next_state);
}
}
// Optional: State switch hook functions (uniformly handle state enter/exit actions)
void enter_state_action(StateType state) {
switch (state) {
case STATE_RECV_DATA:
// Enter receiving data state, start timeout timer
start_timeout_timer();
break;
// Other state enter actions...
default: break;
}
}
void exit_state_action(StateType state) {
switch (state) {
case STATE_RECV_DATA:
// Exit receiving data state, stop timeout timer
stop_timeout_timer();
break;
// Other state exit actions...
default: break;
}
}
Step 6: External Calls (Trigger Events)
In the main program or interrupt service function, send events through<span>fsm_dispatch</span> to drive the state machine operation.
// Example: Trigger "received byte" event in serial receive interrupt
void uart_rx_isr(void) {
FsmContext fsm_ctx; // Global or static variable (ensure lifecycle)
uint8_t recv_byte = uart_read_byte(); // Read serial byte
fsm_ctx.data_buf[fsm_ctx.data_len] = recv_byte; // Store in buffer
fsm_dispatch(&fsm_ctx, EVENT_RECV_BYTE); // Dispatch "received byte" event
}
// Example: Trigger "timeout" event in timer interrupt
void timer_isr(void) {
FsmContext fsm_ctx;
fsm_ctx.timeout_cnt++;
if (fsm_ctx.timeout_cnt >= 1000) { // Timeout threshold (e.g., 1ms timer, 1000=1s)
fsm_ctx.timeout_cnt = 0;
fsm_dispatch(&fsm_ctx, EVENT_TIMEOUT); // Dispatch "timeout" event
}
}
// Main function initialization
int main(void) {
FsmContext fsm_ctx;
fsm_init(&fsm_ctx, 0xAA); // Initialize state machine, target header is 0xAA
while (1) {
// Main loop can handle non-interrupt triggered events (e.g., CRC check results)
if (/* CRC check completed */) {
if (/* Check passed */) {
fsm_dispatch(&fsm_ctx, EVENT_CRC_OK);
} else {
fsm_dispatch(&fsm_ctx, EVENT_CRC_ERR);
}
}
}
}
4. Advanced Optimization Techniques
1. Avoid Global Variables, Support Multiple State Machine Instances
Pass<span>FsmContext</span> as a parameter to all state functions and the scheduler instead of using global variables, allowing multiple independent state machines to run simultaneously (e.g., parsing protocols for two serial ports separately).
2. Simplify State Transitions: Use “State → Event → Next State” Table
For complex state machines (many states, complex transition logic), define a two-dimensional transition table that directly maps “current state + event → next state”, reducing<span>switch-case</span>:
// Transition table: [current state][event] = next state
const StateType transition_table[][EVENT_CRC_ERR + 1] = {
[STATE_IDLE][EVENT_RECV_BYTE] = STATE_WAIT_HEADER,
[STATE_IDLE][EVENT_TIMEOUT] = STATE_IDLE,
[STATE_WAIT_HEADER][EVENT_RECV_BYTE] = STATE_RECV_DATA,
[STATE_WAIT_HEADER][EVENT_TIMEOUT] = STATE_IDLE,
// ... other state transitions
};
3. Decouple Actions from States
Encapsulate “enter state actions”, “exit state actions”, and “event handling actions” as separate functions, with state handling functions only responsible for state transitions, improving code reusability:
// Event handling action: common logic when receiving a byte
void action_recv_byte(FsmContext* ctx, uint8_t byte) {
ctx->data_buf[ctx->data_len++] = byte;
}
// Call action in state handling function
StateType handle_recv_data(FsmContext* ctx, EventType event) {
if (event == EVENT_RECV_BYTE) {
action_recv_byte(ctx, /* received byte */); // Call action
if (ctx->data_len == 10) return STATE_CHECK_CRC;
}
// ...
}
4. Compile-Time Optimizations (Embedded Scenarios)
- Use
<span>static inline</span>to decorate state handling functions, reducing function call overhead; - Use
<span>__attribute__((packed))</span>(GCC) to compress space for state and event enumerations; - Disable unnecessary logs to reduce code size.
5. Applicable Scenarios and Considerations
Applicable Scenarios
- Embedded devices: key handling, LED control, serial/Can protocol parsing, sensor data collection;
- General programming: state transition logic (e.g., order status, user login process), finite state task scheduling.
Considerations
- Avoid State Explosion: Too many states can complicate code; prioritize splitting state machines (e.g., divide into “protocol parsing state machine” and “device control state machine”);
- Handle Boundary Events: Such as buffer overflow, timeout, abnormal data, to avoid state machine deadlock;
- Initialization and Reset: Ensure the state machine has a clear initial state and can reset to a safe state after exceptions;
- Thread Safety: When used in multi-threading/interruption, lock
<span>FsmContext</span>(e.g., disable interrupts, mutex) to avoid concurrent access conflicts.
Conclusion
The core of the C language state machine is “state abstraction + event-driven + controllable transitions“, by defining states/events with enumerations, encapsulating context with structures, and implementing state logic with function pointers, you can write efficient and maintainable code. Beginners are advised to start with simple<span>switch-case</span> implementations and gradually transition to the advanced scheme of “state transition table + function pointers”, choosing between Moore and Mealy state machines based on actual scenarios.