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 实战中的选择建议

我做了这么多年嵌入式,总结了一套自己的选择逻辑:

  1. 单纯唤醒任务 → 用 xTaskNotifyGive(),代码最简洁
  2. 传递数据 → 用 xTaskNotify() 配合 eSetValueWithOverwrite
  3. 事件标志 → 用 xTaskNotify() 配合 eSetBits
  4. 中断里发通知 → 别无选择,只能用 xTaskNotifyFromISR()

你可能会问:为什么不用队列?嗯,任务通知比队列快得多,因为它不需要额外的内存拷贝。但任务通知也有局限——每个任务只有一个通知值。如果你的场景需要传递复杂结构体,那还是老老实实用队列吧。

好了,这三个API的用法和区别,我讲得够清楚了吧?下一节咱们聊聊接收端的API,看看任务怎么优雅地“收”到这些通知。