事件组的位图原理

信号量适合"一件事等另一件事",但当一个任务要同时等待多个条件、或者只关心其中某几个条件是否满足时,信号量就力不从心了。事件组(Event Group)用一段位图表达多路事件:每个 bit 代表一个事件标志,任务等待的是"某几个 bit 的组合"。

FreeRTOS 事件组内部是一个 24 位的整数(低 24 位可用),可以置位、清零、等待,还支持"等待全部"与"等待任一"两种模式。置位方与等待方解耦:任意任务都能置位,任意任务都能等待,事件不会像队列那样被"消费"掉,除非显式清零。

本文核心知识点
  • 位图每个 bit 是一个独立事件标志。
  • 组合等待全部满足或任一满足两种模式。
  • 守护任务软件定时器在专门任务中执行。
  • 组合事件组 + 定时器做超时仲裁。

典型场景:数据采集任务等待"传感器就绪"与"缓冲空闲"同时成立才启动采集;或者任何一路告警发生时立即进入处理流程。用位图表达这种多条件逻辑,比堆叠多个信号量清晰得多。

RTOS 事件组配图:深蓝渐变背景上发光芯片上方彩色光点组成小旗轮廓,右侧圆环时间节点与沙漏光影的扁平科技插画
事件组像一面旗帜矩阵:谁满足条件,谁就举起对应的旗。

事件组 API 实战

事件组的常用 API 就四个:创建、置位、等待、清零。置位用 xEventGroupSetBits,等待用 xEventGroupWaitBits,注意等待函数同时支持"等待后自动清零"和"等待全部 / 等待任一"的位掩码组合。

// 事件位定义
#define EVT_SENSOR_READY  (1 << 0)   // 传感器就绪
#define EVT_BUF_FREE      (1 << 1)   // 缓冲空闲
#define EVT_ALARM         (1 << 2)   // 告警触发

EventGroupHandle_t evt;
evt = xEventGroupCreate();

// 采集任务:等待两个条件同时成立
EventBits_t bits = xEventGroupWaitBits(
    evt, EVT_SENSOR_READY | EVT_BUF_FREE,
    pdTRUE, pdTRUE, portMAX_DELAY);
if ((bits & EVT_ALARM) == 0) {
  start_sample();  // 条件齐了才开始
}

置位方可以是一个中断服务函数:中断里用 xEventGroupSetBitsFromISR,并通过专用宏在守护任务中真正执行置位,避免在中断里做复杂操作。等待方使用超时时间,避免条件永不满足时永久挂起。

软件定时器守护任务

软件定时器不是硬件定时器,它由系统节拍(Tick)驱动,所有定时器回调都运行在同一个"定时器服务任务"(Timer Service Task)里。这意味着:回调里不能阻塞、不能做耗时操作,否则会拖累所有其它定时器。

  • 守护任务:默认优先级通常设得不高,回调按到期顺序排队执行。
  • 命令队列:创建、启动、停止、修改定时器都通过命令队列异步下发。
  • 单次与周期:单次定时器到点执行一次即停,周期定时器持续执行。
  • 精度:软件定时器的精度受系统节拍影响,不适合高精度计时。

软件定时器适合做超时判断、周期性扫描、状态轮询这类毫秒到秒级的任务。硬件定时器负责高精度 PWM 与输入捕获,两者分工明确,不要用软件定时器去凑微秒级时序。

软件定时器 API 实战

创建定时器时用 xTimerCreate 指定周期、是否自动重载以及回调函数;启动用 xTimerStart。回调里收到的是定时器句柄,可以用 pvTimerGetTimerID 取出业务 ID,区分是哪个定时器到期。

// 周期扫描定时器:每秒检查一次传感器状态
TimerHandle_t scan_timer;
void scan_cb(TimerHandle_t t) {
  uint32_t id = (uint32_t)pvTimerGetTimerID(t);
  if (id == 0) check_sensors();   // 定时任务 0
  else if (id == 1) check_link(); // 定时任务 1
}
scan_timer = xTimerCreate("scan", pdMS_TO_TICKS(1000),
                          pdTRUE, (void *)0, scan_cb);
xTimerStart(scan_timer, 0);  // 启动,立即生效

注意 xTimerStart 只是向命令队列发命令,定时器任务可能还没处理完,所以紧跟着查询 xTimerIsTimerActive 不一定立刻为真。需要确认已启动时,用 xTimerStart 配合短暂延时再检查,或者直接信任命令队列。