从裸机到 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 调度器在事件未就绪时切换其他任务。对于多事件同步场景,事件组是比信号量更优雅的选择。