事件组的位图原理
信号量适合"一件事等另一件事",但当一个任务要同时等待多个条件、或者只关心其中某几个条件是否满足时,信号量就力不从心了。事件组(Event Group)用一段位图表达多路事件:每个 bit 代表一个事件标志,任务等待的是"某几个 bit 的组合"。
FreeRTOS 事件组内部是一个 24 位的整数(低 24 位可用),可以置位、清零、等待,还支持"等待全部"与"等待任一"两种模式。置位方与等待方解耦:任意任务都能置位,任意任务都能等待,事件不会像队列那样被"消费"掉,除非显式清零。
- 位图每个 bit 是一个独立事件标志。
- 组合等待全部满足或任一满足两种模式。
- 守护任务软件定时器在专门任务中执行。
- 组合事件组 + 定时器做超时仲裁。
典型场景:数据采集任务等待"传感器就绪"与"缓冲空闲"同时成立才启动采集;或者任何一路告警发生时立即进入处理流程。用位图表达这种多条件逻辑,比堆叠多个信号量清晰得多。
事件组 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 配合短暂延时再检查,或者直接信任命令队列。
事件组与定时器组合场景
组合使用是最实用的进阶玩法:软件定时器负责周期性"唤醒"逻辑,事件组负责多条件仲裁,两者配合能写出既省资源又清晰的并发逻辑。比如设备连接监控:连接事件、数据事件、超时事件全部映射到位图,管理任务一次等待。
// 连接超时监控:定时器到期置位 + 事件组等待
#define EVT_CONNECTED (1 << 0)
#define EVT_DATA (1 << 1)
#define EVT_TIMEOUT (1 << 2)
void timeout_cb(TimerHandle_t t) {
xEventGroupSetBits(evt, EVT_TIMEOUT); // 超时置位
}
// 管理任务:等待任一事件,分别处理
EventBits_t b = xEventGroupWaitBits(evt,
EVT_CONNECTED | EVT_DATA | EVT_TIMEOUT,
pdTRUE, pdFALSE, portMAX_DELAY);
if (b & EVT_TIMEOUT) { reconnect(); }
if (b & EVT_DATA) { handle_data(); }
if (b & EVT_CONNECTED) { on_connected(); }
这个模式把"周期性检查 + 多条件判断"收敛成一个等待点,管理任务不会被多个信号量同时唤醒然后争抢。超时兜底用软件定时器,业务事件用事件组,逻辑一眼能看懂。
常见误区与调试
- 回调里做耗时操作:会阻塞定时器服务任务,所有定时器一起卡顿。
- 在回调里调用会阻塞的 API:同样会拖垮守护任务,回调应只发信号或置位。
- 忘记等待后清零:事件一直挂着,下一次等待立即满足,逻辑错乱。
- 事件位不够用:明确 24 位上限,别把位号定义到 24 以上。
- 高精度计时用软件定时器:节拍抖动大,改回硬件定时器。
调试时可以先在事件组等待函数处加日志,输出当前位图值,一眼看出哪些事件已经满足、哪些一直没来。结合定时器回调日志,基本能把并发时序问题定位到具体环节。
工程实践总结
// 1. 多条件等待用事件组,一对一唤醒用信号量
// 2. 定时器回调保持短小,只做置位与发信号
// 3. 组合场景:定时器做超时,事件组做仲裁
// 4. 发布前检查:等待后是否清零、位号是否越界
事件组让并发条件变得可读,软件定时器让周期逻辑变得可控,两者合一是 RTOS 工程里最常用的组合拳。
把这两套机制用熟,任务间的协同就不再依赖一堆脆弱的全局标志,系统的可维护性会上一个台阶。