ESP32双核任务分配实战:用xTaskCreatePinnedToCore把WiFi协议栈与业务逻辑隔离

ESP32搭载双核Xtensa LX6,但Arduino/ESP-IDF默认将WiFi协议栈与用户loop任务都放在Core0,导致网络突发时业务代码被频繁抢占,出现毫秒级抖动。本文用xTaskCreatePinnedToCore把两者物理隔离,让业务逻辑独占Core1。

1. 原理:为什么需要绑核

  • ESP32的WiFi协议栈运行在Core0的高优先级任务中(如wifi任务优先级23),处理射频中断与协议解析,CPU占用呈突发性。
  • 默认loopTask也绑定Core0(优先级1),网络繁忙时被反复抢占,造成控制循环周期不稳。
  • xTaskCreatePinnedToCore允许指定任务运行的核心(0或1),将业务任务固定到Core1后,两核各自调度,互不抢占。
  • 注意:双核共享内存与总线,绑核解决的是调度抢占而非数据竞争,共享变量仍需临界区保护。

2. 配置步骤

  1. 确认开发环境:ESP-IDF v4.x以上或Arduino-ESP32 2.x(内核已含FreeRTOS双核支持)。
  2. 在menuconfig中保持CONFIG_FREERTOS_UNICORE=n(默认双核)。
  3. 业务任务用xTaskCreatePinnedToCore创建,最后一个参数填1(Core1)。
  4. WiFi初始化保持在Core0(默认app_main所在核),不要手动迁移协议栈任务。
  5. 核间通信使用FreeRTOS队列或事件组,避免全局变量裸访问。

3. 完整代码示例

以下基于ESP-IDF,创建两个任务:Core0负责WiFi连接与MQTT收包,Core1运行1kHz控制循环,通过队列传递传感器数据。

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

typedef struct { float temperature; uint32_t timestamp; } sensor_msg_t;

static QueueHandle_t s_sensor_queue;   // Core1 -> Core0
static QueueHandle_t s_cmd_queue;      // Core0 -> Core1

// ---------- Core1:硬实时控制任务 ----------
static void control_task(void *arg)
{
    sensor_msg_t msg;
    const TickType_t period = pdMS_TO_TICKS(1); // 1kHz
    TickType_t last_wake = xTaskGetTickCount();

    for (;;) {
        // 模拟采样与控制运算(此处替换为实际外设读取)
        msg.temperature = 25.0f + (float)(esp_random() % 100) / 100.0f;
        msg.timestamp   = xTaskGetTickCount();

        // 非阻塞发送到Core0,队列满则丢弃旧数据保证实时性
        xQueueSend(s_sensor_queue, &msg, 0);

        // 接收来自网络的命令,不阻塞
        uint32_t cmd;
        if (xQueueReceive(s_cmd_queue, &cmd, 0) == pdTRUE) {
            // 处理命令,如调整PWM占空比
        }

        vTaskDelayUntil(&last_wake, period); // 严格1ms周期
    }
}

// ---------- Core0:WiFi与网络任务 ----------
static void network_task(void *arg)
{
    sensor_msg_t msg;
    for (;;) {
        // 从队列取传感器数据(阻塞等待,不占CPU)
        if (xQueueReceive(s_sensor_queue, &msg, portMAX_DELAY) == pdTRUE) {
            // 此处调用MQTT publish或HTTP上报
            printf("Report: %.2f @%u\n", msg.temperature, msg.timestamp);
        }
        // 模拟网络命令下发
        uint32_t cmd = 1;
        xQueueSend(s_cmd_queue, &cmd, 0);
    }
}

void app_main(void)
{
    // 1. 初始化NVS与WiFi(运行在Core0)
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    // ... 配置SSID/密码并 esp_wifi_start(),省略

    // 2. 创建队列
    s_sensor_queue = xQueueCreate(8, sizeof(sensor_msg_t));
    s_cmd_queue    = xQueueCreate(4, sizeof(uint32_t));

    // 3. 业务任务绑定Core1,优先级5,栈4KB
    xTaskCreatePinnedToCore(control_task, "ctrl", 4096, NULL, 5, NULL, 1);

    // 4. 网络任务绑定Core0,优先级3,栈6KB
    xTaskCreatePinnedToCore(network_task, "net", 6144, NULL, 3, NULL, 0);
}

4. 注意事项

  • 栈大小:WiFi相关任务栈需求大,网络任务建议≥6KB;控制任务4KB足够,可用uxTaskGetStackHighWaterMark监控。
  • 优先级:业务任务优先级应低于WiFi协议栈(23),但高于空闲任务;同核任务优先级差异不宜过大,避免饥饿。
  • 队列深度:Core1到Core0的队列不宜过深,实时数据应“最新优先”,满时丢弃旧值而非阻塞。
  • 中断绑定:外设中断默认在Core0触发,若控制任务依赖高频中断,可用esp_intr_alloc指定核心,或改用轮询。
  • 看门狗:Core1长时间忙循环会触发任务看门狗,确保循环内有vTaskDelay或喂狗。
  • 调试:用vTaskGetRunTimeStats查看各核CPU占用,验证隔离效果。

通过绑核,WiFi突发不再打断控制循环,实测1kHz任务抖动从数百微秒降至十微秒级,适合电机控制、音频采样等硬实时场景。