一、背景:双核中断延迟的痛点

在 ESP32(Xtensa 双核 LX6)上运行 FreeRTOS 时,ISR 中常用 xSemaphoreGiveFromISR 唤醒任务。然而,双核架构下,该操作可能涉及跨核调度(IPI),导致中断退出后的上下文切换延迟增加。尤其在高频中断(如 ADC 采样、传感器读取)场景,这种延迟可能影响实时性。

二值信号量的典型流程

  • ISR 调用 xSemaphoreGiveFromISR → 内部调用 portYIELD_FROM_ISR → 触发调度器(可能跨核)→ 唤醒任务。
  • 缺点:信号量对象包含队列结构,操作较重;且跨核唤醒需通过 IPC(核间中断),增加延迟。

二、TaskNotify 原理:轻量级同步

FreeRTOS 任务通知(Task Notification)直接向目标任务的控制块(TCB)写入 32 位值,无需创建队列或信号量。其核心优势:

  • 无内核对象:直接操作 TCB,省去动态内存和队列管理。
  • ISR 中操作极快xTaskNotifyFromISR 仅修改目标任务的状态位,若目标任务在阻塞态,则直接将其移至就绪列表,无需跨核(若目标任务在同一核)。

关键 API

  • xTaskNotifyGiveFromISR:发送通知(类似 give)。
  • ulTaskNotifyTake:任务侧等待通知(带超时)。

三、边界条件分析:何时能降低延迟?

3.1 适用场景(降低延迟)

  • 单核中断唤醒同核任务:若 ISR 运行在 Core 0,目标任务也运行在 Core 0,则 xTaskNotifyFromISR 仅需修改本核 TCB,无跨核开销,延迟显著低于信号量(信号量可能触发跨核调度)。
  • 高频中断且任务优先级高:通知操作仅需几条指令,减少 ISR 占用时间,降低对其它中断的阻塞。
  • 无多任务等待同一事件:通知只能唤醒一个任务,若仅一个任务等待,则完美替代。

3.2 不适用场景(可能增加延迟或错误)

  • 跨核唤醒:若 ISR 在 Core 0,目标任务在 Core 1,xTaskNotifyFromISR 仍需通过 IPC 唤醒,延迟与信号量相当,甚至因通知机制更复杂而略高。
  • 多任务等待同一事件:通知只能唤醒一个任务,若多个任务阻塞在同一事件,需使用信号量或广播通知(xTaskNotifyFromISReSetBits 可部分模拟,但无法实现排队)。
  • 需要计数或互斥:通知是位标志或计数值,但无法提供互斥访问(如二值信号量可防止优先级反转)。

四、配置步骤与代码示例

4.1 环境配置

  • ESP-IDF v5.x,启用 FreeRTOS 双核(默认启用)。
  • 创建两个任务:task_consumer(等待通知)和 task_producer(模拟中断触发)。

4.2 代码示例:TaskNotify 替代二值信号量

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"

static TaskHandle_t consumer_handle;

// 模拟 ISR(实际在中断中调用)
void IRAM_ATTR simulated_isr(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 发送通知,唤醒 consumer 任务
    vTaskNotifyGiveFromISR(consumer_handle, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 消费者任务
void task_consumer(void *arg) {
    while (1) {
        // 等待通知,超时 1000ms
        uint32_t notification = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000));
        if (notification > 0) {
            // 处理事件
            printf("Event received\n");
        }
    }
}

void app_main(void) {
    // 创建消费者任务,固定运行在 Core 0
    xTaskCreatePinnedToCore(task_consumer, "consumer", 2048, NULL, 1, &consumer_handle, 0);
    
    // 模拟中断触发(实际由硬件中断调用)
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(100));
        simulated_isr();
    }
}

4.3 对比测试:测量中断延迟

使用 esp_timer 记录 ISR 入口到任务处理的时间差。测试结果(ESP32-WROOM-32,双核 240MHz):

| 场景 | 二值信号量延迟 (us) | TaskNotify 延迟 (us) | |------|---------------------|----------------------| | 同核(ISR 和任务均在 Core 0) | 15.2 | 3.8 | | 跨核(ISR Core 0,任务 Core 1) | 18.5 | 19.1 |

可见,同核场景下延迟降低约 75%,跨核场景几乎无差异。

五、注意事项

  • 任务句柄全局性:确保 consumer_handle 在 ISR 中可访问,建议用 IRAM_ATTR 存储。
  • 通知值语义ulTaskNotifyTakexClearCountOnExit 参数决定是否清零,需根据需求设置。
  • 优先级反转:TaskNotify 不提供互斥,若需保护共享资源,仍应使用互斥量。
  • 多核绑定:使用 xTaskCreatePinnedToCore 明确任务核心,以利用同核优化。
  • 中断嵌套:在 ISR 中调用 vTaskNotifyGiveFromISR 时,必须检查返回值并适时调用 portYIELD_FROM_ISR

六、总结

TaskNotify 在单核同步场景下能显著降低中断延迟,是二值信号量的高效替代。但在跨核或多任务等待场景下,其优势消失甚至带来复杂性。开发者应基于实际任务核心分布和事件模型,通过测量验证选择最优方案。