引言

ESP32最引人注目的特性之一是其双核架构——两个Xtensa LX6处理器核心可以并行执行任务。然而,双核带来的并行能力也引入了工程实践中最棘手的问题:两个核心之间如何安全高效地通信?

我们沧州虎王科技技术团队在开发物联网网关产品时,遇到过一次典型的双核通信故障:Core 0负责MQTT通信和业务逻辑,Core 1负责传感器数据采集和信号处理。初始设计使用共享内存直接交换数据,上线后发现偶发性数据错乱——传感器读数偶尔会出现明显异常值,排查两周后发现是双核共享变量读写竞争导致的。

FreeRTOS为ESP32双核提供了多种通信机制:消息队列(Queue)、信号量(Semaphore)、互斥锁(Mutex)、事件组(Event Group)、任务通知(Task Notification)和流缓冲区(Stream Buffer)。每种机制都有其适用场景和性能特征。本文将深入分析这些机制的底层原理,并通过实际工程案例给出选型指南。


一、ESP32双核架构与FreeRTOS调度

1.1 ESP32双核硬件架构

ESP32的Xtensa LX6双核架构中,两个核心共享同一块SRAM和Flash,但有各自独立的L1缓存和中断控制器。理解这一架构是设计双核通信的基础。

┌──────────────────────────────────────────────────────────────┐
│                   ESP32 双核架构与共享资源                     │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────┐                    ┌─────────────┐        │
│   │   CPU 0     │                    │   CPU 1     │        │
│   │ (PRO_CPU)   │                    │ (APP_CPU)   │        │
│   │             │                    │             │        │
│   │ WiFi/BT栈   │                    │ 应用任务    │        │
│   │ 网络协议栈   │                    │ 信号处理    │        │
│   │ 主任务      │                    │ 传感器采集  │        │
│   └──────┬──────┘                    └──────┬──────┘        │
│          │                                   │               │
│          │    ┌──────────────────┐          │               │
│          └───►│  共享 SRAM        │◄─────────┘               │
│          │    │  (520KB)         │          │               │
│          │    ├──────────────────┤          │               │
│          │    │  FreeRTOS Heap   │          │               │
│          │    │  (统一堆管理)     │          │               │
│          │    ├──────────────────┤          │               │
│          │    │  IPC 通信寄存器   │          │               │
│          └───►│  (硬件级跨核中断) │◄─────────┘               │
│               └──────────────────┘                           │
│                                                              │
│   ┌──────────────────────────────────────────────────┐      │
│   │              外设共享 (SPI/I2C/UART/GPIO)         │      │
│   │  任一核心均可访问,但需要通过互斥锁协调            │      │
│   └──────────────────────────────────────────────────┘      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.2 FreeRTOS在ESP32上的SMP调度

ESP-IDF使用FreeRTOS SMP(对称多处理)版本,两个核心运行同一个FreeRTOS实例。任务可以被分配到任意核心,也可以被固定到特定核心。

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

static const char *TAG = "DUALCORE";

/* 任务核心亲和性配置 */
typedef struct {
    const char   *name;
    BaseType_t    core_id;        /* 0 = PRO_CPU, 1 = APP_CPU, tskNO_AFFINITY = 任意 */
    UBaseType_t  priority;
    uint32_t     stack_size;
} task_config_t;

/* 典型双核任务分配方案 */
static const task_config_t g_task_configs[] = {
    /* Core 0: 通信与系统管理 */
    {"wifi_manager",   0, 6, 4096},
    {"mqtt_client",    0, 5, 6144},
    {"ota_update",     0, 4, 8192},

    /* Core 1: 数据采集与处理 */
    {"sensor_reader",  1, 7, 4096},
    {"signal_process", 1, 6, 8192},
    {"display_update", 1, 3, 4096},

    /* 不固定核心: 低优先级任务 */
    {"log_writer",     tskNO_AFFINITY, 1, 3072},
    {"led_blink",      tskNO_AFFINITY, 1, 1024},
};

#define TASK_NUM (sizeof(g_task_configs) / sizeof(g_task_configs[0]))

/* 创建任务并设置核心亲和性 */
void create_dualcore_tasks(void)
{
    for (int i = 0; i < TASK_NUM; i++) {
        TaskHandle_t handle = NULL;
        BaseType_t ret = xTaskCreatePinnedToCore(
            task_entry_point,                    /* 任务函数 */
            g_task_configs[i].name,              /* 任务名 */
            g_task_configs[i].stack_size,        /* 栈大小 */
            (void *)g_task_configs[i].name,      /* 参数 */
            g_task_configs[i].priority,          /* 优先级 */
            &handle,                            /* 句柄 */
            g_task_configs[i].core_id);          /* 核心绑定 */

        if (ret == pdPASS) {
            ESP_LOGI(TAG, "任务 %s 创建成功 → Core %d, 优先级 %d",
                     g_task_configs[i].name,
                     g_task_configs[i].core_id,
                     g_task_configs[i].priority);
        } else {
            ESP_LOGE(TAG, "任务 %s 创建失败!", g_task_configs[i].name);
        }
    }
}

1.3 核心亲和性选型原则

/* 核心亲和性决策框架 */
typedef enum {
    CORE_POLICY_FIXED_PRO,      /* 固定到CPU0 */
    CORE_POLICY_FIXED_APP,      /* 固定到CPU1 */
    CORE_POLICY_SHARED,         /* 不固定,允许迁移 */
} core_affinity_policy_t;

/**
 * @brief 根据任务特征决定核心亲和性
 * @param uses_wifi  是否使用WiFi/BT(必须在CPU0)
 * @param is_time_critical 是否对实时性要求高
 * @param cpu_intensive 是否CPU密集型
 * @return 建议的核心ID
 */
BaseType_t recommend_core_id(bool uses_wifi, bool is_time_critical, bool cpu_intensive)
{
    /* WiFi/蓝牙协议栈绑定CPU0,相关任务应放在CPU0 */
    if (uses_wifi) {
        return 0;
    }

    /* 实时性要求高的任务放在CPU1,避免被WiFi中断影响 */
    if (is_time_critical) {
        return 1;
    }

    /* CPU密集型任务放在CPU1,与CPU0的网络任务并行 */
    if (cpu_intensive) {
        return 1;
    }

    /* 默认不绑定,让调度器自行决定 */
    return tskNO_AFFINITY;
}

二、FreeRTOS消息队列:跨核通信首选方案

2.1 消息队列工作原理

FreeRTOS的消息队列是跨核通信最安全、最推荐的方式。队列内部实现了完整的锁机制,确保两个核心同时读写时不会发生数据竞争。

┌──────────────────────────────────────────────────────────────┐
│           FreeRTOS 消息队列跨核通信架构                        │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  CPU 0 (PRO_CPU)                    CPU 1 (APP_CPU)         │
│  ┌──────────────┐                   ┌──────────────┐       │
│  │ mqtt_task    │                   │ sensor_task  │       │
│  │              │                   │              │       │
│  │ xQueueReceive│◄─────────────────│ xQueueSend   │       │
│  │ (阻塞等待)    │    队列内部       │ (非阻塞)     │       │
│  │              │    ┌──────┐      │              │       │
│  │              │    │Item 0│      │              │       │
│  │              │    ├──────┤      │              │       │
│  │              │    │Item 1│      │              │       │
│  │              │    ├──────┤      │              │       │
│  │              │    │Item 2│      │              │       │
│  │              │    ├──────┤      │              │       │
│  │              │    │  ... │      │              │       │
│  │              │    ├──────┤      │              │       │
│  │              │    │Item N│      │              │       │
│  │              │    └──────┘      │              │       │
│  │              │     ↑            │              │       │
│  │              │   临界区保护      │              │       │
│  │              │  (portMUX_TYPE)  │              │       │
│  └──────────────┘                   └──────────────┘       │
│                                                              │
│  队列特性:                                                   │
│  ✓ FIFO 先进先出                                            │
│  ✓ 线程安全 (内部自旋锁)                                     │
│  ✓ 支持阻塞/超时                                             │
│  ✓ 拷贝语义 (值传递,非指针)                                  │
│  ✓ 默认上限: 32768 个队列                                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.2 传感器数据队列工程实现

#include "freertos/queue.h"

/* 传感器数据结构 */
typedef struct {
    uint8_t  sensor_id;
    uint8_t  sensor_type;
    int16_t  temperature;       /* 0.1°C 精度 */
    uint16_t humidity;          /* 0.1% 精度 */
    uint32_t pressure;          /* Pa */
    uint32_t timestamp;         /* 毫秒时间戳 */
    uint16_t crc;               /* 校验 */
} sensor_packet_t;

/* 队列配置 */
#define SENSOR_QUEUE_LEN        20
#define SENSOR_QUEUE_ITEM_SIZE sizeof(sensor_packet_t)

static QueueHandle_t g_sensor_queue = NULL;

/**
 * @brief 初始化传感器数据队列
 */
esp_err_t init_sensor_queue(void)
{
    /* 创建队列 */
    g_sensor_queue = xQueueCreate(SENSOR_QUEUE_LEN, SENSOR_QUEUE_ITEM_SIZE);
    if (g_sensor_queue == NULL) {
        ESP_LOGE(TAG, "传感器队列创建失败!");
        return ESP_ERR_NO_MEM;
    }

    /* 注册队列名称便于调试 */
    vQueueAddToMap(g_sensor_queue, "sensor_queue");

    ESP_LOGI(TAG, "传感器队列创建成功: len=%d, item_size=%d",
             SENSOR_QUEUE_LEN, SENSOR_QUEUE_ITEM_SIZE);
    return ESP_OK;
}

/* ===== CPU 1: 传感器采集任务(生产者)===== */
void sensor_reader_task(void *arg)
{
    sensor_packet_t packet;
    uint32_t seq = 0;

    ESP_LOGI(TAG, "[CPU1] 传感器采集任务启动");

    while (1) {
        /* 1. 读取传感器 */
        if (read_bme280(&packet.temperature, &packet.humidity,
                        &packet.pressure) == ESP_OK) {
            packet.sensor_id   = BME280_ADDR;
            packet.sensor_type  = SENSOR_TYPE_ENV;
            packet.timestamp   = esp_timer_get_time() / 1000;
            packet.crc         = calculate_crc16(&packet, sizeof(packet) - 2);
            seq++;

            /* 2. 发送到队列 (带超时) */
            BaseType_t ret = xQueueSend(g_sensor_queue, &packet,
                                        pdMS_TO_TICKS(100));
            if (ret != pdPASS) {
                ESP_LOGW(TAG, "[CPU1] 队列发送失败, seq=%lu, 队列可能已满",
                         (unsigned long)seq);

                /* 队列满时丢弃最旧数据,保证最新数据入队 */
                sensor_packet_t dummy;
                xQueueReceive(g_sensor_queue, &dummy, 0);
                xQueueSend(g_sensor_queue, &packet, 0);
            }
        }

        vTaskDelay(pdMS_TO_TICKS(500)); /* 2Hz采样 */
    }
}

/* ===== CPU 0: 数据处理与上传任务(消费者)===== */
void data_processor_task(void *arg)
{
    sensor_packet_t packet;

    ESP_LOGI(TAG, "[CPU0] 数据处理任务启动");

    while (1) {
        /* 阻塞等待队列数据,超时10秒 */
        BaseType_t ret = xQueueReceive(g_sensor_queue, &packet,
                                       pdMS_TO_TICKS(10000));
        if (ret == pdPASS) {
            /* 1. CRC校验 */
            uint16_t calc_crc = calculate_crc16(&packet, sizeof(packet) - 2);
            if (calc_crc != packet.crc) {
                ESP_LOGW(TAG, "[CPU0] CRC校验失败: calc=0x%04X, recv=0x%04X",
                         calc_crc, packet.crc);
                continue;
            }

            /* 2. 数据处理 */
            float temp_c = packet.temperature / 10.0f;
            float hum_pct = packet.humidity / 10.0f;

            ESP_LOGI(TAG, "[CPU0] 数据: temp=%.1f°C, hum=%.1f%%, press=%luPa",
                     temp_c, hum_pct, (unsigned long)packet.pressure);

            /* 3. 上传到云平台 */
            upload_sensor_data(&packet);
        } else {
            /* 超时未收到数据,可能传感器采集任务异常 */
            ESP_LOGW(TAG, "[CPU0] 队列接收超时,检查传感器任务状态");
        }
    }
}

2.3 多生产者队列模式

在实际项目中,多个传感器任务可能同时向一个队列写入数据:

/* 多生产者场景:不同传感器共享一个队列 */
void multi_sensor_task(void *arg)
{
    uint8_t sensor_id = (uint8_t)(uintptr_t)arg;
    sensor_packet_t packet;

    while (1) {
        packet.sensor_id = sensor_id;
        packet.timestamp = esp_timer_get_time() / 1000;

        /* 根据传感器类型读取不同数据 */
        switch (sensor_id) {
        case SENSOR_BME280:
            read_bme280(&packet.temperature, &packet.humidity, &packet.pressure);
            packet.sensor_type = SENSOR_TYPE_ENV;
            break;
        case SENSOR_SGP30:
            read_sgp30(&packet.co2, &packet.tvoc);
            packet.sensor_type = SENSOR_TYPE_AIR;
            break;
        case SENSOR_DS18B20:
            read_ds18b20(&packet.temperature);
            packet.sensor_type = SENSOR_TYPE_TEMP;
            break;
        }

        packet.crc = calculate_crc16(&packet, sizeof(packet) - 2);

        /* 队列发送时使用队列集(Queue Set)避免优先级反转 */
        xQueueSend(g_sensor_queue, &packet, pdMS_TO_TICKS(50));

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

三、共享内存:高性能场景的双刃剑

3.1 共享内存的性能优势

消息队列采用拷贝语义,每次发送都会复制数据。对于大数据量传输(如音频流、图像数据),拷贝开销显著。共享内存通过传递指针避免了数据拷贝,但需要开发者自行管理同步。

┌──────────────────────────────────────────────────────────────┐
│        消息队列 vs 共享内存 性能对比                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─── 消息队列 (Queue) ────────────────────────────────┐   │
│  │                                                      │   │
│  │  发送方                    接收方                    │   │
│  │  ┌─────────┐              ┌─────────┐               │   │
│  │  │ 数据拷贝 │  → Queue →  │ 数据拷贝 │               │   │
│  │  │ (写复制) │              │ (读复制) │               │   │
│  │  └─────────┘              └─────────┘               │   │
│  │                                                      │   │
│  │  优点: 线程安全, 无需手动加锁                        │   │
│  │  缺点: 两次内存拷贝, 大数据性能差                    │   │
│  │  适用: 小数据(<512B), 控制指令                       │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─── 共享内存 (Shared Memory) ────────────────────────┐   │
│  │                                                      │   │
│  │  发送方                    接收方                    │   │
│  │  ┌─────────┐              ┌─────────┐               │   │
│  │  │ 写入     │  → 指针 →  │ 读取     │               │   │
│  │  │ 共享区   │              │ 共享区   │               │   │
│  │  └────┬────┘              └────┬────┘               │   │
│  │       │    ┌──────────┐         │                    │   │
│  │       └───►│ 互斥锁   │◄────────┘                    │   │
│  │            └──────────┘                              │   │
│  │                                                      │   │
│  │  优点: 零拷贝, 大数据性能优                          │   │
│  │  缺点: 需手动同步, 竞争风险高                         │   │
│  │  适用: 大数据(>4KB), 音频/图像流                     │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.2 安全共享内存实现

#include "freertos/semphr.h"
#include "string.h"

/* 共享内存缓冲区管理 */
#define SHARED_BUF_SIZE     8192
#define SHARED_BUF_COUNT    4   /* 双缓冲 x 2组 */

typedef struct {
    uint8_t  data[SHARED_BUF_SIZE];
    size_t   valid_len;
    bool     ready;          /* true: 数据已就绪可读 */
} shared_buffer_t;

typedef struct {
    shared_buffer_t    buffers[SHARED_BUF_COUNT];
    SemaphoreHandle_t  mutex;        /* 互斥锁保护缓冲区管理 */
    SemaphoreHandle_t  data_ready;   /* 二值信号量通知数据就绪 */
    uint8_t            write_idx;   /* 当前写入缓冲区索引 */
    uint8_t            read_idx;    /* 当前读取缓冲区索引 */
} shared_mem_manager_t;

static shared_mem_manager_t g_shared_mem;

/**
 * @brief 初始化共享内存管理器
 */
esp_err_t init_shared_memory(void)
{
    memset(&g_shared_mem, 0, sizeof(g_shared_mem));

    /* 互斥锁:保护缓冲区索引和状态 */
    g_shared_mem.mutex = xSemaphoreCreateMutex();
    /* 二值信号量:通知接收方有数据可读 */
    g_shared_mem.data_ready = xSemaphoreCreateBinary();

    if (g_shared_mem.mutex == NULL || g_shared_mem.data_ready == NULL) {
        ESP_LOGE(TAG, "共享内存信号量创建失败");
        return ESP_ERR_NO_MEM;
    }

    ESP_LOGI(TAG, "共享内存初始化完成: buf_size=%d, buf_count=%d",
             SHARED_BUF_SIZE, SHARED_BUF_COUNT);
    return ESP_OK;
}

/**
 * @brief 写入共享数据(CPU 1 调用)
 * @param data 数据指针
 * @param len 数据长度
 * @return 实际写入长度
 */
size_t write_shared_data(const uint8_t *data, size_t len)
{
    if (len > SHARED_BUF_SIZE) {
        len = SHARED_BUF_SIZE;
    }

    /* 加锁获取写缓冲区索引 */
    xSemaphoreTake(g_shared_mem.mutex, portMAX_DELAY);
    uint8_t widx = g_shared_mem.write_idx;
    xSemaphoreGive(g_shared_mem.mutex);

    /* 写入数据到缓冲区 */
    memcpy(g_shared_mem.buffers[widx].data, data, len);
    g_shared_mem.buffers[widx].valid_len = len;
    g_shared_mem.buffers[widx].ready = true;

    /* 切换写缓冲区索引(环形缓冲) */
    xSemaphoreTake(g_shared_mem.mutex, portMAX_DELAY);
    g_shared_mem.write_idx = (widx + 1) % SHARED_BUF_COUNT;
    xSemaphoreGive(g_shared_mem.mutex);

    /* 通知接收方有数据可读 */
    xSemaphoreGive(g_shared_mem.data_ready);

    ESP_LOGD(TAG, "共享内存写入: idx=%d, len=%d", widx, len);
    return len;
}

/**
 * @brief 读取共享数据(CPU 0 调用)
 * @param out_data 输出缓冲区
 * @param max_len 最大读取长度
 * @param timeout_ms 超时时间
 * @return 实际读取长度, -1表示超时
 */
int read_shared_data(uint8_t *out_data, size_t max_len, uint32_t timeout_ms)
{
    /* 等待数据就绪信号 */
    if (xSemaphoreTake(g_shared_mem.data_ready, pdMS_TO_TICKS(timeout_ms)) != pdPASS) {
        return -1; /* 超时 */
    }

    /* 加锁获取读缓冲区索引 */
    xSemaphoreTake(g_shared_mem.mutex, portMAX_DELAY);
    uint8_t ridx = g_shared_mem.read_idx;

    /* 找到就绪的缓冲区 */
    while (!g_shared_mem.buffers[ridx].ready) {
        ridx = (ridx + 1) % SHARED_BUF_COUNT;
        if (ridx == g_shared_mem.read_idx) {
            xSemaphoreGive(g_shared_mem.mutex);
            return -1; /* 没有就绪的缓冲区 */
        }
    }

    size_t copy_len = g_shared_mem.buffers[ridx].valid_len;
    if (copy_len > max_len) {
        copy_len = max_len;
    }

    /* 拷贝数据 */
    memcpy(out_data, g_shared_mem.buffers[ridx].data, copy_len);

    /* 标记已读 */
    g_shared_mem.buffers[ridx].ready = false;
    g_shared_mem.read_idx = (ridx + 1) % SHARED_BUF_COUNT;
    xSemaphoreGive(g_shared_mem.mutex);

    ESP_LOGD(TAG, "共享内存读取: idx=%d, len=%d", ridx, copy_len);
    return (int)copy_len;
}

3.3 共享内存典型陷阱

/* ❌ 危险示例:无锁访问共享变量 */
static volatile uint32_t g_shared_counter = 0;

void unsafe_increment_task(void *arg)
{
    while (1) {
        /* 两个核心同时执行这一行可能导致计数丢失 */
        g_shared_counter++;  /* 非原子操作! */
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

/* ✅ 安全做法1:使用互斥锁 */
static portMUX_TYPE g_counter_lock = portMUX_INITIALIZER_UNLOCKED;
static uint32_t g_shared_counter_safe = 0;

void safe_increment_task(void *arg)
{
    while (1) {
        taskENTER_CRITICAL(&g_counter_lock);
        g_shared_counter_safe++;
        taskEXIT_CRITICAL(&g_counter_lock);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

/* ✅ 安全做法2:使用原子操作 */
#include "atom.h"  /* 或使用 stdatomic.h */
static _Atomic uint32_t g_atomic_counter = 0;

void atomic_increment_task(void *arg)
{
    while (1) {
        atomic_fetch_add(&g_atomic_counter, 1);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

/* ✅ 安全做法3:使用队列传递值 */
void queue_increment_task(void *arg)
{
    uint32_t value = 1;
    while (1) {
        xQueueSend(g_counter_queue, &value, 0);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

四、任务通知:轻量级跨核信号

4.1 任务通知机制

FreeRTOS v8.2+引入的任务通知(Task Notification)是最轻量的跨核通信方式。它不需要创建独立的内核对象(如队列或信号量),直接利用任务控制块(TCB)中的通知字段。

/* 任务通知 vs 队列/信号量对比 */
/*
 * ┌──────────────┬──────────────┬──────────────┬──────────────┐
 * │     特性     │  任务通知    │   消息队列   │   信号量     │
 * ├──────────────┼──────────────┼──────────────┼──────────────┤
 * │ 内存开销     │ 0 (内嵌TCB) │ ~80+字节     │ ~40+字节     │
 * │ 速度         │ 最快        │ 快           │ 快           │
 * │ 数据传递     │ 32位值      │ 任意大小     │ 无           │
 * │ 多接收方     │ 不支持      │ 支持         │ 支持         │
 * │ 超时支持     │ 支持        │ 支持         │ 支持         │
 * │ 适用场景     │ 一对一信号  │ 多对多数据   │ 资源计数     │
 * └──────────────┴──────────────┴──────────────┴──────────────┘
 */

/* 任务通知示例:事件触发机制 */
#define NOTIFY_BIT_SENSOR_READY  (1 << 0)
#define NOTIFY_BIT_NETWORK_UP    (1 << 1)
#define NOTIFY_BIT_OTA_READY     (1 << 2)
#define NOTIFY_BIT_ERROR        (1 << 3)

static TaskHandle_t g_main_task_handle = NULL;

/* 事件触发方(任意核心) */
void trigger_event(uint32_t event_bit)
{
    if (g_main_task_handle != NULL) {
        /* 设置通知位 */
        xTaskNotify(g_main_task_handle, event_bit, eSetBits);
    }
}

/* 事件接收方(主任务) */
void event_handler_task(void *arg)
{
    g_main_task_handle = xTaskGetCurrentTaskHandle();
    uint32_t notify_value = 0;

    ESP_LOGI(TAG, "事件处理任务启动, handle=%p", g_main_task_handle);

    while (1) {
        /* 阻塞等待通知,超时5秒 */
        BaseType_t ret = xTaskNotifyWait(0, 0xFFFFFFFF, &notify_value,
                                          pdMS_TO_TICKS(5000));
        if (ret == pdPASS) {
            /* 处理各事件位 */
            if (notify_value & NOTIFY_BIT_SENSOR_READY) {
                ESP_LOGI(TAG, "事件: 传感器就绪");
                handle_sensor_ready();
            }
            if (notify_value & NOTIFY_BIT_NETWORK_UP) {
                ESP_LOGI(TAG, "事件: 网络连接");
                handle_network_up();
            }
            if (notify_value & NOTIFY_BIT_OTA_READY) {
                ESP_LOGI(TAG, "事件: OTA就绪");
                handle_ota_ready();
            }
            if (notify_value & NOTIFY_BIT_ERROR) {
                ESP_LOGE(TAG, "事件: 系统错误");
                handle_system_error();
            }
        } else {
            /* 超时:执行周期性检查 */
            ESP_LOGD(TAG, "通知等待超时, 执行周期检查");
            periodic_health_check();
        }
    }
}

4.2 任务通知的邮箱模式

任务通知还支持"邮箱"模式,可以传递一个32位值:

/* 邮箱模式:传递命令值 */
typedef enum {
    CMD_RESTART = 1,
    CMD_FACTORY_RESET = 2,
    CMD_ENTER_OTA = 3,
    CMD_CHANGE_MODE = 4,
} system_command_t;

/* CPU 1发送命令到CPU 0 */
void send_system_command(system_command_t cmd)
{
    /* eSetValueWithOverwrite: 覆盖当前通知值 */
    xTaskNotify(g_main_task_handle, (uint32_t)cmd, eSetValueWithOverwrite);
}

/* CPU 0接收并处理命令 */
void command_processor_task(void *arg)
{
    uint32_t cmd = 0;

    while (1) {
        if (xTaskNotifyWait(0, 0, &cmd, portMAX_DELAY) == pdPASS) {
            ESP_LOGI(TAG, "收到命令: %lu", (unsigned long)cmd);

            switch (cmd) {
            case CMD_RESTART:
                ESP_LOGW(TAG, "执行重启命令");
                vTaskDelay(pdMS_TO_TICKS(500));
                esp_restart();
                break;
            case CMD_FACTORY_RESET:
                ESP_LOGW(TAG, "执行恢复出厂设置");
                nvs_flash_erase();
                esp_restart();
                break;
            case CMD_ENTER_OTA:
                ESP_LOGI(TAG, "进入OTA模式");
                start_ota_process();
                break;
            case CMD_CHANGE_MODE:
                ESP_LOGI(TAG, "切换运行模式");
                switch_operation_mode();
                break;
            default:
                ESP_LOGW(TAG, "未知命令: %lu", (unsigned long)cmd);
                break;
            }
        }
    }
}

五、流缓冲区与消息缓冲区

5.1 Stream Buffer:连续字节流传输

FreeRTOS v10+引入的Stream Buffer和Message Buffer特别适合音频流和大数据传输场景。Stream Buffer是连续字节流,Message Buffer是变长消息。

#include "freertos/stream_buffer.h"

/* 音频数据流传输 */
#define AUDIO_STREAM_BUF_SIZE   8192
#define AUDIO_STREAM_TRIG_LEVEL 1024  /* 触发接收的水位线 */

static StreamBufferHandle_t g_audio_stream = NULL;

/* 初始化音频流缓冲区 */
void init_audio_stream(void)
{
    g_audio_stream = xStreamBufferCreate(AUDIO_STREAM_BUF_SIZE,
                                          AUDIO_STREAM_TRIG_LEVEL);
    if (g_audio_stream == NULL) {
        ESP_LOGE(TAG, "音频流缓冲区创建失败");
        return;
    }
    ESP_LOGI(TAG, "音频流缓冲区创建: size=%d, trigger=%d",
             AUDIO_STREAM_BUF_SIZE, AUDIO_STREAM_TRIG_LEVEL);
}

/* CPU 1: I2S音频采集 → 写入流缓冲区 */
void audio_capture_task(void *arg)
{
    uint8_t i2s_buf[1024];
    size_t bytes_read;

    while (1) {
        /* 从I2S读取音频数据 */
        bytes_read = i2s_read(I2S_NUM_0, i2s_buf, sizeof(i2s_buf),
                              portMAX_DELAY);
        if (bytes_read > 0) {
            /* 写入流缓冲区(非阻塞,满则丢弃) */
            size_t sent = xStreamBufferSend(g_audio_stream, i2s_buf,
                                             bytes_read, 0);
            if (sent < bytes_read) {
                ESP_LOGW(TAG, "音频缓冲区溢出: 丢弃 %d bytes",
                         (int)(bytes_read - sent));
            }
        }
    }
}

/* CPU 0: 从流缓冲区读取 → 编码上传 */
void audio_encode_task(void *arg)
{
    uint8_t read_buf[2048];

    while (1) {
        /* 阻塞直到触发水位线 */
        size_t received = xStreamBufferReceive(g_audio_stream, read_buf,
                                                sizeof(read_buf),
                                                portMAX_DELAY);
        if (received > 0) {
            /* Opus编码并上传 */
            encode_and_upload_audio(read_buf, received);
        }
    }
}

5.2 Message Buffer:变长消息传输

#include "freertos/message_buffer.h"

#define MSG_BUF_SIZE  4096

static MessageBufferHandle_t g_msg_buffer = NULL;

/* 变长消息结构 */
typedef struct {
    uint8_t  msg_type;
    uint8_t  priority;
    uint16_t payload_len;
    uint8_t  payload[];  /* 柔性数组 */
} flex_message_t;

/* CPU 1: 发送变长消息 */
void send_variable_message(uint8_t type, const uint8_t *payload, size_t len)
{
    /* 构造消息头 */
    uint8_t buf[256];
    if (len + 4 > sizeof(buf)) {
        ESP_LOGE(TAG, "消息过长: %d", (int)len);
        return;
    }

    buf[0] = type;
    buf[1] = 0;  /* priority */
    buf[2] = (len >> 8) & 0xFF;
    buf[3] = len & 0xFF;
    memcpy(&buf[4], payload, len);

    size_t total_len = 4 + len;
    size_t sent = xMessageBufferSend(g_msg_buffer, buf, total_len,
                                      pdMS_TO_TICKS(100));
    if (sent != total_len) {
        ESP_LOGW(TAG, "消息发送不完整: sent=%d, expected=%d",
                 (int)sent, (int)total_len);
    }
}

/* CPU 0: 接收变长消息 */
void message_receiver_task(void *arg)
{
    uint8_t buf[256];

    while (1) {
        size_t received = xMessageBufferReceive(g_msg_buffer, buf,
                                                 sizeof(buf),
                                                 portMAX_DELAY);
        if (received >= 4) {
            uint8_t type = buf[0];
            uint16_t payload_len = (buf[2] << 8) | buf[3];

            ESP_LOGI(TAG, "收到消息: type=%d, payload_len=%d",
                     type, payload_len);

            process_message(type, &buf[4], payload_len);
        }
    }
}

六、工程选型决策框架

6.1 通信机制选型决策树

┌──────────────────────────────────────────────────────────────┐
│              双核通信机制选型决策树                            │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  开始: 需要在CPU0和CPU1之间传递数据/信号?                    │
│                    │                                         │
│                    ▼                                         │
│  ┌── 需要传递数据还是仅信号? ──────────────────────┐       │
│  │                                                   │       │
│  │  仅信号(事件/通知)         数据传输               │       │
│  │       │                        │                  │       │
│  │       ▼                        ▼                  │       │
│  │  ┌─────────┐         ┌── 数据大小? ──┐          │       │
│  │  │ 任务通知 │         │                │          │       │
│  │  │ (最轻量) │         ▼                ▼          │       │
│  │  └─────────┘    < 512B           > 4KB             │       │
│  │                     │                │              │       │
│  │                     ▼                ▼              │       │
│  │               ┌──────────┐   ┌── 数据特征? ──┐  │       │
│  │               │ 消息队列  │   │                 │   │       │
│  │               │ (Queue)  │   ▼                 ▼   │       │
│  │               └──────────┘  定长消息    连续字节流   │       │
│  │                              │              │         │       │
│  │                              ▼              ▼         │       │
│  │                        ┌──────────┐  ┌──────────┐    │       │
│  │                        │ 消息缓冲 │  │ 流缓冲区 │    │       │
│  │                        │ (MsgBuf) │  │(StreamBuf)│   │       │
│  │                        └──────────┘  └──────────┘    │       │
│  │                                                        │       │
│  │  特殊场景: 大数据零拷贝 → 共享内存 + 互斥锁           │       │
│  │                                                        │       │
│  └────────────────────────────────────────────────────────┘       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

6.2 性能基准测试

/* 通信机制性能基准测试 */
#define BENCH_ITERATIONS 10000
#define BENCH_DATA_SIZE  64

typedef struct {
    const char *name;
    uint32_t    total_us;
    uint32_t    per_op_us;
} bench_result_t;

/* 消息队列基准 */
bench_result_t bench_queue(QueueHandle_t q)
{
    uint8_t data[BENCH_DATA_SIZE] = {0};
    uint32_t start = esp_timer_get_time();

    for (int i = 0; i < BENCH_ITERATIONS; i++) {
        xQueueSend(q, data, 0);
        xQueueReceive(q, data, 0);
    }

    uint32_t elapsed = esp_timer_get_time() - start;
    return (bench_result_t){
        .name = "Queue",
        .total_us = elapsed,
        .per_op_us = elapsed / BENCH_ITERATIONS,
    };
}

/* 任务通知基准 */
bench_result_t bench_task_notify(TaskHandle_t task)
{
    uint32_t start = esp_timer_get_time();

    for (int i = 0; i < BENCH_ITERATIONS; i++) {
        xTaskNotifyGive(task);
        uint32_t val = 0;
        xTaskNotifyWait(0, 0, &val, 0);
    }

    uint32_t elapsed = esp_timer_get_time() - start;
    return (bench_result_t){
        .name = "TaskNotify",
        .total_us = elapsed,
        .per_op_us = elapsed / BENCH_ITERATIONS,
    };
}

/* 共享内存基准 */
bench_result_t bench_shared_mem(void)
{
    static uint8_t shared_buf[BENCH_DATA_SIZE];
    uint8_t local_buf[BENCH_DATA_SIZE] = {0};
    uint32_t start = esp_timer_get_time();

    for (int i = 0; i < BENCH_ITERATIONS; i++) {
        taskENTER_CRITICAL(&g_counter_lock);
        memcpy(shared_buf, local_buf, BENCH_DATA_SIZE);
        taskEXIT_CRITICAL(&g_counter_lock);
    }

    uint32_t elapsed = esp_timer_get_time() - start;
    return (bench_result_t){
        .name = "SharedMem",
        .total_us = elapsed,
        .per_op_us = elapsed / BENCH_ITERATIONS,
    };
}

/* 运行所有基准测试 */
void run_all_benchmarks(void)
{
    ESP_LOGI(TAG, "====== 双核通信性能基准测试 ======");
    ESP_LOGI(TAG, "迭代次数: %d, 数据大小: %d bytes", BENCH_ITERATIONS, BENCH_DATA_SIZE);

    QueueHandle_t test_q = xQueueCreate(10, BENCH_DATA_SIZE);
    TaskHandle_t current = xTaskGetCurrentTaskHandle();

    bench_result_t results[3];
    results[0] = bench_queue(test_q);
    results[1] = bench_task_notify(current);
    results[2] = bench_shared_mem();

    for (int i = 0; i < 3; i++) {
        ESP_LOGI(TAG, "%-12s: 总耗时 %lu us, 单次 %lu us",
                 results[i].name,
                 (unsigned long)results[i].total_us,
                 (unsigned long)results[i].per_op_us);
    }

    vQueueDelete(test_q);
}

七、实际工程案例:双核物联网网关

7.1 网关架构设计

我们将上述通信机制综合应用于一个实际的物联网网关项目中:

┌──────────────────────────────────────────────────────────────┐
│           双核物联网网关通信架构                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─── CPU 0 (PRO_CPU) ────────────────────────────────┐    │
│  │                                                      │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐         │   │
│  │  │WiFi管理  │  │MQTT客户端│  │HTTP服务   │         │   │
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘         │   │
│  │       │              │              │                │   │
│  │  ┌────▼──────────────▼──────────────▼─────┐        │   │
│  │  │         网络通信管理器                    │        │   │
│  │  └────────────────────┬─────────────────────┘        │   │
│  │                       │                               │   │
│  │  ┌─────────────────────▼──────────────────────┐     │   │
│  │  │  命令处理任务                                │     │   │
│  │  │  ← xQueueReceive(cmd_queue)                 │     │   │
│  │  │  ← xTaskNotifyWait(event_bits)              │     │   │
│  │  └─────────────────────┬──────────────────────┘     │   │
│  │                        │                              │   │
│  └────────────────────────┼──────────────────────────────┘   │
│                           │                                   │
│              ┌────────────┼──────────────┐                   │
│              │            │              │                    │
│         ┌────▼────┐ ┌────▼────┐ ┌──────▼──────┐             │
│         │sensor   │ │cmd      │ │audio        │             │
│         │queue    │ │queue    │ │stream buf   │             │
│         │(64B x20)│ │(32B x10)│ │(8KB)        │             │
│         └────┬────┘ └────┬────┘ └──────┬──────┘             │
│              │          ▲              │                      │
│              │          │              │                      │
│  ┌───────────┼──────────┼──────────────┼─────────────────┐  │
│  │           │          │              │                  │  │
│  │  ┌────────▼───┐ ┌───┴──────┐ ┌─────▼──────┐         │  │
│  │  │传感器采集  │ │传感器控制 │ │音频采集     │         │  │
│  │  │任务       │ │任务       │ │任务(I2S)   │         │  │
│  │  └────────────┘ └───────────┘ └────────────┘         │  │
│  │                                                      │  │
│  └─── CPU 1 (APP_CPU) ────────────────────────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

7.2 网关通信代码实现

/* 网关通信管理器:统一管理所有跨核通道 */
typedef struct {
    QueueHandle_t       sensor_queue;     /* 传感器数据队列 */
    QueueHandle_t       cmd_queue;        /* 命令队列 */
    StreamBufferHandle_t audio_stream;    /* 音频流缓冲 */
    SemaphoreHandle_t   shared_mutex;     /* 共享数据互斥 */
    TaskHandle_t        cmd_handler_task; /* 命令处理任务句柄 */
} gateway_comm_t;

static gateway_comm_t g_comm;

/* 网关通信初始化 */
esp_err_t gateway_comm_init(void)
{
    /* 传感器数据队列:20个64字节数据包 */
    g_comm.sensor_queue = xQueueCreate(20, sizeof(sensor_packet_t));

    /* 命令队列:10个32位命令 */
    g_comm.cmd_queue = xQueueCreate(10, sizeof(uint32_t));

    /* 音频流缓冲:8KB, 触发水位1KB */
    g_comm.audio_stream = xStreamBufferCreate(8192, 1024);

    /* 共享互斥锁 */
    g_comm.shared_mutex = xSemaphoreCreateMutex();

    if (!g_comm.sensor_queue || !g_comm.cmd_queue ||
        !g_comm.audio_stream || !g_comm.shared_mutex) {
        ESP_LOGE(TAG, "网关通信初始化失败");
        return ESP_ERR_NO_MEM;
    }

    ESP_LOGI(TAG, "网关通信通道初始化完成");
    ESP_LOGI(TAG, "  sensor_queue: %p", g_comm.sensor_queue);
    ESP_LOGI(TAG, "  cmd_queue: %p", g_comm.cmd_queue);
    ESP_LOGI(TAG, "  audio_stream: %p", g_comm.audio_stream);

    return ESP_OK;
}

/* CPU 1传感器任务:采集数据 → sensor_queue → CPU0处理 */
void gateway_sensor_task(void *arg)
{
    sensor_packet_t pkt;
    memset(&pkt, 0, sizeof(pkt));

    while (1) {
        /* 读取多个传感器 */
        read_temperature(&pkt.temperature);
        read_humidity(&pkt.humidity);
        read_pressure(&pkt.pressure);
        pkt.timestamp = esp_timer_get_time() / 1000;

        /* 发送到队列 */
        if (xQueueSend(g_comm.sensor_queue, &pkt, pdMS_TO_TICKS(100)) != pdPASS) {
            ESP_LOGW(TAG, "[CPU1] 传感器队列满");
        }

        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

/* CPU 0命令处理任务:从cmd_queue接收命令 */
void gateway_cmd_handler_task(void *arg)
{
    uint32_t cmd;
    g_comm.cmd_handler_task = xTaskGetCurrentTaskHandle();

    while (1) {
        if (xQueueReceive(g_comm.cmd_queue, &cmd, portMAX_DELAY) == pdPASS) {
            ESP_LOGI(TAG, "[CPU0] 处理命令: 0x%08lX", (unsigned long)cmd);

            /* 解析命令类型 */
            uint8_t cmd_type = (cmd >> 24) & 0xFF;
            uint32_t cmd_param = cmd & 0x00FFFFFF;

            switch (cmd_type) {
            case 0x01: /* 读取传感器 */
                handle_read_sensor_cmd(cmd_param);
                break;
            case 0x02: /* 配置参数 */
                handle_config_cmd(cmd_param);
                break;
            case 0x03: /* 重启设备 */
                esp_restart();
                break;
            default:
                ESP_LOGW(TAG, "未知命令类型: 0x%02X", cmd_type);
                break;
            }
        }
    }
}

八、调试与性能分析

8.1 跨核通信调试技巧

/* 队列状态监控 */
void monitor_queue_status(void)
{
    /* 获取队列当前状态 */
    UBaseType_t sensor_count = uxQueueMessagesWaiting(g_comm.sensor_queue);
    UBaseType_t cmd_count = uxQueueMessagesWaiting(g_comm.cmd_queue);
    size_t audio_bytes = xStreamBufferBytesAvailable(g_comm.audio_stream);

    ESP_LOGI(TAG, "队列状态: sensor=%u/%d, cmd=%u/%d, audio=%u/%d bytes",
             sensor_count, 20,
             cmd_count, 10,
             (unsigned)audio_bytes, 8192);

    /* 检查队列水印(最大使用量) */
    UBaseType_t sensor_high = uxQueueGetHighWaterMark2(g_comm.sensor_queue);
    ESP_LOGD(TAG, "传感器队列最高水位: %u/%d", sensor_high, 20);
}

/* CPU使用率监控 */
void monitor_cpu_usage(void)
{
    TaskStatus_t *task_array;
    UBaseType_t task_count = uxTaskGetNumberOfTasks();
    uint32_t total_runtime;

    task_array = pvPortMalloc(task_count * sizeof(TaskStatus_t));
    if (task_array == NULL) return;

    task_count = uxTaskGetSystemState(task_array, task_count, &total_runtime);

    ESP_LOGI(TAG, "=== 任务CPU使用率 (总运行时间: %lu) ===",
             (unsigned long)total_runtime);
    for (int i = 0; i < task_count; i++) {
        float cpu_pct = (float)task_array[i].ulRunTimeCounter /
                        total_runtime * 100.0f;
        if (cpu_pct > 0.1f) {
            ESP_LOGI(TAG, "  %-16s Core%d 优先级%d CPU:%.1f%% 剩余栈:%u",
                     task_array[i].pcTaskName,
                     task_array[i].xCoreID,
                     task_array[i].uxCurrentPriority,
                     cpu_pct,
                     task_array[i].usStackHighWaterMark);
        }
    }

    vPortFree(task_array);
}

九、工程选型总结与最佳实践

经过大量项目实践,我们总结出以下ESP32双核通信最佳实践:

选型原则:优先使用消息队列,它是线程安全、适用面最广的方案。只有在性能不满足时才考虑共享内存。任务通知适合简单的一对一事件信号场景,它的开销最小但灵活性也最低。

数据大小分界:以512字节为分界线。小于512字节的数据直接用消息队列;大于4KB的连续流数据用Stream Buffer;介于两者之间的变长消息用Message Buffer。零拷贝共享内存只用于音频/视频等对延迟极度敏感的场景。

核心分配原则:WiFi/蓝牙相关任务必须在CPU0,实时性要求高的任务放在CPU1。避免在同一核心上放置多个高优先级CPU密集型任务。

调试建议:开发阶段始终开启队列监控和CPU使用率统计。使用ESP32工具箱V2.0可以可视化查看所有队列的状态和消息流。在硬件层面,随身WiFi硬件调试工具(hardware.czkree.com)支持双核独立串口监控,能同时观察两个核心的日志输出。当设备接入物联网平台后,通信异常和队列溢出事件会自动上报,便于远程诊断。

双核通信是ESP32工程开发中的核心难点,正确选型和规范使用直接决定了系统的稳定性和性能上限。希望本文的实践经验能帮助开发者少走弯路。


作者:沧州虎王科技技术团队
标签:物联网、嵌入式、ESP32
产品推荐:ESP32工具箱V2.0 | 随身WiFi硬件调试工具(hardware.czkree.com) | 物联网平台

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐