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函数,到时候我会结合代码实例,手把手教你用起来。