夜雨聆风学习资料网

ARTICLE · 1020052

FreeRTOS 事件组源码剖析:一个任务等三个传感器的的状态,谁在记着到了几个?

FreeRTOS 事件组源码剖析:一个任务等三个传感器的的状态,谁在记着到了几个?

温度、湿度、气压三个传感器都采集完成才允许计算——三个条件"按位与"组合等待,用信号量怎么做?三个二值信号量、三次 Take,而且没法在一个阻塞点里同时等。反过来,"任一报警源触发就响应"的"按位或"语义,信号量干脆表达不出来。想向多个任务广播同一件事?一次 Give 只能唤醒一个任务,剩下的一直睡。

FreeRTOS 给的答案是事件组——xEventGroupWaitBits 一个调用,按位与、按位或、广播、原子清位全部搞定。但这个答案背后藏着一个和前几篇都不同的秘密:事件组连队列都不是,它就是一个位变量加一条等待链表。信号量是掏空数据区的队列,队列集是装指针的队列,事件组干脆把队列的骨架拆得只剩最后两块:一个状态值 + 一条挂任务的链表。

这篇文章把 FreeRTOS V11.1.0 的 event_groups.c 拆开,看 24 个可用事件位怎么被高 8 位控制位"劫持"、等待条件为什么存在每个任务自己身上而不是事件组里、SetBits 怎么遍历链表广播唤醒、汇合同步 Sync 怎么把"置位+等待"打包成原子操作。读完你会发现:事件组的所有坑(SetBits 返回值不含刚设的位、ISR 不能直接置位、位只有 24 个),都能从"位变量 + 链表 + 控制位"这套结构里推出来。

内核版本:FreeRTOS V11.1.0(Cortex-M3)本工程 configUSE_EVENT_GROUPS = 1,且 configUSE_TRACE_FACILITY / INCLUDE_xTimerPendFunctionCall / configUSE_TIMERS 全开

1. 概述

1.1 事件组是什么

FreeRTOS 事件组是一个 EventBits_t 位变量 + 一条等待任务链表,没有环形缓冲区、没有读写指针、没有队列锁——全部家当就这两个字段:

/* event_groups.c: EventGroup_t 结构体 */typedef struct EventGroupDef_t{    EventBits_t uxEventBits;              /* 事件位本体 */    List_t xTasksWaitingForBits;          /* 等待这些位的任务链表 */    ...} EventGroup_t;

每一位(事件位/事件标志)代表一个布尔事件:1=已发生,0=未发生;位的含义由程序员自行分配(如 BIT0=温度就绪、BIT1=按键按下)。

核心特性:位变量表状态,链表管等待。 改状态是改一个整数,等状态是把自己挂上链表睡觉——所有行为都是这两句话的组合。

1.2 可用位只有 24 个:高 8 位用于内核

本工程 configTICK_TYPE_WIDTH_IN_BITS = TICK_TYPE_WIDTH_32_BITSEventBits_t 是 32 位无符号数。但高 8 位(0xFF000000)被内核征用作控制位(event_groups.h 宏定义区):

/* event_groups.h: 32 位 EventBits_t 时的控制位定义 */#define eventCLEAR_EVENTS_ON_EXIT_BIT    ( ( uint32_t ) 0x01000000U )  /* bit24: 唤醒后清位 */#define eventUNBLOCKED_DUE_TO_BIT_SET    ( ( uint32_t ) 0x02000000U )  /* bit25: 被置位唤醒标记 */#define eventWAIT_FOR_ALL_BITS           ( ( uint32_t ) 0x04000000U )  /* bit26: 按位与 */#define eventEVENT_BITS_CONTROL_BYTES    ( ( uint32_t ) 0xff000000U )  /* 高 8 位: 内核保留 */

所以程序员可用的事件位只有 bit0~bit23,共 24 个。API 入口处都有 configASSERT( ( uxBitsToWaitFor & eventEVENT_BITS_CONTROL_BYTES ) == 0 )——等到/设置 ≥bit24 的位直接断言失败。

1.3 API 家族

API
作用
关键参数
xEventGroupCreate()
创建(从 FreeRTOS 堆分配,约 40B)
xEventGroupSetBits(组, 位)
置位并广播唤醒
返回清除后的组值
xEventGroupWaitBits(组, 位, xClearOnExit, xWaitForAllBits, 超时)
阻塞等待位组合
按位或 / 按位与,原子清位
xEventGroupSync(组, 置位, 等待, 超时)
汇合点同步
置位+等待一次原子完成
xEventGroupClearBits(组, 位)
手动清位(多任务下慎用)
非原子场景见坑 3
xEventGroupSetBitsFromISR(...)
ISR 版置位
延迟到定时器任务执行

1.4 和信号量 / 队列集的定位对比

二值信号量
计数信号量
队列集
事件组
等待语义
单个事件
"数到齐"(连 Take N 次)
任一成员(N 选 1)
任意位的与/或组合
事件带身份吗
无(只有 0/1 状态,不区分来源)无(计数只有数量没有来源)
有(返回成员句柄)
有(哪一位=哪个事件)
唤醒方式
单播(一次 Give 醒一个)
单播
单播
广播(一次 SetBits 醒全部命中者)
计数能力
0/1
N
成员自带
无(位只有 0/1)
ISR 直接操作
不可
(须走 FromISR 转投递)

用计数信号量为什么解决不掉(开篇三个痛点的逐条对照):

  1. 计数没有身份——根本原因
    :计数信号量只能表达"到了几个",不能表达"是哪几个到了"。三个传感器各 Give 一次、计数到 3,消费者确实知道"到齐了",但无从得知是哪三位——更糟的是假冒凑齐它识别不了:温度传感器连 Give 两次、湿度没给,计数照样到 3,事件组的按位与能当场识破(BIT1 缺着),计数不能;
  2. 广播做不到
    :一次 Give 只唤醒一个等待者(单播),第二个消费者 Take 时计数已耗尽——事件组的 while 遍历整条链表全唤醒,这是源码结构差异,不是参数差异;
  3. "全部到齐"要连 Take N 次
    :单消费者场景下这能凑合工作(不存在插队),但每次 Take 是独立的阻塞调用——中间任何一次带着 0 超时查询、或被信号量侧的计数上限反噬(计数信号量满后 Give 失败),凑数逻辑就乱了。事件组的按位与是一次调用一个阻塞点原子判定,没有这些缝。

一句话选型:单个事件用任务通知或二值信号量;纯资源池计数用计数信号量;N 选 1 的"哪个来了"要句柄就用队列集;位组合逻辑(与/或/广播)才是事件组的主场

2. 机制核心:位变量 + 链表 + 控制位

2.1 结论先行

事件组的骨架仍然是"改状态 → 挂链表 → 唤醒"三段式,但与队列家族有一处根本差异:保护手段。队列/信号量用关中断(taskENTER_CRITICAL)保护临界区;事件组的主体操作用挂起调度器(vTaskSuspendAll——任务不许切换,中断仍然响应(tick 被记入 xPendedTicks、恢复时补处理,所以不会丢)。个别固定短路径(如超时醒来后的重测,见 4.2 节)仍用关中断——短路径正是关中断的适用场景。

为什么主体不用关中断?事件组的操作耗时不确定:SetBits 可能要遍历整条链表唤醒任意多个任务,关中断的时长无法预估,关中断区只适合"拷个数据、改个计数"这种固定短路径。挂起调度器允许中断继续响应,代价是——中断不许直接碰事件组(链表正在被任务侧遍历时,ISR 若插进来改链表就乱了)。这就是 ISR 禁止直接调用 xEventGroupSetBits 的机制根源,也是 FromISR 版本要绕道定时器任务的原因(4.6 节)。

2.2 等待条件存在哪:任务自己身上

这是事件组最巧的设计。xEventGroupWaitBits 挂链表时,把"我在等哪些位 + 我的清位偏好 + 与/或偏好"打包塞进当前任务自己的事件列表项值

/* 等待位 + 控制位打包, 存进任务的事件列表项 */vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ),                                ( uxBitsToWaitFor | uxControlBits ),                                xTicksToWait );

事件组自己不登记任何等待条件。于是同一个事件组、同一个位上,可以同时挂着口味完全不同的等待者:有的等 BIT0(或),有的等 BIT0|BIT1(与),有的要清位有的不清——SetBits 遍历时各自用各自的条件独立判断,互不干扰。

对比:队列的等待者只关心"空/非空"一种条件,所以等待者无差别挂在同一条链表上就行;事件组的等待条件是每个任务私有的。

2.3 唤醒时怎么区分"命中"还是"超时"

任务醒来后需要知道自己是被置位唤醒还是睡到超时。办法还是事件列表项值的复用:SetBits 唤醒等待者时,把"唤醒那一刻的事件组值 | eventUNBLOCKED_DUE_TO_BIT_SET"写回它的列表项;任务醒来第一件事 uxTaskResetEventItemValue() 取回这个值——有 bit25 就是命中返回(顺带拿到唤醒瞬间的位快照),没有就是超时返回。

2.4 广播 vs 单播

xEventGroupSetBits 的唤醒循环遍历整条xTasksWaitingForBits 链表,每个命中的等待者都被唤醒——这是广播。信号量 Give 唤醒的是链表头部一个等待者——那是单播。语义差异直接由源码结构决定:一个 while 遍历 vs 一次 xTaskRemoveFromEventList

顺带一个细节:队列的事件链表按优先级插入(vListInsert),事件组的等待链表是尾插 FIFO(listINSERT_END)——所以 SetBits 的遍历顺序是"先来先判断",与优先级无关;谁先上 CPU 仍由调度器按优先级决定。

3. 应用场景

场景
用法
多条件按位与
`WaitBits(BIT0\
BIT1\
BIT2, xWaitForAllBits=pdTRUE)`——全部采集完成才计算
多条件按位或
`WaitBits(BIT0\
BIT1, xWaitForAllBits=pdFALSE)`——任一报警源触发就响应
单个事件多消费者
SetBits(BIT0)
 广播唤醒所有等 BIT0 的任务
多任务汇合点
xEventGroupSync
——全员到齐一起放行
不该用事件组
单个事件一对一(任务通知/二值信号量更快);要传数据(队列);要计数(计数信号量);位数超过 24 个

4. 源码剖析

4.1 创建:xEventGroupCreate

/* event_groups.c: xEventGroupCreate (精简) */EventGroupHandle_t xEventGroupCreate(void){    EventGroup_t * pxEventBits;    pxEventBits = ( EventGroup_t * ) pvPortMallocsizeofEventGroup_t ) );    if( pxEventBits != NULL )    {        pxEventBits->uxEventBits = 0;                        /* 全部位清零 */        vListInitialise( &( pxEventBits->xTasksWaitingForBits ) );  /* 空链表 */        ...    }    return pxEventBits;      /* 分配失败返回 NULL 必须检查 */}

创建即"什么都没发生"(全零)——和二值信号量同款出生状态,和互斥量的"出生即可用"相反。

4.2 等待:xEventGroupWaitBits

/* event_groups.c: xEventGroupWaitBits (精简) */EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,                                 const EventBits_t uxBitsToWaitFor,                                 const BaseType_t xClearOnExit,                                 const BaseType_t xWaitForAllBits,                                 TickType_t xTicksToWait ){    EventBits_t uxReturn, uxControlBits = 0;    /*  不许等内核控制位, 不许空等 */    configASSERT( ( uxBitsToWaitFor & eventEVENT_BITS_CONTROL_BYTES ) == 0 );    configASSERT( uxBitsToWaitFor != 0 );    vTaskSuspendAll();          /* ★ 挂起调度器保护(不是关中断!) */    {        const EventBits_t uxCurrentEventBits = pxEventBits->uxEventBits;        /* 先测条件是否已满足 */        xWaitConditionMet = prvTestWaitCondition( uxCurrentEventBits,                                                 uxBitsToWaitFor, xWaitForAllBits );        if( xWaitConditionMet != pdFALSE )        {            /* 快路径: 条件已满足, 不阻塞直接返回; 按需原子清位 */            uxReturn = uxCurrentEventBits;            xTicksToWait = ( TickType_t ) 0;   /* 必须清零! 否则下面醒来处理段会误入 */            if( xClearOnExit != pdFALSE )            {                pxEventBits->uxEventBits &= ~uxBitsToWaitFor;            }        }        else if( xTicksToWait == ( TickType_t ) 0 )        {            /* 不阻塞只查询: 返回当前值 */            uxReturn = uxCurrentEventBits;        }        else        {            /* 慢路径: 打包偏好, 挂链表睡觉 */            if( xClearOnExit != pdFALSE )                uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT;            if( xWaitForAllBits != pdFALSE )                uxControlBits |= eventWAIT_FOR_ALL_BITS;            vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ),                                            ( uxBitsToWaitFor | uxControlBits ),                                            xTicksToWait );        }    }    xAlreadyYielded = xTaskResumeAll();    if( xTicksToWait != ( TickType_t ) 0 )    {        if( xAlreadyYielded == pdFALSE )            taskYIELD_WITHIN_API();            /* 恢复调度时没发生过切换才让出 CPU 真正睡去 */        /* ---- 醒来: 取回唤醒值, 区分命中还是超时 ---- */        uxReturn = uxTaskResetEventItemValue();        if( ( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == ( EventBits_t ) 0 )        {            /* ★ 超时醒来: 还要重测一次条件—— 醒来到运行之间的缝隙里可能恰好有人置了位 */            taskENTER_CRITICAL();            {                uxReturn = pxEventBits->uxEventBits;                ifprvTestWaitCondition( uxReturn, uxBitsToWaitFor,                                          xWaitForAllBits ) != pdFALSE )                {                    if( xClearOnExit != pdFALSE )                        pxEventBits->uxEventBits &= ~uxBitsToWaitFor;                }            }            taskEXIT_CRITICAL();        }        /* else: 被置位唤醒, uxReturn 已含命中瞬间的位快照 */        uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES;   /* 控制位不外泄 */    }    return uxReturn;}

四个关键点:

  1. 保护是 vTaskSuspendAll
    :测试条件、改位、挂链表全程不允许任务切换,但中断开着——与队列的关中断方案分道扬镳(原因见 2.1);
  2. 偏好存进任务的事件列表项
    uxBitsToWaitFor | uxControlBits),事件组自身零登记——同一位上可挂任意偏好的等待者;
  3. 超时醒来重测条件
    :被唤醒到真正运行之间有缝隙,位置可能恰好到位——不重测会把"成功"误报成"超时"。这和队列"醒来回环重查"同一哲学;
  4. 返回值是命中瞬间的快照
    :从事件列表项取回的值在清位之前写就,xClearOnExit 清掉的位仍会出现在返回值里。

4.3 判定核心:prvTestWaitCondition

/* event_groups.c: prvTestWaitCondition (精简) */static BaseType_t prvTestWaitCondition(const EventBits_t uxCurrentEventBits,                                        const EventBits_t uxBitsToWaitFor,                                        const BaseType_t xWaitForAllBits ){    BaseType_t xWaitConditionMet = pdFALSE;    if( xWaitForAllBits == pdFALSE )    {        /* 按位或: 任一等待位已置位 */        if( ( uxCurrentEventBits & uxBitsToWaitFor ) != ( EventBits_t ) 0 )            xWaitConditionMet = pdTRUE;    }    else    {        /* 按位与: 全部等待位都已置位 */        if( ( uxCurrentEventBits & uxBitsToWaitFor ) == uxBitsToWaitFor )            xWaitConditionMet = pdTRUE;    }    return xWaitConditionMet;}

与/或的全部物理实现就是两个位运算表达式。注意按位与的写法:(A & B) == B(不是 A == B)——组里可以同时有等待范围之外的位置着,只要等待的全在就行。

4.4 置位广播:xEventGroupSetBits

/* event_groups.c: xEventGroupSetBits (精简) */EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,                                const EventBits_t uxBitsToSet ){    EventBits_t uxBitsToClear = 0, uxBitsWaitedFor, uxControlBits;    BaseType_t xMatchFound;    configASSERT( ( uxBitsToSet & eventEVENT_BITS_CONTROL_BYTES ) == 0 );    vTaskSuspendAll();    {        pxListItem = listGET_HEAD_ENTRY( &( pxEventBits->xTasksWaitingForBits ) );        pxEventBits->uxEventBits |= uxBitsToSet;      /* ① 置位 */        /* ② 遍历整条等待链表: 每个等待者用自己存的条件独立判断 */        while( pxListItem != pxListEnd )        {            pxNext = listGET_NEXT( pxListItem );            uxBitsWaitedFor = listGET_LIST_ITEM_VALUE( pxListItem );            xMatchFound = pdFALSE;            /* 拆出控制位和真正的等待位 */            uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES;            uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES;            if( ( uxControlBits & eventWAIT_FOR_ALL_BITS ) == ( EventBits_t ) 0 )            {                /* 该等待者要按位或: 任一位置了就命中 */                if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) != ( EventBits_t ) 0 )                    xMatchFound = pdTRUE;            }            else if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) == uxBitsWaitedFor )            {                /* 该等待者要按位与: 全部到位才命中 */                xMatchFound = pdTRUE;            }            if( xMatchFound != pdFALSE )            {                /* ③ 命中者要求退出时清位 → 记下, 稍后统一清 */                if( ( uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT ) != ( EventBits_t ) 0 )                    uxBitsToClear |= uxBitsWaitedFor;                /* ④ 唤醒: 当前组值|命中标记 写回它的列表项, 摘链入就绪 */                vTaskRemoveFromUnorderedEventList( pxListItem,                            pxEventBits->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET );            }            pxListItem = pxNext;   /* 用预存的 next: 原节点可能已被摘走 */        }        /* ⑤ 统一清位——SetBits 返回值可能不含刚设位的根源 */        pxEventBits->uxEventBits &= ~uxBitsToClear;    }    ( void ) xTaskResumeAll();    return pxEventBits->uxEventBits;   /* 返回清除后的组值! */}

五个步骤里藏着三个高频考点:

  1. 判断逻辑在遍历里逐人进行
    :等待位和控制位从每个任务自己的列表项值里拆出来——"等什么、怎么等"人人不同,这就是广播语义的物理基础;
  2. 清位推迟到循环之后统一执行
    :为什么不在唤醒某个任务时就地清?因为后面还有等待者要判断——如果立刻清掉,第二个等"或"条件的等待者会错过这次置位。但代价是:带 xClearOnExit 的等待者会"预定"清位,函数末尾返回的是清除后的值——刚设置的位可能已经不在返回值里
  3. 遍历用 pxNext 而不是 pxListItem->pxNext
    :命中者被摘出等待链表、插入就绪链表,原节点的 next 指针已经指向别的链表——必须先存后用,否则链表遍历直接跑飞。这是内核链表操作的经典细节。

4.5 汇合同步:xEventGroupSync

把"置位自己的位 + 等待全部位到齐"打包成一次原子操作

/* event_groups.c: xEventGroupSync (精简) */EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,                             const EventBits_t uxBitsToSet,                             const EventBits_t uxBitsToWaitFor,                             TickType_t xTicksToWait ){    vTaskSuspendAll();    {        uxOriginalBitValue = pxEventBits->uxEventBits;        ( void ) xEventGroupSetBits( xEventGroup, uxBitsToSet );   /* 先置自己的位 */        if( ( ( uxOriginalBitValue | uxBitsToSet ) & uxBitsToWaitFor ) == uxBitsToWaitFor )        {            /* 快路径: 我是最后到者(或唯一参与者), 全部位已齐, 无需阻塞 */            uxReturn = ( uxOriginalBitValue | uxBitsToSet );            pxEventBits->uxEventBits &= ~uxBitsToWaitFor;    /* 汇合位必清零 */            xTicksToWait = 0;        }        else if( xTicksToWait != ( TickType_t ) 0 )        {            /* 慢路径: 没到齐, 以"清零+按位与"偏好挂入等待链表 */            vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ),                    ( uxBitsToWaitFor | eventCLEAR_EVENTS_ON_EXIT_BIT                                    | eventWAIT_FOR_ALL_BITS ),                    xTicksToWait );        }        ...    }    xAlreadyYielded = xTaskResumeAll();    ...   /* 醒来取值逻辑与 WaitBits 同款 */}

精髓在最后到者的快路径:它置位时,xEventGroupSetBits 内部的唤醒循环发现所有先到者的"按位与"条件全部命中,把它们一口气全唤醒——汇合完成。汇合位随后自动清零uxBitsToWaitFor 被 CLEAR_EVENTS_ON_EXIT 预定 + 快路径手动清),下一轮汇合从干净状态开始,无需手动清理。

注意判断式用的是 uxOriginalBitValue | uxBitsToSet(旧值并上我刚设的)——只看"到此刻为止谁到了",不含尚未到场的参与者。

主路径时序图:按位与两步置位 + 原子清位

这张图同时解释了两件事:按位与为什么必须"全到位才醒",以及坑 1"SetBits 返回值不含刚设位"的确切成因(第 7 步)。

汇合点时序图:xEventGroupSync

先到者阻塞、后到者快路径放行——Sync 的核心是那个判断式"算上我刚设的位,到齐了吗"。

4.6 ISR 置位:xEventGroupSetBitsFromISR

/* event_groups.c: xEventGroupSetBitsFromISR (精简) */#if ( ( configUSE_TRACE_FACILITY == 1 ) && ( INCLUDE_xTimerPendFunctionCall == 1 ) \        && ( configUSE_TIMERS == 1 ) )    BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,                                          const EventBits_t uxBitsToSet,                                          BaseType_t * pxHigherPriorityTaskWoken )    {        /* 置位是非确定性操作(可能唤醒任意多任务), ISR 里不许直接执行,         * 改为把回调投递到定时器守护任务, 延迟到任务级执行 */        xReturn = xTimerPendFunctionCallFromISR( vEventGroupSetBitsCallback,                    ( void * ) xEventGroup, ( uint32_t ) uxBitsToSet,                    pxHigherPriorityTaskWoken );        return xReturn;    }#endif

要点:

  1. ISR 里从不真正置位
    ——投递 vEventGroupSetBitsCallback 到定时器任务队列,由守护任务在任务级调用真正的 xEventGroupSetBits。这印证了 2.1 的机制根源:事件组用挂起调度器保护、中断不许碰它的链表;
  2. 三个配置宏全开才有这个函数
    configUSE_TRACE_FACILITYINCLUDE_xTimerPendFunctionCallconfigUSE_TIMERS——本工程三者全为 1,函数真实编译进了固件(若未开,链接报 undefined symbol 是第一症状);
  3. 延迟是结构性的
    :置位发生在守护任务被调度到时,不是中断返回时——对时序极敏感的场景要知道这个延迟的存在。

5. 场景示例:按位与组合等待 + 汇合同步

5.1 实验设计

三个任务 + 两个事件组:实验一(组合等待)消费者按位与等待 BIT0|BIT1,任务 A 第 3 秒置 BIT0、任务 B 第 5 秒置 BIT1——验证"全部到位才唤醒 + 原子清位";实验二(汇合)A、B 先后到达汇合点,验证 Sync 的"先到阻塞、后到放行、位自动清零"。每 8 秒一个循环。

核心片段 1:创建两个事件组 + 三个任务

static EventGroupHandle_t xEventBitsGroup = NULL;   /* 实验一: 组合等待 */static EventGroupHandle_t xEventSyncGroup  = NULL;   /* 实验二: 汇合同步 */voidevent_group_demo(void){    /* 两个实验各用独立的事件组, 互不干扰(各约 40B, 从 FreeRTOS 堆分配) */    xEventBitsGroup = xEventGroupCreate();    xEventSyncGroup = xEventGroupCreate();    if ((xEventBitsGroup == NULL) || (xEventSyncGroup == NULL))    {        printf("事件组创建失败:FreeRTOS 堆(%u 字节)不足\r\n",               (unsigned int)configTOTAL_HEAP_SIZE);        for (;;);   /* 停在这里便于断点排查 */    }    xTaskCreate(vEvtGroup_TaskA,    "EvtA",       EVT_PRINTF_STACK, NULL,                tskIDLE_PRIORITY + 1NULL);    xTaskCreate(vEvtGroup_TaskB,    "EvtB",       EVT_PRINTF_STACK, NULL,                tskIDLE_PRIORITY + 1NULL);    xTaskCreate(vEvtGroup_Consumer, "EvtConsumer", EVT_PRINTF_STACK, NULL,                tskIDLE_PRIORITY + 2NULL);}

核心片段 2:两个置位任务(A 早 2 秒、B 晚 2 秒)

staticvoidvEvtGroup_TaskA(void *pvParameters){    for (;;)    {        /* 实验一: 第 3 秒置 BIT0(消费者还差 BIT1, 不会醒) */        vTaskDelay(pdMS_TO_TICKS(3000));        printf("任务A:设置事件位0\r\n");        xEventGroupSetBits(xEventBitsGroup, mainEVT_BIT_0);        /* 实验二: 到达汇合点, 置 SYNC_BIT_A 并等 A|B 全齐 */        vTaskDelay(pdMS_TO_TICKS(3000));        printf("任务A:到达汇合点,等待任务B...\r\n");        xEventGroupSync(xEventSyncGroup, mainSYNC_BIT_A,                        mainSYNC_BIT_A | mainSYNC_BIT_B, portMAX_DELAY);        printf("任务A:双双放行,继续执行\r\n");    }}staticvoidvEvtGroup_TaskB(void *pvParameters){    for (;;)    {        /* 实验一: 第 5 秒置 BIT1 → 两位全齐, 消费者被唤醒 */        vTaskDelay(pdMS_TO_TICKS(5000));        printf("任务B:设置事件位1\r\n");        xEventGroupSetBits(xEventBitsGroup, mainEVT_BIT_1);        /* 实验二: 晚 2 秒到汇合点 → 它的 Sync 直接触发放行 */        vTaskDelay(pdMS_TO_TICKS(3000));        printf("任务B:到达汇合点\r\n");        xEventGroupSync(xEventSyncGroup, mainSYNC_BIT_B,                        mainSYNC_BIT_A | mainSYNC_BIT_B, portMAX_DELAY);        printf("任务B:双双放行,继续执行\r\n");    }}

核心片段 3:消费者——按位与 + 原子清位

staticvoidvEvtGroup_Consumer(void *pvParameters){    for (;;)    {        EventBits_t xBits;        xBits = xEventGroupWaitBits(xEventBitsGroup,                                    mainEVT_BIT_0 | mainEVT_BIT_1,  /* 关心的事件位 */                                    pdTRUE,   /* xClearOnExit: 返回前原子清除这些位 */                                    pdTRUE,   /* xWaitForAllBits: 按位与, 全部置位才返回 */                                    portMAX_DELAY);        /* 返回值是解除阻塞那一刻的快照: BIT0|BIT1 都在, 即 0x3 */        printf("消费者:事件组值=0x%X,BIT0、BIT1 全部置位——两个条件都满足才继续\r\n",               (unsigned int)(xBits & (mainEVT_BIT_0 | mainEVT_BIT_1)));    }}

预期串口输出(每 8 秒循环):

任务A:设置事件位0任务B:设置事件位1消费者:事件组值=0x3,BIT0、BIT1 全部置位——两个条件都满足才继续任务A:到达汇合点,等待任务B...任务B:到达汇合点任务B:双双放行,继续执行任务A:双双放行,继续执行  ......(约 8 秒后重复上述 7 行)......

演示时序图(一个 8 秒循环)

三个观察点:

  1. 按位与生效
    :A 第 3 秒就置了 BIT0,但消费者必须等到第 5 秒 BIT1 到位——把 xWaitForAllBits 改成 pdFALSE 重编译,消费者第 3 秒就会被 BIT0 单独唤醒(按位或),对比理解参数语义;
  2. 汇合先后
    :A 打印"到达汇合点"后卡顿约 2 秒,B 到达后两句"双双放行"几乎同时出现——先到者的卡顿就是 Sync 阻塞的直观体现;
  3. 位自动清零
    :每轮循环事件位都能重新生效——xClearOnExit / Sync 的自动清位在工作,全程没有手动 xEventGroupClearBits

6. 注意事项(坑列表)

  1. xEventGroupSetBits 返回值可能不含刚设置的位
    :命中且带 xClearOnExit 的等待者会"预定"清位,函数末尾统一清除后才返回——判断置位是否成功不能只看返回值(见 4.4 主路径时序图第 7 步);
  2. ISR 里禁止直接调用 xEventGroupSetBits
    :置位可能唤醒任意多个任务(非确定性操作),且事件组靠挂起调度器保护、中断不许碰其链表。必须用 xEventGroupSetBitsFromISR(本质是投递到定时器任务延迟执行);本工程三配置宏全开,FromISR 版本可用;
  3. xClearOnExit 只在条件满足返回时清位,且只清 uxBitsToWaitFor 指定的位
    :超时返回不清。手动"先 WaitBits 再 ClearBits"在多任务下有竞态(两步之间位可能被别人改走),应一律用 xClearOnExit
  4. 事件位只有 24 个(bit0~bit23)
    :32 位 EventBits_t 的高 8 位是内核控制位,API 入口断言直接拦截——等到/设置 ≥bit24 的位当场死机;
  5. 等待"任一位"的返回值不是"哪个位"
    :按位或返回的是命中瞬间的组值快照,可能同时有多个等待范围之外的位在组里——判断"是谁唤醒了我"要用 (返回值 & 我关心的位) 过滤;
  6. 广播是全量的
    :多个任务等同一位时一次 SetBits 全部唤醒——把事件组当信号量做"资源计数"会唤醒风暴;广播 + 多个带清位的等待者还会让位只被清一次但人人醒来都以为自己该处理;
  7. Sync 的 uxBitsToWaitFor 必须覆盖所有参与者的位,且各参与者用不同的 uxBitsToSet
    :若两人置同一位,先到者会把自己置的位误判为对方已到,提前放行;
  8. pdMS_TO_TICKS(<10) 取整为 0
    :本工程 1 tick=10ms,WaitBits 传 0 tick 变成"只查询当前值不阻塞"——超时至少给 10ms;
  9. 堆与优先级反转
    :事件组本身很省(约 40B/个),但保护用挂起调度器而非互斥量——多任务高频 SetBits/WaitBits 时注意调度器挂起期间的任务切换延迟;vEventGroupDelete 前确认无任务阻塞其上(删除时会唤醒全部等待者并返回 0 值)。

总结

事件组最漂亮的地方在于它是队列机制的极简变体——把队列的骨架拆到只剩"状态值 + 等待链表",再用三个巧思撑起全部功能:

  1. 一个位变量表状态
    uxEventBits 的每一位就是一个布尔事件,改状态就是改一个整数——但高 8 位被征用作控制位,可用位只有 24 个;
  2. 等待条件存在任务自己身上
    :"等哪些位、与还是或、要不要清"打包进每个等待者的事件列表项值——事件组自身零登记,同一位上可挂任意偏好的等待者,SetBits 遍历链表逐人判断,天然形成广播语义;
  3. 保护从关中断换成挂起调度器
    :置位唤醒耗时不确定,只有 vTaskSuspendAll(任务不切、中断仍开)能容纳——这个选择直接推导出"ISR 不许直接操作事件组、FromISR 必须转投定时器任务"的限制。

理解了"位变量 + 链表 + 控制位"这三件事,事件组的所有坑(SetBits 返回值不含刚设位、只有 24 个位、广播风暴、Sync 参与者置位必须互异)都能从第一性原理推出来,不用死记。

对比着前几篇看更清楚:队列传数据、信号量管计数、队列集选成员、事件组算逻辑——同一个"挂链表睡觉、改状态唤醒"的骨架,换一个状态表达,就是一个新的同步原语。

— 写在最后 —

如果这篇文章对你有帮助,欢迎点赞、在看、转发三连支持,这是我持续更新的最大动力。

关注我,更多硬核技术剖析持续更新中,下一期再见。

相关学习资料

返回首页浏览学习资料