Introduction
Have you ever written code like this: layers of nested if-else statements, a multitude of condition checks, making maintenance a nightmare?
// Troublesome embedded control logic
<section class="mp_profile_iframe_wrp custom_select_card_wrp" nodeleaf="">
<mp-common-profile class="mpprofile js_uneditable custom_select_card mp_profile_iframe" data-pluginname="mpprofile" data-id="gh_acd22af287b7" data-nickname="An Embedded Programmer" data-headimg="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/mp-logo.png" data-service_type="1" data-verify_status="1"></mp-common-profile>
<br class="ProseMirror-trailingBreak">
</section>
void device_control() {
if (is_power_on) {
if (is_initialized) {
if (has_error) {
if (error_type == CRITICAL) {
// Handle critical error
} else if (error_type == WARNING) {
// Handle warning
}
} else {
if (user_input == BUTTON_PRESSED) {
if (current_mode == NORMAL) {
// Switch to setup mode
} else if (current_mode == SETTING) {
// Save settings
}
}
}
} else {
// Initialization logic
}
} else {
// Startup logic
}
}
This code is not only difficult to understand and maintain, but it is also prone to logical errors. Today, we will learn a more elegant solution: state machines. They can make your embedded code clear, maintainable, and extensible.
What is a State Machine?
A state machine is a mathematical model used to describe the behavior of an object at different times. In embedded systems, state machines are particularly suitable for handling control logic with clear state transitions.
Startup Event
Pause Event
Resume Event
Stop Event
Stop Event
Idle
Working
Paused
Why are State Machines Suitable for Embedded Systems?
- • Clear Logic: Each state has a clear responsibility, and state transitions are regular.
- • Easy to Debug: It is clear to know which state the system is currently in.
- • Easy to Extend: Adding new states does not affect existing logic.
- • Resource Friendly: Simple implementation with low memory and CPU overhead.
Basic Components of a State Machine
A complete state machine consists of four core elements:
State
The working mode or condition of the system at a certain point in time. For example: Idle, Working, Error, Sleep, etc.
Event
External inputs that trigger state changes, which can be:
- • User actions (button presses, touch)
- • External signals (sensor data, communication messages)
- • Timer timeouts
- • Internal conditions being met
Transition
The rules for jumping between states, defining how to transition from one state to another under specific events.
Action
Operations executed during state transitions, such as:
- • Update display
- • Send commands
- • Start timers
- • Log data
Yes
No
Condition Met
Condition Not Met
Current State
Received Event?
Check Transition Conditions
Execute Transition Action
Switch to New State
Execute Entry Action
Four Classic Application Scenarios
Scenario 1: Device State Management
Application Description: The startup and operation management of embedded devices is a classic application of state machines. From power-on to normal operation, devices need to go through a series of orderly state transitions.
Initialization Complete
Self-Test Passed
Self-Test Failed
Start Command
Stop Command
Run Exception
Reset Command
Initialization
Self-Test
Ready
Error
Running
Code Implementation:
typedef enum {
STATE_INIT,
STATE_SELFTEST,
STATE_READY,
STATE_RUNNING,
STATE_ERROR
} DeviceState_t;
typedef enum {
EVENT_POWER_ON,
EVENT_INIT_DONE,
EVENT_TEST_PASS,
EVENT_TEST_FAIL,
EVENT_START_CMD,
EVENT_STOP_CMD,
EVENT_ERROR_OCCUR,
EVENT_RESET_CMD
} DeviceEvent_t;
DeviceState_t current_state = STATE_INIT;
void device_state_machine(DeviceEvent_t event) {
switch (current_state) {
case STATE_INIT:
if (event == EVENT_INIT_DONE) {
printf("Device initialization complete, starting self-test\n");
start_selftest();
current_state = STATE_SELFTEST;
}
break;
case STATE_SELFTEST:
if (event == EVENT_TEST_PASS) {
printf("Self-test passed, device ready\n");
current_state = STATE_READY;
} else if (event == EVENT_TEST_FAIL) {
printf("Self-test failed, entering error state\n");
current_state = STATE_ERROR;
}
break;
case STATE_READY:
if (event == EVENT_START_CMD) {
printf("Starting work\n");
start_working();
current_state = STATE_RUNNING;
}
break;
case STATE_RUNNING:
if (event == EVENT_STOP_CMD) {
printf("Stopping work, returning to ready state\n");
stop_working();
current_state = STATE_READY;
} else if (event == EVENT_ERROR_OCCUR) {
printf("Run exception, entering error state\n");
current_state = STATE_ERROR;
}
break;
case STATE_ERROR:
if (event == EVENT_RESET_CMD) {
printf("Resetting device, reinitializing\n");
reset_device();
current_state = STATE_INIT;
}
break;
}
}
Scenario 2: Protocol Parsing
Application Description: In communication protocols such as UART, SPI, and I2C, the received data needs to be parsed according to a specific protocol format, and state machines can elegantly handle this process.
Received Start Byte
Length Valid
Length Invalid
Data Reception Complete
Checksum Correct
Checksum Error
Processing Complete
Waiting for Start
Receiving Length
Receiving Data
Checking Data
Processing Complete
Code Implementation:
typedef enum {
PARSE_WAIT_HEADER,
PARSE_GET_LENGTH,
PARSE_GET_DATA,
PARSE_CHECKSUM,
PARSE_COMPLETE
} ParseState_t;
typedef struct {
ParseState_t state;
uint8_t buffer[256];
uint8_t length;
uint8_t index;
uint8_t checksum;
} ProtocolParser_t;
ProtocolParser_t parser = {PARSE_WAIT_HEADER, {0}, 0, 0, 0};
void protocol_parse(uint8_t data) {
switch (parser.state) {
case PARSE_WAIT_HEADER:
if (data == 0xAA) { // Protocol start byte
parser.index = 0;
parser.checksum = 0;
parser.state = PARSE_GET_LENGTH;
}
break;
case PARSE_GET_LENGTH:
if (data > 0 && data <= 200) { // Length valid
parser.length = data;
parser.state = PARSE_GET_DATA;
} else {
parser.state = PARSE_WAIT_HEADER; // Length invalid, restart
}
break;
case PARSE_GET_DATA:
parser.buffer[parser.index++] = data;
parser.checksum += data;
if (parser.index >= parser.length) {
parser.state = PARSE_CHECKSUM;
}
break;
case PARSE_CHECKSUM:
if (data == parser.checksum) {
printf("Data reception complete, length: %d\n", parser.length);
process_received_data(parser.buffer, parser.length);
parser.state = PARSE_COMPLETE;
} else {
printf("Checksum error, discarding data\n");
parser.state = PARSE_WAIT_HEADER;
}
break;
case PARSE_COMPLETE:
parser.state = PARSE_WAIT_HEADER; // Ready to receive the next frame
break;
}
}
Scenario 3: User Interface Control
Application Description: The menu system, button responses, and other user interaction logic of embedded devices can be implemented very clearly using state machines.
Setting Key
Information Key
Confirm Key
Back Key
Save/Return
Any Key
Main Menu
Settings Menu
Information Display
Time Setting
Code Implementation:
typedef enum {
UI_MAIN_MENU,
UI_SETTING_MENU,
UI_TIME_SETTING,
UI_INFO_DISPLAY
} UIState_t;
typedef enum {
KEY_UP,
KEY_DOWN,
KEY_ENTER,
KEY_BACK,
KEY_SETTING
} KeyEvent_t;
UIState_t ui_state = UI_MAIN_MENU;
void ui_state_machine(KeyEvent_t key) {
switch (ui_state) {
case UI_MAIN_MENU:
if (key == KEY_SETTING) {
display_setting_menu();
ui_state = UI_SETTING_MENU;
} else if (key == KEY_ENTER) {
display_device_info();
ui_state = UI_INFO_DISPLAY;
}
break;
case UI_SETTING_MENU:
if (key == KEY_ENTER) {
display_time_setting();
ui_state = UI_TIME_SETTING;
} else if (key == KEY_BACK) {
display_main_menu();
ui_state = UI_MAIN_MENU;
}
break;
case UI_TIME_SETTING:
if (key == KEY_ENTER) {
save_time_setting();
display_setting_menu();
ui_state = UI_SETTING_MENU;
} else if (key == KEY_BACK) {
display_setting_menu();
ui_state = UI_SETTING_MENU;
}
break;
case UI_INFO_DISPLAY:
// Any key returns to the main menu
display_main_menu();
ui_state = UI_MAIN_MENU;
break;
}
}
Scenario 4: Control Systems
Application Description: Control systems such as LED blinking, motor control, and temperature regulation can implement complex control logic using state machines.
Start
Mode Switch
Mode Switch
Mode Switch
Mode Switch
Off
Off
Off
Off
Off
Slow Blink
Fast Blink
Always On
Breathing
Code Implementation:
typedef enum {
LED_OFF,
LED_SLOW_BLINK,
LED_FAST_BLINK,
LED_ON,
LED_BREATH
} LEDState_t;
LEDState_t led_state = LED_OFF;
uint32_t led_timer = 0;
uint8_t led_brightness = 0;
void led_control_task() {
static uint32_t last_time = 0;
uint32_t current_time = get_system_tick();
switch (led_state) {
case LED_OFF:
set_led(0); // LED off
break;
case LED_SLOW_BLINK:
if (current_time - last_time > 1000) { // Switch every second
toggle_led();
last_time = current_time;
}
break;
case LED_FAST_BLINK:
if (current_time - last_time > 200) { // Switch every 200ms
toggle_led();
last_time = current_time;
}
break;
case LED_ON:
set_led(255); // LED always on
break;
case LED_BREATH:
// Breathing light effect
if (current_time - last_time > 10) {
led_brightness += (led_brightness < 255) ? 1 : -255;
set_led(led_brightness);
last_time = current_time;
}
break;
}
}
void led_mode_switch() {
switch (led_state) {
case LED_OFF: led_state = LED_SLOW_BLINK; break;
case LED_SLOW_BLINK: led_state = LED_FAST_BLINK; break;
case LED_FAST_BLINK: led_state = LED_ON; break;
case LED_ON: led_state = LED_BREATH; break;
case LED_BREATH: led_state = LED_SLOW_BLINK; break;
}
}
C Language Implementation Techniques
Basic Template Structure
// Standard state machine template
typedef enum {
STATE_IDLE,
STATE_WORKING,
STATE_ERROR
} state_t;
typedef enum {
EVENT_START,
EVENT_STOP,
EVENT_ERROR
} event_t;
state_t current_state = STATE_IDLE;
void state_machine(event_t event) {
switch (current_state) {
case STATE_IDLE:
// Handle events in idle state
break;
case STATE_WORKING:
// Handle events in working state
break;
case STATE_ERROR:
// Handle events in error state
break;
default:
// Handle unknown state
break;
}
}
State Transition Table Method
For more complex state machines, a data-driven approach can be used:
typedef struct {
state_t current_state;
event_t event;
state_t next_state;
void (*action)(void);
} state_transition_t;
const state_transition_t transitions[] = {
{STATE_IDLE, EVENT_START, STATE_WORKING, start_work},
{STATE_WORKING, EVENT_STOP, STATE_IDLE, stop_work},
{STATE_WORKING, EVENT_ERROR, STATE_ERROR, handle_error},
// More transition rules...
};
void execute_state_machine(event_t event) {
for (int i = 0; i < sizeof(transitions)/sizeof(transitions[0]); i++) {
if (transitions[i].current_state == current_state &&
transitions[i].event == event) {
// Execute transition action
if (transitions[i].action != NULL) {
transitions[i].action();
}
// State transition
current_state = transitions[i].next_state;
break;
}
}
}
Design Points and Considerations
Design Principles
- 1. State Minimization: Avoid state explosion by merging similar states.
- 2. Single Responsibility: Each state should have a clear responsibility.
- 3. Clear Transitions: Transition conditions should be clear to avoid ambiguity.
Common Pitfalls
- 1. State Omission: Ensure all possible states are considered.
- 2. Event Omission: Handle possible events in each state.
- 3. Deadlock States: Avoid entering states that cannot be exited.
Debugging Techniques
void log_state_transition(state_t from, state_t to, event_t event) {
printf("State: %s -> %s (Event: %s)\n",
state_name[from], state_name[to], event_name[event]);
}
Conclusion
State machines are a powerful tool in embedded development, capable of:
- • Simplifying Complex Logic: Transforming complex condition checks into clear state transitions.
- • Improving Maintainability: Clear logical structure that is easy to understand and modify.
- • Enhancing Reliability: State transitions have clear rules, reducing logical errors.
- • Facilitating Debugging: Clearly track changes in system states.
Learning Suggestions:
- 1. Start practicing with simple two or three states.
- 2. Draw state transition diagrams before writing code.
- 3. Try using them in actual projects.
Advanced Directions:
- • Hierarchical State Machines
- • UML State Diagram Modeling
- • State Machine Code Generation Tools
Master state machines to make your embedded code more elegant and professional!
Follow me for more practical tips on embedded development!