# 一、问题现象与初步定位 某物联网设备使用 ESP32-WROOM-32,基于 ESP-IDF v4.4 开发。设备运行一段时间后(约 1-3 小时),Wi-Fi 连接突然断开,且无法重连,重启后恢复。日志显示 `wifi: AP not found` 或 `wifi: disconnect reason 4`,但此时路由器正常。 初步排查: - 检查电源、天线、射频干扰,均正常。 - 使用 `heap_caps_get_free_size()` 监控内存,未发现泄漏。 - 开启 `CONFIG_FREERTOS_DEBUG_INTERNAL` 和 `CONFIG_FREERTOS_DEBUG_OCUPPY`,发现 `wifi_task` 和 `wifi_timer` 处于阻塞状态,但无死锁。 进一步分析:问题仅在系统负载较高时出现,且与用户任务优先级设置有关。最终定位为 **FreeRTOS 优先级反转** 导致 Wi-Fi 协议栈任务饿死。 # 二、优先级反转原理与 ESP32 双核特性 ## 2.1 优先级反转是什么? FreeRTOS 基于优先级抢占调度,高优先级任务应优先运行。但若高优先级任务等待一个被低优先级任务持有的资源(如互斥量),而低优先级任务又被中优先级任务抢占,则高优先级任务间接等待中优先级任务,形成“反转”。 经典场景: - 任务 A(高优先级)等待互斥量 M。 - 任务 B(低优先级)持有 M,但被任务 C(中优先级)抢占。 - 任务 C 运行,任务 A 和 B 都无法执行,A 被 C 阻塞,优先级反转。 ## 2.2 ESP32 双核的额外复杂性 ESP32 有两个核(PRO_CPU 和 APP_CPU),FreeRTOS 支持 SMP(对称多处理)。任务可运行在任意核上,但 Wi-Fi 协议栈(`wifi_task`、`wifi_timer`)默认绑定在 PRO_CPU(核心 0),且优先级较高(如 23)。 双核下,优先级反转可能跨核发生: - 一个核上的低优先级任务持有互斥量,另一个核上的高优先级任务等待该互斥量。 - 由于 FreeRTOS 的调度器在每个核上独立运行,若低优先级任务被本核的中优先级任务抢占,则高优先级任务在另一个核上等待,但无法唤醒低优先级任务,导致跨核饥饿。 # 三、案例复现与根因分析 ## 3.1 代码结构 用户创建了三个任务: - `task_sensor`(优先级 10):读取传感器,通过互斥量保护共享数据。 - `task_ui`(优先级 15):处理 UI 刷新,不访问互斥量,但计算量大。 - `task_network`(优先级 20):发送网络数据,需访问同一互斥量。 Wi-Fi 协议栈任务优先级为 23,但 `task_network` 优先级 20 低于 Wi-Fi 任务,却高于 `task_sensor` 和 `task_ui`。 ## 3.2 触发过程 1. `task_sensor` 获取互斥量,开始处理传感器数据(耗时较长)。 2. 此时 `task_ui` 就绪,抢占 `task_sensor`(因为优先级 15 > 10)。 3. `task_ui` 运行大量计算,持续占用 PRO_CPU。 4. `task_network` 等待互斥量,但互斥量被 `task_sensor` 持有,而 `task_sensor` 被 `task_ui` 抢占,无法释放。 5. Wi-Fi 协议栈任务需要与 `task_network` 交互(如发送数据),但 `task_network` 被阻塞,Wi-Fi 任务等待其响应,导致协议栈超时。 6. 最终 Wi-Fi 任务进入错误状态,断开连接。 ## 3.3 根因 - 互斥量未启用优先级继承(FreeRTOS 互斥量默认支持,但若使用二值信号量则无继承)。 - 任务优先级设置不当,中优先级任务 `task_ui` 干扰了低优先级任务释放资源。 - 双核调度加剧了问题:`task_ui` 可能运行在 PRO_CPU,而 `task_sensor` 被迁移到 APP_CPU,但互斥量等待链未跨核处理。 # 四、解决方案与配置步骤 ## 4.1 方案一:使用互斥量并启用优先级继承 FreeRTOS 互斥量(`xSemaphoreCreateMutex()`)自带优先级继承机制,但需确保所有共享资源使用互斥量而非二值信号量。 ```c // 创建互斥量 SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); // 获取互斥量(带超时) if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 临界区代码 xSemaphoreGive(xMutex); } ``` 优先级继承:当高优先级任务等待互斥量时,持有互斥量的低优先级任务临时提升到高优先级,直到释放。这样 `task_sensor` 在 `task_network` 等待时,优先级提升至 20,不会被 `task_ui` 抢占。 ## 4.2 方案二:调整任务优先级 重新规划优先级,确保 Wi-Fi 协议栈任务最高,其次是网络任务,再是 UI 和传感器。建议: - Wi-Fi 任务:23(系统默认) - `task_network`:22 - `task_ui`:15 - `task_sensor`:10 这样 `task_network` 高于 `task_ui`,即使 `task_sensor` 被抢占,`task_network` 也能快速获得 CPU 释放互斥量。 ## 4.3 方案三:使用互斥量与临界区结合 对于短临界区,使用 `taskENTER_CRITICAL()` 和 `taskEXIT_CRITICAL()` 关闭中断,避免调度。但注意在双核上需使用 `portENTER_CRITICAL()` 并指定核。 ```c // 在 PRO_CPU 上使用 portENTER_CRITICAL(&spinlock); // 临界区代码 portEXIT_CRITICAL(&spinlock); ``` ## 4.4 配置步骤(ESP-IDF) 1. 在 `menuconfig` 中启用互斥量优先级继承:`Component config → FreeRTOS → Kernel → Enable priority inheritance`(默认开启)。 2. 确保所有共享资源使用互斥量,而非二值信号量。 3. 设置任务亲和性:将关键任务绑定到指定核,避免跨核调度。 ```c // 创建任务时指定核 xTaskCreatePinnedToCore(task_sensor, "sensor", 4096, NULL, 10, &handle, APP_CPU); ``` # 五、完整代码示例 以下为修复后的关键代码片段。 ```c #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" SemaphoreHandle_t xMutex; void task_sensor(void *arg) { while (1) { if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 模拟传感器处理,耗时 50ms vTaskDelay(pdMS_TO_TICKS(50)); xSemaphoreGive(xMutex); } vTaskDelay(pdMS_TO_TICKS(10)); } } void task_ui(void *arg) { while (1) { // 模拟大量计算,耗时 100ms for (int i = 0; i < 100000; i++) { __asm__("nop"); } vTaskDelay(pdMS_TO_TICKS(10)); } } void task_network(void *arg) { while (1) { if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 发送网络数据 xSemaphoreGive(xMutex); } vTaskDelay(pdMS_TO_TICKS(20)); } } void app_main() { xMutex = xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(task_sensor, "sensor", 4096, NULL, 10, NULL, APP_CPU); xTaskCreatePinnedToCore(task_ui, "ui", 4096, NULL, 15, NULL, PRO_CPU); xTaskCreatePinnedToCore(task_network, "network", 4096, NULL, 22, NULL, APP_CPU); } ``` # 六、调试与验证 ## 6.1 使用 FreeRTOS 跟踪 - 开启 `CONFIG_FREERTOS_USE_TRACE_FACILITY`,使用 `vTaskList()` 查看任务状态。 - 观察任务阻塞原因:若 `task_network` 长时间处于 `Blocked` 且等待互斥量,则可能反转。 ## 6.2 使用优先级继承测试 在互斥量获取前后打印任务优先级,验证继承是否生效。 ```c UBaseType_t prio_before = uxTaskPriorityGet(NULL); xSemaphoreTake(xMutex, portMAX_DELAY); UBaseType_t prio_after = uxTaskPriorityGet(NULL); printf("Priority: %u -> %u\n", prio_before, prio_after); ``` ## 6.3 压力测试 连续运行 24 小时,观察 Wi-Fi 连接稳定性。修复后,问题不再复现。 # 七、注意事项 - 互斥量优先级继承只对互斥量有效,二值信号量无此机制,切勿混用。 - 双核下,任务亲和性影响调度,建议将 Wi-Fi 相关任务固定到 PRO_CPU,用户任务分散到 APP_CPU。 - 避免在临界区中调用阻塞 API(如 `vTaskDelay`),否则会导致系统卡死。 - 优先级设置需遵循“资源持有时间短、优先级高”的原则,但不要高于 Wi-Fi 协议栈任务(23),以免干扰协议栈。 - 若使用 ESP-IDF 的 `esp_event` 或 `esp_netif`,它们内部可能使用互斥量,需确保用户任务不长时间持有这些锁。 # 八、总结 优先级反转是 FreeRTOS 的经典陷阱,在 ESP32 双核环境下更隐蔽。通过使用互斥量、合理设置优先级、绑定任务核,可有效避免 Wi-Fi 协议栈卡死。建议在开发初期就遵循 FreeRTOS 最佳实践,并利用调试工具验证调度行为。