ESP32-S3 双核任务分配实战:把 WiFi 协议栈和实时控制任务隔离开的引脚与中断绑定方法

1. 为什么需要双核隔离?

ESP32-S3 内置两个 Xtensa LX7 核心,主频高达 240 MHz。默认情况下,FreeRTOS 调度器可能将 WiFi 协议栈任务(如 wifi、esp_timer)与你的实时控制任务分配到同一个核心。WiFi 协议栈需要频繁处理射频中断、TCP/IP 协议栈和事件回调,这些操作会抢占 CPU 时间,导致控制任务出现几十甚至几百微秒的抖动。

对于电机控制、数字电源、高速采样等场景,这种抖动是不可接受的。解决方案是:将 WiFi 协议栈固定到 Core 0,将实时控制任务固定到 Core 1,并利用中断亲和性(Interrupt Affinity)将关键外设中断也绑定到 Core 1。

2. 核心原理

  • 任务亲和性:FreeRTOS 的 xTaskCreatePinnedToCore() 可将任务固定到指定核心。
  • 中断亲和性:ESP32-S3 支持将外设中断分配到指定核心,通过 esp_intr_alloc() 的 ESP_INTR_FLAG_IRAM 和 ESP_INTR_FLAG_LEVEL3 等标志,结合 esp_ipc_call() 或直接配置中断矩阵实现。
  • 引脚绑定:GPIO 中断默认由 Core 0 处理,需通过 gpio_install_isr_service() 后使用 gpio_isr_handler_add() 并配合 esp_intr_alloc() 重新分配中断源。

3. 配置步骤

3.1 固定 WiFi 协议栈到 Core 0

在 menuconfig 中设置:

Component config → ESP32-S3 Specific → CPU Core Selection → WiFi Task Core → Core 0

或在代码中初始化 WiFi 前调用:

#include "esp_wifi.h"
#include "esp_netif.h"

void app_main() {
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    cfg.nvs_enable = false;
    cfg.wifi_task_core_id = 0;  // 强制 WiFi 任务运行在 Core 0
    esp_wifi_init(&cfg);
    // ... 后续配置
}

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

#define CONTROL_TASK_CORE 1
#define CONTROL_TASK_PRIO 10

void control_task(void *arg) {
    while (1) {
        // 实时控制逻辑,例如 PID 计算、PWM 更新
        vTaskDelay(pdMS_TO_TICKS(1)); // 1ms 周期
    }
}

void app_main() {
    xTaskCreatePinnedToCore(control_task, "ctrl", 4096, NULL, CONTROL_TASK_PRIO, NULL, CONTROL_TASK_CORE);
}

3.3 将 GPIO 中断绑定到 Core 1

ESP32-S3 的 GPIO 中断默认由 Core 0 处理。要绑定到 Core 1,需使用底层中断分配 API:

#include "driver/gpio.h"
#include "esp_intr_alloc.h"

#define GPIO_INPUT_PIN 4

static void IRAM_ATTR gpio_isr_handler(void *arg) {
    // 快速中断处理,仅置位标志
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    vTaskNotifyGiveFromISR(control_task_handle, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void setup_gpio_isr_on_core1() {
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << GPIO_INPUT_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };
    gpio_config(&io_conf);

    // 安装 ISR 服务,但先不分配中断
    gpio_install_isr_service(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3);

    // 手动分配中断到 Core 1
    int intr_source = ETS_GPIO_INTR_SOURCE;
    intr_handle_t handle;
    esp_intr_alloc(intr_source, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_EDGE,
                   gpio_isr_handler, NULL, &handle);

    // 注意:esp_intr_alloc 默认分配到当前核心,需在 Core 1 上调用此函数
    // 或在 Core 0 上调用后使用 esp_ipc_call 迁移
}

关键点:esp_intr_alloc() 将中断分配到调用它的核心。因此,应在 Core 1 上执行此函数。可以创建一个临时任务固定到 Core 1 来完成初始化:

void init_on_core1(void *arg) {
    setup_gpio_isr_on_core1();
    vTaskDelete(NULL);
}

void app_main() {
    xTaskCreatePinnedToCore(init_on_core1, "init_c1", 4096, NULL, 5, NULL, 1);
}

4. 完整代码示例

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_intr_alloc.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_event.h"

#define GPIO_INPUT_PIN 4
#define CONTROL_TASK_CORE 1
#define CONTROL_TASK_PRIO 10

static TaskHandle_t control_task_handle = NULL;

static void IRAM_ATTR gpio_isr_handler(void *arg) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    vTaskNotifyGiveFromISR(control_task_handle, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void control_task(void *arg) {
    while (1) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        // 实时响应,例如读取编码器、更新 PWM
        gpio_set_level(2, !gpio_get_level(2)); // 翻转调试引脚
    }
}

void init_on_core1(void *arg) {
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << GPIO_INPUT_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };
    gpio_config(&io_conf);

    gpio_install_isr_service(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3);

    intr_handle_t handle;
    esp_intr_alloc(ETS_GPIO_INTR_SOURCE,
                   ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_EDGE,
                   gpio_isr_handler, NULL, &handle);

    vTaskDelete(NULL);
}

void app_main() {
    // 初始化 WiFi 并固定到 Core 0
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    cfg.wifi_task_core_id = 0;
    esp_wifi_init(&cfg);

    // 创建控制任务到 Core 1
    xTaskCreatePinnedToCore(control_task, "ctrl", 4096, NULL, CONTROL_TASK_PRIO, &control_task_handle, CONTROL_TASK_CORE);

    // 在 Core 1 上初始化 GPIO 中断
    xTaskCreatePinnedToCore(init_on_core1, "init_c1", 4096, NULL, 5, NULL, 1);
}

5. 注意事项

  • 中断优先级:ESP_INTR_FLAG_LEVEL3 为高优先级,但不可与 WiFi 中断冲突。WiFi 使用 Level 1,因此 Level 3 可安全抢占。
  • IRAM 安全:ISR 必须放在 IRAM 中(IRAM_ATTR),且调用的函数也需在 IRAM 中,否则 Flash 操作时可能崩溃。
  • 任务优先级:控制任务优先级应高于 WiFi 任务(通常 WiFi 任务优先级为 23),但不要超过 25,以免饿死系统任务。
  • 引脚选择:避免使用 Strapping 引脚(如 GPIO0、GPIO45)和 Flash/PSRAM 专用引脚。
  • 调试:使用 esp_intr_dump() 查看中断分配情况,确保 GPIO 中断确实在 Core 1 上。
  • 功耗:双核全速运行会增加功耗,若电池供电,可考虑动态调频。

通过以上方法,你可以将 WiFi 协议栈与实时控制任务彻底隔离,获得稳定的微秒级响应。