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

ESP32-S3 内置两个 Xtensa LX7 核心,主频高达 240 MHz。默认情况下,Arduino 或 ESP-IDF 的 loopTask 与 Wi-Fi 协议栈可能运行在同一核心(通常是 Core 1),导致:

  • Wi-Fi 收发数据时产生大量中断和任务切换,抢占 CPU 时间;
  • 实时控制任务(如电机 PWM、传感器采样)出现周期性抖动,甚至错过截止时间;
  • 系统响应变慢,控制精度下降。

FreeRTOS 提供了 xTaskCreatePinnedToCore API,允许将任务“钉”在指定核心上运行。合理分配后,Wi-Fi 协议栈独占 Core 0,实时控制独占 Core 1,两者互不干扰。

二、双核调度原理简析

ESP32-S3 的两个核心是对等的,但 ESP-IDF 默认将 Wi-Fi、蓝牙、TCP/IP 等系统任务固定在 Core 0(CONFIG_ESP32_WIFI_TASK_CORE_ID=0)。用户任务若未指定核心,则由调度器自动分配。

关键点:

  • Core 0:通常运行协议栈、系统事件循环、IP 任务;
  • Core 1:通常运行 app_main 之后的用户任务、Arduino loopTask;
  • 每个核心独立调度,但共享内存和中断矩阵;
  • 中断可分配到任意核心,但 Wi-Fi 中断默认在 Core 0。

因此,最佳实践是:Wi-Fi 相关任务留在 Core 0,实时控制任务显式绑定到 Core 1。

三、配置步骤

3.1 确认 IDF 版本与目标

确保使用 ESP-IDF v4.4 或以上,目标芯片为 esp32s3。

idf.py set-target esp32s3

3.2 创建实时控制任务并绑定 Core 1

使用 xTaskCreatePinnedToCore,最后一个参数指定核心编号。

#define CONTROL_TASK_CORE 1
#define CONTROL_TASK_PRIO 10
#define CONTROL_TASK_STACK 4096

void control_task(void *arg)
{
    TickType_t last_wake = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(1); // 1 ms 控制周期

    while (1) {
        // 读取传感器、执行 PID、更新 PWM
        // ...
        vTaskDelayUntil(&last_wake, period);
    }
}

void app_main(void)
{
    xTaskCreatePinnedToCore(
        control_task,       // 任务函数
        "control_task",     // 任务名
        CONTROL_TASK_STACK, // 栈大小(字)
        NULL,               // 参数
        CONTROL_TASK_PRIO,  // 优先级
        NULL,               // 任务句柄
        CONTROL_TASK_CORE   // 绑定到 Core 1
    );
}

3.3 确保 Wi-Fi 协议栈在 Core 0

在 menuconfig 中检查:

Component config → Wi-Fi → WiFi Task Core ID → 0
Component config → ESP System Settings → System event task core → 0

若使用 Arduino 框架,可在 platformio.ini 或 sdkconfig 中设置:

CONFIG_ESP32_WIFI_TASK_CORE_ID=0
CONFIG_ESP32_BT_TASK_CORE_ID=0

3.4 创建低优先级通信任务

Wi-Fi 数据解析、MQTT 发布等任务可放在 Core 0,优先级低于控制任务。

void comm_task(void *arg)
{
    while (1) {
        // 处理网络数据、上报状态
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

xTaskCreatePinnedToCore(comm_task, "comm_task", 4096, NULL, 5, NULL, 0);

四、完整代码示例

以下代码演示双核解耦:Core 1 运行 1 kHz 控制循环,Core 0 运行 Wi-Fi 通信任务。

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

#define CONTROL_CORE 1
#define COMM_CORE    0

static volatile float g_setpoint = 0.0f;
static volatile float g_feedback = 0.0f;

// 模拟传感器读取
static float read_sensor(void) {
    return (float)(esp_random() % 1000) / 1000.0f;
}

// 模拟 PWM 输出
static void set_pwm(float duty) {
    // 实际应操作 LEDC 或 MCPWM
    (void)duty;
}

// 实时控制任务:Core 1,优先级 10
void control_task(void *arg)
{
    TickType_t last_wake = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(1);

    while (1) {
        g_feedback = read_sensor();
        float error = g_setpoint - g_feedback;
        float duty = error * 0.5f; // 简化 P 控制
        if (duty > 1.0f) duty = 1.0f;
        if (duty < 0.0f) duty = 0.0f;
        set_pwm(duty);
        vTaskDelayUntil(&last_wake, period);
    }
}

// 通信任务:Core 0,优先级 5
void comm_task(void *arg)
{
    while (1) {
        // 模拟接收网络指令,更新设定值
        g_setpoint = 0.5f;
        // 模拟上报状态
        printf("Feedback: %.3f\n", g_feedback);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    // 初始化 NVS、Wi-Fi、事件循环
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        nvs_flash_erase();
        nvs_flash_init();
    }
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

    // 创建双核任务
    xTaskCreatePinnedToCore(control_task, "control_task", 4096, NULL, 10, NULL, CONTROL_CORE);
    xTaskCreatePinnedToCore(comm_task,    "comm_task",    4096, NULL, 5,  NULL, COMM_CORE);
}

五、注意事项

  • 优先级设置:控制任务优先级应高于通信任务,但不要高于系统关键任务(如 Wi-Fi 中断服务)。建议控制任务 8~12,通信任务 3~6。
  • 栈大小:控制任务若使用浮点运算或递归,栈至少 4 KB;通信任务若使用 JSON 解析,建议 6 KB 以上。
  • 中断分配:Wi-Fi 中断默认在 Core 0,不要手动迁移到 Core 1,否则会破坏解耦效果。
  • 共享资源保护:双核访问同一变量时,使用 portMUX_TYPE 自旋锁或原子操作,避免竞态。
  • 看门狗:长时间阻塞控制任务会触发任务看门狗,确保 vTaskDelayUntil 周期合理。
  • 调试:使用 xTaskGetCoreID() 确认任务实际运行核心,或用 idf.py monitor 查看任务列表。
  • Arduino 兼容:若使用 Arduino,loopTask 默认在 Core 1,可继续使用,但建议将 Wi-Fi 相关回调放在 Core 0。

通过以上配置,Wi-Fi 协议栈与实时控制任务在物理核心上隔离,控制周期抖动可降低至微秒级,显著提升系统实时性与稳定性。