tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第14章 任务通知" author: "尚硅谷研究院" date: 2026-07-17
用生活理解:任务通知就像同事直接走到你工位前告诉你一件事,不需要通过公司广播(队列)、公告栏(信号量)或旗语系统(事件标志组)。直接、高效,但因为是一对一,没法同时通知所有人。
FreeRTOS 已经有队列、信号量、事件标志组这些 IPC 机制,为什么还需要任务通知?
看一个常见场景:任务 A 做完某件事后,想告诉任务 B"活干完了,该你了"。用队列实现需要先 xQueueCreate 创建队列对象,然后 A 调用 xQueueSend、B 调用 xQueueReceive。用信号量实现也需要先 xSemaphoreCreateBinary 创建信号量对象。
这些 IPC 机制的问题是"中间商赚差价":每次通信都要经过一个额外创建的内核对象(队列/信号量/事件组)。对于"我就想叫一下隔壁任务"这么简单的事,创建一个完整的内核对象太浪费了。
任务通知就是为解决这个问题而生——发送任务直接修改目标任务的 TCB 字段,不需要任何中间对象。
每个 FreeRTOS 任务的 TCB(任务控制块)内部自带两个字段:
ulNotifiedValue:一个 32 位的通知值(可作计数器、位图、消息存储)ucNotifyState:一个 8 位的通知状态(taskNOT_WAITING_NOTIFICATION / taskWAITING_NOTIFICATION / taskNOTIFICATION_RECEIVED)
configTASK_NOTIFICATION_ARRAY_ENTRIES控制通知数组大小,默认 1。设为 N 时每个任务拥有 N 组独立通知值。FreeRTOS V10.4.0 之后支持此特性。
普通 IPC 的通信路径:
任务 A → [创建队列/信号量对象] → 放入数据 → 队列/信号量 → 任务 B 取出数据
└── 中间对象(额外分配内存)──┘
任务通知的通信路径:
任务 A → 直接写入任务 B 的 TCB
└── 不创建任何对象 ──┘
这就是任务通知比队列/信号量快约 45% 的原因——省去了创建、操作、销毁内核对象的所有开销。
任务通知有两步:发送和接收。关键在于接收方可以选择阻塞等待通知的到来:
xTaskNotifyGive() 或 xTaskNotify() → 直接写入目标任务的 TCB 通知值 → 将目标任务的通知状态标记为"已收到通知"ulTaskNotifyTake() 或 xTaskNotifyWait():
通知值的 32 位可以灵活解释,由此衍生出三种使用模式,替代三种传统 IPC:
| 模式 | 对应传统 IPC | 发送函数 | 接收函数 | 32 位值的用法 |
|---|---|---|---|---|
| 信号量模式 | 二值/计数信号量 | xTaskNotifyGive() |
ulTaskNotifyTake() |
当作计数器:Give 加 1,Take 减 1 |
| 消息邮箱模式 | 队列(单消息) | xTaskNotify(..., eSetValueWithOverwrite) |
xTaskNotifyWait() |
当作消息容器:覆写/条件覆写 32 位值 |
| 事件标志组模式 | 事件标志组 | xTaskNotify(..., eSetBits) |
xTaskNotifyWait() |
当作位图:按位 OR 设置标志位 |
一句话选型:
| 对比项 | 队列/信号量/事件组 | 任务通知 |
|---|---|---|
| 通信方向 | 多对多(多个任务可发/收) | 只能一对一(一个任务只有一个通知值) |
| 数据缓存 | 队列可缓存多条消息 | 只能缓存一个 32 位值(覆写会丢数据) |
| ISR 接收 | 可接收 | 不能接收(无 FromISR 版本接收函数) |
| 创建步骤 | 需先 Create 对象 | 无需创建,直接用 |
| 函数 | 参数 | 功能简述 |
|---|---|---|
xTaskNotifyGive() |
xTaskToNotify |
发送通知(信号量模式),通知值 +1 |
ulTaskNotifyTake() |
xClearCountOnExit, xTicksToWait |
接收通知(信号量模式),返回通知值 |
xTaskNotify() |
xTaskToNotify, ulValue, eAction |
发送通知(带值),eAction 控制写/OR/覆写行为 |
xTaskNotifyWait() |
ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait |
接收通知(带值),可获取通知值和清除指定位 |
xTaskNotifyFromISR() |
同上+ISR 版本 | 中断中发送通知 |
vTaskNotifyGiveFromISR() |
xTaskToNotify, pxHigherPriorityTaskWoken |
中断中发送(信号量模式) |
以下实验代码均提取自配套 Keil 项目的
freertos_demo.c,仅展示核心任务函数。
需求: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的通知累积 */
}
}
需求: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, // 退出时清除所有位(通知值归零)
¬ify_value, // 保存接收到的通知值
portMAX_DELAY);
if (res == pdTRUE)
{
printf("Task2收到通知值=%d\r\n", notify_value);
}
}
}
需求: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, // 退出时清除所有位
¬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);
}
}
/* FreeRTOSConfig.h */
#define configUSE_TASK_NOTIFICATIONS 1 /* 使能任务通知(默认已使能) */
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1 /* 通知数组大小,v10.4起支持多通知 */
| 函数 | 参数 | 返回值 | 功能简述 |
|---|---|---|---|
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:与现有值按位 OReIncrement:通知值 +1eSetValueWithOverwrite:无条件覆写eSetValueWithoutOverwrite:仅当接收方未读取时覆写(否则返回 pdFAIL)xTaskNotifyWaitFromISR() 函数。接收任务通知只能在任务上下文中进行。xTaskNotifyGive() 使 32 位通知值递增,溢出后归零且无错误提示。pdTRUE:读取后清零(适合二值信号量)pdFALSE:读取后减 1(适合计数信号量),ulTaskNotifyTake() 返回减 1 前的值eSetValueWithOverwrite 时,如果发送者在接收者读取前多次发送,只有最后一次的值会被保留。