# ESP32 双核下 FreeRTOS 任务优先级反转导致 WiFi 断连的实战分析 ## 背景与问题现象 在 ESP32 开发中,我们常使用 FreeRTOS 管理多任务。某项目使用 ESP32-WROOM-32,双核运行,WiFi 作为 STA 连接路由器。系统中有三个任务: - **WiFi 管理任务**(优先级 5,负责处理 WiFi 事件和 TCP/IP 协议栈) - **传感器采集任务**(优先级 3,周期性读取 I2C 传感器) - **日志打印任务**(优先级 2,通过 UART 输出日志) 运行一段时间后,WiFi 频繁断连,重连后再次断连,且伴随看门狗超时。通过串口日志发现,断连前传感器任务和日志任务执行时间异常增长,而 WiFi 任务似乎被阻塞。 ## 优先级反转原理 FreeRTOS 基于优先级抢占式调度。正常情况下,高优先级任务(WiFi)应优先执行。但优先级反转发生在: - 高优先级任务等待一个被低优先级任务占用的资源(如互斥锁、信号量) - 低优先级任务被中优先级任务抢占,导致高优先级任务无限期等待 在 ESP32 双核上,问题更复杂:两个核独立调度,但共享资源(如 WiFi 驱动内部互斥锁、SPI 总线)需要跨核同步。 ## 案例剖析 ### 资源竞争点 传感器任务通过 I2C 读取数据,而 WiFi 任务在 TCP/IP 协议栈中可能使用 I2C 或 SPI 与外部芯片通信(本例中 WiFi 任务使用 SPI 与射频前端通信)。但关键资源是**FreeRTOS 互斥锁**: - 传感器任务在读取 I2C 时,获取了一个全局互斥锁 `i2c_mutex` 保护总线 - 日志任务在打印时,也获取了 `uart_mutex` 保护 UART ### 反转发生过程 1. 传感器任务获取 `i2c_mutex`,但 I2C 设备响应慢,任务阻塞等待 2. 日志任务(优先级 2)抢占传感器任务(优先级 3)?不,优先级 3 高于 2,所以不会。但若传感器任务在等待 I2C 时主动让出 CPU(`vTaskDelay`),日志任务得以运行 3. 日志任务获取 `uart_mutex`,而 WiFi 任务此时需要获取 `uart_mutex` 来打印调试信息?实际上 WiFi 任务不打印,但 WiFi 任务需要获取 `i2c_mutex` 来配置射频芯片(假设) 4. 此时 WiFi 任务(优先级 5)等待 `i2c_mutex`,而 `i2c_mutex` 被传感器任务(优先级 3)持有,传感器任务又在等待 I2C 硬件完成,同时日志任务(优先级 2)不断运行,但不会释放 `i2c_mutex` 5. 由于日志任务优先级低于传感器,但高于?不,日志任务优先级 2 低于传感器 3,但传感器在等待 I2C 时被阻塞,日志任务得以运行,且日志任务可能长时间占用 CPU(如打印大量日志),导致传感器任务无法继续,进而无法释放 `i2c_mutex` 6. WiFi 任务被饿死,无法处理网络事件,最终 WiFi 断连 ### 双核影响 在双核上,传感器任务可能运行在核 0,日志任务运行在核 1,但互斥锁是全局的。若日志任务在核 1 上持续运行,而传感器任务在核 0 上等待 I2C 中断,但中断处理可能被其他任务延迟,导致传感器任务无法及时完成,锁持有时间过长。 ## 检测方法 ### 1. 使用 FreeRTOS 内核调试功能 - 启用 `configUSE_TRACE_FACILITY` 和 `configUSE_STATS_FORMATTING_FUNCTIONS` - 调用 `vTaskList()` 或 `vTaskGetRunTimeStats()` 查看任务状态和 CPU 使用率 - 观察 WiFi 任务是否长期处于 `Blocked` 状态 ### 2. 添加钩子函数 - 在互斥锁获取时记录时间戳,若等待时间超过阈值则打印警告 ```c void vApplicationMutexTakeHook( SemaphoreHandle_t mutex, TickType_t block_time ) { // 记录当前任务和等待时间 } ``` ### 3. 使用逻辑分析仪或 GPIO 翻转 - 在关键任务切换时翻转 GPIO,观察波形 ## 解决方案 ### 方案一:优先级继承 FreeRTOS 互斥锁(`xSemaphoreCreateMutex`)默认支持优先级继承。但需确保使用互斥锁而非二值信号量。本例中,传感器任务应使用互斥锁,且优先级继承会临时将传感器任务优先级提升至 WiFi 任务级别,从而避免被日志任务抢占。 ```c SemaphoreHandle_t i2c_mutex = xSemaphoreCreateMutex(); // 获取时使用 xSemaphoreTake(i2c_mutex, portMAX_DELAY); ``` ### 方案二:调整任务优先级 - 将日志任务优先级降低,或使用空闲钩子打印日志 - 将 WiFi 任务优先级设为最高,并确保其不等待低优先级任务持有的锁 ### 方案三:使用任务通知或队列代替互斥锁 - 对于 I2C 访问,使用队列将请求发送给专用 I2C 任务,避免锁竞争 ### 方案四:双核亲和性设置 - 将 WiFi 任务固定到核 0,传感器和日志任务固定到核 1,减少跨核锁竞争 ```c xTaskCreatePinnedToCore(wifi_task, "wifi", 4096, NULL, 5, &wifi_handle, 0); ``` ## 完整代码示例 以下是一个简化的演示代码,展示如何正确使用互斥锁和优先级继承: ```c #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" SemaphoreHandle_t i2c_mutex; void sensor_task(void *arg) { while (1) { // 获取互斥锁,支持优先级继承 if (xSemaphoreTake(i2c_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 模拟 I2C 读取 vTaskDelay(pdMS_TO_TICKS(50)); xSemaphoreGive(i2c_mutex); } vTaskDelay(pdMS_TO_TICKS(1000)); } } void wifi_task(void *arg) { while (1) { // 需要访问 I2C 配置射频 if (xSemaphoreTake(i2c_mutex, pdMS_TO_TICKS(500)) == pdTRUE) { // 配置射频 xSemaphoreGive(i2c_mutex); } else { ESP_LOGE("WIFI", "Failed to take mutex"); } vTaskDelay(pdMS_TO_TICKS(10)); } } void app_main() { i2c_mutex = xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(sensor_task, "sensor", 2048, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(wifi_task, "wifi", 4096, NULL, 5, NULL, 0); } ``` ## 注意事项 - 使用互斥锁时,确保所有访问共享资源的任务都使用同一个锁,且获取后及时释放 - 避免在中断服务函数中获取互斥锁,应使用信号量或队列 - 在 ESP32 双核上,注意 `xTaskCreatePinnedToCore` 的使用,合理分配核资源 - 启用 FreeRTOS 的优先级继承特性(默认开启),但需注意继承可能导致高优先级任务被低优先级任务短暂阻塞,这是正常现象 - 若使用二值信号量,则不会继承优先级,需改用互斥锁 ## 总结 优先级反转是实时系统中的经典问题,在 ESP32 双核环境下更容易被忽视。通过合理使用互斥锁、调整任务优先级和核亲和性,可以有效避免 WiFi 断连等异常。建议在开发初期就设计好资源访问模型,并利用 FreeRTOS 调试工具持续监控任务状态。