嵌入式轻量级状态机库:零依赖、确定性FSM实现
有限状态机(FSM)是嵌入式系统中处理事件驱动逻辑的基础模型,其核心在于状态定义、事件响应与迁移控制。SimpleStateProcessor 以纯C实现,遵循确定性、可预测性与最小化设计原则,不依赖动态内存分配或操作系统抽象层,确保单次状态迁移时间复杂度为O(1)。该库适用于裸机环境及RTOS任务内嵌,广泛支撑按键消抖、通信协议解析(如Modbus RTU)、低功耗模式切换等典型场景。通过函数指
1. 项目概述
SimpleStateProcessor 是一个轻量级、零依赖的有限状态机(Finite State Machine, FSM)处理器库,专为资源受限的嵌入式系统设计。其核心目标并非提供图灵完备的复杂状态建模能力,而是以极小的内存开销(典型ROM占用 < 2KB,RAM仅需数个字节)、确定性执行时间(单次状态转移最坏情况为 O(1))和无动态内存分配为前提,解决嵌入式固件中高频出现的状态协调问题——例如按键消抖、通信协议状态同步、传感器采集周期管理、电机启停时序控制、低功耗模式切换等。
该库不引入任何操作系统抽象层(如 FreeRTOS 任务或信号量),亦不依赖 HAL 或 LL 库的外设驱动封装,其接口完全基于纯 C 函数指针与结构体操作,可无缝集成于裸机(Bare-Metal)环境、CMSIS-RTOS 兼容层,或作为 FreeRTOS 任务内部的状态调度器使用。所有状态迁移逻辑均在调用者上下文中同步执行,无隐式上下文切换开销,符合 IEC 61508 SIL-2 等功能安全标准对确定性响应的要求。
1.1 设计哲学与工程取舍
SimpleStateProcessor 的设计严格遵循嵌入式底层开发的三大铁律: 确定性(Determinism)、可预测性(Predictability)、最小化(Minimization) 。
-
确定性 :每个
ssp_process()调用仅触发一次状态检查与至多一次状态迁移,迁移动作由用户定义的on_enter/on_exit回调函数完成,不包含任何阻塞、延时或轮询逻辑。状态迁移本身不修改任何全局变量(除状态机当前状态标识外),避免竞态条件。 -
可预测性 :库不使用
malloc/free,所有数据结构均通过静态声明或栈分配;无递归调用;无浮点运算;无未定义行为(如数组越界访问)。编译后二进制代码大小与执行路径长度均可静态分析。 -
最小化 :不实现层次化状态机(HSM)、正交状态(Orthogonal Regions)或历史状态(History States)等高级特性。其状态集为扁平一维数组,状态 ID 为紧凑的
uint8_t类型,最大支持 256 个状态;事件类型为uint16_t,支持 65536 种离散事件。这种简化使状态跳转可通过查表(state transition table)或线性搜索(linear scan)两种模式实现,开发者可根据 ROM/RAM 权衡自由选择。
该库明确拒绝“通用性”陷阱:它不试图替代 UML 工具生成的完整状态图框架(如 QP/QM),也不提供可视化调试器。它的价值在于——当工程师面对一个需要在 4KB Flash 的 Cortex-M0+ 上实现 UART 帧同步状态机时,能直接复制粘贴 3 个函数、定义 5 个状态结构体,10 分钟内完成可靠部署。
2. 核心数据结构与 API 梳理
SimpleStateProcessor 的运行依赖两个核心结构体: ssp_state_t 描述单个状态的行为, ssp_machine_t 封装整个状态机的上下文。所有 API 均为纯 C 函数,无 C++ 类封装,确保与任意编译器(ARMCC、GCC、IAR)及 C 标准(C99 起)兼容。
2.1 ssp_state_t :状态行为定义
该结构体定义一个状态的全部可观测行为,字段均为函数指针,允许用户按需实现:
| 字段 | 类型 | 说明 |
|---|---|---|
on_enter |
void (*)(void*) |
进入该状态时执行的回调。 void* 参数为用户传入的私有上下文指针(如 struct sensor_ctx* ),用于访问外设寄存器、共享缓冲区等。若无需执行动作,可置为 NULL 。 |
on_exit |
void (*)(void*) |
离开该状态时执行的回调。常用于资源释放(如关闭 ADC 时钟、禁用 GPIO 中断)。同样支持 NULL 。 |
on_event |
ssp_state_id_t (*)(void*, ssp_event_t) |
核心事件处理器 。接收当前上下文与事件 ID,返回下一个状态 ID。若返回 SSP_STATE_UNCHANGED ,表示事件被忽略,状态保持不变;若返回有效状态 ID,则触发迁移;若返回 SSP_STATE_INVALID ,表示非法迁移,库将保持当前状态并返回错误码(见 ssp_process() 返回值)。 |
关键设计说明 :
on_event的返回值机制是 SimpleStateProcessor 区别于其他 FSM 库的关键。它不强制要求“每个事件必须导致迁移”,而是将决策权完全交给用户逻辑。例如,在IDLE状态下收到EVENT_BUTTON_PRESS,可返回STATE_DEBOUNCING;而收到EVENT_SENSOR_DATA_READY则可返回SSP_STATE_UNCHANGED,表示该事件在此状态下无意义。这种显式返回值比布尔型handled/unhandled更利于静态分析与测试覆盖。
2.2 ssp_machine_t :状态机运行时上下文
该结构体保存状态机的当前运行状态,必须由用户静态声明或栈分配:
typedef struct {
const ssp_state_t* states; // 指向状态数组首地址(const,ROM 存储)
uint16_t state_count; // 状态总数(即 states 数组长度)
ssp_state_id_t current; // 当前状态 ID(初始化为 0 或指定初始状态)
void* context; // 用户私有上下文指针(传递给所有回调)
} ssp_machine_t;
states必须指向一个ssp_state_t类型的常量数组,通常存储在 Flash 中。例如:static const ssp_state_t my_fsm_states[] = { [STATE_IDLE] = { .on_enter = idle_enter, .on_event = idle_handler }, [STATE_ACTIVE] = { .on_enter = active_enter, .on_event = active_handler }, [STATE_ERROR] = { .on_exit = error_exit, .on_event = error_handler } };state_count必须精确等于数组元素个数,用于边界检查(防止currentID 越界访问states数组)。current是唯一可变字段,代表状态机当前所处状态。初始化时应设为合法状态 ID(如STATE_IDLE),不可为SSP_STATE_INVALID。context为用户自定义数据指针, 强烈建议 将其指向一个结构体,封装所有与该状态机相关的硬件句柄、计数器、标志位等。例如:typedef struct { UART_HandleTypeDef* huart; // 关联的 UART 句柄 uint8_t rx_buffer[64]; // 接收缓冲区 uint8_t rx_len; // 当前接收长度 uint32_t last_rx_tick; // 上次接收时间戳(用于超时检测) } uart_fsm_ctx_t;
2.3 主要 API 函数详解
ssp_init(ssp_machine_t* machine, const ssp_state_t* states, uint16_t count, ssp_state_id_t initial, void* ctx)
初始化状态机上下文。这是 必须首先调用 的函数,完成 machine 结构体的字段赋值。
// 示例:初始化 UART 协议状态机
uart_fsm_ctx_t uart_ctx = {
.huart = &huart1,
.rx_len = 0,
.last_rx_tick = HAL_GetTick()
};
ssp_machine_t uart_fsm;
ssp_init(&uart_fsm, my_uart_states, ARRAY_SIZE(my_uart_states), STATE_IDLE, &uart_ctx);
states和count必须匹配,initial必须< count,否则行为未定义。- 该函数不调用任何
on_enter回调,仅做结构体填充。首次进入初始状态需手动调用ssp_process()。
ssp_process(ssp_machine_t* machine, ssp_event_t event) -> ssp_result_t
状态机驱动主函数 。每次外部事件发生(如 GPIO 中断触发、定时器溢出、DMA 传输完成)时,必须调用此函数。
// 在 UART RX Complete Callback 中调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
ssp_result_t res = ssp_process(&uart_fsm, EVENT_UART_RX_COMPLETE);
if (res != SSP_OK) {
// 处理错误:如记录日志、触发看门狗复位
ERROR_Handler();
}
}
}
返回值 ssp_result_t 枚举定义如下:
| 枚举值 | 含义 | 工程意义 |
|---|---|---|
SSP_OK |
成功处理事件,可能发生了状态迁移 | 正常路径,无需干预 |
SSP_INVALID_STATE |
machine->current 超出 [0, state_count) 范围 |
严重错误,表明状态机被意外篡改(如内存溢出覆盖 current 字段),需立即复位 |
SSP_INVALID_EVENT_HANDLER |
states[current].on_event 为 NULL |
配置错误:未为当前状态实现事件处理器,属于编译期可捕获缺陷 |
SSP_STATE_UNCHANGED |
on_event 返回 SSP_STATE_UNCHANGED |
事件被当前状态忽略,属预期行为,非错误 |
关键实现细节 :
ssp_process()内部执行严格四步原子操作:
- 检查
current合法性;- 获取
states[current].on_event函数指针;- 调用
on_event(context, event)获取目标状态 ID;- 若目标 ID 有效且不等于
current,则:
a) 调用states[current].on_exit(context);
b) 更新machine->current = target_id;
c) 调用states[target_id].on_enter(context)。
整个过程无锁、无中断禁用(假设回调函数本身是可重入的),但要求用户确保on_enter/on_exit执行时间短且不引发新事件。
ssp_get_current_state(const ssp_machine_t* machine) -> ssp_state_id_t
安全读取当前状态 ID。仅作只读访问,不触发任何回调。
// 调试用途:通过串口打印当前状态
printf("FSM State: %d\r\n", ssp_get_current_state(&uart_fsm));
ssp_force_transition(ssp_machine_t* machine, ssp_state_id_t target) -> ssp_result_t
强制迁移至指定状态,绕过 on_event 逻辑。 仅限调试、错误恢复或系统初始化后首次进入状态时使用 。
// 系统上电后,强制进入 ERROR 状态进行自检
ssp_force_transition(&uart_fsm, STATE_ERROR);
- 若
target非法,返回SSP_INVALID_STATE; - 成功时,先执行
on_exit(若存在),再执行on_enter(若存在); - 禁止在
on_event回调内部调用此函数 ,会导致递归迁移,破坏状态一致性。
3. 典型应用场景与代码示例
SimpleStateProcessor 的价值在具体硬件交互场景中得以充分体现。以下三个示例覆盖嵌入式开发中最常见的三类问题:输入消抖、协议解析、低功耗管理。
3.1 场景一:机械按键消抖状态机(裸机环境)
机械按键存在毫秒级抖动,需软件滤波。传统延时消抖( HAL_Delay(20) )会阻塞 CPU;而基于状态机的边沿检测可实现零等待、高响应。
// 定义状态 ID(枚举提升可读性)
typedef enum {
KEY_IDLE, // 等待按键按下
KEY_DEBOUNCE, // 检测到下降沿,启动消抖计时
KEY_PRESSED, // 消抖完成,确认按下
KEY_RELEASED // 等待释放并消抖
} key_state_id_t;
// 按键上下文:存储 GPIO 端口、引脚及消抖计数器
typedef struct {
GPIO_TypeDef* port;
uint16_t pin;
uint32_t debounce_counter; // 毫秒计数器(由 SysTick 提供)
} key_ctx_t;
// 状态行为定义
static void key_idle_enter(void* ctx) {
key_ctx_t* kctx = (key_ctx_t*)ctx;
// 配置 GPIO 为上拉输入
HAL_GPIO_WritePin(kctx->port, kctx->pin, GPIO_PIN_SET);
}
static ssp_state_id_t key_idle_handler(void* ctx, ssp_event_t ev) {
key_ctx_t* kctx = (key_ctx_t*)ctx;
if (ev == EVENT_GPIO_FALLING) { // 硬件中断检测到下降沿
return KEY_DEBOUNCE;
}
return SSP_STATE_UNCHANGED;
}
static void key_debounce_enter(void* ctx) {
key_ctx_t* kctx = (key_ctx_t*)ctx;
kctx->debounce_counter = 0; // 重置计数器
}
static ssp_state_id_t key_debounce_handler(void* ctx, ssp_event_t ev) {
key_ctx_t* kctx = (key_ctx_t*)ctx;
if (ev == EVENT_SYSTICK_1MS) { // 每毫秒 SysTick 中断触发
kctx->debounce_counter++;
if (kctx->debounce_counter >= 20) { // 20ms 消抖完成
if (HAL_GPIO_ReadPin(kctx->port, kctx->pin) == GPIO_PIN_RESET) {
return KEY_PRESSED;
} else {
return KEY_IDLE; // 抖动结束,恢复空闲
}
}
}
return SSP_STATE_UNCHANGED;
}
static void key_pressed_enter(void* ctx) {
// 触发用户业务逻辑:如点亮 LED、发送消息
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
static ssp_state_id_t key_pressed_handler(void* ctx, ssp_event_t ev) {
key_ctx_t* kctx = (key_ctx_t*)ctx;
if (ev == EVENT_GPIO_RISING) {
return KEY_RELEASED;
}
return SSP_STATE_UNCHANGED;
}
// 状态数组(存储在 Flash)
static const ssp_state_t key_fsm_states[] = {
[KEY_IDLE] = { .on_enter = key_idle_enter, .on_event = key_idle_handler },
[KEY_DEBOUNCE] = { .on_enter = key_debounce_enter, .on_event = key_debounce_handler },
[KEY_PRESSED] = { .on_enter = key_pressed_enter, .on_event = key_pressed_handler },
[KEY_RELEASED] = { .on_exit = key_released_exit, .on_event = key_released_handler }
};
// 初始化与中断服务程序
key_ctx_t key_ctx = { .port = GPIOA, .pin = GPIO_PIN_0 };
ssp_machine_t key_fsm;
ssp_init(&key_fsm, key_fsm_states, ARRAY_SIZE(key_fsm_states), KEY_IDLE, &key_ctx);
// EXTI Line0 IRQ Handler
void EXTI0_IRQHandler(void) {
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
// 根据当前电平判断边沿类型
ssp_event_t ev = (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) ?
EVENT_GPIO_FALLING : EVENT_GPIO_RISING;
ssp_process(&key_fsm, ev);
}
}
工程优势 :
- 消抖逻辑与业务逻辑(LED 控制)完全解耦;
EVENT_SYSTICK_1MS事件由 SysTick 中断统一派发,避免在每个状态中重复调用HAL_GetTick();- 所有状态迁移均在中断上下文中完成,无阻塞,响应延迟 < 1μs(Cortex-M4)。
3.2 场景二:Modbus RTU 从机帧接收状态机(FreeRTOS 任务内)
在 FreeRTOS 任务中处理 Modbus RTU 帧,需严格遵守 3.5 字符间隔超时规则。使用状态机可清晰分离帧头检测、CRC 校验、功能码分发等阶段。
// 在 Modbus 任务中
void modbus_task(void* pvParameters) {
modbus_ctx_t mb_ctx = { .rx_buffer = rx_buf, .rx_len = 0 };
ssp_machine_t mb_fsm;
ssp_init(&mb_fsm, modbus_states, ARRAY_SIZE(modbus_states), STATE_WAIT_START, &mb_ctx);
for(;;) {
// 等待 UART 接收完成信号量
if (xSemaphoreTake(uart_rx_sem, portMAX_DELAY) == pdTRUE) {
// DMA 接收完成,数据已存入 rx_buf
ssp_process(&mb_fsm, EVENT_UART_FRAME_RECEIVED);
}
}
}
// 状态处理器片段(STATE_WAIT_START -> STATE_RECEIVE_HEADER)
static ssp_state_id_t wait_start_handler(void* ctx, ssp_event_t ev) {
modbus_ctx_t* mctx = (modbus_ctx_t*)ctx;
if (ev == EVENT_UART_FRAME_RECEIVED && mctx->rx_len > 0) {
uint8_t addr = mctx->rx_buffer[0];
if (addr == MODBUS_SLAVE_ADDR || addr == MODBUS_BROADCAST_ADDR) {
return STATE_RECEIVE_HEADER;
}
}
return SSP_STATE_UNCHANGED;
}
// STATE_RECEIVE_HEADER 的 on_enter 启动定时器,on_event 检查超时
static void receive_header_enter(void* ctx) {
modbus_ctx_t* mctx = (modbus_ctx_t*)ctx;
mctx->timeout_ticks = xTaskGetTickCount() + MODBUS_T35_TICKS; // 3.5 字符时间
}
static ssp_state_id_t receive_header_handler(void* ctx, ssp_event_t ev) {
modbus_ctx_t* mctx = (modbus_ctx_t*)ctx;
if (ev == EVENT_UART_FRAME_RECEIVED) {
// 追加新数据到缓冲区
memcpy(&mctx->rx_buffer[mctx->rx_len], dma_rx_buf, dma_len);
mctx->rx_len += dma_len;
// 重置超时
mctx->timeout_ticks = xTaskGetTickCount() + MODBUS_T35_TICKS;
} else if (ev == EVENT_TIMEOUT_CHECK) {
if (xTaskGetTickCount() > mctx->timeout_ticks) {
// 超时,丢弃不完整帧
mctx->rx_len = 0;
return STATE_WAIT_START;
}
}
return SSP_STATE_UNCHANGED;
}
集成要点 :
EVENT_TIMEOUT_CHECK由任务循环定期触发(如每 1ms),实现软定时器;on_enter中启动超时计数,on_event中检查,避免在while循环中忙等;- 状态机完全运行在任务上下文中,与 FreeRTOS 的
xQueueSend/xSemaphoreGive无缝协作。
3.3 场景三:电池供电设备的低功耗状态机(LL 库直驱)
面向超低功耗应用,需精细控制外设时钟、GPIO 状态及内核睡眠模式。SimpleStateProcessor 可协调各模块的休眠准备与唤醒恢复。
// 状态:AWAKE -> SLEEP_PREPARE -> DEEP_SLEEP -> AWAKE_ON_WAKEUP
static void sleep_prepare_enter(void* ctx) {
// 关闭所有非必要外设时钟(LL_RCC_APB1_CLK_DISABLE)
LL_APB1_GRP1_DisableClock(LL_APB1_GRP1_PERIPH_TIM2);
LL_APB1_GRP1_DisableClock(LL_APB1_GRP1_PERIPH_ADC1);
// 配置唤醒引脚(如 RTC Alarm)
LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_17); // RTC Alarm
LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_17);
}
static ssp_state_id_t deep_sleep_handler(void* ctx, ssp_event_t ev) {
if (ev == EVENT_RTC_ALARM) {
return STATE_AWAKE_ON_WAKEUP;
}
return SSP_STATE_UNCHANGED;
}
static void awake_on_wakeup_enter(void* ctx) {
// 重新使能时钟、重初始化外设
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_ADC1);
// 清除唤醒标志
LL_RTC_ClearFlag_ALRA(RTC);
}
功耗优化效果 :
SLEEP_PREPARE状态确保所有外设在进入DEEP_SLEEP前处于已知安全状态;AWAKE_ON_WAKEUP状态集中处理唤醒后的恢复逻辑,避免在中断中执行耗时初始化;- 全流程无
HAL_Delay,功耗状态切换时间可精确到微秒级。
4. 配置选项与性能调优
SimpleStateProcessor 提供两个关键编译时配置宏,位于 ssp_config.h (需用户创建):
| 宏定义 | 默认值 | 说明 | 适用场景 |
|---|---|---|---|
SSP_USE_TABLE_DRIVEN |
0 (禁用) |
若启用( #define SSP_USE_TABLE_DRIVEN 1 ),则 ssp_process() 使用二维查表法( transition_table[current][event] )实现 O(1) 迁移;否则使用线性搜索(O(n))。查表法 ROM 开销大( state_count × event_count × sizeof(ssp_state_id_t) ),但速度最快。 |
状态数 < 32 且事件数 < 64 的高频迁移场景(如高速通信协议) |
SSP_ENABLE_ASSERTIONS |
0 (禁用) |
若启用( #define SSP_ENABLE_ASSERTIONS 1 ),则在 ssp_init() 和 ssp_process() 中插入 assert() 检查参数合法性。仅用于开发调试,发布版本必须禁用。 |
调试阶段快速定位配置错误 |
性能实测数据(STM32F030F4P6 @ 48MHz) :
- 线性搜索模式:单次
ssp_process()最坏执行时间 1.2μs(12 个状态,5 个事件); - 查表模式:单次
ssp_process()恒定 0.35μs; - ROM 占用:线性搜索版 1.1KB,查表版(16×16 表)增加 256B;
- RAM 占用:
ssp_machine_t固定 12 字节,无额外堆栈消耗。
5. 错误处理与调试技巧
SimpleStateProcessor 将错误分为两类: 可恢复错误 (如 SSP_STATE_UNCHANGED )与 致命错误 (如 SSP_INVALID_STATE )。后者必须触发系统级响应。
5.1 致命错误的工程化应对
// 在 ssp_process() 调用后必须检查
ssp_result_t res = ssp_process(&fsm, ev);
switch(res) {
case SSP_OK:
break; // 正常
case SSP_INVALID_STATE:
// 立即触发看门狗复位,防止状态机失控
HAL_IWDG_Refresh(&hiwdg);
NVIC_SystemReset(); // 硬件复位
break;
case SSP_INVALID_EVENT_HANDLER:
// 编译期错误,此处应为断言失败
__BKPT(0); // 进入调试器
break;
}
5.2 调试辅助工具
- 状态快照日志 :在
on_enter中添加printf("ENTER STATE %d\r\n", current),配合串口终端实时观察迁移路径; - 事件追踪 :定义
EVENT_DEBUG_LOG事件,在关键位置调用ssp_process(&fsm, EVENT_DEBUG_LOG),统一记录时间戳与上下文; - 静态分析 :使用
cppcheck或PC-lint扫描states数组,确保每个on_event非 NULL,且所有on_event返回值均在合法范围内。
SimpleStateProcessor 的生命力源于其对嵌入式本质的坚守:它不试图成为万能胶,而是作为一枚精密的齿轮,严丝合缝地嵌入到你的裸机循环、FreeRTOS 任务或 CMSIS-RTOS 封装层中,以确定性的节奏,驱动着每一个硬件状态的优雅变迁。当你在凌晨三点调试一个因状态竞争导致的 sporadic crash 时,一个经过充分测试、无隐藏副作用的 FSM 库,就是你最值得信赖的战友。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)