一、为什么你的应用任务会被 Wi-Fi 协议栈“饿死”?

ESP32-S3 内置双核 Xtensa LX7(Core 0 和 Core 1),默认情况下 FreeRTOS 会以对称多处理(SMP)方式调度任务,即任何任务都可能运行在任意核心上。但 Wi-Fi 协议栈(包括 TCP/IP、蓝牙等)在 ESP-IDF 中默认被固定到 Core 0,且优先级通常较高(例如 23)。

如果你的应用任务也运行在 Core 0,且优先级低于 Wi-Fi 任务,那么当 Wi-Fi 有大量数据收发时,Wi-Fi 任务会持续抢占 CPU,导致应用任务长时间得不到调度——这就是“饿死”现象。典型症状:

  • 串口打印卡顿、看门狗复位
  • 用户按键响应延迟超过 1 秒
  • 传感器采样周期严重抖动

解决方案很简单:将应用任务固定到 Core 1,让 Wi-Fi 协议栈独占 Core 0。

二、FreeRTOS 双核调度核心原理

  • xTaskCreate:创建的任务没有核心亲和性,调度器可将其分配到任意核心。
  • xTaskCreatePinnedToCore:额外指定 xCoreID 参数,将任务永久绑定到指定核心(0 或 1)。
  • 优先级:数值越大优先级越高。Wi-Fi 任务优先级通常为 23,应用任务建议设为 5~10。
  • 空闲任务:每个核心都有一个空闲任务(优先级 0),用于清理和低功耗。

关键原则:不要让高优先级任务与 Wi-Fi 共享同一核心,否则低优先级任务永远无法运行。

三、配置步骤:将 Wi-Fi 与应用任务分离

3.1 确认 Wi-Fi 任务的核心绑定

ESP-IDF 默认将 Wi-Fi 任务绑定到 Core 0。你可以通过 menuconfig 检查:

Component config → Wi-Fi → [*] Wi-Fi Task Core Affinity → Core 0

通常保持默认即可。

3.2 创建应用任务并固定到 Core 1

使用 xTaskCreatePinnedToCore,最后一个参数传入 1(Core 1)。

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "APP_TASK";

// 应用任务:模拟传感器采集与处理
void app_task(void *pvParameters) {
    TickType_t last_wake = xTaskGetTickCount();
    while (1) {
        // 读取传感器(此处用打印模拟)
        ESP_LOGI(TAG, "App task running on core %d", xPortGetCoreID());
        // 模拟耗时处理
        vTaskDelay(pdMS_TO_TICKS(100));
        // 固定周期执行
        vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(500));
    }
}

void app_main(void) {
    // 初始化 Wi-Fi(省略具体代码,仅示意)
    // wifi_init_sta();

    // 创建应用任务,固定到 Core 1,优先级 5,栈 4096
    xTaskCreatePinnedToCore(
        app_task,          // 任务函数
        "app_task",        // 任务名称
        4096,              // 栈大小(字节)
        NULL,              // 参数
        5,                 // 优先级
        NULL,              // 任务句柄
        1                  // 核心 ID:1 表示 Core 1
    );

    ESP_LOGI(TAG, "app_main running on core %d", xPortGetCoreID());
}

3.3 验证任务核心绑定

运行后串口输出应显示:

I (1234) APP_TASK: App task running on core 1
I (1234) APP_TASK: app_main running on core 0

如果应用任务仍显示 core 0,检查是否误用了 xTaskCreate 或核心 ID 写错。

四、完整实战代码:Wi-Fi 扫描 + 应用任务并行

以下代码创建一个 Wi-Fi 扫描任务(Core 0)和一个 LED 闪烁任务(Core 1),两者互不干扰。

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

#define LED_GPIO 2
static const char *TAG = "DUAL_CORE";

// Wi-Fi 扫描任务(固定 Core 0)
void wifi_scan_task(void *pv) {
    wifi_scan_config_t scan_cfg = {
        .ssid = NULL,
        .bssid = NULL,
        .channel = 0,
        .show_hidden = true
    };
    while (1) {
        ESP_LOGI(TAG, "Wi-Fi scan start on core %d", xPortGetCoreID());
        esp_wifi_scan_start(&scan_cfg, true);
        uint16_t ap_count = 0;
        esp_wifi_scan_get_ap_num(&ap_count);
        ESP_LOGI(TAG, "Found %d APs", ap_count);
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

// LED 闪烁任务(固定 Core 1)
void led_task(void *pv) {
    gpio_reset_pin(LED_GPIO);
    gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
    while (1) {
        gpio_set_level(LED_GPIO, 1);
        vTaskDelay(pdMS_TO_TICKS(500));
        gpio_set_level(LED_GPIO, 0);
        vTaskDelay(pdMS_TO_TICKS(500));
        ESP_LOGI(TAG, "LED toggle on core %d", xPortGetCoreID());
    }
}

void app_main(void) {
    // 初始化 NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 初始化 Wi-Fi
    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));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 创建 Wi-Fi 扫描任务,固定 Core 0
    xTaskCreatePinnedToCore(wifi_scan_task, "wifi_scan", 4096, NULL, 5, NULL, 0);
    // 创建 LED 任务,固定 Core 1
    xTaskCreatePinnedToCore(led_task, "led_task", 2048, NULL, 5, NULL, 1);

    ESP_LOGI(TAG, "app_main on core %d", xPortGetCoreID());
}

五、注意事项与避坑指南

  • 核心 ID 只能为 0 或 1,传入其他值会导致断言失败。
  • 不要将高优先级任务与 Wi-Fi 放在同一核心,否则低优先级任务可能永远得不到执行。
  • 栈大小要充足:Wi-Fi 相关任务建议 4096 字节以上,普通任务 2048 字节。
  • 使用 vTaskDelayUntil 实现精确周期,避免 vTaskDelay 累积误差。
  • 调试时用 xPortGetCoreID() 打印当前核心,确认绑定生效。
  • 若必须使用 xTaskCreate,可通过 xTaskCreatePinnedToCore 的封装或 vTaskCoreAffinitySet 动态修改亲和性。
  • 注意中断分配:ESP32-S3 的中断默认在 Core 0 处理,高频率中断可能影响 Wi-Fi 性能,必要时用 esp_intr_alloc 指定核心。

六、总结

通过 xTaskCreatePinnedToCore 将 Wi-Fi 协议栈与应用任务物理隔离到不同核心,是解决任务饿死问题最直接有效的方法。记住:Core 0 留给 Wi-Fi/蓝牙,Core 1 跑你的应用逻辑。配合合理的优先级和栈大小,你的 ESP32-S3 将同时拥有流畅的网络通信和实时的任务响应。