--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第14章 任务通知" author: "尚硅谷研究院" date: 2026-07-17 created: 2026-07-17 --- # 任务通知 > **用生活理解**:任务通知就像同事直接走到你工位前告诉你一件事,不需要通过公司广播(队列)、公告栏(信号量)或旗语系统(事件标志组)。直接、高效,但因为是一对一,没法同时通知所有人。 ## 概念介绍 ### 为什么需要任务通知 FreeRTOS 已经有队列、信号量、事件标志组这些 IPC 机制,为什么还需要任务通知? 看一个常见场景:任务 A 做完某件事后,想告诉任务 B"活干完了,该你了"。用队列实现需要先 `xQueueCreate` 创建队列对象,然后 A 调用 `xQueueSend`、B 调用 `xQueueReceive`。用信号量实现也需要先 `xSemaphoreCreateBinary` 创建信号量对象。 **这些 IPC 机制的问题是"中间商赚差价"**:每次通信都要经过一个额外创建的内核对象(队列/信号量/事件组)。对于"我就想叫一下隔壁任务"这么简单的事,创建一个完整的内核对象太浪费了。 任务通知就是为解决这个问题而生——**发送任务直接修改目标任务的 TCB 字段,不需要任何中间对象**。 ### 基本原理 每个 FreeRTOS 任务的 TCB 内部自带一组"通知"(Notifications),每个通知包含: - **一个 32 位无符号整数**(可作计数器、位图、消息存储) - **一个通知状态**("待处理"或"非待定",标记是否有未读通知) > `configTASK_NOTIFICATION_ARRAY_ENTRIES` 控制通知数组大小,默认 1。设为 N 时每个任务拥有 N 组独立通知值。FreeRTOS V10.4.0 之后支持此特性。 **⚠ 注意:`xTaskNotifyGive()` 的本质**:查看源码发现 `xTaskNotifyGive()` 其实是一个**宏**,展开后等价于 `xTaskNotify(xTaskToNotify, 0, eIncrement)`。也就是说"信号量模式"在底层和"消息邮箱/事件组模式"使用的是同一个 `xTaskNotify()` 函数,只是 `eAction` 参数不同。三种模式的发送端完全可以统一理解为"调用 xTaskNotify 配上不同的 eAction 参数"。 **普通 IPC 的通信路径**: ``` 任务 A → [创建队列/信号量对象] → 放入数据 → 队列/信号量 → 任务 B 取出数据 └── 中间对象(额外分配内存)──┘ ``` **任务通知的通信路径**: ``` 任务 A → 直接写入任务 B 的 TCB └── 不创建任何对象 ──┘ ``` 这就是任务通知比队列/信号量**更轻量、更快**的原因——省去了创建、操作、销毁内核对象的所有开销。 ### 通知的工作流程 任务通知有两步:**发送**和**接收**。关键在于**接收方可以选择阻塞等待通知的到来**: 1. **发送方**调用 `xTaskNotifyGive()` 或 `xTaskNotify()` → 直接写入目标任务的 TCB 通知值 → 将目标任务的通知状态标记为"已收到通知" 2. **接收方**调用 `ulTaskNotifyTake()` 或 `xTaskNotifyWait()`: - 如果已经有**挂起的通知**(之前有人给它发过通知但还没处理)→ 立即返回,处理通知值 - 如果**没有挂起的通知** → 任务进入阻塞态,等待通知到来 3. 当发送方再次给这个任务发送通知时,接收方解除阻塞,处理通知值 ### 三种使用模式 通知值的 32 位可以灵活解释,由此衍生出三种使用模式,替代三种传统 IPC: | 模式 | 对应传统 IPC | 发送函数 | 接收函数 | 32 位值的用法 | | ------------------ | --------------- | ------------------------------------------ | -------------------- | ----------------------------------- | | **信号量模式** | 二值/计数信号量 | `xTaskNotifyGive()` | `ulTaskNotifyTake()` | 当作计数器:Give 加 1,Take 减 1 | | **消息邮箱模式** | 队列(单消息) | `xTaskNotify(..., eSetValueWithOverwrite)` | `xTaskNotifyWait()` | 当作消息容器:覆写/条件覆写 32 位值 | | **事件标志组模式** | 事件标志组 | `xTaskNotify(..., eSetBits)` | `xTaskNotifyWait()` | 当作位图:按位 OR 设置标志位 | **一句话选型**: - 只需通知对方"事件发生了" → 信号量模式(最轻量) - 需要传递一个数值给对方 → 消息邮箱模式 - 需要多个条件的组合触发 → 事件标志组模式 ### 核心劣势(与传统 IPC 对比) | 对比项 | 队列/信号量/事件组 | 任务通知 | | -------- | ------------------------- | --------------------------------------- | | 通信方向 | 多对多(多个任务可发/收) | 只能一对一(一个任务只有一个通知值) | | 数据缓存 | 队列可缓存多条消息 | 只能缓存一个 32 位值(覆写会丢数据) | | ISR 接收 | 可接收 | **不能接收**(无 FromISR 版本接收函数) | | 创建步骤 | 需先 Create 对象 | **无需创建**,直接用 | ### 核心API | 函数 | 参数 | 功能简述 | | --------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------- | | `xTaskNotifyGive()` | `xTaskToNotify` | 发送通知(信号量模式),通知值 +1 | | `ulTaskNotifyTake()` | `xClearCountOnExit`, `xTicksToWait` | 接收通知(信号量模式),返回通知值 | | `xTaskNotify()` | `xTaskToNotify`, `ulValue`, `eAction` | 发送通知(带值),eAction 控制写/OR/覆写行为 | | `xTaskNotifyWait()` | `ulBitsToClearOnEntry`, `ulBitsToClearOnExit`, `pulNotificationValue`, `xTicksToWait` | 接收通知(带值),可获取通知值和清除指定位 | | `xTaskNotifyIndexed()` | `xTaskToNotify, uxIndex, ulValue, eAction` | 写目标任务通知数组指定元素 | | `xTaskNotifyWaitIndexed()` | `uxIndex, ...` | 阻塞读本任务通知数组指定元素 | | `ulTaskNotifyTakeIndexed()` | `uxIndex, ...` | 信号量模式取通知数组指定元素 | | `xTaskNotifyFromISR()` | 同上+ISR 版本 | 中断中发送通知 | | `vTaskNotifyGiveFromISR()` | `xTaskToNotify`, `pxHigherPriorityTaskWoken` | 中断中发送(信号量模式) | ## 实验 > 以下实验代码均提取自配套 Keil 项目的 `freertos_demo.c`,仅展示核心任务函数。 ### 实验1:任务通知模拟信号量 (P16) **需求**:KEY1 → `xTaskNotifyGive(task2_handle)` 通知 task2,task2 通过 `ulTaskNotifyTake()` 阻塞等待通知。 **代码来源:**`03_代码/16_任务通知模拟信号量/Core/Src/freertos_demo.c` ```c /* task1:KEY1按下,向task2发送任务通知(信号量模式:通知值+1) */ void task1(void *pvParameters) { uint8_t key = 0; BaseType_t res = 0; while (1) { key = Key_Detect(); if (key == KEY1_PRESS) { /* 向task2发送通知,通知值计数+1 */ res = xTaskNotifyGive(task2_handle); if (res == pdPASS) { printf("task1向task2发送任务通知成功!\r\n"); } } vTaskDelay(500); } } /* task2:阻塞等待任务通知 */ void task2(void *pvParameters) { uint32_t notify_value = 0; while (1) { printf("task2等待...\r\n"); /* pdFALSE = 收到通知后通知值减1(适合计数信号量) pdTRUE = 收到通知后通知值清零(适合二值信号量) */ notify_value = ulTaskNotifyTake(pdFALSE, portMAX_DELAY); printf("Task2收到通知值=%d\r\n", notify_value); vTaskDelay(5000); /* 模拟长时间工作,期间task1的通知累积 */ } } ``` ### 实验2:任务通知模拟消息邮箱 (P17) **需求**:KEY1/KEY2 → `xTaskNotify(task2_handle, key, eSetValueWithOverwrite)` 覆写通知值,task2 接收后打印。 **代码来源:**`03_代码/17_任务通知模拟消息队列/Core/Src/freertos_demo.c` ```c /* task1:扫描按键,通过任务通知将键值发送给task2 */ void task1(void *pvParameters) { uint8_t key = 0; BaseType_t res = 0; while (1) { key = Key_Detect(); if (key == KEY1_PRESS || key == KEY2_PRESS) { /* 直接覆写task2的通知值(消息邮箱模式) */ res = xTaskNotify( task2_handle, key, // 要发送的值(按键编号) eSetValueWithOverwrite // 无条件覆写通知值 ); if (res == pdPASS) { printf("task1向task2发送通知值[%d]成功...\r\n", key); } } vTaskDelay(500); } } /* task2:接收通知值并打印 */ void task2(void *pvParameters) { uint32_t notify_value = 0; BaseType_t res = 0; while (1) { printf("task2等待...\r\n"); /* 入口不清零(0x00000000),出口全清零(0xffffffff) */ res = xTaskNotifyWait( 0x00000000, // 进入时不清除任何位 0xffffffff, // 退出时清除所有位(通知值归零) ¬ify_value, // 保存接收到的通知值 portMAX_DELAY); if (res == pdTRUE) { printf("Task2收到通知值=%d\r\n", notify_value); } } } ``` ### 实验3:任务通知模拟事件标志组 (P18) **需求**:KEY1 → 通知值 bit0 置 1,KEY2 → 通知值 bit1 置 1,task2 累加检查。 **代码来源:**`03_代码/18_任务通知模拟事件标志组/Core/Src/freertos_demo.c` ```c #define EVENTBIT_0 (1 << 0) #define EVENTBIT_1 (1 << 1) /* task1:KEY1 -> 置bit0,KEY2 -> 置bit1 */ void task1(void *pvParameters) { uint8_t key = 0; BaseType_t res = 0; while (1) { key = Key_Detect(); if (key == KEY1_PRESS) { /* 使用eSetBits模式,按位OR将bit0置1 */ res = xTaskNotify(task2_handle, EVENTBIT_0, eSetBits); if (res == pdPASS) printf("KEY1按下将bit0置1..\r\n"); } else if (key == KEY2_PRESS) { res = xTaskNotify(task2_handle, EVENTBIT_1, eSetBits); if (res == pdPASS) printf("KEY2按下将bit1置1..\r\n"); } vTaskDelay(500); } } /* task2:累加收到的位,两个位都收到后打印信号 */ void task2(void *pvParameters) { uint32_t notify_value = 0; uint32_t expect_value = 0; /* 累加累计收到的位 */ BaseType_t res = 0; while (1) { printf("task2等待...\r\n"); res = xTaskNotifyWait( 0x00000000, // 进入时不清除 0xffffffff, // 退出时清除所有位 ¬ify_value, portMAX_DELAY); if (notify_value & EVENTBIT_0) { printf("接收到bit0=1\r\n"); expect_value |= EVENTBIT_0; } if (notify_value & EVENTBIT_1) { printf("接收到bit1=1\r\n"); expect_value |= EVENTBIT_1; } /* 两个位都已收到则输出 */ if (expect_value == (EVENTBIT_0 | EVENTBIT_1)) { printf("两个事件均已收到 expect_value=%d\r\n", expect_value); } vTaskDelay(500); } } ``` ### 实验4:xTaskNotifyIndexed — 通知数组 1对多 (P19) **需求**:KEY1 → task2 通知数组[0] = 10,KEY2 → task2 通知数组[1] = 20,KEY3 → task3 通知数组[0] = 99。每个任务通过 `xTaskNotifyWaitIndexed(index)` 阻塞读取指定数组元素。 **原理**:`configTASK_NOTIFICATION_ARRAY_ENTRIES = 3` 使每个任务的 TCB 中有一个长度 3 的 `uint32_t` 通知数组。`xTaskNotifyIndexed(task, index, ...)` 本质是**写 task->notifications[index]**,`xTaskNotifyWaitIndexed(index, ...)` 本质是**阻塞读 task->notifications[index]**。 **代码来源:**`22_EX_TaskNotify/Core/Src/freertos_demo.c` ```c /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 3 使每个任务拥有 3 个通知数组元素 */ void task1(void *const pvParameters) { uint8_t key = 0; BaseType_t res = 0; while (1) { key = Key_Detect(); if (key == KEY1_PRESS) { /* xTaskNotifyIndexed = 写 task2 通知数组[0] = 10(覆写) */ res = xTaskNotifyIndexed( task2_handle, 0, 10, eSetValueWithOverwrite ); if (res == pdPASS) printf("task1: task2->通知数组[0] = 10\r\n"); } else if (key == KEY2_PRESS) { /* xTaskNotifyIndexed = 写 task2 通知数组[1] = 20(覆写) */ res = xTaskNotifyIndexed( task2_handle, 1, 20, eSetValueWithOverwrite ); if (res == pdPASS) printf("task1: task2->通知数组[1] = 20\r\n"); } else if (key == KEY3_PRESS) { /* xTaskNotifyIndexed = 写 task3 通知数组[0] = 99(覆写) */ res = xTaskNotifyIndexed( task3_handle, 0, 99, eSetValueWithOverwrite ); if (res == pdPASS) printf("task1: task3->通知数组[0] = 99\r\n"); } vTaskDelay(500); } } void task2(void *const pvParameters) { uint32_t notify_value = 0; BaseType_t res = 0; while (1) { printf("task2阻塞等待通知数组[0]...\r\n"); /* xTaskNotifyWaitIndexed(0) = 阻塞读通知数组[0],出口清零 */ res = xTaskNotifyWaitIndexed( 0, 0x00000000, 0xffffffff, ¬ify_value, portMAX_DELAY ); if (res == pdTRUE) printf("task2: 通知数组[0] = %ld\r\n", notify_value); printf("task2阻塞等待通知数组[1]...\r\n"); /* xTaskNotifyWaitIndexed(1) = 阻塞读通知数组[1],出口清零 */ res = xTaskNotifyWaitIndexed( 1, 0x00000000, 0xffffffff, ¬ify_value, portMAX_DELAY ); if (res == pdTRUE) printf("task2: 通知数组[1] = %ld\r\n", notify_value); vTaskDelay(200); } } void task3(void *const pvParameters) { uint32_t notify_value = 0; BaseType_t res = 0; while (1) { printf("task3阻塞等待通知数组[0]...\r\n"); /* xTaskNotifyWaitIndexed(0) = 阻塞读通知数组[0],出口清零 */ res = xTaskNotifyWaitIndexed( 0, 0x00000000, 0xffffffff, ¬ify_value, portMAX_DELAY ); if (res == pdTRUE) printf("task3: 通知数组[0] = %ld\r\n", notify_value); vTaskDelay(200); } } ``` ### 关键配置 ```c /* FreeRTOSConfig.h */ #define configUSE_TASK_NOTIFICATIONS 1 /* 使能任务通知(默认已使能) */ #define configTASK_NOTIFICATION_ARRAY_ENTRIES 3 /* 通知数组大小,v10.4起支持多通知。实验1-3用1,实验4用3 */ ``` ## 核心函数速查表 | 函数 | 参数 | 返回值 | 功能简述 | | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------- | -------------------------------- | | `BaseType_t xTaskNotifyGive(xTaskToNotify)` | 目标任务句柄 | pdPASS/pdFAIL | 发送通知(信号量模式,通知值+1) | | `uint32_t ulTaskNotifyTake(xClearCountOnExit, xTicksToWait)` | 退出时清零?, 超时 | 通知值 | 接收通知(信号量模式,阻塞) | | `BaseType_t xTaskNotify(xTaskToNotify, ulValue, eAction)` | 目标任务, 值, 动作(覆写/OR/增量等) | pdPASS/pdFAIL | 发送通知(带值) | | `BaseType_t xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait)` | 入口清除, 出口清除, 值指针, 超时 | pdPASS/pdFAIL | 接收通知(阻塞等待) | | `BaseType_t xTaskNotifyFromISR(xTaskToNotify, ulValue, eAction, pxHigherPriorityTaskWoken)` | 同 xTaskNotify+唤醒标记 | pdPASS/pdFAIL | ISR中发送通知 | | `void vTaskNotifyGiveFromISR(xTaskToNotify, pxHigherPriorityTaskWoken)` | 目标任务, 唤醒标记 | 无 | ISR中信号量模式发送 | | `BaseType_t xTaskNotifyAndQuery(xTaskToNotify, ulValue, eAction, pulPreviousValue)` | 同 xTaskNotify+原值 | pdPASS/pdFAIL | 发送并获取原通知值 | **eAction 枚举值**: - `eNoAction`:仅发送通知(不修改值) - `eSetBits`:与现有值按位 OR - `eIncrement`:通知值 +1 - `eSetValueWithOverwrite`:无条件覆写 - `eSetValueWithoutOverwrite`:仅当接收方未读取时覆写(否则返回 pdFAIL) ## 常见问题与避坑 1. **任务通知只能一对一**:v10.4 之前一个任务只有一个通知值。不能像事件标志组那样多个任务等待同一个事件。 2. **ISR 不可接收通知**:没有 `xTaskNotifyWaitFromISR()` 函数。接收任务通知只能在任务上下文中进行。 3. **通知值溢出不提示**:`xTaskNotifyGive()` 使 32 位通知值递增,溢出后归零且无错误提示。 4. **信号量模式的 xClearCountOnExit**: - `pdTRUE`:读取后清零(适合二值信号量) - `pdFALSE`:读取后减 1(适合计数信号量),`ulTaskNotifyTake()` 返回减 1 前的值 5. **消息邮箱模式的数据丢失**:使用 `eSetValueWithOverwrite` 时,如果发送者在接收者读取前多次发送,只有最后一次的值会被保留。 6. **性能优势的来源**:任务通知不经过队列内核对象,直接在 TCB 中操作,省去了队列的入队/出队开销和等待列表管理。