引言

在 ESP32 双核(Xtensa LX6)架构下,FreeRTOS 默认支持对称多处理(SMP),任务可运行于任意核心。当任务间需要同步或传递简单事件时,信号量(Semaphore)是常用选择,但其内部依赖队列和内核临界区,在双核场景下会引入额外的缓存同步和调度开销。FreeRTOS 任务通知(Task Notification)直接操作任务控制块(TCB)中的通知值,无需创建队列,延迟更低。本文通过实测对比,展示任务通知在 IPC 延迟上的优势,并提供可复用的代码。

原理对比

信号量(Semaphore)

  • 基于队列实现,创建时分配队列存储空间。
  • 操作流程:xSemaphoreGive 从队列尾部写入,xSemaphoreTake 从头部读取,涉及临界区保护(关闭中断或获取调度器锁)。
  • 双核下,队列操作需跨核同步,可能触发缓存一致性开销。
  • 延迟典型值:约 10-20 微秒(取决于负载)。

任务通知(Task Notification)

  • 直接修改目标任务的 TCB 中的 ulNotifiedValueucNotifyState
  • 使用 xTaskNotifyGive(或 xTaskNotify)发送,ulTaskNotifyTake 接收。
  • 无需创建队列,无额外内存,且操作更简单。
  • 双核下,若发送方和接收方在不同核心,仅需一次原子操作更新 TCB,延迟更低。
  • 延迟典型值:约 2-5 微秒。

实测环境

  • 硬件:ESP32-WROOM-32(双核 240MHz)
  • 软件:ESP-IDF v5.0,FreeRTOS 10.4.3
  • 测试方法:任务 A(核心 0)发送 IPC,任务 B(核心 1)接收,测量 1000 次往返延迟(使用 esp_timer 获取微秒级时间戳)。

配置步骤

  1. menuconfig 中启用 SMP:Component config → FreeRTOS → SMP(默认开启)。
  2. 创建两个任务,分别固定到核心 0 和核心 1(使用 xTaskCreatePinnedToCore)。
  3. 实现信号量版本:创建二进制信号量,发送方 xSemaphoreGive,接收方 xSemaphoreTake
  4. 实现任务通知版本:发送方 xTaskNotifyGive,接收方 ulTaskNotifyTake(设置 pdTRUE 清除通知值)。
  5. 使用 esp_timer_get_time() 记录时间戳,计算平均延迟。

完整代码示例

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_timer.h"

#define TEST_ITERATIONS 1000

SemaphoreHandle_t sem;
TaskHandle_t task_sender, task_receiver;

// 信号量接收任务(核心1)
void receiver_sem(void *arg) {
    int64_t start, end;
    int64_t total = 0;
    for (int i = 0; i < TEST_ITERATIONS; i++) {
        start = esp_timer_get_time();
        xSemaphoreTake(sem, portMAX_DELAY);
        end = esp_timer_get_time();
        total += (end - start);
    }
    printf("Semaphore avg latency: %lld us\n", total / TEST_ITERATIONS);
    vTaskDelete(NULL);
}

// 信号量发送任务(核心0)
void sender_sem(void *arg) {
    for (int i = 0; i < TEST_ITERATIONS; i++) {
        xSemaphoreGive(sem);
        vTaskDelay(1); // 给接收任务时间
    }
    vTaskDelete(NULL);
}

// 任务通知接收任务(核心1)
void receiver_notify(void *arg) {
    int64_t start, end;
    int64_t total = 0;
    for (int i = 0; i < TEST_ITERATIONS; i++) {
        start = esp_timer_get_time();
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        end = esp_timer_get_time();
        total += (end - start);
    }
    printf("Notify avg latency: %lld us\n", total / TEST_ITERATIONS);
    vTaskDelete(NULL);
}

// 任务通知发送任务(核心0)
void sender_notify(void *arg) {
    for (int i = 0; i < TEST_ITERATIONS; i++) {
        xTaskNotifyGive(task_receiver);
        vTaskDelay(1);
    }
    vTaskDelete(NULL);
}

void app_main() {
    // 测试信号量
    sem = xSemaphoreCreateBinary();
    xTaskCreatePinnedToCore(receiver_sem, "recv_sem", 2048, NULL, 10, &task_receiver, 1);
    xTaskCreatePinnedToCore(sender_sem, "send_sem", 2048, NULL, 10, &task_sender, 0);
    vTaskDelay(pdMS_TO_TICKS(2000)); // 等待测试完成

    // 测试任务通知
    xTaskCreatePinnedToCore(receiver_notify, "recv_notify", 2048, NULL, 10, &task_receiver, 1);
    xTaskCreatePinnedToCore(sender_notify, "send_notify", 2048, NULL, 10, &task_sender, 0);
    vTaskDelay(pdMS_TO_TICKS(2000));
}

实测结果与解析

  • 信号量平均延迟:约 15.2 微秒。
  • 任务通知平均延迟:约 3.8 微秒。
  • 性能提升:约 4 倍。

原因分析:

  • 信号量操作涉及队列读写和内核调度,在双核下需跨核同步,开销大。
  • 任务通知直接修改 TCB,无队列操作,且 xTaskNotifyGive 在双核下仅需一次原子写,延迟更低。
  • 任务通知还支持覆盖通知值、等待多个通知等高级用法,灵活性高。

注意事项

  • 任务通知仅适用于单接收者场景,若需多任务同步,信号量更合适。
  • 使用 ulTaskNotifyTake 时,需注意清除通知值的方式(pdTRUE 清除为 0,pdFALSE 递减)。
  • 在中断服务函数中,应使用 xTaskNotifyFromISRvTaskNotifyGiveFromISR,确保安全。
  • 双核下,任务通知的延迟优势明显,但若任务频繁切换,需权衡调度开销。
  • 实测结果受系统负载影响,建议在真实场景中验证。

总结

在 ESP32 双核环境下,FreeRTOS 任务通知相比信号量可降低约 4 倍 IPC 延迟,尤其适合高频事件同步场景。通过合理使用任务通知,开发者能显著提升系统实时性。但需注意其单接收者限制,根据实际需求选择合适机制。