欢迎光临
我们一直在努力

FreeRTOS Task Notification详解:轻量级任务通知、计数与ISR通信

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 总结

eAction对 Notification Value 的影响常见用途
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

调用返回值调用后 Value典型语义
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 总结

API使用环境主要作用
xTaskNotify() Task 向指定Task发送通用通知
ulTaskNotifyTake() Task 等待并消费当前Task的通知值
xTaskNotifyFromISR() ISR 从ISR发送通用任务通知
vTaskNotifyGiveFromISR() ISR 从ISR发送一次计数型通知
portYIELD_FROM_ISR() ISR 必要时请求ISR退出后的任务切换

xTaskNotify() 中最重要的 Action:

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 知识体系中的位置。

赞(0)
未经允许不得转载:171主机测评 » FreeRTOS Task Notification详解:轻量级任务通知、计数与ISR通信
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址