任务通知(Task Notification)是 FreeRTOS 中最轻量、最快的任务间通信方式。之前学的队列、信号量、事件组都需要先创建一个中间对象(队列/信号量结构体),发送方和接收方通过这个中间对象间接通信。任务通知则跳过中间对象,直接从一个任务发送到另一个任务。

传统 IPC:
Task1 → 队列/信号量/事件组 → Task2   (需要中间对象,有内存开销)

任务通知:
Task1 ────────────────────→ Task2    (直接通知,无中间对象)

每个任务自带的通知字段

启用任务通知功能后(configUSE_TASK_NOTIFICATIONS = 1,默认开启),每个任务的 TCB(任务控制块)中自动包含:

字段类型说明
通知状态uint8_ttaskWAITING_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-内存管理

相关:03-队列04-中断处理07-事件组返回目录