从裸机到 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 配置要点

  1. FreeRTOS:启用 USE_FREERTOS,选择 CMSIS_V2 接口。
  2. 时钟:确保 SysTick 与 FreeRTOS 时基不冲突,建议使用 TIM 作为 HAL 时基。
  3. GPIO:按键引脚配置为 GPIO_EXTI,下降沿触发,使能 NVIC。
  4. 低功耗:在 FreeRTOSConfig.h 中定义 configPRE_SLEEP_PROCESSINGconfigPOST_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 在无事可做时安心睡觉。