一、为什么需要双核任务分配?

ESP32-S3 内置双核 Xtensa LX7,主频高达 240 MHz,并集成 2.4 GHz Wi-Fi 与蓝牙 5。默认情况下,Arduino 或 ESP-IDF 的 loopTask 与 Wi-Fi 协议栈任务(如 wifi、bt)可能运行在同一个核心上。当 Wi-Fi 进行扫描、连接或大量数据收发时,会频繁抢占 CPU,导致你的电机控制、传感器采样等实时任务出现毫秒级抖动,严重时甚至丢步、失控。

FreeRTOS 提供了 xTaskCreatePinnedToCore API,允许将任务“钉”在指定核心上运行。通过合理分配:

  • Core 0:运行 Wi-Fi/蓝牙协议栈、TCP/IP 任务、系统事件循环。
  • Core 1:运行实时控制任务(如 PID 计算、PWM 更新、编码器读取)。

这样两个核心各司其职,互不抢占,实时性大幅提升。

二、核心原理:任务亲和力与优先级

ESP32-S3 的 FreeRTOS 支持 任务亲和力(Task Affinity),即任务可以绑定到某个核心。xTaskCreatePinnedToCore 的最后一个参数 xCoreID 指定核心编号(0 或 1),若为 tskNO_AFFINITY 则允许调度器自由分配。

关键点:

  • Wi-Fi 协议栈默认运行在 Core 0(可通过 CONFIG_ESP32_WIFI_TASK_CORE_ID 配置)。
  • 实时控制任务应设置为 高优先级(如 configMAX_PRIORITIES - 2),并绑定到 Core 1。
  • 避免在实时任务中调用阻塞式 API(如 vTaskDelay 过长、WiFi.print 等),否则会破坏实时性。
  • 双核间通信建议使用 FreeRTOS 队列、信号量或环形缓冲区,而非全局变量直接访问。

三、配置步骤

3.1 环境准备

  • 使用 ESP-IDF v5.x 或 Arduino-ESP32 v2.0.14+(基于 ESP-IDF)。
  • 确保 CONFIG_FREERTOS_UNICORE 未启用(默认双核)。
  • 在 menuconfig 中检查 Component config → FreeRTOS → Run FreeRTOS only on first core 应关闭。

3.2 任务划分策略

| 任务 | 核心 | 优先级 | 说明 | |------|------|--------|------| | Wi-Fi 协议栈 | Core 0 | 默认 | 由系统自动创建 | | 网络应用任务 | Core 0 | 2 | 处理 MQTT/HTTP | | 实时控制任务 | Core 1 | 5 | PID、PWM、编码器 | | 监控任务 | Core 1 | 1 | 打印状态、低优先级 |

3.3 创建任务

使用 xTaskCreatePinnedToCore 创建实时控制任务,并绑定到 Core 1。

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

#define TAG "DUAL_CORE"

// 实时控制任务:运行在 Core 1
void realtime_control_task(void *pvParameters) {
    TickType_t last_wake = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(1); // 1ms 周期

    while (1) {
        // 1. 读取编码器(模拟)
        int encoder = 0; // 实际使用 GPIO 或 PCNT
        // 2. 计算 PID
        float output = 0.0f; // PID 计算
        // 3. 更新 PWM
        ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, (uint32_t)output);
        ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);

        // 严格周期延时,保证实时性
        vTaskDelayUntil(&last_wake, period);
    }
}

// 网络任务:运行在 Core 0
void network_task(void *pvParameters) {
    while (1) {
        // 处理 Wi-Fi 数据、MQTT 等
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void) {
    // 初始化 LEDC
    ledc_timer_config_t timer_conf = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .timer_num = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .freq_hz = 20000,
        .clk_cfg = LEDC_AUTO_CLK
    };
    ledc_timer_config(&timer_conf);

    ledc_channel_config_t ch_conf = {
        .gpio_num = 4,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL_0,
        .timer_sel = LEDC_TIMER_0,
        .duty = 0,
        .hpoint = 0
    };
    ledc_channel_config(&ch_conf);

    // 创建实时控制任务,绑定到 Core 1,优先级 5
    xTaskCreatePinnedToCore(
        realtime_control_task,
        "rt_control",
        4096,
        NULL,
        5,
        NULL,
        1  // Core 1
    );

    // 创建网络任务,绑定到 Core 0,优先级 2
    xTaskCreatePinnedToCore(
        network_task,
        "network",
        4096,
        NULL,
        2,
        NULL,
        0  // Core 0
    );

    ESP_LOGI(TAG, "Tasks created, realtime on Core 1, network on Core 0");
}

四、双核通信:队列传递数据

实时任务需要将传感器数据发送给网络任务,应使用 FreeRTOS 队列,避免共享内存竞争。

QueueHandle_t sensor_queue;

typedef struct {
    int encoder;
    float setpoint;
} sensor_msg_t;

// 在 app_main 中创建队列
sensor_queue = xQueueCreate(10, sizeof(sensor_msg_t));

// 实时任务中发送
sensor_msg_t msg = { .encoder = encoder, .setpoint = 100.0f };
if (xQueueSend(sensor_queue, &msg, 0) != pdTRUE) {
    // 队列满,丢弃或处理
}

// 网络任务中接收
sensor_msg_t rx_msg;
if (xQueueReceive(sensor_queue, &rx_msg, portMAX_DELAY) == pdTRUE) {
    // 打包发送到 MQTT
}

五、注意事项与避坑指南

  • 不要将 Wi-Fi 任务绑定到 Core 1:Wi-Fi 协议栈默认在 Core 0,若强行迁移可能导致性能下降或崩溃。
  • 实时任务避免调用 printf/ESP_LOG:串口输出是阻塞的,会破坏 1ms 周期。建议用环形缓冲区暂存日志,由低优先级任务输出。
  • 优先级设置:实时任务优先级应高于网络任务,但不要超过 configMAX_PRIORITIES - 1(通常为 24)。
  • 栈大小:实时任务栈建议 4096 字节以上,若使用浮点运算或递归,需适当增加。
  • 看门狗:若实时任务长时间不喂狗,需在 menuconfig 中调整 Task Watchdog 或添加 esp_task_wdt_reset()。
  • 双核启动顺序:app_main 默认运行在 Core 0,创建任务后可通过 xPortGetCoreID() 验证任务所在核心。
  • Arduino 环境:若使用 Arduino,loopTask 默认在 Core 1,Wi-Fi 在 Core 0,可直接在 setup() 中创建绑定 Core 1 的任务,但注意 loop() 本身也会占用 Core 1。

六、总结

通过 xTaskCreatePinnedToCore 将 Wi-Fi 协议栈与实时控制任务分配到不同核心,是 ESP32-S3 双核开发的核心技巧。合理划分任务、设置优先级、使用队列通信,可以显著提升系统的实时性与稳定性。建议在项目初期就规划好核心绑定,避免后期重构。