ESP32-S3 双核任务分配实战:把 WiFi 协议栈和实时控制任务隔核运行的配置与踩坑记录

一、为什么需要隔核运行?

ESP32-S3 内置双核 Xtensa LX7,默认情况下 FreeRTOS 调度器允许任务在任意核心上运行。但 WiFi 协议栈任务(如 wifi、esp_timer)通常优先级较高且执行时间不确定,若与电机控制、PID 调节等实时任务共享同一核心,会导致:

  • 实时任务被抢占,控制周期抖动增大
  • 中断响应延迟不可预测
  • 严重时触发看门狗复位

隔核运行的核心思想:将 WiFi 协议栈绑定到 Core 0,将实时控制任务绑定到 Core 1,利用硬件双核实现物理隔离。

二、FreeRTOS 任务亲和力原理

ESP-IDF 基于 FreeRTOS,提供 xTaskCreatePinnedToCore() API 将任务固定到指定核心。关键参数:

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pvTaskCode,   // 任务函数
    const char *pcName,          // 任务名
    uint32_t usStackDepth,       // 栈深度(字)
    void *pvParameters,          // 参数
    UBaseType_t uxPriority,      // 优先级
    TaskHandle_t *pxCreatedTask, // 句柄
    const BaseType_t xCoreID     // 核心ID:0或1
);
  • xCoreID = 0:绑定到 Core 0(协议核)
  • xCoreID = 1:绑定到 Core 1(应用核)
  • tskNO_AFFINITY:不限制核心

注意:WiFi 协议栈任务由 ESP-IDF 自动创建,默认运行在 Core 0。我们只需确保实时任务不跑到 Core 0 即可。

三、配置步骤

3.1 确认 WiFi 任务核心

在 menuconfig 中检查:

Component config → FreeRTOS → Run FreeRTOS only on first core

不要勾选此选项,否则所有任务强制运行在 Core 0。

3.2 创建实时控制任务

#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); // 1ms 控制周期

    while (1) {
        // 读取传感器
        float adc_val = adc1_get_raw(ADC1_CHANNEL_0);
        // PID 计算
        float output = pid_calculate(&pid, adc_val);
        // 输出 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);
    }
}

void app_main(void)
{
    // 初始化 WiFi(协议栈任务自动运行在 Core 0)
    wifi_init_sta();

    // 将控制任务固定到 Core 1
    xTaskCreatePinnedToCore(
        control_task,       // 任务函数
        "control_task",     // 任务名
        CONTROL_TASK_STACK, // 栈大小
        NULL,               // 参数
        CONTROL_TASK_PRIO,  // 优先级
        NULL,               // 句柄
        1                   // 核心ID = 1
    );
}

3.3 验证任务核心

void print_task_core(void)
{
    printf("Current core: %d\n", xPortGetCoreID());
}

在 control_task 中调用,应输出 1。

四、完整代码示例

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

#define CONTROL_TASK_PRIO   10
#define CONTROL_TASK_STACK  4096

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    }
}

void wifi_init_sta(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                               &wifi_event_handler, NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "YourSSID",
            .password = "YourPassword",
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
}

void control_task(void *arg)
{
    // 配置 ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);

    // 配置 PWM
    ledc_timer_config_t ledc_timer = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .timer_num = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .freq_hz = 5000,
        .clk_cfg = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&ledc_timer);

    ledc_channel_config_t ledc_channel = {
        .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(&ledc_channel);

    TickType_t last_wake = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(1);

    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_wake, period);
    }
}

void app_main(void)
{
    wifi_init_sta();

    xTaskCreatePinnedToCore(
        control_task,
        "control_task",
        CONTROL_TASK_STACK,
        NULL,
        CONTROL_TASK_PRIO,
        NULL,
        1  // Core 1
    );
}

五、踩坑记录与注意事项

5.1 优先级反转

  • 现象:控制任务优先级设为 10,WiFi 任务优先级为 23,若控制任务在 Core 0 上运行,会被 WiFi 任务频繁抢占。
  • 解决:确保控制任务固定在 Core 1,且优先级不要超过 configMAX_PRIORITIES - 1。

5.2 缓存争用

  • 现象:双核同时访问 PSRAM 或 Flash 时,性能下降。
  • 解决:将实时任务的数据放在内部 SRAM,避免跨核缓存同步。

5.3 看门狗超时

  • 现象:控制任务长时间阻塞,触发 Task Watchdog。
  • 解决:在控制任务中定期调用 vTaskDelay 或 esp_task_wdt_reset(),或调整看门狗超时时间。

5.4 中断分配

  • 现象:WiFi 中断默认在 Core 0,但某些外设中断可能分配到 Core 1。
  • 解决:使用 esp_intr_alloc() 时指定核心,或通过 esp_ipc_call() 跨核调用。

5.5 栈大小

  • 现象:控制任务栈溢出,导致崩溃。
  • 解决:使用 uxTaskGetStackHighWaterMark() 监控栈使用,建议至少 4096 字。

六、总结

通过 xTaskCreatePinnedToCore() 将实时控制任务固定到 Core 1,WiFi 协议栈自动运行在 Core 0,可显著降低控制周期抖动。实际项目中还需注意优先级设置、缓存一致性、看门狗配置等细节。建议在开发初期就使用 xPortGetCoreID() 验证任务核心,避免后期调试困难。