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

ESP32-S3 内置两个 Xtensa LX7 核心,主频高达 240 MHz,支持 FreeRTOS 对称多处理(SMP)。但在默认配置下,Wi-Fi 协议栈、蓝牙协议栈、TCP/IP 任务以及用户任务都可能被调度到任意核心。当 Wi-Fi 进行扫描、连接或大量收发数据时,协议栈任务会频繁抢占 CPU,导致运行在同一核心上的实时控制任务出现毫秒级抖动,对于步进电机、PWM 调光、数字电源等场景,这种抖动可能直接造成丢步或输出异常。

解决思路很直接:把 Wi-Fi 协议栈固定到 Core 0,把实时控制任务固定到 Core 1,让两者物理隔离,互不抢占。

原理:任务亲和力与协议栈核心绑定

FreeRTOS 提供 xTaskCreatePinnedToCore() 接口,允许创建任务时指定运行核心。ESP-IDF 中,Wi-Fi 协议栈任务默认由系统创建,但可以通过菜单配置将其绑定到指定核心。

关键配置项位于 menuconfig:

  • Component config → Wi-Fi → Wi-Fi Task Core:选择 Wi-Fi 任务运行核心,默认 Core 0。
  • Component config → FreeRTOS → Kernel → Run FreeRTOS only on first core:若勾选,则所有任务只在 Core 0 运行,失去双核意义,不要勾选。
  • Component config → ESP System Settings → CPU frequency:保持 240 MHz,避免降频影响实时性。

此外,中断分配也影响实时性。ESP32-S3 允许将外设中断绑定到指定核心,例如将控制相关定时器中断绑定到 Core 1,避免被 Wi-Fi 中断干扰。

配置步骤

  1. 打开工程配置:idf.py menuconfig。
  2. 进入 Component config → Wi-Fi,确认 Wi-Fi Task Core 为 Core 0。
  3. 进入 Component config → FreeRTOS → Kernel,确保 Run FreeRTOS only on first core 未勾选。
  4. 进入 Component config → ESP System Settings,将 CPU frequency 设为 240 MHz。
  5. 保存并退出。

完整代码示例

下面示例创建两个任务:一个运行在 Core 0 处理 Wi-Fi 事件与网络通信,另一个运行在 Core 1 执行 1 kHz 实时控制循环。

#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  GPIO_NUM_4
#define CONTROL_PERIOD_US  1000  // 1 kHz

// 实时控制任务:绑定到 Core 1
static void control_task(void *arg)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << CONTROL_GPIO),
        .mode = GPIO_MODE_OUTPUT,
    };
    gpio_config(&io_conf);

    int64_t next = esp_timer_get_time();
    while (1) {
        // 模拟实时控制:翻转 GPIO
        gpio_set_level(CONTROL_GPIO, 1);
        gpio_set_level(CONTROL_GPIO, 0);

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

// Wi-Fi 事件处理任务:绑定到 Core 0
static void wifi_event_task(void *arg)
{
    // 初始化 NVS、网络接口、事件循环
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

    // 保持任务存活,处理后续事件
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    // Wi-Fi 相关任务固定到 Core 0
    xTaskCreatePinnedToCore(wifi_event_task, "wifi_task", 4096, NULL, 5, NULL, 0);

    // 实时控制任务固定到 Core 1,优先级高于 Wi-Fi 任务
    xTaskCreatePinnedToCore(control_task, "control_task", 2048, NULL, 10, NULL, 1);
}

注意事项

  • 优先级设置:实时控制任务优先级应高于 Wi-Fi 任务,但不要高于系统关键任务(如 esp_timer),否则可能触发看门狗。
  • 栈大小:Wi-Fi 任务栈建议不低于 4096 字节,控制任务根据实际调用深度调整,避免溢出。
  • 中断绑定:若控制依赖硬件定时器,可使用 esp_intr_alloc() 指定核心,或使用 xt_set_interrupt_handler 配合核心亲和力。
  • 共享资源:双核访问同一全局变量时需加锁(互斥量或临界区),否则可能出现数据竞争。
  • 功耗管理:开启自动 Light-sleep 时,Wi-Fi 与实时控制可能冲突,建议在实时控制场景关闭省电模式。
  • 调试观察:使用 vTaskGetRunTimeStats() 或 idf.py monitor 查看各核心负载,确认分配效果。

小结

通过 xTaskCreatePinnedToCore() 和 menuconfig 中的 Wi-Fi 核心绑定,可以轻松将协议栈与实时控制隔离到不同核心。实测表明,在 Wi-Fi 持续收发数据时,Core 1 上的 1 kHz 控制循环抖动可从数百微秒降至几微秒,显著提升系统实时性。对于电机控制、数字电源、精密 PWM 等应用,这种双核分工是低成本且高效的选择。