引言
在 ESP32 双核(Xtensa LX6)架构下,FreeRTOS 默认支持对称多处理(SMP),任务可运行于任意核心。当任务间需要同步或传递简单事件时,信号量(Semaphore)是常用选择,但其内部依赖队列和内核临界区,在双核场景下会引入额外的缓存同步和调度开销。FreeRTOS 任务通知(Task Notification)直接操作任务控制块(TCB)中的通知值,无需创建队列,延迟更低。本文通过实测对比,展示任务通知在 IPC 延迟上的优势,并提供可复用的代码。
原理对比
信号量(Semaphore)
- 基于队列实现,创建时分配队列存储空间。
- 操作流程:
xSemaphoreGive从队列尾部写入,xSemaphoreTake从头部读取,涉及临界区保护(关闭中断或获取调度器锁)。 - 双核下,队列操作需跨核同步,可能触发缓存一致性开销。
- 延迟典型值:约 10-20 微秒(取决于负载)。
任务通知(Task Notification)
- 直接修改目标任务的 TCB 中的
ulNotifiedValue和ucNotifyState。 - 使用
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获取微秒级时间戳)。
配置步骤
- 在
menuconfig中启用 SMP:Component config → FreeRTOS → SMP(默认开启)。 - 创建两个任务,分别固定到核心 0 和核心 1(使用
xTaskCreatePinnedToCore)。 - 实现信号量版本:创建二进制信号量,发送方
xSemaphoreGive,接收方xSemaphoreTake。 - 实现任务通知版本:发送方
xTaskNotifyGive,接收方ulTaskNotifyTake(设置pdTRUE清除通知值)。 - 使用
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递减)。 - 在中断服务函数中,应使用
xTaskNotifyFromISR或vTaskNotifyGiveFromISR,确保安全。 - 双核下,任务通知的延迟优势明显,但若任务频繁切换,需权衡调度开销。
- 实测结果受系统负载影响,建议在真实场景中验证。
总结
在 ESP32 双核环境下,FreeRTOS 任务通知相比信号量可降低约 4 倍 IPC 延迟,尤其适合高频事件同步场景。通过合理使用任务通知,开发者能显著提升系统实时性。但需注意其单接收者限制,根据实际需求选择合适机制。