FreeRTOS内核(五)事件组与任务通知的核心机制
操作队列/信号量事件组临界区保护(关中断)(仅禁调度)原因支持 ISR 访问,需防中断干扰等待仅限任务;设置 ISR 版走代理模式,实际修改在任务中唤醒策略通常唤醒一个任务 (FIFO 或优先级)遍历链表,唤醒所有满足条件的任务数据清除队列出队即删除数据可选(自动清零) 或保留 (手动清零结构体复用Queue_t独立。
一、事件组是什么
核心理念:
- 用途:用于多事件同步。一个任务可以等待多个事件中的任意一个(OR 关系)或所有事件(AND 关系)。
- 本质:一个 32 位的标志寄存器 (EventBits_t) + 一个等待任务链表。
- 独立性:事件组不再复用队列代码,而是独立实现的结构体。
- 中断安全:严禁在中断中等待事件;在中断中设置事件时,通过间接机制(写队列唤醒守护任务)来保证中断执行时间的确定性。
两种最经典场景:
- 等待所有事件发生(与)张三、李四、王五都到齐,才能出发。
- 等待任意事件发生(或)张三、李四、王五谁先交稿,就用谁的
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. 高优先级任务唤醒标志
);
设置事件同样只是“禁止调度器”,完成操作后在放入再开启调度器
基本执行步骤为
- 置位:先把事件标志位打上(
uxEventBits |= uxBitsToSet)。 - 遍历与筛选:拿着新打的标志,去等待链表里一个个问:“你的条件满足了吗?”
- 唤醒与清理:满足条件的 -> 移除链表、加入就绪队列;需要清位的 -> 记录一下,最后统一清除。
核心难点:中断中的设置 (xEventGroupSetBitsFromISR)
问题:如果在 ISR 中直接遍历链表唤醒多个任务,耗时是不确定的(取决于有多少任务在等)。这违反了 RTOS 中断处理“快进快出”的原则。
解决方案:代理模式 (Daemon Task)
- ISR 中:
xEventGroupSetBitsFromISR不直接修改标志位,也不遍历链表。- 它只是向一个特殊的队列发送一条消息(包含“要设置哪些位”和“哪个事件组”)。
- 这个操作非常快且时间确定。
- 守护任务 (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。 |
- | NotWaiting → Waiting |
| 2. 阻塞 | 加入延迟链表,休眠。CPU 切给其他任务。 | - | Blocked (在延迟链表) |
| 3. 发送 | - | 调用 Notify(A)。1. 发现 A 状态是 Waiting。2. 修改 A 的值。 3. 设 A 状态为 Received。4. 将 A 从延迟链表移至就绪链表。 5. 若 A 优先级高,触发切换。 |
Waiting → ReceivedBlocked → Ready |
| 4. 恢复 | 被唤醒!代码从 portYIELD 后继续执行。再次进入临界区,读取值,清零/减 1。 状态复位为 NotWaiting。返回结果。 |
继续执行或已被抢占。 | Received → NotWaiting |
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)