从裸机到 RTOS:用 FreeRTOS 事件组重构按键与低功耗任务
1. 裸机方案的痛点
在裸机程序中,按键扫描与低功耗通常这样写:
while (1) {
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
HAL_Delay(20); // 消抖
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
// 处理按键
}
}
// 其他任务
__WFI(); // 空闲时休眠
}
问题很明显:
-
响应慢:
HAL_Delay阻塞 CPU,其他任务无法执行。 - 功耗高:轮询间隔短则 CPU 频繁唤醒,间隔长则按键响应迟钝。
- 耦合严重:按键逻辑与低功耗逻辑混在一起,难以扩展。
RTOS 的事件组(Event Group)正是解决这类“多事件同步”问题的利器。
2. FreeRTOS 事件组原理
事件组是一个 32 位(可配置)的“事件标志集合”,每个位代表一个事件。任务可以:
- 等待一个或多个位被置位(支持“与”“或”逻辑)。
- 设置位(从中断或任务中)。
- 清除位。
与信号量、队列相比,事件组更适合“多个事件触发同一个任务”或“任务等待多个条件组合”的场景。
关键 API:
EventGroupHandle_t xEventGroupCreate(void);
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet);
EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait);
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken);
3. 系统设计:按键 + 低功耗协作模型
我们定义两个事件位:
-
KEY_EVENT_BIT:按键按下(由 EXTI 中断置位)。 -
TIMEOUT_EVENT_BIT:超时无操作(由软件定时器置位)。
创建一个“电源管理任务”,它等待这两个事件:
- 若
KEY_EVENT_BIT先到:处理按键,重置超时定时器,保持运行。 - 若
TIMEOUT_EVENT_BIT先到:进入低功耗模式,等待按键唤醒。
这样,按键响应由中断保证实时性,低功耗由事件驱动,CPU 在无事件时自动进入空闲任务休眠。
4. CubeMX 配置要点
-
FreeRTOS:启用
USE_FREERTOS,选择 CMSIS_V2 接口。 - 时钟:确保 SysTick 与 FreeRTOS 时基不冲突,建议使用 TIM 作为 HAL 时基。
-
GPIO:按键引脚配置为
GPIO_EXTI,下降沿触发,使能 NVIC。 -
低功耗:在
FreeRTOSConfig.h中定义configPRE_SLEEP_PROCESSING和configPOST_SLEEP_PROCESSING,或使用vPortSuppressTicksAndSleep。
5. 完整代码示例
5.1 创建事件组与任务
#include "FreeRTOS.h"
#include "event_groups.h"
#include "task.h"
#include "timers.h"
#define KEY_EVENT_BIT (1 << 0)
#define TIMEOUT_EVENT_BIT (1 << 1)
EventGroupHandle_t xEventGroup;
TimerHandle_t xTimeoutTimer;
void PowerTask(void *argument);
void TimeoutCallback(TimerHandle_t xTimer);
void MX_FREERTOS_Init(void) {
xEventGroup = xEventGroupCreate();
xTimeoutTimer = xTimerCreate("Timeout", pdMS_TO_TICKS(5000), pdFALSE, 0, TimeoutCallback);
xTaskCreate(PowerTask, "Power", 256, NULL, 2, NULL);
}
5.2 按键中断回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == KEY_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(xEventGroup, KEY_EVENT_BIT, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
5.3 超时定时器回调
void TimeoutCallback(TimerHandle_t xTimer) {
xEventGroupSetBits(xEventGroup, TIMEOUT_EVENT_BIT);
}
5.4 电源管理任务
void PowerTask(void *argument) {
EventBits_t bits;
for (;;) {
bits = xEventGroupWaitBits(xEventGroup,
KEY_EVENT_BIT | TIMEOUT_EVENT_BIT,
pdTRUE, // 清除等待到的位
pdFALSE, // 任意一个满足即可
portMAX_DELAY);
if (bits & KEY_EVENT_BIT) {
// 按键处理
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
// 重置超时定时器
xTimerReset(xTimeoutTimer, 0);
}
if (bits & TIMEOUT_EVENT_BIT) {
// 进入低功耗:停止调度器,等待中断唤醒
vTaskSuspendAll();
__WFI();
xTaskResumeAll();
// 唤醒后重置定时器
xTimerReset(xTimeoutTimer, 0);
}
}
}
6. 注意事项
-
中断安全:
xEventGroupSetBitsFromISR必须使用FromISR版本,并检查返回值。 - 优先级:电源管理任务优先级不宜过高,避免阻塞其他任务。
-
低功耗唤醒:
__WFI()需配合 EXTI 中断,确保唤醒源已使能。 -
事件组位宽:默认 32 位,若需更多事件可配置
configUSE_16_BIT_TICKS为 0。 - 定时器服务任务:软件定时器回调运行在定时器服务任务中,不要执行阻塞操作。
-
调试:使用
uxTaskGetSystemState或 Tracealyzer 观察任务状态,确认低功耗时 CPU 确实空闲。
7. 总结
通过 FreeRTOS 事件组,我们将按键扫描与低功耗管理解耦:中断只负责置位事件,任务负责处理逻辑。这种“事件驱动 + 空闲休眠”的模型,既保证了按键响应的实时性,又显著降低了平均功耗。从裸机到 RTOS,关键不是 API 的堆砌,而是思维方式的转变——让事件流动起来,让 CPU 在无事可做时安心睡觉。