一、为什么需要双核解耦?

ESP32-S3 内置双核 Xtensa LX7,主频高达 240 MHz,同时集成 2.4 GHz Wi-Fi 与 Bluetooth 5 (LE)。默认情况下,ESP-IDF 的 FreeRTOS 调度器允许任务在任意核心上运行,而 Wi-Fi 协议栈任务(如 wifi、btController)通常运行在 Core 0。当用户创建一个高优先级实时控制任务(如电机 PWM 更新、传感器采样)时,它可能被调度到 Core 0,与 Wi-Fi 任务竞争 CPU,导致控制周期抖动甚至丢步。

解耦的核心思想:将 Wi-Fi 协议栈固定到 Core 0,把实时控制任务固定到 Core 1,并通过合理的优先级与中断分配,让两个核心各司其职。

二、FreeRTOS 双核调度原理

ESP-IDF 使用对称多处理(SMP)模式的 FreeRTOS,每个核心独立运行调度器,但共享同一份任务就绪列表。关键 API:

  • xTaskCreatePinnedToCore():创建任务时指定核心亲和性(0 或 1)。
  • xTaskGetAffinity():查询任务当前绑定的核心。
  • vTaskCoreAffinitySet():动态修改任务核心亲和性。
  • xPortGetCoreID():获取当前运行的核心编号。

默认情况下,CONFIG_FREERTOS_UNICORE 为 n,即双核模式。Wi-Fi 任务由 ESP-IDF 内部创建,通常已绑定到 Core 0(可通过 CONFIG_ESP32_WIFI_TASK_CORE_ID 配置)。

三、配置步骤

3.1 固定 Wi-Fi 协议栈到 Core 0

在 menuconfig 中设置:

Component config → Wi-Fi → WiFi Task Core ID → Core 0
Component config → Bluetooth → Bluedroid Options → BT Controller Task Core ID → Core 0

对应 sdkconfig 中的宏:

CONFIG_ESP32_WIFI_TASK_CORE_ID=0
CONFIG_BT_CTRL_PINNED_TO_CORE=0

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

使用 xTaskCreatePinnedToCore(),将控制任务固定到 Core 1,并设置较高优先级(例如 10,高于 Wi-Fi 任务的 5~7)。

3.3 中断分配

ESP32-S3 的外设中断默认可能分配到任意核心。对于实时控制相关的中断(如定时器、GPIO),建议通过 esp_intr_alloc() 指定核心:

esp_intr_alloc(ETS_TG0_T0_LEVEL_INTR_SOURCE, 0, timer_isr, NULL, NULL);

最后一个参数为 intr_alloc_flags,其中 ESP_INTR_FLAG_IRAM 可让 ISR 在 IRAM 中执行,减少 Flash 访问延迟。

四、完整代码示例

以下代码演示:Core 0 运行 Wi-Fi 连接,Core 1 运行 1 kHz 实时控制循环,通过队列传递控制命令。

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "driver/gptimer.h"

#define CONTROL_PERIOD_US 1000  // 1 kHz

static const char *TAG = "DUAL_CORE";
static QueueHandle_t cmd_queue;

typedef struct {
    float setpoint;
} control_cmd_t;

// 实时控制任务:绑定到 Core 1
static void control_task(void *arg)
{
    control_cmd_t cmd;
    int64_t next_wake = esp_timer_get_time();

    while (1) {
        // 精确周期控制
        next_wake += CONTROL_PERIOD_US;
        int64_t now = esp_timer_get_time();
        if (next_wake > now) {
            esp_rom_delay_us(next_wake - now);
        } else {
            next_wake = now; // 防止累积误差
        }

        // 非阻塞读取命令
        if (xQueueReceive(cmd_queue, &cmd, 0) == pdTRUE) {
            // 更新控制参数
            ESP_DRAM_LOGI(TAG, "Core %d: setpoint=%.2f", xPortGetCoreID(), cmd.setpoint);
        }

        // 模拟实时控制:读取传感器、计算 PID、更新 PWM
        // ...
    }
}

// Wi-Fi 事件处理:运行在 Core 0
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();
    } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        ESP_LOGI(TAG, "Core %d: got IP", xPortGetCoreID());
    }
}

void app_main(void)
{
    // 1. 创建命令队列
    cmd_queue = xQueueCreate(10, sizeof(control_cmd_t));

    // 2. 初始化 Wi-Fi(默认在 Core 0)
    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));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                               &wifi_event_handler, NULL));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 3. 创建实时控制任务,绑定到 Core 1,优先级 10
    xTaskCreatePinnedToCore(control_task, "control", 4096, NULL, 10, NULL, 1);

    // 4. 主任务(app_main)默认在 Core 0,可继续处理网络命令
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
        control_cmd_t cmd = { .setpoint = 42.0f };
        xQueueSend(cmd_queue, &cmd, 0);
    }
}

五、注意事项

  • 优先级设置:实时控制任务优先级应高于 Wi-Fi 任务(通常 5~7),但不要高于 configMAX_PRIORITIES - 1,以免阻塞系统任务。
  • 中断延迟:Wi-Fi 中断默认在 Core 0,若控制任务在 Core 1,中断不会直接抢占控制任务,但需注意跨核中断(如 IPC)的延迟。
  • 内存访问:双核共享 PSRAM 时,需使用 heap_caps_malloc() 并指定 MALLOC_CAP_SPIRAM,避免缓存一致性问题。
  • 看门狗:实时任务若长时间不喂狗,需在 menuconfig 中调整 Task Watchdog 或调用 esp_task_wdt_reset()。
  • 调试:使用 xPortGetCoreID() 打印日志,确认任务实际运行核心;通过 vTaskCoreAffinitySet() 可动态调整。
  • 功耗:双核同时运行会增加功耗,若电池供电,可考虑动态频率调节(DFS)或让 Core 1 在空闲时进入轻睡眠。

六、总结

通过将 Wi-Fi 协议栈固定到 Core 0、实时控制任务绑定到 Core 1,并合理配置优先级与中断,ESP32-S3 可以同时满足无线通信与硬实时控制需求。实测表明,解耦后 1 kHz 控制循环的抖动可从数百微秒降至 10 微秒以内。建议在项目初期就规划好核心亲和性,避免后期重构。