一、问题现象与初步定位
某物联网设备使用 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 触发过程
-
task_sensor获取互斥量,开始处理传感器数据(耗时较长)。 - 此时
task_ui就绪,抢占task_sensor(因为优先级 15 > 10)。 -
task_ui运行大量计算,持续占用 PRO_CPU。 -
task_network等待互斥量,但互斥量被task_sensor持有,而task_sensor被task_ui抢占,无法释放。 - Wi-Fi 协议栈任务需要与
task_network交互(如发送数据),但task_network被阻塞,Wi-Fi 任务等待其响应,导致协议栈超时。 - 最终 Wi-Fi 任务进入错误状态,断开连接。
3.3 根因
- 互斥量未启用优先级继承(FreeRTOS 互斥量默认支持,但若使用二值信号量则无继承)。
- 任务优先级设置不当,中优先级任务
task_ui干扰了低优先级任务释放资源。 - 双核调度加剧了问题:
task_ui可能运行在 PRO_CPU,而task_sensor被迁移到 APP_CPU,但互斥量等待链未跨核处理。
四、解决方案与配置步骤
4.1 方案一:使用互斥量并启用优先级继承
FreeRTOS 互斥量(xSemaphoreCreateMutex())自带优先级继承机制,但需确保所有共享资源使用互斥量而非二值信号量。
// 创建互斥量
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() 并指定核。
// 在 PRO_CPU 上使用
portENTER_CRITICAL(&spinlock);
// 临界区代码
portEXIT_CRITICAL(&spinlock);
4.4 配置步骤(ESP-IDF)
- 在
menuconfig中启用互斥量优先级继承:Component config → FreeRTOS → Kernel → Enable priority inheritance(默认开启)。 - 确保所有共享资源使用互斥量,而非二值信号量。
- 设置任务亲和性:将关键任务绑定到指定核,避免跨核调度。
// 创建任务时指定核
xTaskCreatePinnedToCore(task_sensor, "sensor", 4096, NULL, 10, &handle, APP_CPU);
五、完整代码示例
以下为修复后的关键代码片段。
#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 使用优先级继承测试
在互斥量获取前后打印任务优先级,验证继承是否生效。
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 最佳实践,并利用调试工具验证调度行为。