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

ESP32-S3 内置双核 Xtensa LX7,主频高达 240MHz。默认情况下,FreeRTOS 调度器会将任务动态分配到两个核心上,WiFi 协议栈、蓝牙、TCP/IP 等系统任务与用户任务共享 CPU 时间。

对于电机控制、数字电源、传感器融合等硬实时场景,这种动态调度会带来两个致命问题:

  • 抖动大:WiFi 中断和协议栈任务随时可能抢占 CPU,导致控制周期从 100us 抖动到 500us 以上。
  • 优先级反转:即使控制任务优先级设为最高,WiFi 任务仍可能在其他核心上运行并争抢总线、Cache 和内存带宽。

解决方案:利用 FreeRTOS 的任务亲和力(Core Affinity),把 WiFi 协议栈钉在 Core0,把实时控制任务钉在 Core1,实现物理隔离。

二、原理:ESP32-S3 双核与 FreeRTOS 亲和力

2.1 核心编号与默认行为

  • PRO_CPU_NUM(Core0):协议栈 CPU,默认运行 WiFi、蓝牙、TCP/IP 等任务。
  • APP_CPU_NUM(Core1):应用 CPU,默认运行 app_main 及用户任务。

但“默认”并不等于“固定”。FreeRTOS 的 xTaskCreatePinnedToCore() 才是真正的绑定手段。

2.2 任务亲和力 API

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pvTaskCode,
    const char *pcName,
    uint32_t usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *pxCreatedTask,
    BaseType_t xCoreID   // 0 或 1,或 tskNO_AFFINITY
);
  • xCoreID = 0:任务只在 Core0 运行。
  • xCoreID = 1:任务只在 Core1 运行。
  • tskNO_AFFINITY:允许在两个核心间迁移。

三、配置步骤:把 WiFi 钉在 Core0,控制钉在 Core1

3.1 步骤 1:确认 WiFi 任务的核心归属

ESP-IDF 默认将 WiFi 任务创建在 Core0。可通过 menuconfig 确认:

Component config → Wi-Fi → [*] WiFi Task Core ID (Core 0)

若使用蓝牙,同样在 Component config → Bluetooth → Bluedroid Options → Bluetooth task core 中选择 Core0。

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

#define CONTROL_TASK_PRIO   10
#define CONTROL_TASK_STACK  4096

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

    while (1) {
        // 1. 读取传感器
        int adc = adc1_get_raw(ADC1_CHANNEL_0);
        // 2. 执行控制算法(PID 等)
        float duty = pid_calc(adc);
        // 3. 更新 PWM
        ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
        ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);

        vTaskDelayUntil(&last_wake, period);
    }
}

void app_main(void)
{
    // 初始化外设...
    xTaskCreatePinnedToCore(
        control_task,
        "ctrl_task",
        CONTROL_TASK_STACK,
        NULL,
        CONTROL_TASK_PRIO,
        NULL,
        1   // 绑定到 Core1
    );
}

3.3 步骤 3:将 WiFi 初始化和事件处理也绑定到 Core0

WiFi 事件回调默认在 Core0 的 wifi 任务中执行。若在回调中做耗时操作,建议再创建一个专用任务并绑定 Core0:

void wifi_event_task(void *arg)
{
    // 处理 WiFi 事件,如重连、MQTT 发布
    while (1) {
        // 等待事件组或队列
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

xTaskCreatePinnedToCore(wifi_event_task, "wifi_evt", 4096, NULL, 5, NULL, 0);

3.4 步骤 4:关闭不必要的核心迁移

menuconfig 中关闭 FreeRTOS → Enable task snapshot 等调试功能,减少调度开销。同时确保 CONFIG_FREERTOS_UNICOREn,即双核模式。

四、完整代码示例:双核隔离的 WiFi + 控制工程

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

#define CONTROL_PERIOD_MS   1
#define CONTROL_TASK_PRIO   10

static void control_task(void *arg)
{
    TickType_t last = xTaskGetTickCount();
    while (1) {
        int raw = adc1_get_raw(ADC1_CHANNEL_0);
        uint32_t duty = (raw * 1023) / 4095;
        ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
        ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
        vTaskDelayUntil(&last, pdMS_TO_TICKS(CONTROL_PERIOD_MS));
    }
}

static void wifi_event_handler(void *arg, esp_event_base_t base,
                               int32_t id, void *data)
{
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    }
}

void app_main(void)
{
    // 1. NVS 初始化
    nvs_flash_init();

    // 2. 外设初始化
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
    ledc_timer_config_t timer = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .timer_num = LEDC_TIMER_0,
        .freq_hz = 20000,
        .clk_cfg = LEDC_AUTO_CLK
    };
    ledc_timer_config(&timer);
    ledc_channel_config_t ch = {
        .gpio_num = 4,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL_0,
        .timer_sel = LEDC_TIMER_0,
        .duty = 0
    };
    ledc_channel_config(&ch);

    // 3. WiFi 初始化(默认在 Core0)
    esp_netif_init();
    esp_event_loop_create_default();
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL);
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

    // 4. 创建控制任务并绑定 Core1
    xTaskCreatePinnedToCore(control_task, "ctrl", 4096, NULL,
                            CONTROL_TASK_PRIO, NULL, 1);
}

五、注意事项与调优建议

  • 栈大小:控制任务建议 4KB 以上,WiFi 事件任务 4KB 以上,避免栈溢出。
  • 优先级:控制任务优先级应高于 WiFi 任务(通常 WiFi 任务优先级为 23),但不要超过 configMAX_PRIORITIES - 1
  • 中断分配:ESP32-S3 的外设中断可绑定到指定核心,使用 esp_intr_alloc() 时指定 ESP_INTR_FLAG_IRAM 和核心编号。
  • Cache 争抢:双核共享 Cache,若控制任务对时间敏感,可将关键代码放入 IRAM(IRAM_ATTR)。
  • 看门狗:Core1 上的控制任务若长时间不喂狗,会触发任务看门狗,需在循环中调用 vTaskDelay 或喂狗。
  • 调试:使用 xTaskGetAffinity() 确认任务实际运行的核心,使用 esp_timer 测量控制周期抖动。

通过以上配置,WiFi 协议栈固定在 Core0,实时控制任务固定在 Core1,两者物理隔离,控制周期抖动可控制在 10us 以内,满足大多数嵌入式实时控制需求。