一、事件组是什么

核心理念:

  • 用途:用于多事件同步。一个任务可以等待多个事件中的任意一个(OR 关系)或所有事件(AND 关系)。
  • 本质:一个 32 位的标志寄存器 (EventBits_t) + 一个等待任务链表。
  • 独立性:事件组不再复用队列代码,而是独立实现的结构体。
  • 中断安全:严禁在中断中等待事件;在中断中设置事件时,通过间接机制(写队列唤醒守护任务)来保证中断执行时间的确定性。

两种最经典场景:

  1. 等待所有事件发生(与)张三、李四、王五都到齐,才能出发。
  2. 等待任意事件发生(或)张三、李四、王五谁先交稿,就用谁的
typedef struct EventGroupDef {
    EventBits_t uxEventBits;        /* 【核心】32 位标志位,每一位代表一个事件 */
    List_t xTasksWaitingBlocks;     /* 【核心】等待该事件组的任务链表 */
    
    /* 其他辅助成员,如创建时间等,不影响核心逻辑 */
} EventGroup_t;
  • uxEventBits:当前发生的事件集合。
  • xTasksWaitingBlocks:所有因为等待该事件组而阻塞的任务都挂在这个同一个链表上。
    • 注意:不需要为每个 Bit 单独建链表。设置事件时,遍历这个链表,链表中的节点不仅存了任务 TCB 指针,还存了该任务等待的具体条件(等待哪些 Bit,是 AND 还是 OR)。取出每个节点的“等待条件”与“当前事件值”进行比对。匹配成功的才唤醒,不匹配的继续留着。

二、事件组核心代码与机制

1.等待事件xEventGroupWaitBits

EventBits_t xEventGroupWaitBits( 
    EventGroupHandle_t xEventGroup,   // 1. 事件组句柄
    const EventBits_t uxBitsToWaitFor,// 2. 等待哪些位
    BaseType_t xClearOnExit,          // 3. 退出前是否清除位
    BaseType_t xWaitForAllBits,       // 4. 等待逻辑 (与/或)
    TickType_t xTicksToWait           // 5. 超时时间
);

可以看到,在等待时,并没有关中断,而是把任务挂起

关键点:为什么只“禁止调度器”而不“关中断”?
  • 队列/信号量:既可以在任务中用,也可以在中断中用 (xQueueSendFromISR)。为了防止中断打断任务导致数据竞争(Count++问题),必须关中断
  • 事件组
    • 等待操作只能在任务上下文中使用(没有 xEventGroupWaitBitsFromISR)。既然中断里不能等,就不需要防中断打断等待逻辑。
    • 设置操作:虽然可以在中断中设置,但 FreeRTOS 采用了间接机制(见下文),实际修改标志位的动作发生在任务上下文中。
    • 结论:只需要防止其他任务同时修改链表或标志位,所以调用 vTaskSuspendAll() (禁止调度器) 就够了,无需关全局中断,提高了系统响应速度。

等待的核心步骤

* A. 将当前任务 (pxCurrentTCB) 插入到 pxEventBits->xTasksWaitingForBits 链表末尾。

* B. 将任务等待的位掩码和控制标志写入任务的 eventListItem 中。

* C. 计算唤醒时间点,将任务插入到内核全局的 Delay List (延迟链表)。

* D. 将任务状态从 Ready 改为 Blocked。

            /* Store the bits that the calling task is waiting for in the
             * task's event list item so the kernel knows when a match is
             * found.  Then enter the blocked state. */
            vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ), ( uxBitsToWaitFor | uxControlBits ), xTicksToWait );




void vTaskPlaceOnUnorderedEventList( List_t * pxEventList,
                                     const TickType_t xItemValue,
                                     const TickType_t xTicksToWait )
{
    // 【前置检查】
    configASSERT( pxEventList );
    // 确保调度器已暂停 (因为我们在 vTaskSuspendAll() 保护下调用)
    configASSERT( uxSchedulerSuspended != 0 ); 

    /* ======================================================== */
    /* 第一步:记录“等待条件” (存入任务自己的口袋)              */
    /* ======================================================== */
    /* 
     * 这里的 pxCurrentTCB->xEventListItem 是当前任务控制块 (TCB) 
     * 里的一个专用“小本子” (列表项)。
     * 
     * 我们要把“等哪些位”、“是 AND 还是 OR”、“是否清位”这些信息
     * 全部打包存进这个小本子的 value 字段里。
     * 
     * 为什么要存这里?
     * 因为将来别人设置事件时,会遍历链表拿到这个 item,
     * 读取 value 值,才知道你这个任务具体在等什么!
     */
    listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xEventListItem ), 
                             xItemValue | taskEVENT_LIST_ITEM_VALUE_IN_USE );
    // xItemValue = (uxBitsToWaitFor | uxControlBits)
    // taskEVENT_LIST_ITEM_VALUE_INUSE 是一个标志位,表示“我正在使用中”


    /* ======================================================== */
    /* 第二步:加入“事件组等待队列” (挂到事件组名下)            */
    /* ======================================================== */
    /* 
     * pxEventList: 传入的是 &(pxEventBits->xTasksWaitingForBits)
     * 这是事件组结构体里的唯一链表。
     * 
     * vListInsertEnd: 把任务的“小本子” (xEventListItem) 
     * 直接插到这个链表的**末尾**。
     * 
     * 为什么叫 "Unordered" (无序)?
     * 因为直接插尾部最快 (O(1)),不需要按时间或优先级排序。
     * 唤醒时,系统会遍历整个链表,逐个检查谁的条件满足了。
     */
    vListInsertEnd( pxEventList, &( pxCurrentTCB->xEventListItem ) );


    /* ======================================================== */
    /* 第三步:加入“全局延迟队列” (设定闹钟,真正休眠)          */
    /* ======================================================== */
    /* 
     * 光挂在事件组链表上还不够,万一永远没人设置事件怎么办?
     * 所以需要设定一个“超时时间”。
     * 
     * prvAddCurrentTaskToDelayedList:
     * 1. 计算唤醒时间点 (当前 Tick + xTicksToWait)。
     * 2. 把任务从【就绪链表】移除。
     * 3. 按时间顺序插入到内核全局的【延迟链表 (Delay List)】。
     * 4. 任务状态正式变为 Blocked。
     * 
     * 这就是任务“消失”去睡觉的关键一步!
     */
    prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
}

2.设置事件 xEventGroupSetBits & FromISR(Clear也差不多)

EventBits_t xEventGroupSetBits( 
    EventGroupHandle_t xEventGroup,   // 1. 事件组句柄
    const EventBits_t uxBitsToSet     // 2. 要设置的位掩码
);

BaseType_t xEventGroupSetBitsFromISR( 
    EventGroupHandle_t xEventGroup,   // 1. 事件组句柄
    const EventBits_t uxBitsToSet,    // 2. 要设置的位掩码
    BaseType_t *pxHigherPriorityTaskWoken // 3. 高优先级任务唤醒标志
);

设置事件同样只是“禁止调度器”,完成操作后在放入再开启调度器

基本执行步骤为

  1. 置位:先把事件标志位打上(uxEventBits |= uxBitsToSet)。
  2. 遍历与筛选:拿着新打的标志,去等待链表里一个个问:“你的条件满足了吗?”
  3. 唤醒与清理:满足条件的 -> 移除链表、加入就绪队列;需要清位的 -> 记录一下,最后统一清除。

核心难点:中断中的设置 (xEventGroupSetBitsFromISR)

问题:如果在 ISR 中直接遍历链表唤醒多个任务,耗时是不确定的(取决于有多少任务在等)。这违反了 RTOS 中断处理“快进快出”的原则。

解决方案:代理模式 (Daemon Task)

  1. ISR 中xEventGroupSetBitsFromISR 不直接修改标志位,也不遍历链表。
    • 它只是向一个特殊的队列发送一条消息(包含“要设置哪些位”和“哪个事件组”)。
    • 这个操作非常快且时间确定。
  2. 守护任务 (Timer/daemon task)
    • 有一个高优先级的系统守护任务一直在等待这个队列。
    • 一旦收到消息,它在任务上下文中执行真正的 xEventGroupSetBits
    • 此时可以安全地遍历链表、唤醒多个任务,耗时再长也不会影响中断响应。
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
                                      const EventBits_t uxBitsToSet,
                                      BaseType_t * pxHigherPriorityTaskWoken )
{
    BaseType_t xReturn;

    // 1. 追踪标记 (调试用)
    traceEVENT_GROUP_SET_BITS_FROM_ISR( xEventGroup, uxBitsToSet );

    // 2. 【核心魔法】发送“跑腿订单”
    /* 
     * 参数解读:
     * vEventGroupSetBitsCallback: 回调函数。告诉守护任务:“收到货后,请执行这个函数”。
     * (void *)xEventGroup:      参数 1。告诉守护任务:“操作哪个事件组”。
     * (uint32_t)uxBitsToSet:    参数 2。告诉守护任务:“要设置哪些位”。
     * pxHigherPriorityTaskWoken: 唤醒标志。如果守护任务优先级高且被唤醒,这里会置 1。
     * 
     * 底层动作:
     * 这个函数内部其实是向 "Timer Service Queue" (定时器服务队列) 发送了一条消息。
     * 消息内容 = { 函数指针:vEventGroupSetBitsCallback, 参数 1, 参数 2 }
     */
    xReturn = xTimerPendFunctionCallFromISR( 
                vEventGroupSetBitsCallback, 
                ( void * ) xEventGroup, 
                ( uint32_t ) uxBitsToSet, 
                pxHigherPriorityTaskWoken 
              );

    return xReturn;
}


//回调函数就是发送事件组置为
void vEventGroupSetBitsCallback( void * pvEventGroup,
                                 const uint32_t ulBitsToSet )
{
    ( void ) xEventGroupSetBits( pvEventGroup, ( EventBits_t ) ulBitsToSet ); 
}


BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend,
                                          void * pvParameter1,
                                          uint32_t ulParameter2,
                                          BaseType_t * pxHigherPriorityTaskWoken )
{
    // 1. 定义一个局部变量,用于构造发送给守护任务的消息包
    DaemonTaskMessage_t xMessage;
    
    // 用于存储队列发送结果的变量
    BaseType_t xReturn;

    /* --------------------------------------------------------------
     * 步骤 A: 构造消息体 (打包 "快递包裹")
     * 将用户传入的函数指针和参数填入消息结构体中。
     * -------------------------------------------------------------- */
    
    // 设置消息 ID:告诉守护任务,这是一个“从中断发来的回调执行请求”。
    // 守护任务收到后会进入对应的 case 分支处理,而不是当作普通定时器超时处理。
    xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;

    // 填入【函数指针】:告诉守护任务“收到货后执行哪个函数”。
    // 对于事件组,这里存的是 vEventGroupSetBitsCallback 的地址。
    xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;

    // 填入【参数 1】:通常是对象句柄 (如事件组句柄)。
    xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;

    // 填入【参数 2】:通常是数值参数 (如位掩码)。
    // 注意:这里发生了隐式或显式的类型转换,将参数视为 32 位整数存储。
    xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;

    /* --------------------------------------------------------------
     * 步骤 B: 发送消息 (投递 "快递")
     * 将构造好的消息发送到全局的 "定时器服务队列" (xTimerQueue)。
     * 守护任务 (prvTimerTask) 正阻塞在这个队列上等待消息。
     * -------------------------------------------------------------- */
    
    // 调用中断安全的队列发送函数。
    // 此操作非常快 (仅内存拷贝),不会遍历链表或执行复杂逻辑,保证中断实时性。
    // 如果发送成功导致高优先级守护任务唤醒,pxHigherPriorityTaskWoken 会被置为 pdTRUE。
    xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );

    /* --------------------------------------------------------------
     * 步骤 C: 追踪与返回
     * -------------------------------------------------------------- */
    
    // 调用追踪宏 (Trace Macro),用于系统性能分析或调试工具 (如 Percepio Tracealyzer)。
    // 记录这次函数挂起请求的详细信息。
    tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );

    // 返回发送结果给调用者 (通常用于判断是否发送成功)
    return xReturn;
}

三、事件组对比总结

操作 队列/信号量 事件组
临界区保护 taskENTER_CRITICAL() (关中断) vTaskSuspendAll() (仅禁调度)
原因 支持 ISR 访问,需防中断干扰 等待仅限任务;设置 ISR 版走代理模式,实际修改在任务中
唤醒策略 通常唤醒一个任务 (FIFO 或优先级) 遍历链表,唤醒所有满足条件的任务
数据清除 队列出队即删除数据 可选 ClearOnExit (自动清零) 或保留 (手动清零 xEventGroupClearBits)
结构体 复用 Queue_t 独立 EventGroup_t

四、任务通知

1.传统机制 (队列/信号量/事件组) 的痛点

  • 需要创建对象:必须先 xQueueCreate 或 xSemaphoreCreate,消耗 RAM。
  • 需要查找接收者
    • 写任务不知道谁在等,只能把数据扔进队列/事件组。
    • 内核必须维护等待链表 (xTasksWaitingToReceive)。
    • 发送时,内核要遍历链表找到等待的任务并唤醒。
  • 开销大:涉及链表操作、内存拷贝 (队列)、多次关中断。

2.任务通知的优势

  • 去中介化:不需要创建队列、信号量、事件组等内核对象。直接向特定任务发送通知
  • 极速高效:数据直接存放在任务的 TCB (任务控制块) 中,无需遍历链表查找接收者。
  • 轻量级:比队列快 45%,比二值信号量快 28%,且节省 RAM(不需要额外的结构体内存)。
  • 本质:每个任务内置了一个 32 位的通知值 (ulNotifiedValue) 和一个 通知状态 (eNotifyState)
字段名称 数据类型 核心作用
ulNotifiedValue uint32_t
(32 位无符号整数)
承载 “通知的具体内容”:可以是数值、标志位、计数值(你定义含义)
eNotifyState eNotifyState
(枚举类型)
标记 “通知的状态”:① 无通知(eNotWaitingNotification)② 有未处理通知(eNotified)③ 任务正在等待通知(eWaitingNotification)
状态枚举 含义 触发场景
eNotWaitingNotification (0) 未在等待 默认状态。表示任务没有调用等待函数,或者已经处理完上一次通知。
eWaitingNotification (1) 正在等待 任务调用了 xTaskNotifyWait 但此时没有新通知,于是进入阻塞状态。
eNotificationReceived (2) 已收到通知 发送者发来了通知,但接收者还没来取。此时任务处于就绪态 (或被唤醒)。

3.任务通知的缺点,不可适用之处

  • 多任务等待同一事件:需要广播功能时 (用事件组)。
  • 复杂的多条件同步:需要 AND 逻辑时 (用事件组)。
  • 传递大数据:超过 32 位的数据结构 (用队列)。
  • 需要历史消息缓冲:通知没有缓冲区,新通知会覆盖旧值 (取决于策略),而队列可以存多条消息。

4.发送方流程:xTaskNotify (发送通知)

a.定位目标通过传入的任务句柄,直接拿到目标任务的 TCB 指针

b.进入临界区(关中断)

c.保存目标任务原本的状态(目的:用来判断它刚才是不是在等待)

d.更新状态:无论之前是什么状态,现在都标记为 已收到通知 (Notification Received)

e.处理数据(根据函数传参)

  • 置位:把指定位置 1 (|=)。
  • 累加:数值 +1 (++)。
  • 覆盖:直接赋新值。
  • 条件覆盖:只有对方没读旧值时才覆盖,否则失败。
  • 无动作:只改状态,不改数值(纯同步)。

f.判断是否需要“叫醒”

  • 检查第 3 步记录的原本状态
  • 如果原本是 正在等待 (Waiting)
    • 动作 1:把对方从“延迟链表”拽出。
    • 动作 2:把对方放进“就绪链表”。
    • 动作 3:检查优先级。如果对方的优先级比当前任务(发送者)还高,立即触发任务切换,让对方马上运行。
  • 如果原本不是 正在等待
    • 说明对方要么在运行,要么没在等这个通知。
    • 动作:什么都不用做,不用唤醒。对方下次自己来查状态时。

g.退出临界区(开中断)

  • 操作完成,返回成功或失败标志。

5.接收方流程:ulTaskNotifyTake (等待通知)

a.进入临界区(关中断)

b.读取当前任务 TCB 中的 ulNotifiedValue

  • 情况 A:值不为 0
    • 说明之前已经有人发过通知了,只是还没读。
    • 动作:跳过阻塞步骤,直接进入读取环节。
  • 情况 B:值为 0
    • 说明目前没有未读通知。
    • 动作 1:将任务状态标记为 正在等待 (Waiting)
    • 动作 2:判断超时时间。
      • 超时时间 > 0:从“就绪链表”移除,放入“延迟链表”,切换任务运行(接续下面c.步骤,唤醒)。
      • 超时时间 = 0:直接退出临界区,准备返回 0。

c.被唤醒(收到了通知,或者超时了)

d.再次进入临界区(关中断)

e.读取当前的 ulNotifiedValue 作为返回值。

  • 根据参数处理计数值
    • 如果选“清零模式”:把值设为 0(类似二值信号量)。
    • 如果选“减 1 模式”:把值减 1(类似计数信号量)。
  • 复位状态:将任务状态改回 未在等待 (Not Waiting)

f.退出临界区(开中断)并返回

------------------------------------------------------------

一个交互例子:

步骤 任务 A (接收者) 任务 B (发送者) TCB 状态变化 (任务 A)
1. 初始 调用 NotifyTake,发现值=0。 - NotWaitingWaiting
2. 阻塞 加入延迟链表,休眠。CPU 切给其他任务。 - Blocked (在延迟链表)
3. 发送 - 调用 Notify(A)
1. 发现 A 状态是 Waiting
2. 修改 A 的值。
3. 设 A 状态为 Received
4. 将 A 从延迟链表移至就绪链表。
5. 若 A 优先级高,触发切换。
WaitingReceived
Blocked → Ready
4. 恢复 被唤醒!代码从 portYIELD 后继续执行。
再次进入临界区,读取值,清零/减 1。
状态复位为 NotWaiting
返回结果。
继续执行或已被抢占。 ReceivedNotWaiting
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐