14-任务通知.md 19 KB


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

/* 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

/* 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,    // 退出时清除所有位(通知值归零)
            &notify_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

#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,    // 退出时清除所有位
            &notify_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

/* 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, &notify_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, &notify_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, &notify_value, portMAX_DELAY
    );
    if (res == pdTRUE)
      printf("task3: 通知数组[0] = %ld\r\n", notify_value);

    vTaskDelay(200);
  }
}

关键配置

/* 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 中操作,省去了队列的入队/出队开销和等待列表管理。