4、事件标志组置位操作:xEventGroupSetBits()、xEventGroupSetBitsFromISR()、置位原理
好,咱们接着聊事件标志组的核心操作——置位。
说白了,置位就是给某个事件标志位「打个勾」。你想想看,任务A在等某个条件成立,任务B或者中断服务程序把对应的位设成1,任务A就被唤醒了。这个机制在实战中非常常用,我个人几乎每个项目都会用到。
4.1 xEventGroupSetBits():任务级置位
这个函数是我们在任务代码里用的。原型长这样:
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet
);
参数就两个:事件标志组的句柄,以及你要置哪些位。第二个参数是个位掩码,比如你想置位第0位和第3位,就传 0x09(二进制 0000 1001)。
返回值是调用函数那一刻的事件标志组值。嗯,这里要注意,返回值不是设置之后的值,而是设置之前的值。我刚开始用的时候也搞混过。
核心要点:xEventGroupSetBits() 不能在中断服务程序里调用。如果你在ISR里需要置位,请用它的中断版本。
来看个实际例子。我在做一款工业数据采集器时,需要等三个传感器都采集完毕才能做数据融合。代码大概是这样的:
// 定义事件位
#define SENSOR1_READY (1 << 0) // 位0
#define SENSOR2_READY (1 << 1) // 位1
#define SENSOR3_READY (1 << 2) // 位2
// 传感器采集任务
void vSensor1Task(void *pvParameters) {
for (;;) {
// 采集传感器1数据
ReadSensor1();
// 置位,通知数据已就绪
xEventGroupSetBits(xDataEventGroup, SENSOR1_READY);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 类似地,Sensor2和Sensor3任务也各自置位
你看,每个传感器采集完就置自己的位,简单直接。
4.2 xEventGroupSetBitsFromISR():中断级置位
这个函数是给中断用的。原型:
BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken
);
多了一个参数 pxHigherPriorityTaskWoken。这个参数很重要,它告诉调用者:有没有更高优先级的任务因为这次置位而被唤醒?如果有,你需要在中断退出后做一次上下文切换。
我曾经踩过的坑:有一次在ISR里调用了xEventGroupSetBitsFromISR(),但忘了检查pxHigherPriorityTaskWoken。结果被唤醒的高优先级任务一直没得到执行,系统响应变得很慢。排查了半天才发现是这个问题。所以,千万别偷懒,该检查的一定要检查。
正确的用法是这样的:
void vTimerISR(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 在中断中置位
xEventGroupSetBitsFromISR(
xDataEventGroup,
TIMER_EXPIRED,
&xHigherPriorityTaskWoken
);
// 如果有更高优先级的任务被唤醒,请求切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
为什么中断版本和任务版本不一样?因为中断上下文里不能阻塞,不能等待。所以中断版本实际上是把置位请求放到了一个「守护任务」的队列里,由守护任务在任务上下文中真正执行置位操作。
4.3 置位原理:到底发生了什么?
好,咱们深入一点,看看置位操作内部是怎么工作的。
事件标志组本质上就是一个 EventBits_t 类型的变量,通常是32位的无符号整数。每个位代表一个事件。
置位操作的核心逻辑,说白了就是一次「按位或」运算:
// 伪代码,实际实现更复杂
eventGroup->uxEventBits |= uxBitsToSet;
但FreeRTOS不会这么简单粗暴。它还要做几件重要的事:
- 关中断:防止在置位过程中被中断打断,造成数据不一致
- 执行按位或:把要置的位设成1
- 检查等待任务:遍历所有等待在这个事件标志组上的任务,看看有没有任务的条件满足了
- 唤醒任务:如果有任务的条件满足,把它从阻塞态移到就绪态
- 开中断:恢复中断状态
这里有个细节值得注意:置位操作会一次性检查所有等待的任务。不管你是用「与」模式还是「或」模式等待,都会检查一遍。所以如果你有多个任务在等不同的事件组合,一次置位可能唤醒多个任务。
我的经验:在设计事件标志位时,尽量让每个位代表一个独立且明确的事件。不要把多个含义混在一个位里。比如「数据就绪」和「错误发生」最好用不同的位。这样代码可读性高,调试也方便。
4.4 置位操作的注意事项
| 场景 | 使用函数 | 注意事项 |
|---|---|---|
| 任务中置位 | xEventGroupSetBits() | 不能在ISR中使用 |
| ISR中置位 | xEventGroupSetBitsFromISR() | 必须检查pxHigherPriorityTaskWoken |
| 置位后立即读取 | 返回值或xEventGroupGetBits() | 注意返回值是置位前的值 |
嗯,这里还要提一点。有些新手会问:我能不能在中断里直接调用 xEventGroupSetBits()?答案是:绝对不行。这个函数内部会调用可能引起任务切换的API,在中断里调用会导致不可预知的行为。我见过有人因为这个导致系统死锁,排查起来特别痛苦。
另外,置位操作本身是原子性的,你不用担心多个任务同时置位会出问题。FreeRTOS内部已经用关中断的方式保证了这一点。
好了,关于置位操作就聊这么多。下一节咱们聊聊怎么等待这些事件位,也就是「取位」操作。到时候你会看到,置位和取位是如何配合起来,构建出灵活的事件驱动机制的。