2、通知发送API详解:xTaskNotify()、xTaskNotifyGive()、xTaskNotifyFromISR()的用法与区别
好,咱们直接进入正题。任务通知的核心在于“发”和“收”。今天我就把三个发送API给你掰开揉碎了讲清楚。你想想看,这三个函数就像三把不同的钥匙,开的是同一把锁,但用法和场景完全不同。
2.1 先说说最通用的:xTaskNotify()
这个函数,我个人用得最多。它就像一个瑞士军刀,功能最全。它的原型长这样:
BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify, // 通知哪个任务
uint32_t ulValue, // 要发送的值
eNotifyAction eAction // 通知动作类型
);
第三个参数 eAction 是关键。它决定了你怎么操作目标任务的通知值。我整理了一张表,你一看就明白:
| 动作类型 | 含义 | 典型场景 |
|---|---|---|
| eNoAction | 只发送通知,不修改通知值 | 单纯唤醒任务,类似二进制信号量 |
| eSetBits | 按位或操作,设置指定bit | 事件标志组,多个事件同时通知 |
| eIncrement | 通知值加1 | 计数信号量,统计事件次数 |
| eSetValueWithOverwrite | 直接覆盖写入新值 | 传递最新数据,旧数据丢了无所谓 |
| eSetValueWithoutOverwrite | 如果通知值未被取走,则写入失败 | 传递数据,且不能丢失旧数据 |
重点来了: 返回值为 pdPASS 表示通知成功,pdFAIL 表示通知失败(比如对方任务还没取走上次的值)。
我在项目中遇到过一个问题:用 eSetValueWithoutOverwrite 时,如果接收任务处理得慢,发送方就会一直返回失败。后来我改成了 eSetValueWithOverwrite,只保留最新数据,问题就解决了。嗯,这里要注意,不是所有场景都适合覆盖。
2.2 最简洁的:xTaskNotifyGive()
这个函数,说白了就是 xTaskNotify() 的简化版。它等价于:
xTaskNotify(xTaskToNotify, 0, eIncrement);
它的原型更简单:
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
为什么要有这个函数?我个人习惯是:当只需要计数功能时,就用它。代码更干净,意图更明确。你想想看,别人一看 xTaskNotifyGive,就知道你是在“给一个信号”,而不是在“传递一个数据”。
我的建议: 如果你只是想让任务“做一次事情”,比如处理一个按键、处理一个网络包,用 xTaskNotifyGive 就够了。别把简单的事情搞复杂。
2.3 中断里的专用函数:xTaskNotifyFromISR()
这个函数,是专门给中断服务函数用的。它的原型多了两个参数:
BaseType_t xTaskNotifyFromISR(
TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken
);
最后一个参数 pxHigherPriorityTaskWoken 是重点。它用来告诉系统:这个通知是否唤醒了一个更高优先级的任务。如果是,你需要在中断退出前做一次上下文切换。
我曾经踩过的坑: 刚开始用 FreeRTOS 时,我在中断里直接调用了 xTaskNotify(),结果系统偶尔会死机。后来才发现,中断里必须用 xTaskNotifyFromISR(),而且一定要检查 pxHigherPriorityTaskWoken 的值,并在中断末尾调用 portYIELD_FROM_ISR()。
标准用法是这样的:
void vTimerISR(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 发送通知
xTaskNotifyFromISR(xTaskHandle, 0, eIncrement, &xHigherPriorityTaskWoken);
// 如果需要,进行上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
2.4 三个API的区别总结
我直接给你列个对比表,这样更直观:
| 函数 | 调用环境 | 功能特点 | 返回值 |
|---|---|---|---|
| xTaskNotify() | 任务级 | 功能最全,支持5种动作 | pdPASS / pdFAIL |
| xTaskNotifyGive() | 任务级 | 简化版,只做计数加1 | pdPASS / pdFAIL |
| xTaskNotifyFromISR() | 中断级 | 支持中断上下文,带任务切换标志 | pdPASS / pdFAIL |
核心区别就一句话: 任务里用 xTaskNotify() 或 xTaskNotifyGive(),中断里必须用 xTaskNotifyFromISR()。千万别搞混了,否则系统会给你颜色看。
2.5 实战中的选择建议
我做了这么多年嵌入式,总结了一套自己的选择逻辑:
- 单纯唤醒任务 → 用
xTaskNotifyGive(),代码最简洁 - 传递数据 → 用
xTaskNotify()配合eSetValueWithOverwrite - 事件标志 → 用
xTaskNotify()配合eSetBits - 中断里发通知 → 别无选择,只能用
xTaskNotifyFromISR()
你可能会问:为什么不用队列?嗯,任务通知比队列快得多,因为它不需要额外的内存拷贝。但任务通知也有局限——每个任务只有一个通知值。如果你的场景需要传递复杂结构体,那还是老老实实用队列吧。
好了,这三个API的用法和区别,我讲得够清楚了吧?下一节咱们聊聊接收端的API,看看任务怎么优雅地“收”到这些通知。