1. 事件标志组基础概念
各位同学,咱们今天聊聊事件标志组。说实话,我刚接触FreeRTOS那会儿,总觉得这东西跟信号量差不多,后来踩了坑才明白——完全是两码事。
1.1 什么是事件标志组
事件标志组,说白了就是一个整数。但这个整数不一般,它的每一个bit都可以独立使用。你想想看,一个32位的整数,就能表示32个独立的事件状态。
我习惯把它想象成一个控制面板,上面有32个指示灯。每个灯亮或不亮,代表某个事件是否发生。比如bit0亮了,表示数据接收完成;bit1亮了,表示定时器超时;bit2亮了,表示按键被按下。
核心定义:事件标志组是FreeRTOS提供的一种同步机制,允许任务通过位操作来等待多个事件的发生。
在FreeRTOS里,事件标志组的数据类型是 EventGroupHandle_t。创建时用 xEventGroupCreate(),操作时用 xEventGroupSetBits() 和 xEventGroupWaitBits()。
// 创建一个事件标志组
EventGroupHandle_t xEventGroup = xEventGroupCreate();
// 设置bit0和bit2
xEventGroupSetBits(xEventGroup, (1 << 0) | (1 << 2));
// 等待bit0或bit1被设置
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup, // 事件标志组句柄
(1 << 0) | (1 << 1), // 等待的bit位
pdTRUE, // 退出时清除这些bit
pdFALSE, // 任意一个满足即可
portMAX_DELAY // 无限等待
);
1.2 与信号量的区别
很多初学者会问:信号量也能做同步,为啥还要事件标志组?
嗯,这里要注意。信号量是「计数」的,它关心的是资源还有多少个。而事件标志组是「位标记」的,它关心的是哪些事件发生了。
| 对比项 | 信号量 | 事件标志组 |
|---|---|---|
| 本质 | 计数器 | 位掩码 |
| 等待条件 | 计数值 > 0 | 指定位被置1 |
| 多事件等待 | 不支持 | 支持(与/或) |
| 事件保留 | 取走后计数减1 | 可手动清除 |
| 典型场景 | 资源管理、任务同步 | 多条件触发 |
举个例子你就明白了。假设你有个设备,需要同时满足「温度达标」和「压力正常」两个条件才能启动。用信号量你得搞两个信号量,还得嵌套等待,代码写起来很别扭。但用事件标志组,一行 xEventGroupWaitBits 就搞定了。
我的经验:信号量适合「谁先来谁先用」的场景,事件标志组适合「等齐了再干」的场景。选哪个,看你想要的是「计数」还是「位标记」。
1.3 应用场景分析
我在实际项目中用过不少事件标志组,给你说说几个典型场景。
场景一:多条件触发
这是最常见的用法。一个任务需要等待多个事件同时发生,或者任意一个发生。比如智能家居的安防系统,需要门磁、红外、烟雾三个传感器都正常,才进入布防模式。
// 定义事件位
#define EVENT_DOOR (1 << 0) // 门磁正常
#define EVENT_IR (1 << 1) // 红外正常
#define EVENT_SMOKE (1 << 2) // 烟雾正常
// 等待三个条件都满足
xEventGroupWaitBits(
xEventGroup,
EVENT_DOOR | EVENT_IR | EVENT_SMOKE,
pdTRUE, // 清除所有位
pdTRUE, // 全部满足才返回
pdMS_TO_TICKS(5000) // 5秒超时
);
场景二:任务同步
多个任务各自完成一部分工作,最后汇总。比如数据采集系统,一个任务采温度,一个任务采湿度,一个任务采气压。等三个都采完了,主任务才开始处理。
避坑指南:我曾经在一个项目里,用事件标志组做任务同步,结果忘了设置 pdTRUE 清除位,导致第二次等待时直接返回了。嗯,从那以后我每次都会检查清除标志。
场景三:状态通知
有时候你不需要等待,只是想知道某个事件是否发生过。事件标志组的位可以一直保持,直到你手动清除。这比信号量灵活多了。
// 中断中设置事件位
void vTimerCallback(TimerHandle_t xTimer) {
// 定时器超时,设置bit3
xEventGroupSetBitsFromISR(xEventGroup, (1 << 3), NULL);
}
// 任务中查询事件
void vCheckTask(void *pvParameters) {
EventBits_t uxBits;
for(;;) {
uxBits = xEventGroupGetBits(xEventGroup);
if(uxBits & (1 << 3)) {
// 定时器超时事件发生过
// 处理逻辑...
xEventGroupClearBits(xEventGroup, (1 << 3));
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
1.4 使用注意事项
最后说几个容易踩的坑。
- 位数量有限:FreeRTOS的事件标志组最多支持24个用户位(高8位被系统保留)。别想着用32个,实际只有24个。
- 中断中使用:在ISR里设置事件位,一定要用
xEventGroupSetBitsFromISR(),别用普通版本。我见过有人在这上面栽跟头。 - 超时处理:等待事件时一定要设超时,别用
portMAX_DELAY死等。万一事件永远不来,任务就卡死了。 - 位冲突:多个任务同时设置同一个位,不会冲突。但如果你用同一个位表示不同的事件,那逻辑就乱了。
我的习惯:用宏定义给每个事件位起个名字,别直接用数字。代码可读性会好很多,也方便后期维护。
好了,事件标志组的基础概念就聊到这儿。说白了,它就是一块32位的「事件黑板」,每个bit代表一件事。跟信号量比,它更适合多条件等待的场景。下一节咱们聊聊事件标志组的API函数,到时候我会结合代码实例,手把手教你用起来。