Chapter 06 · FreeRTOS 实时操作系统

事件组 Event Group

用一组位标志实现"等待多个条件"的同步:AND/OR 组合等待、多任务同步点,一个任务醒来处理多路事件。

课程进度50%

关键概念

事件组 (Event Group)
一个包含多个二进制位标志的内核对象,每个位代表一个独立事件(如"WiFi 连上""数据就绪""校准完成")。多个任务可同时等待其中的位以特定组合被置位。它擅长表达"需要 A 和 B 同时满足"或"A 或 B 任一发生"这类多条件同步,是信号量无法直接表达的。
事件位 (Event Bits / Flags)
事件组中的每一个 bit。类型 EventBits_t 通常为 24 位可用(配置 configUSE_16_BIT_TICKS 会变 8 位),高 8 位被内核保留。用宏定义清晰命名,如 #define BIT_WIFI (1<<0),避免魔法数字。
xEventGroupSetBits
置位一个或多个事件位(按位或),表示对应事件已发生。置位会检查所有等待该组的任务,凡满足其等待条件的都被解除阻塞。ISR 中不能直接调用,须用 xEventGroupSetBitsFromISR,且它通过定时器服务任务延迟执行(返回 pdPASS 表示成功挂到 daemon)。
xEventGroupWaitBits
等待指定位组合的核心 API,四个关键参数:uxBitsToWaitFor(关心哪些位)、xWaitForAllBits(pdTRUE=AND 全满足 / pdFALSE=OR 任一满足)、xClearOnExit(返回前是否清除这些位)、xTicksToWait(超时)。返回等待结束时的位值。
AND 等待 vs OR 等待
AND(xWaitForAllBits=pdTRUE):所有指定位都被置位才唤醒,用于"多个前置条件全部就绪才能开始"。OR(pdFALSE):任意一个指定位被置位就唤醒,用于"任一事件到来都要处理"。这是事件组相对信号量最独特的能力。
xClearOnExit(退出时清除)
xEventGroupWaitBits 的参数。pdTRUE 时,条件满足、任务被唤醒后自动清除所关心的位(相当于"消费"了事件),适合边沿触发语义;pdFALSE 时保留位,需手动 xEventGroupClearBits,适合表示持久状态(如"WiFi 一直在线")。多任务同时等待时用 pdTRUE 要小心竞争。
任务同步点 (Task Sync / Rendezvous)
xEventGroupSync 实现"多个任务互相等待、到齐了一起放行"的屏障(barrier)。每个任务置自己的位并等待所有指定位都置位,最后到达的那个任务触发全体同时解除阻塞并自动清位。用于多任务分阶段协同,如"三个初始化任务都完成后再进入主循环"。
Daemon 延迟执行(ISR 置位)
在中断里给事件组置位不能直接改内核状态,xEventGroupSetBitsFromISR 会把置位请求发给定时器服务任务(Timer Daemon,见第 7 章)异步执行。因此需 configUSE_TIMERS=1,且置位并非在中断里立刻生效,而是在 daemon 得到运行时。这是事件组与信号量在 ISR 行为上的重要差异。

为什么需要事件组:信号量做不到的"组合等待"

设想一个联网任务:必须等到 WiFi 已连接 时间已同步 配置已加载 三件事全部就绪,才能开始上报。用三个信号量要写复杂的嵌套等待逻辑,用事件组一行 AND 等待即可:

事件组:一组位标志表达多条件同步 ═══════════════════════════════════════════════════════════ EventGroupHandle_t eg ┌─ bit0 WIFI_UP ─┐ ├─ bit1 TIME_OK ─┤ 24 位可用 各任务/中断置位 ───► ├─ bit2 CFG_OK ─┤ └─ bit3 ... ─┘ │ ┌────────────────────┴────────────────────┐ AND 等待 WaitBits(WIFI|TIME|CFG, pdTRUE, ...) → 三位全置位才唤醒(全部前置就绪) OR 等待 WaitBits(WIFI|TIME|CFG, pdFALSE, ...) → 任一位置位就唤醒(任意事件到来)

AND 等待:全部条件就绪

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

#define BIT_WIFI  (1UL << 0)
#define BIT_TIME  (1UL << 1)
#define BIT_CFG   (1UL << 2)
#define ALL_READY (BIT_WIFI | BIT_TIME | BIT_CFG)

EventGroupHandle_t g_sysEvents;

void ReportTask(void *arg) {
  for (;;) {
    /* 等三位全部置位(AND),超时 5s;退出时清除这些位 */
    EventBits_t bits = xEventGroupWaitBits(
        g_sysEvents,
        ALL_READY,          /* 关心的位 */
        pdTRUE,             /* xClearOnExit:清除 */
        pdTRUE,             /* xWaitForAllBits:AND */
        pdMS_TO_TICKS(5000));

    if ((bits & ALL_READY) == ALL_READY) {
      start_reporting();   /* 三者齐备,开始上报 */
    } else {
      printf("not ready in 5s, got 0x%lx\r\n", bits);
    }
  }
}

/* 其他任务在各自完成时置位 */
void on_wifi_up(void) { xEventGroupSetBits(g_sysEvents, BIT_WIFI); }
void on_time_ok(void) { xEventGroupSetBits(g_sysEvents, BIT_TIME); }
void on_cfg_ok(void)  { xEventGroupSetBits(g_sysEvents, BIT_CFG);  }

OR 等待:任一事件都处理

#define EVT_BTN   (1UL << 0)
#define EVT_ALARM (1UL << 1)
#define EVT_TIMER (1UL << 2)
#define ANY_EVT   (EVT_BTN | EVT_ALARM | EVT_TIMER)

void UiTask(void *arg) {
  for (;;) {
    EventBits_t b = xEventGroupWaitBits(
        g_sysEvents, ANY_EVT,
        pdTRUE,      /* 清除 */
        pdFALSE,     /* OR:任一即可 */
        portMAX_DELAY);
    if (b & EVT_BTN)   on_button();
    if (b & EVT_ALARM) on_alarm();
    if (b & EVT_TIMER) on_timer();
  }
}
检查返回值,而不是假设

xEventGroupWaitBits 返回的是"等待结束那一刻的位值"。超时返回时位可能并未满足条件,因此务必用 (bits & MASK) == MASK(AND 场景)或 bits & MASK(OR 场景)显式判断,不要只凭"函数返回了"就认为条件成立。

任务同步点:多任务集合屏障

#define SYNC_A (1UL << 0)
#define SYNC_B (1UL << 1)
#define SYNC_C (1UL << 2)
#define SYNC_ALL (SYNC_A | SYNC_B | SYNC_C)

/* 三个初始化任务,各自跑完在同步点集合,全到齐再放行 */
void InitTaskA(void *arg) {
  init_hardware();
  /* 置自己的位 SYNC_A,等待所有位到齐 */
  xEventGroupSync(g_sysEvents, SYNC_A, SYNC_ALL, portMAX_DELAY);
  run_phase2();   /* 三任务同时抵达此处 */
  vTaskDelete(NULL);
}
/* InitTaskB 用 SYNC_B,InitTaskC 用 SYNC_C,逻辑相同 */

事件组 vs 队列 vs 信号量:如何选

需求推荐说明
等待"多个条件全就绪"(AND)事件组信号量无法组合
等待"任一事件发生"(OR)事件组 / 队列集事件组更简洁
多任务集合屏障事件组 xEventGroupSync唯一直接支持
传递数据内容队列事件组只有位,不带数据
单一事件通知 + 计数信号量 / 任务通知更轻量
单接收者的位标志任务通知 eSetBits省一个内核对象
事件组不携带数据,也不计数

事件位只有"置位/清零"两种状态,不能像队列那样承载消息内容,也不像计数信号量那样累计次数——同一个位置位两次和一次效果相同。若需要"发生了几次"或"具体是什么数据",必须搭配队列使用:事件组通知"有事发生",队列传递"具体内容"。

本章小结

事件组是一组二进制事件位EventBits_t,通常 24 位可用),每位代表一个独立事件。核心能力是组合等待xEventGroupWaitBitsxWaitForAllBits 选择 AND(全满足)或 OR(任一满足),这是信号量做不到的。xClearOnExit 决定唤醒后是否自动清位(边沿 vs 状态语义)。xEventGroupSetBits 置位并唤醒满足条件的等待者;ISR 里用 xEventGroupSetBitsFromISR,它通过定时器 Daemon 延迟执行(需 configUSE_TIMERS=1)。xEventGroupSync 实现多任务集合屏障,全体到齐才一起放行。事件组不带数据也不计数,需传内容时配合队列。选型:多条件 AND/OR 用事件组,多任务屏障用 Sync,传数据用队列,单接收者位标志用任务通知 eSetBits。务必用 (bits & MASK)==MASK 校验返回值。