# 引言 ESP32 作为双核 MCU,其 FreeRTOS 支持对称多处理(SMP),但 WiFi 协议栈运行在 Core 0 上,且其任务优先级往往高于用户任务。当用户任务依赖事件组进行同步时,WiFi 任务的高优先级抢占可能导致事件组标志位无法及时被消费,进而引发优先级反转——低优先级任务持有资源,高优先级任务等待,而中间优先级任务阻塞低优先级任务。本文通过一个实战案例,展示如何定位并解决此类问题。 # 问题场景 假设我们有一个数据采集系统,任务 A(优先级 5)负责从传感器读取数据,任务 B(优先级 3)负责处理数据,任务 C(优先级 4)负责网络上传。任务 A 和 B 通过事件组同步,但 WiFi 协议栈任务(优先级 10)频繁抢占。 现象:系统运行一段时间后,任务 B 迟迟不执行,导致数据积压,甚至看门狗超时。 # 根因分析 ## 1. 双核调度与优先级 ESP32 的 FreeRTOS 中,每个核有独立的就绪队列,但任务可以绑定到特定核(通过 xTaskCreatePinnedToCore)。WiFi 协议栈任务通常固定运行在 Core 0,且优先级较高(如 10)。当 WiFi 任务就绪时,它会抢占 Core 0 上所有低优先级任务,包括用户任务。 ## 2. 事件组的原子性与阻塞 事件组操作(xEventGroupSetBits / xEventGroupWaitBits)是原子的,但等待事件时,任务会进入阻塞态。如果任务 A 在 Core 1 上设置事件位,而任务 B 在 Core 0 上等待,那么当 WiFi 任务抢占 Core 0 时,任务 B 无法被调度,即使事件位已置位。 ## 3. 优先级反转的链条 - 任务 C(优先级 4)持有某个互斥锁,等待任务 B 处理数据。 - 任务 B(优先级 3)等待事件组,但被 WiFi 任务(优先级 10)抢占。 - 任务 C 被阻塞,而 WiFi 任务继续运行,导致高优先级任务(WiFi)间接阻塞了中等优先级任务(C),低优先级任务(B)反而无法运行。 # 排查步骤 ## 1. 确认任务优先级与核绑定 使用 `uxTaskGetSystemState` 或 `vTaskList` 打印任务状态,检查各任务的优先级和运行核。 ```c void print_task_info(void) { char task_list[512]; vTaskList(task_list); ESP_LOGI("TASK", "Task List:\n%s", task_list); } ``` ## 2. 监控事件组状态 在任务 B 中,定期打印事件组的值,确认事件位是否被设置。 ```c EventBits_t bits = xEventGroupGetBits(event_group); ESP_LOGI("EVENT", "Bits: 0x%x", bits); ``` ## 3. 使用 Trace 工具 ESP-IDF 提供 SystemView 或 FreeRTOS 内核追踪,可以直观看到任务调度序列。 # 解决方案 ## 方案一:调整任务优先级 将任务 B 的优先级提高到高于 WiFi 任务?不推荐,因为 WiFi 任务优先级是系统保留的,随意修改可能导致协议栈不稳定。 ## 方案二:使用事件组 + 超时机制 在任务 B 中,使用带超时的等待,并增加重试逻辑,避免无限阻塞。 ```c EventBits_t bits = xEventGroupWaitBits(event_group, BIT_0 | BIT_1, pdTRUE, pdTRUE, pdMS_TO_TICKS(1000)); if ((bits & (BIT_0 | BIT_1)) == 0) { ESP_LOGW("TASK_B", "Timeout waiting for events"); } ``` ## 方案三:将任务绑定到不同核 将任务 A 和 B 都绑定到 Core 1,避免与 WiFi 任务竞争 Core 0。 ```c xTaskCreatePinnedToCore(task_a, "TaskA", 4096, NULL, 5, &handle_a, 1); xTaskCreatePinnedToCore(task_b, "TaskB", 4096, NULL, 3, &handle_b, 1); ``` ## 方案四:使用互斥量替代事件组(如果场景允许) 如果同步逻辑简单,可以用二值信号量或互斥量,并启用优先级继承。 ```c SemaphoreHandle_t sem = xSemaphoreCreateMutex(); // 在任务 A 中释放 xSemaphoreGive(sem); // 在任务 B 中获取,带超时 if (xSemaphoreTake(sem, pdMS_TO_TICKS(1000)) == pdTRUE) { // 处理数据 } ``` # 完整代码示例 以下是一个修复后的示例,结合了核绑定和超时机制。 ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_log.h" #define BIT_SENSOR_READY (1 << 0) #define BIT_DATA_PROCESSED (1 << 1) static EventGroupHandle_t event_group; static void task_a(void *arg) { while (1) { // 模拟传感器读取 vTaskDelay(pdMS_TO_TICKS(500)); xEventGroupSetBits(event_group, BIT_SENSOR_READY); ESP_LOGI("TASK_A", "Sensor data ready"); } } static void task_b(void *arg) { while (1) { EventBits_t bits = xEventGroupWaitBits(event_group, BIT_SENSOR_READY, pdTRUE, pdTRUE, pdMS_TO_TICKS(1000)); if ((bits & BIT_SENSOR_READY) != 0) { ESP_LOGI("TASK_B", "Processing data..."); // 模拟处理 vTaskDelay(pdMS_TO_TICKS(200)); xEventGroupSetBits(event_group, BIT_DATA_PROCESSED); } else { ESP_LOGW("TASK_B", "Timeout waiting for sensor"); } } } static void task_c(void *arg) { while (1) { // 模拟网络上传,持有某个锁 vTaskDelay(pdMS_TO_TICKS(1000)); ESP_LOGI("TASK_C", "Uploading..."); } } void app_main(void) { event_group = xEventGroupCreate(); // 绑定到 Core 1,避免与 WiFi 任务竞争 xTaskCreatePinnedToCore(task_a, "TaskA", 4096, NULL, 5, NULL, 1); xTaskCreatePinnedToCore(task_b, "TaskB", 4096, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(task_c, "TaskC", 4096, NULL, 4, NULL, 1); // 初始化 WiFi(略) } ``` # 注意事项 - **不要随意修改 WiFi 任务优先级**:ESP-IDF 中 WiFi 任务优先级在 `esp_wifi.h` 中定义,修改可能导致不可预测行为。 - **事件组等待必须带超时**:避免因其他任务异常导致永久阻塞。 - **核绑定需谨慎**:Core 0 通常处理 WiFi 和蓝牙,用户任务尽量放在 Core 1,但也要考虑负载均衡。 - **使用优先级继承**:如果使用互斥量,FreeRTOS 默认支持优先级继承,可缓解反转。 - **调试工具**:启用 `CONFIG_FREERTOS_USE_TRACE_FACILITY` 和 `CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS`,方便打印任务状态。 # 总结 ESP32 双核环境下的优先级反转往往源于 WiFi 协议栈的高优先级抢占。通过合理绑定核、使用超时机制和监控任务状态,可以有效避免。本文提供的排查思路和代码示例,希望能帮助开发者快速定位类似问题,提升系统稳定性。