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

ESP32-S3 内置两个 Xtensa LX7 核心(Core 0 和 Core 1),主频高达 240 MHz。默认情况下,FreeRTOS 的调度器会将任务动态分配到任意核心,而 Wi-Fi 协议栈、蓝牙协议栈以及系统任务(如 ipc0、ipc1、esp_timer)通常运行在 Core 0。如果你的实时控制任务(如电机 PWM、传感器采样)也被调度到 Core 0,就会与 Wi-Fi 中断、协议栈处理产生资源竞争,导致控制周期抖动甚至丢步。

核心思路:利用 FreeRTOS 的 xTaskCreatePinnedToCore() 将实时任务固定到 Core 1,同时通过 menuconfig 将 Wi-Fi 协议栈固定到 Core 0,实现物理隔离。

原理:核心亲和力与中断分配

  • 核心亲和力(Core Affinity):FreeRTOS 允许创建任务时指定运行核心。tskNO_AFFINITY 表示不限制,0 或 1 表示固定核心。
  • Wi-Fi 协议栈核心:ESP-IDF 中 Wi-Fi 任务默认在 Core 0,但可通过 CONFIG_ESP_WIFI_TASK_CORE_ID 显式指定。
  • 中断分配:Wi-Fi 中断默认绑定到 Core 0,可通过 esp_intr_alloc() 的 ESP_INTR_FLAG_IRAM 和核心参数调整,但一般无需手动修改。
  • IPC 机制:双核间通过 Inter-Processor Call 通信,跨核调用会有微小延迟,实时任务应避免频繁跨核同步。

配置步骤

1. 设置 Wi-Fi 协议栈核心

在项目根目录执行 idf.py menuconfig,进入:

Component config → Wi-Fi → WiFi Task Core ID

选择 Core 0。对应宏为 CONFIG_ESP_WIFI_TASK_CORE_ID=0。

同时建议关闭 Wi-Fi 省电模式以降低延迟:

Component config → Wi-Fi → WiFi Sleep → None

2. 配置 FreeRTOS 运行核心

确保 CONFIG_FREERTOS_UNICORE 为 n(默认双核)。

3. 创建固定核心的任务

使用 xTaskCreatePinnedToCore() 创建实时控制任务,指定核心 1。

完整代码示例

以下代码创建一个 1 kHz 的实时控制任务(模拟电机控制),固定到 Core 1,同时初始化 Wi-Fi 并固定到 Core 0。

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

#define CONTROL_GPIO 2
#define CONTROL_PERIOD_US 1000  // 1 kHz

// 实时控制任务:固定到 Core 1
static void realtime_control_task(void *arg)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << CONTROL_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&io_conf);

    int64_t next_wake = esp_timer_get_time();
    while (1) {
        // 模拟控制算法:翻转 GPIO
        gpio_set_level(CONTROL_GPIO, 1);
        // ... 执行控制计算 ...
        gpio_set_level(CONTROL_GPIO, 0);

        // 精确周期控制
        next_wake += CONTROL_PERIOD_US;
        int64_t now = esp_timer_get_time();
        int64_t delay = next_wake - now;
        if (delay > 0) {
            esp_rom_delay_us(delay);
        } else {
            // 超时,重置基准
            next_wake = now;
        }
    }
}

// Wi-Fi 初始化任务:固定到 Core 0
static void wifi_init_task(void *arg)
{
    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_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 连接 Wi-Fi 的代码略...
    vTaskDelete(NULL);
}

void app_main(void)
{
    // 创建实时控制任务,固定到 Core 1,优先级 10
    xTaskCreatePinnedToCore(
        realtime_control_task,
        "rt_control",
        4096,
        NULL,
        10,
        NULL,
        1  // Core 1
    );

    // 创建 Wi-Fi 初始化任务,固定到 Core 0,优先级 5
    xTaskCreatePinnedToCore(
        wifi_init_task,
        "wifi_init",
        4096,
        NULL,
        5,
        NULL,
        0  // Core 0
    );
}

注意事项与避坑指南

  • 优先级设置:实时控制任务优先级应高于 Wi-Fi 任务,但不要高于系统关键任务(如 ipc 任务,优先级通常为 24)。建议控制在 10~20 之间。
  • 中断延迟:即使任务固定核心,Wi-Fi 中断仍可能在 Core 1 上触发(如果未显式绑定)。可通过 esp_intr_alloc() 将 Wi-Fi 中断绑定到 Core 0,但需谨慎操作。
  • 跨核通信:使用 xQueueSend 等跨核通信时,会有约 1~2 µs 的 IPC 延迟,实时任务应尽量减少跨核同步。
  • 内存分配:xTaskCreatePinnedToCore 分配的任务栈默认在内部 RAM,若需外部 RAM 可使用 xTaskCreateStaticPinnedToCore 或 heap_caps_malloc。
  • 调试技巧:使用 vTaskGetRunTimeStats() 或 esp_cpu_get_cycle_count() 测量任务实际运行核心和耗时。
  • Wi-Fi 省电模式:务必关闭 WIFI_PS_MIN_MODEM 或 WIFI_PS_MAX_MODEM,否则 Wi-Fi 会周期性休眠,导致 Core 0 任务调度异常。
  • 双核启动顺序:app_main 默认运行在 Core 0,创建任务后应尽快让出 CPU,避免阻塞 Core 0 上的 Wi-Fi 初始化。

验证与测试

烧录后,使用 idf.py monitor 查看日志。可通过以下代码打印任务所在核心:

printf("Task running on core %d\n", xPortGetCoreID());

同时用示波器观察 GPIO 翻转波形,若周期稳定在 1 ms ± 5 µs 以内,说明双核隔离生效。

总结

通过 menuconfig 固定 Wi-Fi 协议栈到 Core 0,并用 xTaskCreatePinnedToCore 将实时控制任务固定到 Core 1,可以显著降低控制抖动。关键在于:合理设置优先级、避免跨核同步、关闭 Wi-Fi 省电模式。这套方法同样适用于蓝牙、以太网等协议栈与实时任务的隔离,是 ESP32-S3 嵌入式开发中提升实时性的必备技能。