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 必须精确等于数组元素个数,用于边界检查(防止 current ID 越界访问 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() 内部执行严格四步原子操作:

  1. 检查 current 合法性;
  2. 获取 states[current].on_event 函数指针;
  3. 调用 on_event(context, event) 获取目标状态 ID;
  4. 若目标 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 库,就是你最值得信赖的战友。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐