所谓"任务通知",你可以反过来读"通知任务"。
我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。
使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:

使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知":

一:知识点
1. 什么是任务通知?
想象一下,在 FreeRTOS 的世界里,每一个 任务 (Task) 都是一个独立的“工人”。
在传统的通信方式(如队列、信号量)中,如果工人 A 想给 工人 B 发消息,他们需要通过一个中间的公共信箱(队列对象)。
-
A 把信放进公共信箱。
-
B 去公共信箱取信。
而在 任务通知 中,每个工人(任务)随身自带了一个私人的“小口袋”(这就是任务控制块 TCB 中的一个 32位变量)。
-
直接交互: 工人 A 不需要找公共信箱,直接把东西塞进 工人 B 的“小口袋”里,或者拍一下 B 的肩膀。
2. 为什么要用它?(优点)
对于初学者来说,你只需要记住两个词:更快、更省。
-
速度更快: 因为不需要创建中间的“公共信箱”对象,直接操作任务,处理速度比信号量和队列快 45% 左右。
-
内存更省: 不需要额外的 RAM 来创建队列或信号量结构体,因为它利用了任务本身自带的空间。
3. 它能做什么?(三种主要用法)
这个“小口袋”虽然简单,但玩法很多。通过改变发送的方式,它可以替代三种传统工具:
A. 代替“二值信号量” (Binary Semaphore)
-
场景: 任务 A 完成了一件事,通知任务 B “嘿,可以开工了”。
-
原理: 就像拍一下肩膀。不管拍几下,B 醒来只知道“有人拍我了”。
-
API 关键词:
xTaskNotifyGive()(发送),ulTaskNotifyTake()(接收).
B. 代替“计数信号量” (Counting Semaphore)
-
场景: 任务 A 按了 3 次按钮,任务 B 需要处理 3 次。
-
原理: 就像在小口袋里放豆子。A 放一颗,数字加 1;B 拿一颗,数字减 1。
-
API 关键词: 同上,但接收时参数不同。
C. 代替“轻量级邮箱” (Mailbox/Queue)
-
场景: 任务 A 采集到了一个传感器数值(比如温度 25℃),直接传给任务 B。
-
原理: A 直接把 32位的数据(如
25)写进 B 的口袋里。 -
注意: 只能传一个 32位的数据(或者指针),不能像队列那样存很长的数据串。
-
API 关键词:
xTaskNotify()(发送),xTaskNotifyWait()(接收).
4. 代码实战示例
我们来看一个最简单的场景:任务 1 (发送者) 每隔 1 秒叫醒 任务 2 (接收者) 打印一条消息。
接收者代码 (Task 2):
void Task2_Handler(void *pvParameters)
{
while(1)
{
// 1. 等待通知
// 参数1: pdTRUE 表示退出时把计数清零
// 参数2: portMAX_DELAY 表示死等,直到有通知来
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 2. 收到通知后执行的代码
printf("收到通知,干活啦!\r\n");
}
}
发送者代码 (Task 1):
void Task1_Handler(void *pvParameters)
{
while(1)
{
// 延时 1000ms
vTaskDelay(pdMS_TO_TICKS(1000));
// 1. 发送通知给 Task2
// xHandleTask2 是创建 Task2 时得到的句柄
xTaskNotifyGive(xHandleTask2);
printf("已发送通知\r\n");
}
}
5. 什么时候不能用它?(局限性)
虽然任务通知很好用,但它不是万能的。以下情况你必须使用传统的队列或信号量:
-
一对多广播: 任务通知只能是 一对一 的(指定发给某个任务)。如果你想发一个信号,让 5 个任务同时动起来,得用事件组(Event Group)。
-
ISR 发送给 ISR: 中断服务函数之间不能用。
-
发送方无法阻塞: 如果 B 的口袋满了(还没有处理),A 想把数据塞进去,A 无法进入“等待状态”直到口袋变空。A 只能选择覆盖旧数据或者放弃发送。
-
传输复杂大数据: 如果你要传一个很大的结构体,队列更方便。
总结
-
简单理解: 它是 FreeRTOS 任务自带的“私聊”功能。
-
核心优势: 极快、极省内存。
-
推荐策略: 只要能用任务通知解决的问题,就优先用它,不要用信号量。 只有当它满足不了需求时(比如需要多对多通信),才考虑用传统的 IPC 机制。
二:通知状态和通知值
在 FreeRTOS 的任务控制块(TCB,即任务的“身份证”)里,专门留了两个位置给任务通知:
-
通知值 (Notification Value)
-
通知状态 (Notification State)
我们继续用**“私人小口袋”**的比喻来拆解。
1. 通知值 (Notification Value)
定义:它本质上就是一个 32位的无符号整数 (uint32_t)。
作用:它是用来存放东西的。
就像口袋里的“内容”。
-
当做信号量时:这个数值代表“计数值”。(例如:
3代表口袋里有 3 颗豆子)。 -
当做事件组时:这个数值的每一位(Bit)代表不同的标志。(例如:
0x01代表按键被按下)。 -
当做消息邮箱时:这个数值就是传递的具体数据。(例如:
100代表当前速度是 100)。
初始状态:任务刚创建时,这个值通常是 0。
2. 通知状态 (Notification State)
定义:它是一个枚举变量 (uint8_t),用来记录**“接收者当前在干什么”以及“口袋里有没有新东西”**。
作用:它是用来做判断的。
就像口袋外面挂了一个“红旗标志”或者“状态牌”。
FreeRTOS 内部主要有三种状态:
-
未等待通知 (taskNOT_WAITING_NOTIFICATION)
-
含义:任务正在忙别的事,没有在查收消息。或者任务虽然没事做,但还没有准备好接收通知。
-
比喻:工人在干活,根本没看口袋。
-
-
等待通知 (taskWAITING_NOTIFICATION)
-
含义:任务调用了接收函数(如
ulTaskNotifyTake),进入了**阻塞(Blocked)**状态,正在死等消息的到来。 -
比喻:工人停下手里的活,盯着口袋看,说:“谁来给我发个消息啊,不发我就不动了。”
-
-
收到通知 (taskNOTIFICATION_RECEIVED)
-
含义:发送者已经发了通知,把状态改成了“已收到”,但接收者还没来得及处理。
-
比喻:有人往口袋里塞了东西,并把口袋外的红旗举起来了:“嘿!里面有新货,下次你检查的时候直接拿走就行,不用等了。”
-
3. 它们是如何配合工作的?(全过程演示)
让我们看一个完整的流程,看看这两个变量是如何变化的。
场景:任务 A (发送者) -> 任务 B (接收者)
阶段一:初始状态
-
任务 B 的值:
0 -
任务 B 的状态:
未等待(Not Waiting) -
情况:任务 B 刚启动,正在运行其他代码。
阶段二:任务 B 想收消息 (阻塞等待)
-
任务 B 调用
ulTaskNotifyTake(..., portMAX_DELAY)。 -
FreeRTOS 内核检查 B 的状态:
-
发现状态不是“已收到”,说明没消息。
-
于是将 B 的状态改为
等待通知(Waiting)。 -
结果:任务 B 进入休眠(Blocked),让出 CPU。
-
阶段三:任务 A 发送消息
-
任务 A 调用
xTaskNotifyGive(TaskB_Handle)。 -
FreeRTOS 内核操作 B 的 TCB:
-
更新 通知值:把值从
0加到1。 -
检查 通知状态:发现 B 正在
等待通知。 -
关键动作:内核把 B 叫醒(也就是把 B 从阻塞列表中移到就绪列表)。
-
将 B 的状态临时标记为
收到通知(或者在唤醒后处理)。
-
阶段四:任务 B 醒来处理
-
任务 B 恢复运行。
-
它检查 通知状态,发现是“收到通知”。
-
它读取 通知值(读到
1)。 -
它根据参数决定是否清零通知值。
-
最后:将 通知状态 重置回
未等待,准备下一次循环。
4. 为什么要有“状态”?(核心理解)
你可能会问:“为什么不直接看数值是不是 0?非要搞个状态变量?”
这是为了处理**“时间差”**问题:
-
如果 B 还没等,A 先发了:
A 把值改为 1,并把状态设为 已收到。当 B 以后想去拿的时候,不用等,看到状态是 已收到,直接拿走运行。这叫“未卜先知”(Pending)。
-
避免虚假唤醒:
状态机明确了任务是因为“真的收到了通知”醒来的,还是因为“超时了”醒来的。
总结图示
| 组件 | 名称 | 形象比喻 | 关注点 |
| 通知值 | Value | 信件的内容 | 数据 (是多少?按了几次?) |
| 通知状态 | State | 信箱的红旗 | 逻辑 (有人在等吗?有新信件吗?) |
二:任务通知的函数及使用
FreeRTOS 的任务通知函数虽然看起来很多,但其实非常有规律。
对于初学者,我建议把它们分成两类来记忆:
-
“傻瓜模式”:专门用来代替信号量,简单好用(Give / Take)。
-
“万能模式”:功能最强,用来传数据或标志位(Notify / Wait)。
我们一个个来看:
| 简化版 | 专业版 | |
|---|---|---|
| 发出通知 | xTaskNotifyGive vTaskNotifyGiveFromISR | xTaskNotify xTaskNotifyFromISR |
| 取出通知 | ulTaskNotifyTake | xTaskNotifyWait |
第一类:傻瓜模式 (模拟信号量)
这一组函数最简单,专门用来做二值信号量或计数信号量。它的特点是不传具体数据,只传“次数”。
1. 发送通知:xTaskNotifyGive()
作用:往目标任务的通知值里 +1(相当于释放信号量)。
// 参数:任务句柄
xTaskNotifyGive( xTaskB_Handle );
2. 接收通知:ulTaskNotifyTake()
作用:等待通知值大于 0,然后把它减 1 或清零(相当于获取信号量)。
// 返回值:在减 1 或清零之前的通知值
uint32_t ulValue = ulTaskNotifyTake( xClearCountOnExit, xTicksToWait );
-
参数
xClearCountOnExit(退出时是否清零):-
pdTRUE:收到通知后,把通知值直接清零。(二值信号量模式,不管你发了多少次,我一次全收走)。 -
pdFALSE:收到通知后,把通知值减 1。(计数信号量模式,你发了3次,我得调用3次这个函数才能收完)。
-
-
参数
xTicksToWait:等待时间(可以用portMAX_DELAY死等)。
第二类:万能模式 (传递数值/标志位)
这一组函数更底层,参数更多,决定了通知值具体怎么变(是覆盖、是位操作、还是加法)。
1. 发送通知:xTaskNotify()
这是最强大的发送函数。
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, // 目标任务
uint32_t ulValue, // 要发送的数值
eNotifyAction eAction ); // 【关键】决定怎么更新值
最核心的参数 eAction (枚举类型):
这个参数决定了你是在发信号量、发事件还是发消息。
| eAction 参数 | 行为描述 | 适用场景 |
eSetBits | 目标任务的原通知值 按位或 (OR) 上 ulValue。 | 事件组 (如:置位 bit0 表示按键,bit1 表示串口) |
eIncrement | 目标通知值 +1。忽略 ulValue 参数。 | 信号量 (效果等同于 xTaskNotifyGive) |
eSetValueWithOverwrite | 不管目标只有没有旧数据,强制覆盖为 ulValue。 | 更新传感器数据 (我不关心旧数据,只要最新的) |
eSetValueWithoutOverwrite | 如果目标原本是 0 (没数据),就更新为 ulValue;如果目标有旧数据,则发送失败 (返回 pdFAIL)。 | 保险的消息传递 (不能覆盖还没处理的旧消息) |
eNoAction | 啥也不改,只把目标的状态改为“已收到”。 | 纯粹的唤醒 (嘿,醒醒,但我啥也没给你带) |
2. 接收通知:xTaskNotifyWait()
这是最强大的接收函数,它允许你在读取前后对数据进行“位清理”。
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, // 进来时清除哪些位
uint32_t ulBitsToClearOnExit, // 出去时清除哪些位
uint32_t *pulNotificationValue,// 保存收到的值
TickType_t xTicksToWait ); // 等待时间
-
ulBitsToClearOnEntry(入口掩码):-
在函数判断有没有通知之前,先清除通知值里的某些位。
-
通常设为
0(不清除)。
-
-
ulBitsToClearOnExit(出口掩码):-
函数返回之后,清除通知值里的某些位。
-
如果你想把值清零:设为
0xFFFFFFFF(ULONG_MAX)。 -
如果你想保留值不动:设为
0。 -
如果你想只清除 bit0 (处理完按键事件):设为
0x01。
-
第三类:中断专用版 (ISR)
初学者必须注意:如果你在中断服务函数(如定时器中断、串口中断)里发送通知,绝对不能用上面的函数,必须带 FromISR 后缀!
-
vTaskNotifyGiveFromISR() -
xTaskNotifyFromISR()
不仅名字变了,参数也多了一个核心的指针:
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken );
-
pxHigherPriorityTaskWoken:-
这是一个“出参”(指针)。
-
作用:如果发送通知后,发现接收任务的优先级比当前正在运行的任务(被中断打断的那个)还要高,FreeRTOS 会把这个变量置为
pdTRUE。 -
你需要做:在中断退出的最后,检查这个变量。如果是
pdTRUE,你需要手动触发一次“任务切换”(Context Switch),这样中断结束后会立刻切到那个高优先级任务,而不是切回原来被打断的低优先级任务。这保证了实时性。
-
总结速查表
| 你的需求 | 推荐发送函数 | 推荐接收函数 | eAction 设置 |
| 二值信号量 (0/1) | xTaskNotifyGive | ulTaskNotifyTake(pdTRUE, ...) | (内部自动处理) |
| 计数信号量 (Count) | xTaskNotifyGive | ulTaskNotifyTake(pdFALSE, ...) | (内部自动处理) |
| 事件组 (Flags) | xTaskNotify | xTaskNotifyWait | eSetBits |
| 消息邮箱 (Data) | xTaskNotify | xTaskNotifyWait | eSetValueWithOverwrite |
作为初学者,建议先从 xTaskNotifyGive 和 ulTaskNotifyTake 用起,把任务间的同步跑通了,再去研究那个复杂的 xTaskNotifyWait。
三:任务通知的使用
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。
任务通知之所以能模拟这么多功能,核心在于怎么操作那个 32 位的通知值 (Value) 以及 接收端怎么处理通知状态。
为了方便演示,假设我们都有一个全局句柄 xTaskHandle 指向接收任务。
1. 模拟“二值信号量” (Binary Semaphore)
应用场景:同步。比如:串口接收完数据(ISR)通知处理任务(Task)“数据好了,快去拿”。
核心逻辑:
-
发送:不管发多少次,通知值不重要,关键是把任务叫醒。
-
接收:收到后,一定要清零(pdTRUE)。就像把唯一的令牌拿走了。
代码示范:
/* ================= 发送端 (比如中断或任务A) ================= */
// 相当于 xSemaphoreGive()
// 只是简单地把通知值 +1,并唤醒任务
xTaskNotifyGive(xTaskHandle);
/* ================= 接收端 (任务B) ================= */
// 相当于 xSemaphoreTake()
void TaskB(void *pvParameters)
{
while(1)
{
// 参数1: pdTRUE -> 退出时把通知值 清零 (变为0)
// 参数2: portMAX_DELAY -> 死等
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 执行到这里说明收到了信号
printf("二值信号量:处理完一次同步事件\r\n");
}
}
2. 模拟“计数型信号量” (Counting Semaphore)
应用场景:资源计数。比如:按键按了 3 次,任务就需要处理 3 次日志打印。
核心逻辑:
-
发送:每发一次,通知值 +1。
-
接收:每收一次,通知值 -1(pdFALSE)。直到减到 0 为止。
代码示范
/* ================= 发送端 ================= */
// 连发3次
xTaskNotifyGive(xTaskHandle); // 值变 1
xTaskNotifyGive(xTaskHandle); // 值变 2
xTaskNotifyGive(xTaskHandle); // 值变 3
/* ================= 接收端 ================= */
void TaskB(void *pvParameters)
{
uint32_t ulCurrentValue;
while(1)
{
// 参数1: pdFALSE -> 退出时把通知值 减 1 (而不是清零)
// 返回值: 减1之前的数值
ulCurrentValue = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
printf("计数信号量:处理一次,剩余次数: %d\r\n", ulCurrentValue - 1);
}
}
3. 模拟“事件组” (Event Group)
应用场景:多事件标志。比如:Bit 0 代表“温度过高”,Bit 1 代表“电量过低”。
核心逻辑:
-
发送:使用
eSetBits,将具体的某一位设为 1(按位或运算)。 -
接收:使用
xTaskNotifyWait,读取并清除具体的位。
代码示范:
/* ================= 发送端 ================= */
#define EVENT_TEMP_HIGH (1 << 0) // 0x01
#define EVENT_BATT_LOW (1 << 1) // 0x02
// 设置 Bit 0,通知温度高
xTaskNotify(xTaskHandle, EVENT_TEMP_HIGH, eSetBits);
// 设置 Bit 1,通知电量低 (此时通知值可能是 0x03)
xTaskNotify(xTaskHandle, EVENT_BATT_LOW, eSetBits);
/* ================= 接收端 ================= */
void TaskB(void *pvParameters)
{
uint32_t ulNotifiedValue;
while(1)
{
// 参数1: 0 -> 进入函数前不清除任何位
// 参数2: 0xFFFFFFFF -> 退出函数后清除所有位 (相当于拿走所有事件)
// 参数3: 保存收到的值
xTaskNotifyWait(0, 0xFFFFFFFF, &ulNotifiedValue, portMAX_DELAY);
if ((ulNotifiedValue & EVENT_TEMP_HIGH) != 0)
{
printf("处理温度报警...\r\n");
}
if ((ulNotifiedValue & EVENT_BATT_LOW) != 0)
{
printf("处理低电量...\r\n");
}
}
}
4. 模拟“邮箱” (Mailbox - 覆盖模式)
应用场景:广播传感器数据。比如陀螺仪一直在更新数据,但我只需要最新的那个值,旧的丢掉没关系。
核心逻辑:
-
发送:使用
eSetValueWithOverwrite。不管接收方有没有读,强制覆盖。 -
接收:读取数据。
代码示范:
/* ================= 发送端 ================= */
uint32_t sensor_data = 100;
// 强制写入 100,不管里面有没有旧数据
xTaskNotify(xTaskHandle, sensor_data, eSetValueWithOverwrite);
sensor_data = 200;
// 再次强制写入 200,旧的 100 被覆盖了
xTaskNotify(xTaskHandle, sensor_data, eSetValueWithOverwrite);
/* ================= 接收端 ================= */
void TaskB(void *pvParameters)
{
uint32_t ulCreateValue;
while(1)
{
// 参数1,2 都为 0,表示我不清除数据,我只是读一下
// 或者 参数2 为 0xFFFFFFFF 表示读完就清空
xTaskNotifyWait(0, 0, &ulCreateValue, portMAX_DELAY);
printf("收到最新数据: %d\r\n", ulCreateValue);
}
}
5. 模拟“轻量级队列” (Queue - 长度为1,不覆盖)
应用场景:传递重要指令。比如“保存配置”。如果上一条指令还没处理完,下一条指令不能发,否则会丢失。
核心逻辑:
-
发送:使用
eSetValueWithoutOverwrite。如果接收方上次没读走(状态是 pending),发送会失败。 -
接收:读走数据,并将状态复位。
代码示范:
/* ================= 发送端 ================= */
uint32_t cmd_code = 0x55;
BaseType_t xStatus;
// 尝试发送
xStatus = xTaskNotify(xTaskHandle, cmd_code, eSetValueWithoutOverwrite);
if (xStatus == pdPASS) {
printf("指令发送成功\r\n");
} else {
printf("发送失败!对方还没处理完上一条消息\r\n");
}
/* ================= 接收端 ================= */
void TaskB(void *pvParameters)
{
uint32_t ulRecvValue;
while(1)
{
// 必须处理数据,让状态变为“未等待”,这样发送方才能发下一条
xTaskNotifyWait(0, 0xFFFFFFFF, &ulRecvValue, portMAX_DELAY);
printf("处理指令: %x\r\n", ulRecvValue);
// 模拟处理很慢,导致发送方可能失败
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
总结对比
| 模式 | 发送参数 (eAction) | 接收函数 | 核心特点 |
| 二值信号量 | xTaskNotifyGive | ulTaskNotifyTake(pdTRUE) | 收到就清零,只看“有没有” |
| 计数信号量 | xTaskNotifyGive | ulTaskNotifyTake(pdFALSE) | 收到减 1,看“有多少” |
| 事件组 | eSetBits | xTaskNotifyWait | 按位操作,多事件并存 |
| 邮箱 | eSetValueWithOverwrite | xTaskNotifyWait | 喜新厌旧,只留最新的 |
| 队列(长1) | eSetValueWithoutOverwrite | xTaskNotifyWait | 先来后到,满了就报错 |
建议:最常用的其实是 二值信号量(做同步)和 邮箱(传简单数值)。先把这两个练熟!
四:任务通知局限性
既然任务通知这么好用,为什么还要用计数型信号量、二进制信号量、事件组等函数?
这是一个非常深刻的问题!很多初学者(甚至有经验的工程师)都会有这个疑惑:既然有一把瑞士军刀(任务通知),为什么还需要专门的螺丝刀、锤子和扳手(信号量、队列、事件组)?
答案在于:任务通知虽然快,但它“太独了”。它的局限性决定了它无法完全取代传统的通信机制。
我们可以把任务通知比作**“发私信”(微信私聊),把队列/信号量比作“公共公告板”或“公共储物柜”**。
以下是必须使用传统 IPC(信号量、队列、事件组)的四大场景:
1. 场景一:向“一群人”发送信号 (一对多广播)
任务通知的局限: 它是一对一的。
你必须知道接收者的“名字”(任务句柄),而且只能发给它一个人。如果你想让 5 个任务同时开始工作,你需要写个循环,调用 5 次 xTaskNotifyGive。
传统优势 (事件组 Event Group):
事件组是一对多的。
就像在广场上鸣枪,所有听到枪声的运动员(任务)都会同时起跑。
-
例子: 系统遇到致命故障,需要通知“网络任务”、“电机任务”、“显示任务”全部立刻停止。用事件组只需设置一个标志位,所有任务都能读到。
2. 场景二:谁有空谁来做 (多对一 / 抢占式处理)
任务通知的局限: 你必须指定具体要把活儿派给谁。
传统优势 (队列/计数信号量):
这就像一个**“公共任务池”**。
假设你有 3 个“工人任务”负责处理网络数据包,但你不关心具体是工人 A、B 还是 C 来处理,只要有人处理就行。
-
做法: 你把数据扔进公共队列。
-
结果: 3 个工人谁现在闲着,谁就去队列里拿数据。
-
任务通知做不到: 因为发送时你必须填
xTaskToNotify参数,你得硬性指定给某个人,哪怕那个人正忙得不可开交,而其他人正闲着。
3. 场景三:发送方需要“排队等待” (阻塞发送)
任务通知的局限: 发送方不能阻塞。
这是最容易被忽视的区别!
-
当你调用
xTaskNotify()时,如果接收方的“口袋”满了(上一次通知还没处理),发送方只有两个选择:-
覆盖旧数据(旧数据丢失)。
-
放弃发送(返回失败)。
-
发送方无法说: “我不走,我就死等在这里,直到你的口袋空出来为止。”
-
传统优势 (队列 Queue):
队列允许发送阻塞。
-
xQueueSend(xQueue, &data, portMAX_DELAY); -
含义: 如果队列满了,发送任务会进入休眠状态,直到队列有空位了,它才会被唤醒把数据放进去。这在流量控制(Flow Control)中非常关键,防止生产速度快于消费速度导致的数据丢失。
4. 场景四:传递“大”数据
任务通知的局限: 只能传 32位(4字节)。
虽然你可以传一个指针(地址),但这就涉及到了内存管理(谁申请?谁释放?这就复杂了)。
传统优势 (队列 Queue):
队列可以复制任意大小的数据结构。
-
你可以定义一个
struct { int id; char msg[20]; float temp; },直接扔进队列里。FreeRTOS 会帮你把整个结构体复制一份存起来,非常安全且省心。
总结:决战紫禁之巅
为了方便记忆,我做了一个对比表:
| 特性 | 任务通知 (私信) | 队列/信号量/事件组 (公共设施) |
| 速度 | 极快 (快45%) | 普通 |
| 内存占用 | 极低 (无需额外创建对象) | 较高 (需要申请堆空间) |
| 通信对象 | 明确指定的 1 个任务 | 任何持有句柄的任务 (不限人数) |
| 发送方阻塞 | 不支持 (满了只能走或覆盖) | 支持 (满了可以死等) |
| 接收方 | 只能是拥有该通知的任务 | 任何任务都可以去读/取 |
| 数据大小 | 仅 32位 (数值或指针) | 任意大小 (结构体、数组) |
| ISR 交互 | 只能 ISR->Task | 支持 ISR->Task, Task->ISR, Task->Task |
1085

被折叠的 条评论
为什么被折叠?



