(2026/8/3))
目录一任务通知1、为什么需要任务通知2、任务通知 优劣3、任务发送方API① 计数通知 — 替代计数信号量② 二值通知 — 替代二值信号量③ 32 位值传递 — 传简单数据④ Bit 通知 — 替代事件组单任务场景4、任务接收方API5API总结6、跟其他机制的全面对比7、任务通知为什么快8、实际项目使用频率9、任务通知的本质二任务通知模拟二值信号量rtos\17\ 改动清单freertos_demo.cmain.c三任务通知模拟计数信号量rtos\18\ 改动清单freertos_demo.cmain.c两个实验的唯一区别四任务通知模拟事件标志组rtos\19\ 改动清单freertos_demo.cmain.c任务通知三个实验总结五任务通知模拟队列传输数据rtos\20\ 改动清单freertos_demo.cmain.c任务通知四个实验完整对比总结一任务通知1、为什么需要任务通知信号量和队列需要独立的对象// 信号量需要先创建一个独立的内核对象 SemaphoreHandle_t dma_sem xSemaphoreCreateBinary(); // 队列也是独立对象 QueueHandle_t uart_queue xQueueCreate(16, sizeof(uint8_t));但很多场景根本不需要传数据——任务只想知道事情发生了起来干活DMA 传输完成 → 通知任务 → 处理数据 定时器到期 → 通知任务 → 采集传感器 ADC 采样完成 → 通知任务 → 滤波计算 外部中断触发 → 通知任务 → 响应事件每个都建一个信号量太多。FreeRTOS 就把通知功能直接塞进每个任务的 TCB 里// TCB 内部天然带着通知功能任务创建时就有了不用额外申请 typedef struct tskTaskControlBlock { ... volatile uint32_t ulNotifiedValue; // ★ 32 位通知值 volatile uint8_t ucNotifyState; // 通知状态等待中 / 已收到 ... } tskTCB; 任务通知 每个任务自带一个 32 位信箱不需要单独创建内核对象。通知状态ulNotifiedValueTCB 里 ucNotifyState 这个字段的三个状态跟 API 调用关系如下 taskNOT_WAITING_NOTIFICATION默认 │ ├── 接收方调 ulTaskNotifyTake() 或 xTaskNotifyWait() │ → ucNotifyState taskWAITING_NOTIFICATION阻塞等 │ ├── 还没人等发送方就先发了 │ 调 xTaskNotify(task, val, action) │ → ucNotifyState taskNOTIFICATION_RECEIVED通知挂着等 │ └── 接收方后来调了 Take/Wait 发现有挂着的通知 → 立刻拿到 → ucNotifyState 恢复默认 一句话发送方和接收方谁先调都行。接收方先调 → 阻塞等着发送方先调 → 通知挂着对方来了立刻匹配上。不像队列/信号量必须先创建对象才能用。2、任务通知 优劣优势/劣势你说对不对更快不走队列内核对象直接写 TCB 两个字段✅ 对更省内存不需要xXxxCreate()TCB 自带✅ 对每个任务创建时就自带通知功能ISR 不能接收ISR 没有 TCB✅ 对ISR 可以发送xTaskNotifyFromISR()/vTaskNotifyGiveFromISR()✅ 对不能广播只能发给指定任务一对一✅ 对不能缓存多条只有ulNotifiedValue一个 32 位值✅ 对队列能存 N 条通知只能 1 个发送方不阻塞xTaskNotify从不阻塞发了就跑✅ 对不像xQueueSend(..., wait)能死等说白了一句话任务通知 一对一、不缓存、最轻最快的通知方式。 一对多/要缓存数据 → 用队列。一对一/只通知 → 用任务通知。3、任务发送方API发送方 xTaskNotify() 的 eAction 参数四种更新方式 BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ); // ← 这个决定怎么更新 形参1任务句柄 形参2 32 位通知值你填 形参3eAction 值 这个决定怎么更新 eAction 值 通知值的更新方式 等价于 eNoAction 不改变值仅唤醒任务 信号量 Give eIncrement 计数 1 计数信号量 eSetValueWithOverwrite 覆盖写入不管旧值 覆盖写 32 位 eSetValueWithoutOverwrite 不覆盖只有旧值0 时才写 二值信号量0→值 eSetBits OR 置位更新指定 bit 事件组 SetBits ① 计数通知 — 替代计数信号量// 发送方任务/中断计数 1发 3 次 xTaskNotify(taskHandle, 0, eIncrement); // 通知值 1 xTaskNotify(taskHandle, 0, eIncrement); // 通知值 2 xTaskNotify(taskHandle, 0, eIncrement); // 通知值 3 // 接收方目标任务每次 Take 计数 -1 while (1) { uint32_t count ulTaskNotifyTake(pdFALSE, // 模式pdTRUE清零, pdFALSE减1到底 portMAX_DELAY); // 死等 // count 本次 Take 之前还剩多少 } 效果等价于计数信号量但更快更省。② 二值通知 — 替代二值信号量最简单模式0 → Give → 1 → Take → 0。跟二值信号量一模一样但没有独立内核对象。// 发送方ISR/任务唤醒一次通知值 0→1 vTaskNotifyGiveFromISR(taskHandle, wake); // ISR 版 // 或 xTaskNotifyGive(taskHandle); // 任务版 // xTaskNotifyGive() — 任务版 // 内部就是 BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ) { return xTaskNotify( xTaskToNotify, 0, eIncrement ); // ↑ ↑ // 值没用 计数 1跟 eNoAction 效果一样 } // vTaskNotifyGiveFromISR() — ISR 版 // 内部就是 void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *wake ) { (void) xTaskNotifyFromISR( xTaskHandle, 0, eIncrement, wake ); // ↑ ↑ // 值没用 计数 1 } 二值通知的本质就是 计数 10→1→ 任务 Take 后清零。因为用得最多代替二值信号量FreeRTOS 给了两个直接的函数让你不用记 eAction。当然你也可以用完整版 // 这两行等效 xTaskNotifyGive(taskHandle); xTaskNotify(taskHandle, 0, eIncrement); // 接收方醒来通知值归零 while (1) { ulTaskNotifyTake(pdTRUE, // ★ pdTRUE 醒来时全部清零二值模式 portMAX_DELAY); // 死等 // DMA 数据到了 → 处理 } 对比二值信号量xSemaphoreGive / xSemaphoreTake。区别不需要 xSemaphoreCreateBinary() ③ 32 位值传递 — 传简单数据通知值本身就是 32 位可以传状态码、错误码等小数据// 发送方覆盖写入错误码 xTaskNotify(errTaskHandle, 0x05, // ★ 要传的 32 位数据 eSetValueWithOverwrite); // ★ 覆盖不管旧值有没有被读 // 接收方拿到 32 位值 uint32_t val; BaseType_t ret xTaskNotifyWait(0, // 不清任何 bit 0xFFFFFFFF, // 退出时清零全部 bit val, // ★ 输出收到的 32 位值 portMAX_DELAY); // ret pdTRUE → val 0x05 发送方 action 含义 eSetValueWithOverwrite 覆盖旧值不管有没有被读过 eSetValueWithoutOverwrite 旧值没被读≠0时不覆盖返回 pdFAIL ④ Bit 通知 — 替代事件组单任务场景32 位值当 32 个标志位用#define WIFI_OK (1 0) #define CAN_OK (1 1) #define ADC_OK (1 2) 形参eSetBits OR 置位更新指定 bit // 发送方各任务/中断各自负责自己的 bit xTaskNotify(appTaskHandle, WIFI_OK, eSetBits); // WiFi 完成置 bit0 xTaskNotify(appTaskHandle, CAN_OK, eSetBits); // Can 完成置 bit1 // 通知值从 0 变成: 00000011 // 接收方等指定 bit 全满 uint32_t bits; xTaskNotifyWait(0, // 不清任何 bit ALL_BITS, // ★ 退出时把等到的 bit 清零 bits, // ★ 输出收到时的通知值 portMAX_DELAY); // bits 00000011同时 ALL_BITS 被清零下一轮继续等 跟事件组的区别事件组可以多个任务等同一个 EventGroupHandle_t。任务通知是一对一——只能一个目标任务等但速度更快不需要创建事件组对象。4、任务接收方API接收方就两个 API API 模式 通知值怎么处理 ulTaskNotifyTake(pdTRUE, wait) 二值/计数 醒来清零 / 每次 -1返值是旧值 xTaskNotifyWait(0, mask, val, wait) 值传递/Bit 返回收到的 32 位值可选清 bit // ① 二值/计数模式 — ulTaskNotifyTake uint32_t count; count ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 二值醒来清零 count ulTaskNotifyTake(pdFALSE, portMAX_DELAY); // 计数每次 -1 // ② 值传递/Bit 模式 — xTaskNotifyWait uint32_t val; BaseType_t ret xTaskNotifyWait(0, 0xFFFFFFFF, val, portMAX_DELAY); // ret pdTRUE → val 发来的 32 位数据5API总结发送和接收各两个 发送方两个 xTaskNotifyGive() → 最简单的 Give计数 1不用传值 xTaskNotify(val, action) → 完整版可传值 指定更新方式 接收方两个 ulTaskNotifyTake() → 二值/计数模式返回旧值自动清零或 -1 xTaskNotifyWait() → 值传递/Bit 模式返回收到的 32 位值 action 的 5 个选项 action 效果 eNoAction 只唤醒不改值 eIncrement 计数 1 eSetValueWithOverwrite 覆盖写 32 位 eSetValueWithoutOverwrite 旧值≠0 时不覆盖 eSetBits OR 置位 补充一个用得不多的发送函数API 函数• xTaskNotifyAndQuery() 描述• 发送通知带有通知值并且保留接收任务的原通知值 底层是同一个函数 xTaskGenericNotify只是多一个输出参数 // 普通版不管旧值发了就走 #define xTaskNotify(task, val, action) \ xTaskGenericNotify(task, val, action, NULL) // ↑ 不关心旧值传 NULL // 查询版同时把旧值带出来 #define xTaskNotifyAndQuery(task, val, action, prev) \ xTaskGenericNotify(task, val, action, prev) // ↑ 输出更新前的通知值 // 使用示例 uint32_t prev; xTaskNotifyAndQuery(taskHandle, 0x55, eSetValueWithOverwrite, prev); // prev 更新前通知值是啥比如上次还没来得及读的值 用得不多——只有需要知道发送前通知值是什么的时候才用。比如检测上一次通知有没有被接收方取走或者 debug 时追踪通知丢失。日常用 xTaskNotify 就够了。6、跟其他机制的全面对比二值信号量计数信号量队列事件组任务通知独立对象需要需要需要需要不需要传数据❌❌✅❌✅ 32 位多任务等同一个源✅✅✅✅❌ 一对一速度快快快一般快最快RAM一般一般多少最少适合场景中断通知资源计数数据传递多条件同步一对一通知7、任务通知为什么快信号量的 Give 走的是队列发送路径xQueueGenericSend→ 找等待列表 →pvOwner→ 挂回 ReadyList有完整的内核对象操作。任务通知直接操作 TCB 里的两个字段信号量路径 Give → 队列结构体 → 等待列表 → ListItem → pvOwner → TCB → ReadyList 任务通知路径 Give → 目标 TCB.ulNotifiedValue → TCB.ucNotifyState → ReadyList ↑ 直接写不走队列那套没有中间对象没有pvPortMalloc没有链表操作。就两个字段读写所以快。8、实际项目使用频率Queue ★★★★★ 数据传递不可替代 Task Notification ★★★★★ 中断通知任务首选 Semaphore ★★★★ 多任务等同一个源时用 Event Group ★★★★ 多条件状态同步 Mutex ★★★★ 共享资源保护 Queue Set ★★ 多路选择一般有替代方案中断通知任务是任务通知最核心的用法——ISR 里vTaskNotifyGiveFromISR任务里ulTaskNotifyTake零额外开销。9、任务通知的本质FreeRTOS 内核的设计模式所有内核对象 等待链表 状态变量。任务通知是这种思想的极致——把状态变量和等待链表直接塞进 TCB跳过了独立内核对象这一层做到了最简最快。二任务通知模拟二值信号量rtos\17\ 改动清单freertos_demo.c行号内容14无额外头文件——任务通知 API 在task.h里不需要semphr.h/queue.h44无内核对象句柄——没有xSemaphoreCreateXxx()没有QueueHandle_t98-108task1xTaskNotifyGive(Task2_Handler)— 不需要句柄直接发120-126task2ulTaskNotifyTake(pdTRUE, 死等)— 不需要句柄直接等main.c行号内容26printf 标题 →FreeRTOS Task Notify Test!不需要开任何新宏——configUSE_TASK_NOTIFICATIONS 1早在 rtos\2\ 就开了。跟二值信号量实验rtos\11\对比二值信号量任务通知创建对象sem xSemaphoreCreateBinary()不需要发送xSemaphoreGive(sem)xTaskNotifyGive(Task2_Handler)接收xSemaphoreTake(sem, 死等)ulTaskNotifyTake(pdTRUE, 死等)额外头文件semphr.h无task.h自带可以编译烧录了。三任务通知模拟计数信号量rtos\18\ 改动清单freertos_demo.c行号内容105task1xTaskNotifyGive(Task2_Handler)— 跟二值版完全一样120task2ulTaskNotifyTake(pdFALSE, portMAX_DELAY)—pdFALSE 计数模式每次 -1123rev - 1— 打印减完后还剩多少main.c行号内容26printf 标题 →FreeRTOS Task Notify Counting Test!两个实验的唯一区别rtos\17\二值ulTaskNotifyTake(pdTRUE, ...) → 醒来全部清零 rtos\18\计数ulTaskNotifyTake(pdFALSE, ...) → 每次只减 1运行效果task2 处理慢睡 1 秒你连按 3 次 KEY1 → 输出剩余2剩余1剩余0然后 task2 重新阻塞。四任务通知模拟事件标志组rtos\19\ 改动清单freertos_demo.c行号内容51-52EVENTBIT_0/EVENTBIT_1位定义100-109task1xTaskNotify(Task2, EVENTBIT_0, eSetBits)— Bit 模式置位131-139task2xTaskNotifyWait(0, 0xFFFFFFFF, val, 死等)— 值传递模式接收142-148手动累计event_bit | ...→ 两个 bit 都齐 → 打印 → 清零main.c行号内容26printf 标题 →FreeRTOS Task Notify Event Group Test!任务通知三个实验总结rtos\17\ 二值rtos\18\ 计数rtos\19\ 事件组发送xTaskNotifyGive()xTaskNotifyGive()xTaskNotify(task, bits, eSetBits)接收ulTaskNotifyTake(pdTRUE)ulTaskNotifyTake(pdFALSE)xTaskNotifyWait(0, 0xFF.., val, wait)替代对象二值信号量计数信号量事件组// 事件组rtos\16\一行搞定 bits xEventGroupWaitBits(eg, BIT_0|BIT_1, pdTRUE, pdTRUE, 死等); // 判断是否到齐 → 自动清零 → 返回。不需要手动累计不需要 if。 // 任务通知rtos\19\得自己手动累计 xTaskNotifyWait(0, 0xFFFFFFFF, val, 死等); if (val BIT_0) event_bit | BIT_0; // 手动累计 if (val BIT_1) event_bit | BIT_1; // 手动累计 if (event_bit ALL) { ... event_bit 0; } // 手动判断、手动清零事件组内部有xTasksWaitingForBits链表哪几个任务在等、等的哪些 bit、AND 还是 OR、要不要自动清零——内核全管了。任务通知只给你一个 32 位值和唤醒机制逻辑你得自己写。所以一对一、简单场景用任务通知快、省。一对多、组合条件、自动清零用事件组省心。五任务通知模拟队列传输数据rtos\20\ 改动清单freertos_demo.c行号内容89-101task1xTaskNotify(Task2, 1或2, eSetValueWithOverwrite)— 传实际数据127-134task2xTaskNotifyWait(0, 0xFFFFFFFF, val, 死等)→ 打印val137-141收到 1 → 亮红灯收到 2 → 灭红灯main.c行号内容26printf 标题 →FreeRTOS Task Notify Mailbox Test!任务通知四个实验完整对比rtos\17\rtos\18\rtos\19\rtos\20\模拟对象二值信号量计数信号量事件组消息邮箱发送 APINotifyGiveNotifyGiveNotify(..., eSetBits)Notify(..., eSetValue)接收 APITake(pdTRUE)Take(pdFALSE)Wait(0, mask, val)Wait(0, mask, val)通知值含义开关计数bit 标志实际数据总结正点的实验都是最小化演示——一个发送方一个接收方串口打印结果。目的就是让你知道 API 怎么调、参数怎么填。真正的用法是后面做项目时自己组合按键→任务通知→处理、DMA中断→任务通知→处理、CAN中断→队列→处理、SPI Flash→互斥锁→保护。正点给了积木块怎么搭是你的事。