FreeRTOS Task Notification
1. Task Notification 是什么
Task Notification(任务通知)是 FreeRTOS 中用于 任务之间、ISR 与任务之间进行事件通知、任务同步以及传递简单数据的一种轻量级通信机制。
它解决的核心问题是:
一个执行单元产生事件后,需要快速通知某一个明确的任务,而目标任务在没有事件时可以进入 Blocked,不需要持续轮询占用 CPU。
最典型的模型:
发送任务 / ISR
│
│ Task Notification
↓
目标任务
│
↓
目标任务自己的 TCB
│
├── Notification Value
│ 任务通知值
│
└── Notification State
通知状态
例如 BMS 中:
FaultDetect_Task
│
│ 检测到故障
↓
xTaskNotify()
│
↓
Fault_Task
│
↓
故障处理
Task Notification 的价值不只是“发一个通知”。
它可以同时实现:
Task Notification
│
┌──────────────┼──────────────┐
↓ ↓ ↓
事件通知 简单数据 任务同步
│ │ │
└──────────────┴──────────────┘
↓
任务阻塞与唤醒
因此 Task Notification 可以理解成:
直接面向某一个指定 Task 的轻量级通信与同步机制。
2. 为什么需要 Task Notification
2.1 使用 Semaphore 通知事件
以前学习 Binary Semaphore 时,可以这样通知某个任务:
ADC DMA完成
│
↓
ISR
│
│ Give
↓
Binary Semaphore
│
│ Take
↓
ADC_Process_Task
这种方式完全可以工作。
但是系统需要额外存在一个:
Binary Semaphore 对象
整体路径:
ISR
↓
Semaphore
↓
Task
2.2 Task Notification 的思路
如果这个事件最终只需要通知:
ADC_Process_Task
那么可以直接:
ADC DMA ISR
│
│ Task Notification
↓
ADC_Process_Task
不需要额外创建一个 Semaphore。
整体变成:
发送方
│
↓
TaskHandle_t
│
↓
目标 Task
│
↓
目标 Task 自己的通知信息
这就是:
Direct-to-Task Notification
也就是:
直接到任务的通知。
3. Task Notification 的核心组成
Task Notification 最重要的两个概念:
Task Notification
│
┌────────────┴────────────┐
↓ ↓
Notification Value Notification State
任务通知值 通知状态
这两个概念必须区分清楚。
4. Notification Value 是什么
Notification Value:
任务通知值
它本质上是一个 32 位无符号整数。
可以理解成:
/* 定义一个32位无符号变量,用来说明任务通知值的数据类型。 */
uint32_t ulNotificationValue;
根据业务不同,它可以被解释为:
Notification Value
│
┌──────────────┼──────────────┐
↓ ↓ ↓
计数值 Bit标志 简单数据
│ │ │
↓ ↓ ↓
事件发生次数 多事件状态 32位数值
例如:
Notification Value = 3
可以表示:
累计发生了3次事件
也可以:
Notification Value = 0x05
转换成二进制:
0000 0101
表示:
bit0 = 1
bit2 = 1
业务上可以解释为:
事件0发生
+
事件2发生
5. Notification State 是什么
Notification State:
通知状态
它用于记录 Task Notification 当前处于什么状态,例如:
任务是否正在等待通知
任务是否存在尚未处理的通知
为什么不能只依赖 Notification Value?
假设:
Notification Value = 0
可能存在两种情况。
情况1:
根本没有收到通知
情况2:
已经收到通知
但是通知携带的数据本身就是0
所以仅仅看到:
Value = 0
不能确定:
到底有没有通知到达。
因此 FreeRTOS 还需要 Notification State。
最终应该建立:
Task
↓
TCB
│
├── Notification Value
│ ↓
│ 通知数据
│
└── Notification State
↓
通知状态
6. Task Notification 在 TCB 中的位置
FreeRTOS 中,每一个任务都有自己的 TCB。
TCB 用来保存:
任务栈顶指针
任务优先级
任务状态
链表节点
任务名称
…
启用 Task Notification 后,TCB 中还存在通知相关信息。
逻辑模型:
Task
│
↓
TCB
│
┌───────────────┼───────────────┐
↓ ↓ ↓
任务栈 优先级 通知信息
│
┌─────────┴─────────┐
↓ ↓
Notification Value Notification State
因此 Task Notification 不需要再额外创建一个独立的:
Notification 对象
而是直接使用:
目标 Task 已经存在的 TCB
7. 为什么 Task Notification 比 Queue 更轻量
7.1 Queue 的结构
Queue 需要独立的 Queue 控制对象和数据存储区。
逻辑上可以理解为:
Queue
│
┌─────────────────┼─────────────────┐
↓ ↓ ↓
控制结构 数据 Buffer 等待任务列表
内部还要维护:
Queue长度
每条消息大小
当前消息数量
读位置
写位置
等待发送任务
等待接收任务
锁状态
数据传递通常还存在:
发送方数据
│
│ Copy
↓
Queue Buffer
│
│ Copy
↓
接收方变量
7.2 Task Notification 的结构
Task Notification:
发送 Task / ISR
│
↓
TaskHandle_t
│
↓
目标 Task
│
↓
TCB
│
↓
Notification Value / State
没有额外的:
Queue Buffer
独立 Queue 控制对象
独立 Semaphore 对象
因此对于:
简单事件
事件计数
bit状态
简单32位数据
Task Notification 通常具有:
RAM占用更少
内核对象更少
数据处理路径更短
执行开销更低
7.3 更轻量不代表可以替代所有 Queue
错误理解:
Task Notification 更轻量
↓
Queue 全部不用
这是错误的。
如果需要传递:
CAN Frame1
CAN Frame2
CAN Frame3
并且三帧必须全部保留:
┌────────┬────────┬────────┐
│ Frame1 │ Frame2 │ Frame3 │
└────────┴────────┴────────┘
这种需求 Queue 更合适。
8. Task Notification、Queue、Semaphore 的区别
| Task Notification | 指定 Task 的事件、计数、bit、简单32位数据 |
| Queue | 完整数据传递、多条消息排队 |
| Binary Semaphore | 一般事件同步 |
| Counting Semaphore | 资源数量、事件累计 |
| Mutex | 共享资源互斥 |
简单选型逻辑:
需要通知某一个明确 Task?
│
↓
只是简单事件 / 计数 / bit?
│
是
↓
Task Notification
如果:
需要保存完整数据
↓
需要多条消息排队
↓
Queue
9. TaskHandle_t 在 Task Notification 中的作用
Task Notification 必须知道:
到底通知哪个 Task。
因此需要目标任务的 TaskHandle_t。
例如:
/* 保存Fault_Task对应的任务句柄。 */
static TaskHandle_t xFaultTaskHandle = NULL;
创建任务:
/* 创建Fault_Task,并保存创建出来的任务句柄。 */
xTaskCreate(
Fault_Task,
"Fault",
128U,
NULL,
4U,
&xFaultTaskHandle
);
逻辑:
xFaultTaskHandle
│
↓
找到 Fault_Task
│
↓
找到对应 TCB
│
↓
操作 Task Notification
所以:
TaskHandle_t 是发送方定位目标 Task 的关键。
10. xTaskNotify()
10.1 API 作用
xTaskNotify() 用于:
从 Task 上下文向指定 Task 发送任务通知。
基本形式:
/* 从Task上下文向目标Task发送一次通知。 */
xTaskNotify(
xTaskToNotify,
ulValue,
eAction
);
三个核心参数:
| xTaskToNotify | 通知哪个Task |
| ulValue | 本次通知使用的32位值 |
| eAction | 如何修改目标Task原来的Notification Value |
整体逻辑:
xTaskNotify()
│
├── xTaskToNotify
│ ↓
│ 找到目标Task
│
├── ulValue
│ ↓
│ 提供32位数据
│
└── eAction
↓
决定如何修改旧Value
11. eAction 有什么作用
eAction 是 xTaskNotify() 最核心的参数之一。
它决定:
新的通知如何作用于目标任务原来的 Notification Value。
主要方式包括:
eNoAction
eIncrement
eSetBits
eSetValueWithOverwrite
eSetValueWithoutOverwrite
12. eNoAction
含义:
产生通知,但不修改 Notification Value。
例如:
/* 只通知Fault_Task有事件发生,不修改原来的任务通知值。 */
xTaskNotify(
xFaultTaskHandle,
0U,
eNoAction
);
假设原来:
Notification Value = 10
执行后:
Notification Value 仍然 = 10
但是:
Notification State 发生变化
所以:
Notification Value
≠
Notification State
13. eIncrement
含义:
任务通知值加1。
例如:
/* 向Process_Task发送一次计数型任务通知。 */
xTaskNotify(
xProcessTaskHandle,
0U,
eIncrement
);
假设原来:
Notification Value = 5
结果:
5 → 6
特别注意:
eIncrement
不是:
原Value + ulValue
例如:
/* 使用eIncrement时,100不会参与运算,任务通知值只增加1。 */
xTaskNotify(
xProcessTaskHandle,
100U,
eIncrement
);
如果原来:
5
结果仍然:
6
而不是:
105
所以 eIncrement 特别适合:
事件次数累计
中断次数累计
生产事件累计
14. eSetBits
含义:
新的 ulValue 和旧 Notification Value 做按位 OR。
例如旧值:
0000 0001
新值:
0000 0100
运算:
0000 0001
OR
0000 0100
———–
0000 0101
最终:
0x05
代码:
/* 将bit2加入Fault_Task现有的通知值,同时保留原有bit。 */
xTaskNotify(
xFaultTaskHandle,
0x04U,
eSetBits
);
这种方式特别适合:
多个事件标志
多个故障标志
状态Bit集合
15. eSetValueWithOverwrite
含义:
无论旧通知是否已经处理,都直接使用新值覆盖旧值。
例如:
旧 Value = 10
新 Value = 25
代码:
/* 无论旧通知是否处理,都直接使用25覆盖原任务通知值。 */
xTaskNotify(
xTaskHandle,
25U,
eSetValueWithOverwrite
);
结果:
Notification Value = 25
风险:
旧通知还没有处理
↓
新的Value直接覆盖
↓
旧信息可能丢失
所以适合:
只关心最新值
不适合:
每一条通知都必须完整保留
16. eSetValueWithoutOverwrite
含义:
目标 Task 已经有一个尚未处理的通知时,不允许覆盖旧值。
例如:
Pending = Yes
Notification Value = 20
代码:
/* 尝试写入50,但禁止覆盖当前尚未处理的通知值。 */
xTaskNotify(
xTaskHandle,
50U,
eSetValueWithoutOverwrite
);
结果:
Notification Value 仍然 = 20
返回:
pdFAIL
如果目标任务当前没有 Pending Notification:
新Value成功写入
返回:
pdPASS
因此:
使用 eSetValueWithoutOverwrite 时必须关注返回值。
17. eAction 总结
| eNoAction | 不修改 | 单纯事件提醒 |
| eIncrement | +1 | 事件计数 |
| eSetBits | 按位OR | 多事件、多故障 |
| eSetValueWithOverwrite | 强制覆盖 | 只关心最新值 |
| eSetValueWithoutOverwrite | 有Pending时拒绝覆盖 | 防止旧值被覆盖 |
18. xTaskNotify() 与任务状态变化
假设:
Fault_Task
优先级 = 4
Blocked
当前:
CAN_Task
优先级 = 3
Running
CAN_Task 执行:
/* 向Fault_Task发送一次计数型任务通知。 */
xTaskNotify(
xFaultTaskHandle,
0U,
eIncrement
);
首先:
Fault_Task
Blocked
↓
Ready
然后调度器判断:
Fault_Task优先级4
>
CAN_Task优先级3
如果使用抢占式调度:
Fault_Task Ready
↓
触发调度判断
↓
需要任务切换
↓
PendSV
↓
Fault_Task Running
必须牢记:
被通知后首先是 Blocked → Ready,而不是 Blocked → Running。
19. ulTaskNotifyTake()
19.1 API 作用
ulTaskNotifyTake() 用于:
当前 Task 等待并消费自己的任务通知值。
注意方向区别:
xTaskNotify()
↓
操作目标Task
ulTaskNotifyTake()
↓
当前Task操作自己的通知
基本形式:
/* 当前Task等待并取得自己的任务通知。 */
ulTaskNotifyTake(
xClearCountOnExit,
xTicksToWait
);
两个参数:
| xClearCountOnExit | 成功取得通知后如何处理通知值 |
| xTicksToWait | 没有通知时最多等待多久 |
20. pdTRUE
如果第一个参数是:
pdTRUE
表示:
成功取得通知后,将 Notification Value 清零。
例如:
调用前:
Notification Value = 5
执行:
/* 取得当前累计通知,并在退出时把任务通知值清零。 */
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
结果:
返回值 = 5
Notification Value:
5 → 0
这种方式比较接近:
Binary Semaphore
适合:
只关心有没有事件发生
例如:
OLED刷新请求
OLED刷新请求
OLED刷新请求
↓
其实只需要刷新一次最新数据
可以:
累计通知
↓
Task醒来
↓
一次性清零
21. pdFALSE
如果第一个参数是:
pdFALSE
表示:
每次成功 Take 后,Notification Value 只减1。
例如:
调用前:
Notification Value = 5
执行:
/* 每次只消费一个累计通知计数。 */
ulTaskNotifyTake(
pdFALSE,
portMAX_DELAY
);
结果:
返回值 = 5
Notification Value:
5 → 4
继续:
4 → 3
3 → 2
2 → 1
1 → 0
这种方式非常接近:
Counting Semaphore
即:
Notify一次
↓
Count +1
Take一次
↓
Count -1
22. ulTaskNotifyTake() 的返回值
这个知识点非常容易出错。
假设:
Notification Value = 5
执行:
/* 每次消费一个通知,并保存API返回值。 */
uint32_t ulCount = ulTaskNotifyTake(
pdFALSE,
portMAX_DELAY
);
结果:
ulCount = 5
而不是:
4
因为它返回的是:
修改之前的 Notification Value。
内部之后才:
5 → 4
同样:
/* 一次性清零累计通知,并保存清零前的通知值。 */
uint32_t ulCount = ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
如果原值:
5
则:
返回 = 5
剩余 = 0
23. pdTRUE 与 pdFALSE 对比
假设调用前:
Notification Value = 5
| ulTaskNotifyTake(pdTRUE, …) | 5 | 0 | Binary式 |
| ulTaskNotifyTake(pdFALSE, …) | 5 | 4 | Counting式 |
核心记忆:
pdTRUE
↓
清零
pdFALSE
↓
减1
24. xTicksToWait
第二个参数:
xTicksToWait
决定:
Notification Value 为0时,任务最多等待多久。
24.1 不等待
/* 当前没有通知时不进入Blocked,立即返回。 */
ulTaskNotifyTake(
pdTRUE,
0U
);
如果当前:
Notification Value = 0
则:
立即返回0
24.2 有限等待
例如:
/* 当前没有通知时最多阻塞等待100ms。 */
ulTaskNotifyTake(
pdTRUE,
pdMS_TO_TICKS(100U)
);
流程:
Notification Value = 0
↓
Running
↓
ulTaskNotifyTake()
↓
Blocked
↓
等待通知
↓
/ \\
通知到达 超时
↓ ↓
Ready Ready
24.3 portMAX_DELAY
/* 当前没有通知时长期阻塞等待。 */
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
适合事件驱动任务:
Task
↓
等待Notification
↓
Blocked
↓
事件发生
↓
被唤醒
↓
处理事件
↓
再次等待
25. Blocked 等待不会一直占 CPU
错误思想:
/* 定义一个用于轮询的事件标志。 */
volatile uint8_t g_ucEventFlag = 0U;
/* 错误示例:使用忙等待持续检查事件标志。 */
while (g_ucEventFlag == 0U)
{
/* 执行空操作,但CPU仍然持续运行当前循环。 */
__NOP();
}
这种方式:
检查
↓
检查
↓
检查
↓
检查
叫:
Busy Waiting,忙等待。
而 Task Notification:
/* 没有通知时让Task进入Blocked,而不是持续轮询。 */
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
会:
Running
↓
Blocked
↓
CPU运行其他Ready Task
因此:
Blocked 等待不是 CPU 空转。
26. Task Notification 模拟 Binary Semaphore
发送端:
/* 向目标Task发送一次计数型通知。 */
xTaskNotify(
xTaskHandle,
0U,
eIncrement
);
接收端:
/* 收到通知后将累计通知值全部清零。 */
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
整体:
事件发生
↓
Value变成非0
↓
Task被唤醒
↓
Take
↓
Value清0
这与:
Binary Semaphore
Give
↓
Take
具有类似的事件同步语义。
27. Task Notification 模拟 Counting Semaphore
发送:
/* 每发送一次通知,使目标Task通知值增加1。 */
xTaskNotify(
xTaskHandle,
0U,
eIncrement
);
接收:
/* 每次只消费一个累计通知。 */
ulTaskNotifyTake(
pdFALSE,
portMAX_DELAY
);
形成:
Notify
↓
Value +1
Notify
↓
Value +1
Notify
↓
Value +1
Take
↓
Value -1
可以形成:
0 → 1 → 2 → 3
↓
Take
↓
2
因此:
eIncrement + ulTaskNotifyTake(pdFALSE) 可以形成轻量级 Counting Semaphore 类似语义。
28. ISR 为什么不能直接使用普通 Task API
ISR:
Interrupt Service Routine,中断服务程序。
ISR 不是一个普通的 FreeRTOS Task。
ISR没有:
自己的Task TCB
自己的Task Blocked状态
普通Task的调度上下文
因此不能把:
普通Task API
直接当成:
ISR API
使用。
ISR需要专门的:
FromISR API
例如:
Task:
xTaskNotify()
对应:
ISR:
xTaskNotifyFromISR()
关键原因不是:
FromISR比较快
而是:
Task上下文和ISR上下文不同。
29. xTaskNotifyFromISR()
xTaskNotifyFromISR() 用于:
从ISR中向指定Task发送任务通知。
基本形式:
/* 从ISR向指定Task发送任务通知。 */
xTaskNotifyFromISR(
xTaskHandle,
ulValue,
eAction,
&xHigherPriorityTaskWoken
);
相比:
xTaskNotify()
多了一个:
xHigherPriorityTaskWoken
整体:
ISR
↓
xTaskNotifyFromISR()
↓
修改目标Task通知信息
↓
可能让目标Task:
Blocked → Ready
30. xHigherPriorityTaskWoken
典型初始化:
/* 初始认为本次ISR还没有唤醒更高优先级Task。 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
假设:
Task_A
优先级2
Running
Task_B
优先级4
Blocked
发生 IRQ:
Task_A Running
↓
IRQ
↓
ISR
ISR通知 Task_B:
Task_B
Blocked
↓
Ready
因为:
Task_B优先级4
>
被中断Task_A优先级2
所以:
xHigherPriorityTaskWoken = pdTRUE
如果:
Task_A优先级4
Task_B优先级2
即使 Task_B:
Blocked → Ready
也:
xHigherPriorityTaskWoken = pdFALSE
因此它真正表示:
这次 ISR 中的 FreeRTOS 操作是否使一个比当前被中断 Task 优先级更高的 Task 解除阻塞。
31. portYIELD_FROM_ISR()
如果:
xHigherPriorityTaskWoken = pdTRUE
说明:
一个更高优先级Task已经Ready
ISR退出前需要考虑请求任务切换:
/* 如果本次ISR唤醒了更高优先级Task,则请求退出ISR后进行调度切换。 */
portYIELD_FROM_ISR(
xHigherPriorityTaskWoken
);
它不是:
直接运行目标Task
而是:
请求进行一次任务调度和上下文切换。
Cortex-M3 下完整逻辑:
ISR通知Task
↓
Blocked → Ready
↓
发现高优先级Task需要运行
↓
xHigherPriorityTaskWoken = pdTRUE
↓
portYIELD_FROM_ISR()
↓
请求任务切换
↓
ISR退出
↓
PendSV
↓
保存旧Task现场
↓
恢复新Task现场
↓
高优先级Task Running
32. vTaskNotifyGiveFromISR()
如果 ISR 只需要表达:
“事件发生一次。”
而不需要复杂的:
eSetBits
Overwrite
复杂Value操作
可以使用:
/* 从ISR给目标Task增加一次通知计数。 */
vTaskNotifyGiveFromISR(
xTaskHandle,
&xHigherPriorityTaskWoken
);
可以近似理解为:
Notification Value +1
因此特别适合:
ADC DMA完成
UART DMA完成
按键中断
定时器事件
简单硬件完成事件
33. vTaskNotifyGiveFromISR() 与 ulTaskNotifyTake()
ISR:
/* 初始认为本次ISR没有唤醒更高优先级Task。 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 从ISR给Process_Task增加一次通知计数。 */
vTaskNotifyGiveFromISR(
xProcessTaskHandle,
&xHigherPriorityTaskWoken
);
/* 根据唤醒情况决定是否请求退出ISR后的任务切换。 */
portYIELD_FROM_ISR(
xHigherPriorityTaskWoken
);
Task:
/* 每次只消费一个ISR产生的通知事件。 */
ulTaskNotifyTake(
pdFALSE,
portMAX_DELAY
);
完整流程:
硬件事件
↓
ISR
↓
vTaskNotifyGiveFromISR()
↓
Notification Value +1
↓
目标Task:
Blocked → Ready
↓
调度器
↓
目标Task Running
↓
ulTaskNotifyTake(pdFALSE)
↓
Notification Value -1
34. ISR 通知任务的完整流程
硬件事件
↓
产生IRQ
↓
CPU进入ISR
↓
清除硬件中断标志
↓
xHigherPriorityTaskWoken = pdFALSE
↓
xTaskNotifyFromISR()
或
vTaskNotifyGiveFromISR()
↓
修改目标Task通知信息
↓
目标Task是否正在等待?
│
├── 否
│ ↓
│ Notification保持Pending
│
└── 是
↓
Blocked → Ready
↓
目标Task是否比被中断Task优先级更高?
│
├── 否
│ ↓
│ xHigherPriorityTaskWoken = pdFALSE
│
└── 是
↓
xHigherPriorityTaskWoken = pdTRUE
↓
portYIELD_FROM_ISR()
↓
ISR退出
↓
PendSV
↓
任务切换
↓
目标Task Running
35. ISR 设计原则
ISR 应该尽可能:
快速
短小
不阻塞
少做复杂业务
典型正确架构:
硬件事件
↓
ISR
↓
读取必要硬件状态
↓
清除中断标志
↓
通知Task
↓
退出ISR
↓
Task处理复杂业务
不推荐:
ISR
↓
读取数据
↓
复杂协议解析
↓
状态机
↓
OLED刷新
↓
Flash写入
↓
大量printf
更合理:
ISR
↓
快速通知
↓
Task
↓
复杂业务
即:
ISR负责快速响应硬件,Task负责复杂业务处理。
36. ISR 调用 FreeRTOS API 的中断优先级限制
不是所有 STM32 中断都可以随便调用:
xTaskNotifyFromISR()
vTaskNotifyGiveFromISR()
xQueueSendFromISR()
xSemaphoreGiveFromISR()
如果 IRQ 要调用 FreeRTOS FromISR API:
该中断优先级必须满足 FreeRTOS 对系统调用中断优先级的限制。
否则可能出现:
configASSERT
HardFault
内核链表异常
调度异常
随机崩溃
所以调试 ISR + FreeRTOS 问题时,一定检查:
NVIC Priority
↓
FreeRTOSConfig.h
↓
是否满足FromISR API调用范围
37. Task Notification 底层工作逻辑
发送端可以建立下面这个逻辑模型:
xTaskNotify()
↓
根据TaskHandle找到目标TCB
↓
找到目标Task通知信息
↓
保存旧Notification State
↓
根据eAction处理Notification Value
↓
更新Notification State
↓
目标Task是否正在等待Notification?
│
├── 否
│ ↓
│ 通知保持Pending
│
└── 是
↓
Blocked → Ready
↓
检查任务优先级
↓
必要时触发调度
接收端:
ulTaskNotifyTake()
↓
检查自己的Notification Value
↓
Value > 0?
┌──────┴──────┐
↓ ↓
是 否
↓ ↓
直接取得 xTicksToWait > 0?
│
┌────┴────┐
↓ ↓
否 是
↓ ↓
返回0 Blocked
↓
等待通知
↓
Blocked → Ready
↓
Scheduler
↓
Running
↓
根据pdTRUE / pdFALSE
处理Value
38. Task Notification 与调度器的关系
Task Notification 本身:
不是调度器。
它主要负责:
通知是否发生
通知值如何修改
等待Task是否可以解除Blocked
调度器负责:
Ready Task中谁应该运行
两者关系:
Notification到达
↓
目标Task解除Blocked
↓
Ready
↓
Scheduler
↓
比较优先级
↓
需要切换?
┌───┴───┐
↓ ↓
否 是
↓ ↓
当前Task PendSV
继续 ↓
任务切换
所以再次强调:
Notification唤醒Task,只负责让任务进入Ready;真正是否Running由调度器决定。
39. BMS 为什么适合使用 Task Notification
例如 BMS 中存在:
ADC_Task
↓
采集电压、温度等数据
FaultDetect_Task
↓
检测过压、欠压、过温
Fault_Task
↓
统一故障处理
不推荐:
FaultDetect_Task
↓
检测故障
↓
直接控制输出
↓
直接发送CAN
↓
直接写Flash
↓
直接刷新OLED
这样会导致:
FaultDetect_Task职责越来越多
↓
模块耦合越来越严重
更加合理:
FaultDetect_Task
↓
只负责故障判断
↓
Task Notification
↓
Fault_Task
↓
统一故障处理
形成:
故障检测与故障处理解耦。
40. BMS Fault Bits 设计
可以把 Notification Value 的不同 bit 定义成不同故障。
例如:
bit0 = 过压
bit1 = 欠压
bit2 = 过温
bit3 = 过流
代码:
/* 定义过压故障位,对应bit0。 */
#define BMS_FAULT_OV (1UL << 0)
/* 定义欠压故障位,对应bit1。 */
#define BMS_FAULT_UV (1UL << 1)
/* 定义过温故障位,对应bit2。 */
#define BMS_FAULT_OT (1UL << 2)
/* 定义过流故障位,对应bit3。 */
#define BMS_FAULT_OC (1UL << 3)
如果:
过压
+
过温
则:
0000 0001
OR
0000 0100
———–
0000 0101
即:
0x05
41. BMS 为什么使用 eSetBits
假设 Fault_Task 还没有处理过压故障:
Notification Value:
0000 0001
随后又发生过温:
0000 0100
如果使用:
eSetBits
则:
0000 0001
OR
0000 0100
———–
0000 0101
两个故障同时保留。
代码:
/* 将当前检测到的故障bit合并到Fault_Task现有通知值中。 */
xTaskNotify(
xFaultTaskHandle,
ulFaultBits,
eSetBits
);
而如果使用:
eSetValueWithOverwrite
可能:
过压
↓
尚未处理
↓
过温到来
↓
直接覆盖
↓
过压信息丢失
所以:
多个故障bit需要聚合时,eSetBits非常合适。
42. 为什么 Fault Bits 不能使用 pdFALSE 消费
这是一个非常重要的工程问题。
假设:
Notification Value = 0x06
二进制:
0110
定义:
bit1 = 欠压
bit2 = 过温
所以:
0110
=
欠压 + 过温
如果:
/* 错误示例:bitmask模式不应该简单通过减1消费通知。 */
ulTaskNotifyTake(
pdFALSE,
portMAX_DELAY
);
pdFALSE 会执行:
0x06 – 1
得到:
0x05
二进制:
0101
此时业务意义变成:
bit0 = 1
bit2 = 1
即:
过压 + 过温
原来明明是:
欠压 + 过温
却被减1变成:
过压 + 过温
因此:
Bitmask本质不是计数器,不能使用 pdFALSE 的减1逻辑逐个消费bit。
43. BMS Fault Task 为什么可以设置较高优先级
例如:
Fault_Task 优先级4
CAN_Task 优先级3
ADC_Task 优先级2
OLED_Task 优先级1
Fault_Task 平时:
等待Notification
↓
Blocked
所以即使它优先级最高:
平时也不占CPU
故障发生:
FaultDetect_Task
↓
xTaskNotify()
↓
Fault_Task:
Blocked → Ready
因为:
Fault_Task优先级最高
所以:
Ready
↓
Scheduler
↓
抢占
↓
Fault_Task Running
形成:
高优先级 + 平时Blocked + 故障时快速响应。
这才是合理的高优先级任务设计。
错误方式:
/* 错误示例:高优先级Task永久空循环会持续占用CPU。 */
while (1)
{
/* 空循环会造成低优先级任务可能长期得不到执行。 */
__NOP();
}
44. BMS Fault Notification 完整流程
ADC / CAN等数据
↓
FaultDetect_Task
↓
判断故障条件
↓
生成ulFaultBits
↓
xTaskNotify()
↓
eSetBits
↓
Fault_Task Notification Value
↓
多个故障bit可以累积
↓
Fault_Task:
Blocked → Ready
↓
Scheduler
↓
高优先级Fault_Task Running
↓
取得故障信息
↓
判断不同Fault Bit
↓
执行故障处理
45. Task Notification 与 Queue 在 BMS 中怎么选
45.1 ADC DMA 完成事件
只需要通知:
ADC_Process_Task:
“DMA已经完成了”
适合:
Task Notification
45.2 CAN 完整报文
数据:
CAN ID
DLC
Data[8]
而且:
Frame1
Frame2
Frame3
必须全部保存。
适合:
Queue
45.3 Fault Task 唤醒
只是通知:
过压
欠压
过温
可以压缩成bit。
适合:
Task Notification
45.4 完整 Fault Record
例如:
/* 定义一条完整BMS故障记录。 */
typedef struct
{
/* 保存故障代码。 */
uint32_t ulFaultCode;
/* 保存故障发生时的电压。 */
uint16_t usVoltageMv;
/* 保存故障发生时的温度。 */
int16_t sTemperatureC;
/* 保存故障发生时的时间戳。 */
uint32_t ulTimestamp;
} BMS_FaultRecord_t;
如果:
每一条FaultRecord都必须完整保存
则更适合:
Queue
因此:
简单事件 / bit / 计数
↓
Task Notification
完整结构体 / 多条消息
↓
Queue
46. Task Notification 完整小项目
下面模拟一个 BMS 故障通知系统。
系统架构:
FaultDetect_Task
│
│ 检测电压、温度
↓
生成Fault Bits
│
│ xTaskNotify()
↓
Fault_Task
│
↓
统一处理故障
故障定义:
bit0 = 过压
bit1 = 欠压
bit2 = 过温
完整示例:
/* 引入FreeRTOS核心配置和基础类型。 */
#include "FreeRTOS.h"
/* 引入Task以及Task Notification相关API。 */
#include "task.h"
/* 定义过压故障位,对应Notification Value的bit0。 */
#define BMS_FAULT_OV (1UL << 0)
/* 定义欠压故障位,对应Notification Value的bit1。 */
#define BMS_FAULT_UV (1UL << 1)
/* 定义过温故障位,对应Notification Value的bit2。 */
#define BMS_FAULT_OT (1UL << 2)
/* 定义过压阈值,单位为mV。 */
#define BMS_OV_THRESHOLD_MV 4200U
/* 定义欠压阈值,单位为mV。 */
#define BMS_UV_THRESHOLD_MV 3000U
/* 定义过温阈值,单位为摄氏度。 */
#define BMS_OT_THRESHOLD_C 60
/* 保存Fault_Task对应的任务句柄。 */
static TaskHandle_t xFaultTaskHandle = NULL;
/* 保存最近一次Fault_Task收到的故障bit。 */
volatile uint32_t g_ulLastFaultBits = 0U;
/* 保存过压故障处理次数。 */
volatile uint32_t g_ulOverVoltageCount = 0U;
/* 保存欠压故障处理次数。 */
volatile uint32_t g_ulUnderVoltageCount = 0U;
/* 保存过温故障处理次数。 */
volatile uint32_t g_ulOverTemperatureCount = 0U;
/* 声明故障检测任务。 */
static void FaultDetect_Task(void *pvParameters);
/* 声明故障处理任务。 */
static void Fault_Task(void *pvParameters);
/* 定义程序入口函数。 */
int main(void)
{
/* 保存Fault_Task创建结果。 */
BaseType_t xFaultCreateResult;
/* 保存FaultDetect_Task创建结果。 */
BaseType_t xDetectCreateResult;
/* 创建高优先级Fault_Task。 */
xFaultCreateResult = xTaskCreate(
Fault_Task,
"Fault",
128U,
NULL,
4U,
&xFaultTaskHandle
);
/* 判断Fault_Task是否创建成功。 */
if (xFaultCreateResult != pdPASS)
{
/* Task创建失败后进入永久保护循环。 */
while (1)
{
/* 执行空操作,方便Keil调试定位。 */
__NOP();
}
}
/* 创建故障检测任务。 */
xDetectCreateResult = xTaskCreate(
FaultDetect_Task,
"FaultDetect",
128U,
NULL,
2U,
NULL
);
/* 判断FaultDetect_Task是否创建成功。 */
if (xDetectCreateResult != pdPASS)
{
/* Task创建失败后进入永久保护循环。 */
while (1)
{
/* 执行空操作,方便Keil调试定位。 */
__NOP();
}
}
/* 启动FreeRTOS调度器。 */
vTaskStartScheduler();
/* 正常情况下调度器启动后不会返回这里。 */
while (1)
{
/* 执行空操作,用于异常情况下调试。 */
__NOP();
}
}
/* 定义故障检测任务。 */
static void FaultDetect_Task(void *pvParameters)
{
/* 定义模拟电池电压,单位为mV。 */
uint16_t usVoltageMv = 3700U;
/* 定义模拟电池温度,单位为摄氏度。 */
int16_t sTemperatureC = 25;
/* 保存当前检测周期产生的故障bit。 */
uint32_t ulFaultBits = 0U;
/* 当前任务不使用入口参数。 */
(void)pvParameters;
/* FaultDetect_Task永久周期运行。 */
while (1)
{
/* 每轮故障检测前先清除上一次临时故障bit。 */
ulFaultBits = 0U;
/* 判断当前电压是否超过过压阈值。 */
if (usVoltageMv > BMS_OV_THRESHOLD_MV)
{
/* 设置过压故障bit。 */
ulFaultBits |= BMS_FAULT_OV;
}
/* 判断当前电压是否低于欠压阈值。 */
if (usVoltageMv < BMS_UV_THRESHOLD_MV)
{
/* 设置欠压故障bit。 */
ulFaultBits |= BMS_FAULT_UV;
}
/* 判断当前温度是否超过过温阈值。 */
if (sTemperatureC > BMS_OT_THRESHOLD_C)
{
/* 设置过温故障bit。 */
ulFaultBits |= BMS_FAULT_OT;
}
/* 判断本轮是否至少检测到一种故障。 */
if (ulFaultBits != 0U)
{
/* 使用eSetBits将本轮故障bit通知给Fault_Task。 */
(void)xTaskNotify(
xFaultTaskHandle,
ulFaultBits,
eSetBits
);
}
/* 故障检测任务每100ms执行一次。 */
vTaskDelay(
pdMS_TO_TICKS(100U)
);
}
}
/* 定义故障处理任务。 */
static void Fault_Task(void *pvParameters)
{
/* 保存本次取得的故障通知值。 */
uint32_t ulReceivedFaultBits = 0U;
/* 当前任务不使用入口参数。 */
(void)pvParameters;
/* Fault_Task永久运行。 */
while (1)
{
/* 阻塞等待故障通知,并在取得后将Notification Value清零。 */
ulReceivedFaultBits = ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
/* 保存最近一次故障bit,方便Keil Watch窗口观察。 */
g_ulLastFaultBits = ulReceivedFaultBits;
/* 判断本次通知是否包含过压故障。 */
if ((ulReceivedFaultBits & BMS_FAULT_OV) != 0U)
{
/* 记录一次过压故障处理。 */
g_ulOverVoltageCount++;
}
/* 判断本次通知是否包含欠压故障。 */
if ((ulReceivedFaultBits & BMS_FAULT_UV) != 0U)
{
/* 记录一次欠压故障处理。 */
g_ulUnderVoltageCount++;
}
/* 判断本次通知是否包含过温故障。 */
if ((ulReceivedFaultBits & BMS_FAULT_OT) != 0U)
{
/* 记录一次过温故障处理。 */
g_ulOverTemperatureCount++;
}
}
}
47. ISR 通知任务示例
假设按键通过 EXTI0 产生中断。
ISR:
/* 定义EXTI0中断服务程序。 */
void EXTI0_IRQHandler(void)
{
/* 初始认为当前ISR没有唤醒更高优先级Task。 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 判断EXTI0中断Pending标志是否已经置位。 */
if ((EXTI->PR & EXTI_PR_PR0) != 0U)
{
/* 写1清除EXTI0 Pending标志。 */
EXTI->PR = EXTI_PR_PR0;
/* 从ISR给Button_Task增加一次通知计数。 */
vTaskNotifyGiveFromISR(
xButtonTaskHandle,
&xHigherPriorityTaskWoken
);
/* 如果唤醒了更高优先级Task,则请求ISR退出后的任务切换。 */
portYIELD_FROM_ISR(
xHigherPriorityTaskWoken
);
}
}
对应 Task:
/* 定义按键事件处理任务。 */
static void Button_Task(void *pvParameters)
{
/* 当前任务不使用入口参数。 */
(void)pvParameters;
/* Button_Task永久运行。 */
while (1)
{
/* 阻塞等待ISR产生的按键通知,每次只消费一个事件。 */
(void)ulTaskNotifyTake(
pdFALSE,
portMAX_DELAY
);
/* 调用真正的按键业务处理函数。 */
Button_Process();
}
}
完整模型:
按键硬件
↓
EXTI
↓
EXTI ISR
↓
vTaskNotifyGiveFromISR()
↓
Button_Task:
Blocked → Ready
↓
portYIELD_FROM_ISR()
↓
PendSV
↓
Button_Task Running
↓
Button_Process()
48. Task Notification 常见错误
48.1 TaskHandle_t 还是 NULL
错误:
xFaultTaskHandle = NULL
↓
直接xTaskNotify()
正确顺序:
创建目标Task
↓
获得TaskHandle
↓
允许发送方运行
↓
发送Notification
48.2 把 eIncrement 理解成加 ulValue
错误:
Value = 5
ulValue = 100
认为结果 = 105
正确:
5 → 6
48.3 无脑使用 Overwrite
旧通知尚未处理
↓
新通知覆盖
↓
旧信息丢失
48.4 忽略 eSetValueWithoutOverwrite 的返回值
如果:
pdFAIL
说明:
旧Pending通知仍然存在
↓
新Value没有成功写入
48.5 Notify 后认为目标 Task 一定立即 Running
错误:
Notify
↓
Running
正确:
Notify
↓
Blocked → Ready
↓
Scheduler
↓
可能Running
48.6 把 pdTRUE 理解成减1
错误:
pdTRUE
↓
Value -1
正确:
pdTRUE
↓
Value = 0
48.7 把 pdFALSE 理解成不修改
正确:
pdFALSE
↓
Value -1
48.8 把 ulTaskNotifyTake() 返回值理解成修改后的值
正确:
返回修改前的Notification Value
48.9 ISR 使用普通 Task API
错误思想:
ISR
↓
xTaskNotify()
正确:
ISR
↓
xTaskNotifyFromISR()
或者:
ISR
↓
vTaskNotifyGiveFromISR()
48.10 忘记 portYIELD_FROM_ISR()
可能出现:
高优先级Task已经Ready
↓
没有及时请求任务切换
48.11 IRQ 优先级配置错误
代码本身可能没有明显问题,但运行出现:
assert
HardFault
随机异常
需要检查:
NVIC Priority
↓
FreeRTOSConfig.h
48.12 Bitmask 错误配合 pdFALSE
例如:
0x06
↓
减1
↓
0x05
直接改变事件bit含义。
49. Task Notification 调试流程
如果:
目标 Task 一直收不到通知
按照下面顺序排查:
目标Task一直Blocked
↓
① 发送代码有没有执行?
↓
② TaskHandle是否正确?
↓
③ TaskHandle是不是NULL?
↓
④ xTaskNotify返回值是否正常?
↓
⑤ eAction是否符合业务要求?
↓
⑥ Notification Value是否发生变化?
↓
⑦ 接收Task是否真的在等待Notification?
↓
⑧ xTicksToWait配置是否正确?
↓
⑨ Task是否从Blocked进入Ready?
↓
⑩ Ready以后是不是被更高优先级Task压住?
ISR场景继续检查:
ISR通知失败
↓
ISR有没有真正进入?
↓
硬件中断标志是否正确?
↓
中断标志有没有正确清除?
↓
FromISR API是否使用正确?
↓
IRQ Priority是否符合FreeRTOS要求?
↓
xHigherPriorityTaskWoken是什么?
↓
portYIELD_FROM_ISR是否执行?
50. Keil 调试时建议观察什么
可以在 Watch 中观察:
目标TaskHandle
业务计数变量
最近一次Notification Value
xHigherPriorityTaskWoken
故障bit
Task状态
例如:
/* 保存最近一次Task收到的通知值,方便Keil Watch观察。 */
volatile uint32_t g_ulLastNotificationValue = 0U;
以及:
/* 保存Task已经处理的事件总次数。 */
volatile uint32_t g_ulEventProcessCount = 0U;
调试目的不是只看:
有没有进入函数
而是要判断:
没有发送?
↓
发送了但没唤醒?
↓
唤醒了但没有Ready?
↓
Ready了但没得到CPU?
↓
还是业务处理本身出了问题?
51. Task Notification API 总结
| xTaskNotify() | Task | 向指定Task发送通用通知 |
| ulTaskNotifyTake() | Task | 等待并消费当前Task的通知值 |
| xTaskNotifyFromISR() | ISR | 从ISR发送通用任务通知 |
| vTaskNotifyGiveFromISR() | ISR | 从ISR发送一次计数型通知 |
| portYIELD_FROM_ISR() | ISR | 必要时请求ISR退出后的任务切换 |
xTaskNotify() 中最重要的 Action:
| eNoAction | 不修改通知值,只产生通知 |
| eIncrement | 通知值加1 |
| eSetBits | 按位OR |
| eSetValueWithOverwrite | 强制覆盖 |
| eSetValueWithoutOverwrite | 有Pending时不覆盖 |
52. Task Notification 最重要的原则
原则1:Task Notification 是直接通知指定 Task
发送方
↓
TaskHandle_t
↓
目标Task
原则2:Notification Value 是32位值
可以被解释成:
计数
bit
简单数据
原则3:Notification Value 和 Notification State 不是一回事
Value
↓
通知数据
State
↓
通知状态
原则4:xTaskNotify() 用于发送
Task
↓
xTaskNotify()
↓
目标Task
原则5:ulTaskNotifyTake() 用于当前 Task 等待通知
当前Task
↓
ulTaskNotifyTake()
↓
没有通知
↓
Blocked
原则6:pdTRUE 清零
5 → 0
原则7:pdFALSE 减1
5 → 4
原则8:ulTaskNotifyTake() 返回修改前的值
调用前 = 5
返回 = 5
原则9:被通知不等于立即 Running
Blocked
↓
Ready
↓
Scheduler
↓
Running
原则10:ISR 必须使用 FromISR API
Task:
xTaskNotify()
ISR:
xTaskNotifyFromISR()
原则11:xHigherPriorityTaskWoken 表示是否唤醒更高优先级 Task
Blocked高优先级Task
↓
被ISR唤醒
↓
Ready
↓
xHigherPriorityTaskWoken = pdTRUE
原则12:portYIELD_FROM_ISR() 不是直接运行 Task
而是:
请求切换
↓
PendSV
↓
真正任务切换
原则13:Task Notification 更轻量,但不是万能
简单事件
↓
Notification
完整消息
↓
Queue
53. 面试必须掌握的问题
Q1:Task Notification 是什么?
Task Notification 是 FreeRTOS 提供的一种直接向指定 Task 发送事件或32位通知值的轻量级通信机制。通知信息直接保存在目标 Task 的 TCB 通知字段中,因此在简单的一对一事件同步场景中,不需要额外创建 Queue 或 Semaphore 对象。
Q2:为什么 Task Notification 比 Queue 更轻量?
Queue 需要独立的 Queue 控制结构以及消息 Buffer,并涉及消息入队、出队和数据复制。Task Notification 直接利用目标 Task 已有 TCB 中的通知信息,因此在简单通知场景下 RAM 和处理路径通常更小。
Q3:Notification Value 是什么?
Notification Value 是任务自己的32位通知值,可以用于事件计数、bit标志或简单32位数据。
Q4:为什么还需要 Notification State?
因为仅靠 Notification Value 无法完全表示有没有收到通知,例如 Value 为0既可能代表没有通知,也可能代表收到的值本身就是0,因此还需要独立通知状态。
Q5:xTaskNotify() 三个参数分别是什么?
xTaskToNotify
→ 通知哪个Task
ulValue
→ 通知使用的32位值
eAction
→ 如何处理目标Task原来的通知值
Q6:eIncrement 是什么?
每次通知使 Notification Value 增加1,ulValue 不参与加法。
Q7:eSetBits 适合什么场景?
将新的通知值和原通知值按位OR,因此适合多个事件bit、故障bit同时存在的场景。
Q8:eSetValueWithoutOverwrite 什么时候返回 pdFAIL?
如果目标 Task 已经存在尚未处理的 Pending Notification,则不会覆盖旧值,并返回 pdFAIL。
Q9:pdTRUE 和 pdFALSE 有什么区别?
pdTRUE 在成功取得通知后把任务通知值清零,适合类似Binary Semaphore语义;pdFALSE 只把通知值减1,适合Counting Semaphore语义。
Q10:ulTaskNotifyTake() 返回什么?
返回修改之前的 Notification Value。
Q11:为什么 Task 等待 Notification 不占 CPU?
当没有通知并允许等待时,Task 会进入 Blocked,从 Ready 竞争中移除,CPU 可以运行其他 Ready Task。
Q12:为什么 ISR 要使用 FromISR API?
ISR 不是 FreeRTOS Task,没有普通任务的 TCB、Blocked 等任务上下文,不能走普通 Task API 的执行路径,因此需要专门的 FromISR API。
Q13:xHigherPriorityTaskWoken 是什么?
表示这次 ISR 中的 FreeRTOS 操作是否使一个比当前被中断 Task 优先级更高的 Task 解除阻塞。
Q14:portYIELD_FROM_ISR() 有什么作用?
如果 ISR 唤醒了更高优先级 Task,通过 portYIELD_FROM_ISR() 请求在 ISR 退出后进行调度切换;Cortex-M3 上最终与 PendSV 上下文切换机制关联。
Q15:Task Notification 能完全替代 Queue 吗?
不能。Queue 能保存多条完整消息和结构体数据,而 Task Notification 主要适合简单事件、计数、bit和有限的32位数据。
Q16:为什么 Fault_Task 可以设计成高优先级?
Fault_Task 平时阻塞等待通知,不持续占用 CPU;故障发生后再进入 Ready,因此可以设置较高优先级,实现快速故障响应。
Q17:为什么 CAN 完整帧通常使用 Queue,而不是 Task Notification?
CAN 完整帧包含 ID、DLC 和 Data,并且可能连续收到多帧,需要完整保存和排队,因此 Queue 更符合需求。
54. Task Notification 完整知识体系
Task Notification
│
┌──────────────────┴──────────────────┐
↓ ↓
Task发送 ISR发送
│ │
xTaskNotify() xTaskNotifyFromISR()
│ vTaskNotifyGiveFromISR()
│ │
└──────────────────┬──────────────────┘
↓
目标Task TCB
│
┌─────────────┴─────────────┐
↓ ↓
Notification Value Notification State
│ │
┌─────────┼─────────┐ │
↓ ↓ ↓ │
Increment Bits Value │
↓ ↓ ↓ │
计数 事件bit 简单数据 │
└─────────┴─────────┘ │
│ │
└─────────────┬─────────────┘
↓
目标Task正在等待?
│ │
否 是
│ ↓
│ Blocked → Ready
│ ↓
└────→ Scheduler
↓
Running
↓
ulTaskNotifyTake()
│
┌──────────┴──────────┐
↓ ↓
pdTRUE pdFALSE
↓ ↓
清零 Value-1
↓ ↓
Binary式 Counting式
55. ISR Task Notification 最终记忆模型
硬件事件
↓
IRQ
↓
ISR
↓
清中断标志
↓
xHigherPriorityTaskWoken = pdFALSE
↓
FromISR Notification
↓
目标Task:
Blocked → Ready
↓
目标Task优先级更高?
│
├── 否
│ ↓
│ 当前Task继续
│
└── 是
↓
xHigherPriorityTaskWoken = pdTRUE
↓
portYIELD_FROM_ISR()
↓
ISR退出
↓
PendSV
↓
任务切换
↓
目标Task Running
56. BMS Task Notification 最终模型
BMS数据
↓
FaultDetect_Task
↓
判断过压 / 欠压 / 过温
↓
Fault Bits
↓
xTaskNotify(…, eSetBits)
↓
Fault_Task
↓
Blocked → Ready
↓
Scheduler
↓
Fault_Task Running
↓
读取故障Bit集合
↓
┌────────────┼────────────┐
↓ ↓ ↓
过压处理 欠压处理 过温处理
↓ ↓ ↓
└────────────┴────────────┘
↓
CAN / 状态机 / 日志等业务
57. Task Notification 最终记忆模型
不要把 Task Notification 记成几个孤立 API。
真正需要记住的是:
Task为什么需要通知?
↓
事件驱动,避免轮询
↓
通知信息放在哪里?
↓
目标Task自己的TCB
↓
核心有什么?
↓
Notification Value
+
Notification State
↓
Task怎么发送?
↓
xTaskNotify()
↓
Value怎么处理?
↓
eNoAction
eIncrement
eSetBits
Overwrite
WithoutOverwrite
↓
Task怎么等待?
↓
ulTaskNotifyTake()
↓
没有Notification
↓
Blocked
↓
Notification到达
↓
Blocked → Ready
↓
Scheduler
↓
Running
↓
pdTRUE?
↓
清零
↓
Binary式
pdFALSE?
↓
减1
↓
Counting式
↓
ISR怎么通知?
↓
FromISR API
↓
xHigherPriorityTaskWoken
↓
portYIELD_FROM_ISR()
↓
PendSV
↓
任务切换
↓
什么时候用Notification?
↓
简单事件 / 计数 / bit
什么时候用Queue?
↓
完整数据 / 多条消息排队
58. Task Notification 在 FreeRTOS 整体中的位置
到这里,前面的知识应该形成完整链路:
Task
↓
每个Task有自己的TCB和栈
↓
Task存在Ready / Running / Blocked等状态
↓
Scheduler决定哪个Task运行
↓
PendSV负责真正任务切换
↓
任务之间需要通信和同步
↓
Queue可以传递完整数据
↓
Semaphore可以进行事件同步
↓
Task Notification可以直接通知指定Task
↓
没有事件时Task进入Blocked
↓
事件到达后:
Blocked → Ready
↓
Scheduler重新判断
↓
必要时PendSV切换
↓
目标Task继续运行
因此 Task Notification 不是一个孤立 API。
它实际上把前面学习的:
Task
+
TCB
+
任务状态
+
优先级
+
Scheduler
+
PendSV
+
ISR
真正连接到了:
轻量级任务间通信与事件同步
这就是 Task Notification 在整个 FreeRTOS 知识体系中的位置。





