任务通知(Task Notification)是 FreeRTOS 中最轻量、最快的任务间通信方式。之前学的队列、信号量、事件组都需要先创建一个中间对象(队列/信号量结构体),发送方和接收方通过这个中间对象间接通信。任务通知则跳过中间对象,直接从一个任务发送到另一个任务。
传统 IPC:
Task1 → 队列/信号量/事件组 → Task2 (需要中间对象,有内存开销)
任务通知:
Task1 ────────────────────→ Task2 (直接通知,无中间对象)
每个任务自带的通知字段
启用任务通知功能后(configUSE_TASK_NOTIFICATIONS = 1,默认开启),每个任务的 TCB(任务控制块)中自动包含:
| 字段 | 类型 | 说明 |
|---|---|---|
| 通知状态 | uint8_t | taskWAITING_NOTIFICATION(Pending)或 taskNOT_WAITING_NOTIFICATION(Not-Pending) |
| 通知值 | uint32_t | 一个 32 位无符号整数,可以当计数器、位掩码、事件组、或任意数据用 |
工作流程:
Task1 发通知给 Task2
→ Task2 的通知状态 = Pending(有未处理通知)
Task2 读取通知值
→ Task2 的通知状态 = Not-Pending(通知已处理)
优势对比
| 中间对象方式(队列/信号量/事件组) | 任务通知 | |
|---|---|---|
| 需要创建中间对象 | 需要 | 不需要 |
| 内存占用 | 多(每个队列 ~80+ 字节) | 每个任务多 8 字节(TCB 内) |
| 速度 | 较慢 | 快 45% 以上(官方测试) |
| 能通知多个任务 | 能 | ❌ 只能通知一个指定任务 |
| 发送端 | 任意任务/ISR | 任意任务/ISR |
| 接收端 | 任意任务等待 | 只能是被通知的那个任务 |
| 能否广播 | 能 | ❌ 不能 |
什么时候用任务通知
只要通信模式是”一个事件源通知一个任务”(绝大多数中断→任务的场景都符合),优先用任务通知,更快更省内存。只有需要广播、多接收者、或传递大块数据时才用队列/事件组。
两套 API
简化版(最常用)
// 发送通知:通知值加1,状态置为 Pending(等价于二值/计数信号量的 Give)
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
// ISR 版本
void vTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken);
// 接收通知:等待通知到达
uint32_t ulTaskNotifyTake(
BaseType_t xClearCountOnExit, // pdTRUE=退出时清零;pdFALSE=退出时减1
TickType_t xTicksToWait
);
xTaskNotifyGive() 的行为:
发送前:通知状态 = Not-Pending,通知值 = 0
调用后:通知状态 = Pending, 通知值 = 1
再调用一次:通知值 = 2(累加)
ulTaskNotifyTake() 的行为:
xClearCountOnExit = pdTRUE:
收到通知 → 状态变 Not-Pending → 通知值清零为 0
(类似二值信号量,一次处理所有积累的通知)
xClearCountOnExit = pdFALSE:
收到通知 → 状态变 Not-Pending → 通知值减1
(类似计数信号量,一次处理一个通知)
返回值:清零/减1之前的通知值(可以用来判断积累了多少个通知)。
中断→任务同步的最简写法
// ISR 中:只需要一行
void EXTI_IRQHandler(void) {
// 清中断标志...
vTaskNotifyGiveFromISR(xHandlerTask, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 处理任务:
void vHandlerTask(void *pv) {
for(;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 等待通知
// 处理中断事件
}
}
比用二值信号量少了创建信号量的步骤,代码更简洁,性能更好。
完整版(灵活但复杂)
BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction // 通知动作类型
);
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 进入时清除哪些 bit
uint32_t ulBitsToClearOnExit, // 退出时清除哪些 bit
uint32_t *pulNotificationValue, // 出参:原始通知值(可为 NULL)
TickType_t xTicksToWait
);
eNotifyAction:通知的五种操作
xTaskNotify() 的第三个参数 eAction 指定如何更新通知值:
| 动作 | 行为 | 等价于 |
|---|---|---|
eIncrement | 通知值加 1 | 等价于 xTaskNotifyGive(),当计数信号量用 |
eNoAction | 不修改通知值,只把状态设为 Pending | 等价于二值信号量,纯粹通知”有事件了” |
eSetBits | 通知值 |= ulValue(按位或) | 等价于轻量级事件组,设置指定 bit |
eSetValueWithOverwrite | 通知值 = ulValue(直接覆盖,不管之前的值) | 等价于邮箱,更新为最新值 |
eSetValueWithoutOverwrite | 如果状态是 Not-Pending 才覆盖,否则返回 pdFAIL | 发送消息但不覆盖未处理的值 |
用 eSetBits 代替事件组
// 发送端:设置 bit
#define EVENT_DATA_READY (1 << 0)
#define EVENT_SEND_DONE (1 << 1)
xTaskNotify(xTask, EVENT_DATA_READY, eSetBits);
// 接收端:等待多个 bit
uint32_t ulNotifiedValue;
xTaskNotifyWait(
0, // 进入时不清除任何 bit
0xFFFFFFFF, // 退出时清除所有 bit
&ulNotifiedValue, // 取出通知值
portMAX_DELAY
);
if(ulNotifiedValue & EVENT_DATA_READY) { /* 数据就绪 */ }
优势:不需要创建事件组对象,性能更好。限制:只能通知一个任务(这正是大多数场景的需求)。
用 eSetValueWithOverwrite 传递数据
// 发送端:直接传递一个 32 位值(如 ADC 采样值)
xTaskNotify(xTask, adc_value, eSetValueWithOverwrite);
// 接收端
uint32_t ulValue;
xTaskNotifyWait(0, 0xFFFFFFFF, &ulValue, portMAX_DELAY);
// ulValue 就是发送过来的 adc_value
可以传递任何能塞进 32 位的值:整数、枚举、甚至指针(32位平台上指针是32位)。
xTaskNotifyWait 详解
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 进入等待前,先清除这些 bit
uint32_t ulBitsToClearOnExit, // 收到通知、退出前,清除这些 bit
uint32_t *pulNotificationValue, // 传出:清除前的通知值
TickType_t xTicksToWait
);
ulBitsToClearOnEntry 的作用
进入函数时(开始等待前),先把通知值中指定的 bit 清零:
xTaskNotifyWait(0x00, ...); // 进入时不清零任何 bit
xTaskNotifyWait(0xFFFFFFFF, ...); // 进入时清零所有 bit(重新开始等)
为什么需要?因为你可能想在处理完一批事件后,干净地等待新事件,不希望旧的 bit 残留。
ulBitsToClearOnExit 的作用
收到通知后、退出函数前,清除指定的 bit:
xTaskNotifyWait(..., 0xFFFFFFFF, ...); // 退出时清零所有 bit(类似二值信号量)
xTaskNotifyWait(..., 0x01, ...); // 退出时只清 bit0(留着其他 bit)
取出通知值
如果 pulNotificationValue 不为 NULL,在执行 ulBitsToClearOnExit 清除之前,会把原始通知值传出来。这样你可以检查哪些 bit 被置位了。
uint32_t ulValue;
xTaskNotifyWait(0, 0xFFFFFFFF, &ulValue, portMAX_DELAY);
// ulValue 里是清零之前的通知值,可以检查各个 bit
常见使用模式
模式1:中断→任务同步(替代二值信号量)
// ISR
vTaskNotifyGiveFromISR(xTask, &xWoken);
// Task
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 清零,类似二值信号量
模式2:计数信号量(限流)
// ISR(多次触发)
vTaskNotifyGiveFromISR(xTask, &xWoken);
// Task
while(ulTaskNotifyTake(pdFALSE, 0)) { // 减1,处理一个
process_one_event();
}
模式3:轻量级事件组
// 多个地方发送通知,设置不同 bit
xTaskNotify(xTask, BIT0, eSetBits);
xTaskNotify(xTask, BIT1, eSetBits);
// Task
uint32_t v;
xTaskNotifyWait(0, 0xFFFFFFFF, &v, portMAX_DELAY);
if(v & BIT0) handle_bit0();
if(v & BIT1) handle_bit1();
模式4:传递数据值
xTaskNotify(xTask, sensor_value, eSetValueWithOverwrite);
// Task
uint32_t val;
xTaskNotifyWait(0, 0xFFFFFFFF, &val, portMAX_DELAY);
// val == sensor_value
模式5:传递指针(大数据)
xTaskNotify(xTask, (uint32_t)buffer_ptr, eSetValueWithOverwrite);
// Task
uint32_t tmp;
xTaskNotifyWait(0, 0xFFFFFFFF, &tmp, portMAX_DELAY);
buffer_t *buf = (buffer_t*)tmp;
传递指针的注意事项
指针指向的内存必须是持久有效的(不能是发送方栈上的局部变量),且要确保接收方处理完之前发送方不会修改或释放这块内存。这和队列传指针的注意事项一样。
下一步:09-内存管理