资讯动态

一文吃透 FreeRTOS 事件标志(Event Group):从概念到 7 个实战场景

发布时间:2026/8/28 8:03:55 来源:尧图企业网站定制
面向 STM32 / 嵌入式开发的 CMSIS-RTOS2 事件标志入门到进阶配套 QEMU 可复现实验。所有示例均来自本仓库的projects/06_event_flag可直接编译运行复现文中的输出。该仓库基于ST官方的开发板B-L475E-IOT01A构建以QEMU平台为基础无需购买硬件开发板即可学习FreeRTOS仓库地址https://gitcode.com/qingshan1206/easy-freertos在 FreeRTOS 的世界里任务之间要“打招呼”常见的工具是消息队列传数据和信号量计数/互斥。但当你要表达的是“多件事同时/任一旦发生就让我继续”这样的多条件同步时队列和信号量都会变得繁琐。这时的主角就是事件标志Event GroupARM CMSIS中的对应名称为Event Flags。这篇文章用一个完整的工程从“它是怎么存储的”讲到“7 个真实场景该怎么用”并把每一步的底层 FreeRTOS 函数都标注出来帮你不仅会用还能看懂 CMSIS-RTOS2 这层抽象到底做了什么。1. 什么是事件标志事件标志Event Flags本质是一个位掩码一个事件组里用 24 个 bitbit0-bit23bit24-bit31 保留各代表一种“事件”。某个 bit 是 0 表示“该事件没发生”是 1 表示“发生了”。一个事件组可以同时表达最多 24 个独立事件。关键点事件标志里不携带数据它只表达“是否发生”。所以它不适合传值但特别适合“多条件同步”。如图温度、湿度、气压、系统初始化完成……分别占一个 bit。任务可以同时等待多个事件并以OR任一满足或AND全部满足的方式组合。这正是消息队列和信号量难以优雅表达的场景。2. 事件标志的 API 与底层映射CMSIS-RTOS2 把 FreeRTOS 的“事件组Event Group”封装成了高层接口。本文用的这套 API 与底层原生函数一一对应其中值得注意的一点osEventFlagsNew/Set/Wait/Clear/Get/Delete都有对应的 FreeRTOS 实现而osEventFlagsGetName只有声明、没有实现——因为 FreeRTOS 的EventGroup_t结构体里根本没有存储 name 的字段只有uxEventBits、等待任务列表等。所以它无法返回一个有意义的名称。这也是下面所有演示都绕开它的原因。3. 基础操作创建 / 设置 / 读取 / 清除 / 删除先看最基础的一组操作。它们用宏把每个 bit 命名成有意义的标志#defineFLAG_TEMP_READY0x00000001UL/* bit 0 */#defineFLAG_HUMIDITY_READY0x00000002UL/* bit 1 */#defineFLAG_PRESSURE_READY0x00000004UL/* bit 2 */#defineFLAG_SYS_INIT_DONE0x00000008UL/* bit 3 */创建后先 Get 一次确认初始为 0接着 Set、再 Get、再 Clear观察值的变化osEventFlagsId_tefosEventFlagsNew(efAttr);/* - xEventGroupCreate() */uint32_tbitsosEventFlagsSet(ef,FLAG_TEMP_READY|FLAG_SYS_INIT_DONE);/* 返回值 0x00000009bit0、bit3 被置 1*/osEventFlagsClear(ef,FLAG_TEMP_READY);/* - xEventGroupClearBits() */这里有两个“返回值”细节非常容易踩坑osEventFlagsSet返回的是“设置后”的整体值osEventFlagsClear返回的是“清除前”的整体值。所以不要以为 Set/Clear 返回“成功与否”它们返回的是当时的事件组总值。上图完整还原了 demo 里的事件组值变化0x00 → Set(TEMP|SYS_INIT)→0x09 → Set(HUMIDITY)→0x0B → Clear(TEMP)→0x0A → Clear(ALL)→0x00。记住结论Set 只置位、Clear 只清零、Get 只读不清。4. 场景一用事件标志做任务间同步事件标志最简单的用法类似“通知—等待”一个任务阻塞等待某个标志另一个任务在某个时机把它置位。/* 发送者延迟后置位 */voidsignalSenderTask(void*arg){osEventFlagsId_tef(osEventFlagsId_t)arg;osDelay(150);osEventFlagsSet(ef,FLAG_EXT_TRIGGER);/* 唤醒等待者 */}/* 等待者无限期阻塞等待 */voidsignalWaiterTask(void*arg){osEventFlagsId_tef(osEventFlagsId_t)arg;uint32_tresultosEventFlagsWait(ef,FLAG_EXT_TRIGGER,osFlagsWaitAny,osWaitForever);LOGI(收到信号! result0x%08lX,(unsignedlong)result);}运行输出QEMU[等待者] 启动等待 FLAG_EXT_TRIGGER... [发送者] 启动将在 150 ticks 后发送信号... [发送者] 信号已发送 (FLAG_EXT_TRIGGER) [等待者] 收到信号! result0x00000020 ✓底层机制osEventFlagsWait调xEventGroupWaitBits()把任务挂入等待队列进入阻塞态osEventFlagsSet调xEventGroupSetBits()置位并唤醒满足条件的等待者。这比二值信号量更能“携带事件类型”——不同的 bit 代表不同类型的事件。5. 场景二多标志 OR 等待任一满足即唤醒三个传感器以不同速率采样50/120/250 ticks融合任务用OR模式等待只要有一个传感器就绪就立即处理。uint32_tresultosEventFlagsWait(ef,FLAG_ALL_SENSORS_READY,osFlagsWaitAny|osFlagsNoClear,500U);注意这里使用了osFlagsNoClear——等待返回后不清除已匹配的标志位。配合手动osEventFlagsClear来“追踪处理进度”避免一次把一个传感器的数据漏掉uint32_tnewly_ready(resultFLAG_ALL_SENSORS_READY)~processed;processed|newly_ready;osEventFlagsClear(ef,newly_ready);运行输出[融合OR] 启动等待任意传感器就绪... [温度] 就绪 (50 ticks) [融合OR] 收到温度数据 → 开始处理 [湿度] 就绪 (120 ticks) [融合OR] 收到湿度数据 → 开始处理 [气压] 就绪 (250 ticks) [融合OR] 收到气压数据 → 开始处理 [融合OR] 所有传感器数据处理完成OR 语义谁先就绪就先处理谁适合“多路数据采集任一通道有新数据就消耗”的场景优点是响应快、低延迟。6. 场景三多标志 AND 等待全部满足才唤醒同样是三个传感器融合任务这次用AND模式必须等三路数据全部到齐才开始融合。uint32_tresultosEventFlagsWait(ef,FLAG_ALL_SENSORS_READY,osFlagsWaitAll,osWaitForever);默认不指定osFlagsNoClear因此唤醒后会自动清除匹配位。等待期间温度虽然 50 ticks 就绪了但融合任务仍要等最慢的气压250 ticks——这就是 AND 的“等最慢的那一个”。运行输出[融合AND] 启动等待全部传感器就绪... [温度] 就绪 (50 ticks) [湿度] 就绪 (120 ticks) [气压] 就绪 (250 ticks) [融合AND] 三路传感器数据到齐 ✓ [融合AND] result0x00000007 → 开始融合处理 [融合AND] 处理后标志位 0x00000000 (自动清除)AND vs OR 一句话总结ORosFlagsWaitAnyANDosFlagsWaitAll触发条件任意一个就绪全部就绪等待时长取决于最快取决于最慢适用场景低延迟、分步处理批量、需要完整数据7. 场景四超时与非阻塞等待osEventFlagsWait的timeout参数决定了“等多久”。demo 里用三种取值对比了它们的行为osEventFlagsWait(ef,FLAG_TEMP_READY,osFlagsWaitAny,0U);/* 非阻塞 */osEventFlagsWait(ef,FLAG_EXT_TRIGGER,osFlagsWaitAny,100U);/* 有限等待 */osEventFlagsWait(ef,FLAG_EXT_TRIGGER,osFlagsWaitAny,osWaitForever);/* 永久 */timeout 0非阻塞。条件不满足立刻返回错误码osErrorResourcetimeout N最多等 N ticks超时返回osErrorTimeouttimeout osWaitForever永久等待直到满足或事件组被删除。resultosEventFlagsWait(ef,FLAG_TEMP_READY,osFlagsWaitAny,0U);if((int32_t)result0)/* 返回值最高位置 1 表示发生错误 */LOGI(返回错误osErrorResource);经验给等待加一个上限往往更安全——它能防止“某个事件永远不会来”导致任务永久卡死。8. 场景五osFlagsNoClear——保留标志位默认情况下osEventFlagsWait返回时会自动清除已匹配的标志位相当于“消费”掉这次事件。而加上osFlagsNoClear后标志位会保留下一次 Wait 仍会立即返回。resultosEventFlagsWait(ef,FLAG_TEMP_READY,osFlagsWaitAny,0U);/* 结束后 Get 值为 0默认自动清除 */resultosEventFlagsWait(ef,FLAG_HUMIDITY_READY,osFlagsWaitAny|osFlagsNoClear,0U);/* 结束后 Get 值仍为 FLAG_HUMIDITY_READY标志位保留 */适用场景广播多个任务等待同一个标志每个都要能看到它重复检查同一个任务需要多次读取同一标志。注意使用osFlagsNoClear时一定要有别的机制去清除这些位否则每次 Wait 都会立即成功永远等不到“新事件”。另外CMSIS 规范里指出osFlagsNoClear与“osFlagsWaitAll 非零超时”组合是未定义行为应避免。9. 场景六栅栏同步Barrier——事件标志的高光时刻栅栏Barrier是另一种经典的多任务同步N 个任务各自完成准备后在“栅栏点”互相等待所有人都到齐才一起继续。用信号量实现要额外维护计数器逻辑繁琐而事件标志的AND 等待一句就能搞定。/* 每个任务先置自己的位再 WaitAll 等待其他人的位 */osEventFlagsSet(ef,BARRIER_BIT(id));/* 声明自己到了 */osEventFlagsWait(ef,BARRIER_TASK_BITS,osFlagsWaitAll|osFlagsNoClear,osWaitForever);/* 等所有人 */osmFlagsWaitAll会等到BARRIER_TASK_BITS中所有位都被置 1也就是所有任务都到达栅栏。最后一个到达的任务会“触发”其余所有等待者一起被唤醒。运行输出[任务0] 开始准备工作... [任务1] 开始准备工作... [任务2] 开始准备工作... [任务3] 开始准备工作... [任务0] 准备完成在栅栏处等待其他任务... [任务1] 准备完成在栅栏处等待其他任务... [任务2] 准备完成在栅栏处等待其他任务... [任务3] 准备完成在栅栏处等待其他任务... [任务3] 全部到齐通过栅栏 ✓ [任务0] 全部到齐通过栅栏 ✓ [任务1] 全部到齐通过栅栏 ✓ [任务2] 全部到齐通过栅栏 ✓这里用osFlagsNoClear保留标志位正是为了让每个任务都能看到“所有人都到了”。10. 场景七主从任务通信——用事件标志传命令事件标志不仅能表达“事件是否发生”还可以编码离散命令。demo 用 bit16~19 表示 4 种命令主控任务依次发出从属任务用WaitAny循环接收。#defineCMD_START_ACQ0x00010000UL/* bit16 */#defineCMD_STOP_ACQ0x00020000UL/* bit17 */#defineCMD_CALIBRATE0x00040000UL/* bit18 */#defineCMD_SLEEP0x00080000UL/* bit19 *//* 主控发新命令前先清空旧命令防止读到过期命令 */osEventFlagsClear(ef,CMD_ALL_MASKS);osEventFlagsSet(ef,CMD_START_ACQ);/* 从属OR 循环等待任意命令 */uint32_tresultosEventFlagsWait(ef,CMD_ALL_MASKS,osFlagsWaitAny,osWaitForever);if(resultCMD_START_ACQ){...}elseif(resultCMD_CALIBRATE){...}运行输出节选[从属] 就绪等待主控命令... [主控] 发送 CMD_START_ACQ [从属] 收到 CMD_START_ACQ → 启动数据采集 [主控] 发送 CMD_CALIBRATE [从属] 收到 CMD_CALIBRATE → 执行校准流程 [主控] 发送 CMD_STOP_ACQ [从属] 收到 CMD_STOP_ACQ → 停止数据采集 [主控] 发送 CMD_SLEEP [从属] 收到 CMD_SLEEP → 进入低功耗模式...相比于消息队列用事件标志传命令更轻量——不用复制数据只需要置/清一个 bit。互相排斥的命令还可以共用一个 bit 实现“翻转toggle”但为了可读性通常还是每个命令一个 bit。11. 总结与最佳实践什么时候用事件标志多条件同步要同时等好几个事件OR / AND——这是事件标志的主场事件“有没有发生”而不是要传具体数值轻量命令/状态切换一条命令一个 bit开销极小。什么时候别用需要传递数据→ 用消息队列需要资源计数/互斥→ 用信号量 / 互斥锁只是一对一的简单唤醒 → 二值信号量或任务通知Task Notify更简洁。容易踩的坑osEventFlagsClear返回的是清除前的值不要拿它当“成功与否”判断。osFlagsNoClearosFlagsWaitAll 非零超时是未定义行为避开。用了osFlagsNoClear就一定要另有机制清位否则 Wait 永远立即返回。错误检测返回值最高位为一(int32_t)result 0表示发生了错误如osErrorResource、osErrorTimeout、osErrorParameter要留给处理。osEventFlagsGetName在 FreeRTOS 封装中未实现EventGroup_t无 name 字段不要指望拿来描述事件组。如何在本地复现# 构建 06_event_flag 工程cmake-S.-Bbuild-DBUILD_PROJECT06_event_flagcmake--buildbuild# 手动运行不建议在 QEMU 中运行semihosting 输出到终端./tools/qemu/qemu.sh# 自动运行建议在vscode中点击调试按钮进行调试和运行# VSCode - F5

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价