从裸机到 RTOS:用 FreeRTOS 事件组重构状态机并消除轮询延迟

1. 裸机状态机的痛点

在裸机开发中,状态机通常依赖全局标志位和 while(1) 轮询:

volatile uint8_t flag_uart = 0;
volatile uint8_t flag_timer = 0;

void main_loop(void) {
    while (1) {
        if (flag_uart && flag_timer) {
            flag_uart = 0;
            flag_timer = 0;
            process_data();
        }
        // 其他任务...
    }
}

这种方式存在两个致命问题:

  • CPU 空转:即使没有事件,循环也在全速运行,功耗高。
  • 响应延迟:如果循环中还有其他耗时操作,事件响应可能延迟数十毫秒。

2. FreeRTOS 事件组原理

事件组(Event Group)是 FreeRTOS 提供的一种任务间同步机制。它本质上是一个 EventBits_t 类型的变量(通常 32 位,可配置),每个位代表一个事件。任务可以:

  • 等待多个事件位:使用 xEventGroupWaitBits(),可指定等待哪些位、是否清除、是否要求所有位同时满足。
  • 设置事件位:使用 xEventGroupSetBits(),可在中断中使用 xEventGroupSetBitsFromISR()。

当任务等待的事件位满足条件时,任务立即从阻塞态变为就绪态,无轮询、无延迟。

3. 重构目标:多事件触发的状态机

假设一个数据采集状态机,需要同时等待:

  • 串口收到完整帧(事件位 0)
  • 定时器采样周期到(事件位 1)

只有两者都满足时才处理数据。裸机下用两个标志位轮询,现在用事件组重构。

4. 配置步骤

4.1 FreeRTOS 配置

在 FreeRTOSConfig.h 中确保:

#define configUSE_EVENT_GROUPS          1
#define configUSE_TIMERS                1  // 若使用软件定时器
#define configTICK_RATE_HZ              1000

4.2 创建事件组句柄

#include "FreeRTOS.h"
#include "event_groups.h"

EventGroupHandle_t xDataEvents;

#define BIT_UART_FRAME  (1 << 0)
#define BIT_TIMER_TICK  (1 << 1)

4.3 初始化

void app_init(void) {
    xDataEvents = xEventGroupCreate();
    configASSERT(xDataEvents != NULL);
    // 启动串口接收、定时器等
}

5. 完整代码示例

5.1 中断服务程序设置事件位

// 串口接收完成中断
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (USART1->SR & USART_SR_IDLE) {
        // 清除标志,处理数据...
        xEventGroupSetBitsFromISR(xDataEvents, BIT_UART_FRAME, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

// 定时器中断(例如 TIM2)
void TIM2_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    TIM2->SR &= ~TIM_SR_UIF;
    xEventGroupSetBitsFromISR(xDataEvents, BIT_TIMER_TICK, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

5.2 状态机任务

void vDataTask(void *pvParameters) {
    EventBits_t uxBits;
    const EventBits_t xWaitBits = BIT_UART_FRAME | BIT_TIMER_TICK;

    for (;;) {
        // 等待两个事件位都置位,并自动清除
        uxBits = xEventGroupWaitBits(
            xDataEvents,        // 事件组句柄
            xWaitBits,          // 等待的位
            pdTRUE,             // 退出时清除这些位
            pdTRUE,             // 要求所有位都置位
            portMAX_DELAY       // 无限等待
        );

        if ((uxBits & xWaitBits) == xWaitBits) {
            // 两个事件都满足,执行状态机动作
            process_data();
        }
    }
}

5.3 主函数创建任务

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_TIM2_Init();

    app_init();

    xTaskCreate(vDataTask, "DataTask", 256, NULL, 3, NULL);

    vTaskStartScheduler();

    while (1);
}

6. 注意事项

  • 中断安全:在 ISR 中必须使用 xEventGroupSetBitsFromISR(),且需传入 pxHigherPriorityTaskWoken,最后调用 portYIELD_FROM_ISR()。
  • 事件位清除:xEventGroupWaitBits() 的 xClearOnExit 参数设为 pdTRUE 可自动清除,避免手动清除遗漏。
  • 优先级反转:事件组不处理优先级继承,若多个任务等待同一事件,需合理设计优先级。
  • 内存占用:每个事件组占用少量 RAM,创建后不可删除(除非使用 vEventGroupDelete())。
  • 位宽限制:默认 32 位,若需更多事件可修改 configUSE_16_BIT_TICKS 或使用多个事件组。

7. 效果对比

| 指标 | 裸机轮询 | FreeRTOS 事件组 | |------|----------|----------------| | CPU 占用 | 100% 空转 | 接近 0%(阻塞时) | | 响应延迟 | 取决于循环周期,可能 >10ms | 微秒级(任务切换开销) | | 代码可维护性 | 差,标志位分散 | 好,事件集中管理 |

8. 总结

用 FreeRTOS 事件组重构裸机状态机,不仅消除了轮询延迟,还大幅降低 CPU 占用。关键在于将“等待多个条件”的轮询逻辑改为阻塞等待,让 RTOS 调度器在事件未就绪时切换其他任务。对于多事件同步场景,事件组是比信号量更优雅的选择。