# ESP32 双核模式下 FreeRTOS 任务与 WiFi 协议栈 CPU 占用冲突的排查方法 ## 引言 ESP32 集成了两个 Xtensa LX6 处理器核心(Core 0 和 Core 1),FreeRTOS 默认将 WiFi 协议栈(包括 TCP/IP 协议栈 lwIP)绑定在 Core 0 上运行,而用户任务通常运行在 Core 1 上。这种架构虽然提高了并行处理能力,但也引入了新的问题:如果用户任务设计不当(如长时间占用 CPU、优先级过高或频繁触发调度),会与 WiFi 协议栈产生 CPU 占用冲突,导致网络延迟、吞吐量下降甚至系统看门狗复位。本文将从原理出发,系统讲解排查方法。 ## 双核调度与 WiFi 协议栈的绑定机制 ### 1. FreeRTOS 双核调度原理 ESP32 的 FreeRTOS 是 Symmetric Multiprocessing (SMP) 版本,每个核心独立运行调度器,但共享全局就绪队列。任务可以通过 `xTaskCreatePinnedToCore()` 指定运行核心,若不指定,则系统自动分配。默认配置下: - **Core 0**:运行 WiFi 协议栈、TCP/IP 协议栈(lwIP)、蓝牙协议栈以及系统事件任务。 - **Core 1**:运行用户主任务(`app_main`)及大多数用户创建的任务。 ### 2. WiFi 协议栈的 CPU 占用特性 WiFi 协议栈包含多个高优先级任务(如 `wifi_task`、`ipc_task`),它们通过 IPC(Inter-Process Communication)机制与用户任务交互。当 WiFi 收发数据时,协议栈任务会占用大量 CPU 时间,尤其在吞吐量高或信号弱的情况下。若用户任务在 Core 0 上频繁抢占,会直接阻塞协议栈任务,导致丢包和重传。 ## 冲突的常见症状与根因分析 ### 常见症状 - 网络吞吐量显著下降(如从 10 Mbps 降至 1 Mbps)。 - 系统日志出现 `Task watchdog got triggered` 或 `WiFi: AP not started`。 - 任务响应延迟增加,甚至触发看门狗复位。 ### 根因分析 1. **优先级配置不当**:用户任务优先级高于 WiFi 协议栈任务(如 `WIFI_TASK_PRIORITY = 23`),导致协议栈任务饥饿。 2. **任务未绑定核心**:任务被调度到 Core 0,与协议栈竞争 CPU。 3. **长时间临界区或中断禁用**:用户代码在临界区中执行耗时操作,阻塞了协议栈的 IPC 处理。 4. **内存分配竞争**:使用 `malloc` 或 `free` 时,由于堆锁竞争,导致协议栈任务等待。 ## 排查方法 ### 1. 检查任务优先级与核心绑定 首先,查看当前任务的优先级和核心分配。在代码中打印任务信息: ```c void print_task_info() { char buffer[128]; vTaskList(buffer); ESP_LOGI("TASK", "Task List:\n%s", buffer); } ``` 调用 `vTaskList()` 会输出任务名称、状态、优先级、栈高水位线等。重点检查: - 用户任务的优先级是否高于 `WIFI_TASK_PRIORITY`(通常为 23)。 - 用户任务是否被绑定到 Core 0(`xCoreID` 为 0)。 **解决方案**:将用户任务绑定到 Core 1,并降低优先级至 5-10 之间。 ```c xTaskCreatePinnedToCore(task_func, "user_task", 4096, NULL, 5, &task_handle, 1); ``` ### 2. 监控 CPU 占用率 使用 ESP-IDF 提供的 `esp_timer` 和 `vTaskGetRunTimeStats()` 统计各任务 CPU 占用率。 ```c void monitor_cpu() { TaskStatus_t *task_array; UBaseType_t task_count = uxTaskGetNumberOfTasks(); task_array = calloc(task_count, sizeof(TaskStatus_t)); uxTaskGetSystemState(task_array, task_count, NULL); for (int i = 0; i < task_count; i++) { ESP_LOGI("CPU", "Task: %s, CPU%%: %lu", task_array[i].pcTaskName, task_array[i].ulRunTimeCounter / (configTICK_RATE_HZ / 100)); } free(task_array); } ``` 在运行高负载网络测试时,周期性调用此函数,观察 WiFi 协议栈任务(如 `wifi_task`)的 CPU 占用率是否异常低(<10%),而用户任务占用率极高(>90%)。若如此,则说明冲突存在。 ### 3. 使用性能计数器定位中断与临界区 ESP32 的 `esp_intr_alloc` 和 `portENTER_CRITICAL` 可能引入长阻塞。使用 `esp_timer` 测量临界区耗时: ```c portMUX_TYPE my_mux = portMUX_INITIALIZER_UNLOCKED; int64_t start = esp_timer_get_time(); portENTER_CRITICAL(&my_mux); // 临界区代码 portEXIT_CRITICAL(&my_mux); int64_t elapsed = esp_timer_get_time() - start; if (elapsed > 1000) { // 超过 1ms ESP_LOGW("CRIT", "Critical section took %lld us", elapsed); } ``` 如果发现临界区耗时过长,应优化代码,避免在临界区中进行复杂计算或外设访问。 ### 4. 调整 FreeRTOS 调度策略 在 `menuconfig` 中,可以启用 `CONFIG_FREERTOS_TIME_SLICING` 或 `CONFIG_FREERTOS_HZ` 调整时间片。但更有效的是使用 `vTaskDelay()` 主动让出 CPU,避免忙等。 ```c // 避免忙等 while (flag == 0) { vTaskDelay(pdMS_TO_TICKS(10)); } ``` ### 5. 使用任务通知替代全局变量同步 任务间通信时,避免使用全局变量加锁,改用 `xTaskNotify` 或队列,减少锁竞争。 ```c // 发送通知 xTaskNotifyGive(task_handle); // 接收通知 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ``` ## 完整代码示例 以下是一个完整的示例,演示如何正确创建任务并监控 CPU 占用: ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_wifi.h" static const char *TAG = "CPU_CONFLICT"; void user_task(void *arg) { while (1) { // 模拟工作负载 for (int i = 0; i < 10000; i++) { __asm__ __volatile__("nop"); } vTaskDelay(pdMS_TO_TICKS(10)); } } void monitor_task(void *arg) { while (1) { TaskStatus_t *task_array; UBaseType_t task_count = uxTaskGetNumberOfTasks(); task_array = calloc(task_count, sizeof(TaskStatus_t)); uxTaskGetSystemState(task_array, task_count, NULL); for (int i = 0; i < task_count; i++) { if (task_array[i].eCurrentState == eRunning || task_array[i].eCurrentState == eReady) { ESP_LOGI(TAG, "Task: %s, Priority: %u, CPU%%: %lu", task_array[i].pcTaskName, task_array[i].uxCurrentPriority, task_array[i].ulRunTimeCounter / (configTICK_RATE_HZ / 100)); } } free(task_array); vTaskDelay(pdMS_TO_TICKS(5000)); } } void app_main(void) { // 初始化 WiFi(略) // 创建用户任务,绑定到 Core 1,优先级 5 xTaskCreatePinnedToCore(user_task, "user_task", 4096, NULL, 5, NULL, 1); // 创建监控任务,绑定到 Core 0,优先级 1 xTaskCreatePinnedToCore(monitor_task, "monitor_task", 4096, NULL, 1, NULL, 0); } ``` ## 注意事项 - **优先级范围**:ESP-IDF 中任务优先级 0-24,WiFi 协议栈任务优先级约为 23,用户任务建议不超过 10。 - **核心绑定**:除非必要,不要将任务绑定到 Core 0,以免干扰协议栈。 - **栈大小**:任务栈过小会导致溢出,影响系统稳定性,建议至少 2048 字节。 - **看门狗**:长时间占用 CPU 会触发任务看门狗,可在 `menuconfig` 中调整超时时间,但应优先优化代码。 - **内存分配**:使用 `heap_caps_malloc` 分配 DMA 内存时,注意指定 `MALLOC_CAP_DMA`,避免与协议栈竞争。 ## 总结 ESP32 双核模式下的 CPU 冲突问题,本质是资源竞争。通过合理设置任务优先级、绑定核心、优化临界区和通信机制,可以有效避免冲突。本文提供的排查方法(任务列表、CPU 统计、临界区测量)能快速定位问题,配合示例代码,开发者可以轻松应用于实际项目。记住:设计任务时,始终将 WiFi 协议栈视为高优先级“租户”,为其预留足够的 CPU 时间。