为什么需要双核任务分配

ESP32-S3 内置两个 Xtensa LX7 核心,主频最高 240 MHz。默认的 Arduino/ESP-IDF 工程中,loopTask 和 WiFi 协议栈任务并没有严格绑定核心,FreeRTOS 调度器可能把实时控制任务调度到正在处理 WiFi 中断的 Core0 上,导致控制周期抖动甚至超时。

典型症状:

  • 电机 PWM 更新周期从 1 ms 抖动到 5 ms 以上
  • WiFi 吞吐量一高,控制环就丢步
  • vTaskDelayUntil 的实际唤醒时间偏差超过 20%

解决思路很直接:把 WiFi 协议栈钉在 Core0,把实时控制任务钉在 Core1,让两个核心各司其职。

双核架构与 FreeRTOS 调度原理

ESP32-S3 的两个核心是对称的(SMP),但 ESP-IDF 在启动时做了非对称初始化:

  • Core0(PRO_CPU):负责启动流程、WiFi/BT 协议栈、大部分系统中断
  • Core1(APP_CPU):默认运行 app_main 之后创建的应用任务

WiFi 协议栈任务(如 wifi、esp_timer、ipc0)在 ESP-IDF 中默认绑定 Core0,优先级较高(通常 18~23)。如果你的控制任务没有显式绑定核心,调度器可能把它放到 Core0,与 WiFi 任务竞争。

关键 API:

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pvTaskCode,
    const char *pcName,
    uint32_t usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *pvCreatedTask,
    BaseType_t xCoreID   // 0 或 1,或 tskNO_AFFINITY
);

xCoreID 设为 0 表示绑定 Core0,设为 1 表示绑定 Core1。

配置步骤

步骤 1:确认 IDF 版本与核心数

idf.py --version   # 建议 ESP-IDF v5.0 及以上

在 menuconfig 中确认:

  • Component config → FreeRTOS → Run FreeRTOS only on first core 保持 关闭(否则只有单核可用)
  • Component config → ESP System Settings → CPU frequency 设为 240 MHz

步骤 2:创建绑定 Core1 的实时控制任务

控制任务优先级要高于 WiFi 任务,但不要高到阻塞系统任务。建议优先级 10~15。

#define CONTROL_TASK_PRIO   12
#define CONTROL_TASK_STACK  4096

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

    while (1) {
        // 读取传感器、执行 PID、更新 PWM
        pid_update();
        pwm_set_duty(pid_output);

        // 使用 vTaskDelayUntil 保证周期稳定
        vTaskDelayUntil(&last_wake, period);
    }
}

void app_main(void)
{
    // 绑定到 Core1,优先级 12
    xTaskCreatePinnedToCore(
        control_task, "control",
        CONTROL_TASK_STACK, NULL,
        CONTROL_TASK_PRIO, NULL, 1);

    wifi_init_sta(); // WiFi 协议栈默认在 Core0
}

步骤 3:确保 WiFi 协议栈留在 Core0

ESP-IDF 的 WiFi 任务默认绑定 Core0,但 esp_event 回调可能在任意核心执行。建议在事件回调中只做入队操作,把耗时处理交给 Core1 的任务。

static QueueHandle_t wifi_evt_queue;

static void wifi_event_handler(void *arg, esp_event_base_t base,
                               int32_t id, void *data)
{
    // 只入队,不处理
    xQueueSendFromISR(wifi_evt_queue, &id, NULL);
}

static void wifi_proc_task(void *arg)
{
    int32_t id;
    while (1) {
        if (xQueueReceive(wifi_evt_queue, &id, portMAX_DELAY)) {
            // 处理 WiFi 事件,可绑定 Core0
        }
    }
}

步骤 4:验证核心绑定

void print_core_id(void)
{
    printf("Task running on core %d\n", xPortGetCoreID());
}

在控制任务和 WiFi 任务中分别调用,确认输出为 1 和 0。

完整代码示例

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

#define CONTROL_TASK_PRIO   12
#define CONTROL_TASK_STACK  4096

static void control_task(void *arg)
{
    TickType_t last_wake = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(1);

    while (1) {
        // 实时控制逻辑
        pid_update();
        pwm_set_duty(pid_output);
        vTaskDelayUntil(&last_wake, period);
    }
}

static void wifi_init_sta(void)
{
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);

    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                        &wifi_event_handler, NULL, NULL);

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "your_ssid",
            .password = "your_password",
        },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    esp_wifi_start();
    esp_wifi_connect();
}

void app_main(void)
{
    nvs_flash_init();

    xTaskCreatePinnedToCore(control_task, "control",
                            CONTROL_TASK_STACK, NULL,
                            CONTROL_TASK_PRIO, NULL, 1);

    wifi_init_sta();
}

常见踩坑与注意事项

  • 缓存一致性:两个核心共享 L2 缓存,但外设寄存器访问需注意内存屏障。使用 portENTER_CRITICAL 保护共享变量。
  • 中断分配:ESP32-S3 的中断可分配到指定核心。WiFi 中断默认在 Core0,不要手动改到 Core1,否则会破坏协议栈实时性。
  • 看门狗:Core1 上的控制任务如果长时间不喂狗,会触发任务看门狗。确保 vTaskDelayUntil 周期合理,或在任务中调用 esp_task_wdt_reset()。
  • 优先级反转:控制任务优先级不要高于 WiFi 任务太多,否则 WiFi 可能断流。建议控制任务 10~15,WiFi 任务保持默认。
  • 栈大小:控制任务栈建议 4 KB 以上,WiFi 事件处理任务栈建议 6 KB。
  • 调试输出:printf 会占用大量 CPU,调试完成后关闭或改用 ESP_LOGD。

总结

双核任务分配的核心是显式绑定核心和合理设置优先级。把 WiFi 协议栈留在 Core0,把实时控制任务用 xTaskCreatePinnedToCore 钉在 Core1,配合 vTaskDelayUntil 保证周期稳定,可以显著降低控制抖动。实际项目中还需结合中断分配、看门狗和缓存一致性做综合调优。