引言

ESP32 作为双核 MCU,其 FreeRTOS 支持对称多处理(SMP),但 WiFi 协议栈运行在专用任务中,与用户任务共享资源时,优先级反转问题可能悄然发生。本文以一个实际项目为例,展示事件组与 WiFi 任务交互时触发的优先级反转,并给出系统化排查与解决方案。

问题现象

某设备使用 ESP32 采集传感器数据,通过 WiFi 上传。系统创建两个任务:

  • sensor_task(优先级 5):周期性读取传感器,并设置事件组位。
  • wifi_task(优先级 3):等待事件组位,然后发送数据。

运行一段时间后,wifi_task 响应延迟从毫秒级恶化到秒级,甚至触发看门狗复位。初步怀疑是 WiFi 协议栈干扰,但深入分析后发现是优先级反转。

优先级反转原理

在 FreeRTOS 中,高优先级任务被低优先级任务阻塞,而中优先级任务抢占低优先级任务,导致高优先级任务等待时间不可控。经典场景:

  • 高优先级任务 H 等待信号量,该信号量被低优先级任务 L 持有。
  • 中优先级任务 M 运行,抢占 L,L 无法释放信号量,H 被无限期阻塞。

在 ESP32 双核环境中,优先级反转可能跨核发生。WiFi 协议栈任务(如 wifi_task)运行在 Core 0,而用户任务可能运行在 Core 1,但 FreeRTOS 的互斥量(Mutex)具有优先级继承机制,而事件组(Event Group)不具备该机制,导致反转风险更高。

事件组与 WiFi 任务共存的问题

事件组是 FreeRTOS 提供的同步机制,用于任务间事件通知。但事件组本身不提供优先级继承,当多个任务等待同一事件组时,若低优先级任务持有事件组(通过设置位),而高优先级任务等待,中优先级任务可能抢占低优先级任务,造成反转。

在 ESP32 中,WiFi 协议栈任务(如 esp_event_task)优先级通常为 18,高于用户任务。当用户任务与 WiFi 任务共享事件组时,若 WiFi 任务等待事件组,而用户任务设置事件组后未及时释放 CPU,WiFi 任务可能被其他中优先级任务阻塞。

实战排查步骤

1. 复现与监控

使用 vTaskGetRunTimeStats() 统计任务运行时间,发现 wifi_task 运行时间异常低,而 sensor_task 运行时间正常。同时,通过 uxTaskGetStackHighWaterMark() 检查栈溢出,排除栈问题。

2. 启用 FreeRTOS 追踪

配置 configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS,使用 vTaskList() 查看任务状态。发现 wifi_task 长时间处于 Blocked 状态,而 sensor_task 和另一个中优先级任务 log_task(优先级 4)频繁运行。

3. 分析事件组使用

代码中,sensor_task 设置事件组位,wifi_task 等待该位。但 log_task 不涉及事件组,却导致 wifi_task 延迟。原因:sensor_task 在设置事件组后,可能被 log_task 抢占,而 wifi_task 等待的事件组位已设置,但 wifi_task 优先级低于 log_task,因此 log_task 持续运行,wifi_task 无法获得 CPU。

4. 验证优先级反转

临时将 log_task 优先级降低到 1,问题消失,确认反转。

解决方案

方案一:使用互斥量替代事件组

互斥量支持优先级继承,可缓解反转。但事件组适合多事件等待,互斥量不适合。若场景简单,可改用二进制信号量。

方案二:调整任务优先级

wifi_task 优先级提高到高于所有可能阻塞它的任务。但需注意 WiFi 协议栈任务优先级,避免冲突。

方案三:使用任务通知(Task Notification)

任务通知比事件组更轻量,且支持优先级继承(通过 xTaskNotifyGiveulTaskNotifyTake)。但任务通知只能点对点,不适合多任务同步。

方案四:在关键区保护事件组操作

使用 taskENTER_CRITICAL() 包裹事件组设置和等待操作,但会阻塞中断,需谨慎。

最终采用:组合策略

  • wifi_task 优先级提升到 6(高于 log_task 的 4),并保持 sensor_task 优先级为 5。
  • sensor_task 设置事件组后,调用 taskYIELD() 主动让出 CPU,给 wifi_task 运行机会。
  • 使用 xEventGroupSetBitsFromISR 替代 xEventGroupSetBits(若在中断中设置)。

完整代码示例

以下为修正后的核心代码(基于 ESP-IDF v5.x):

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

#define EVENT_BIT_SENSOR (1 << 0)

static EventGroupHandle_t s_event_group;

// 传感器任务,优先级 5
void sensor_task(void *arg) {
    while (1) {
        // 模拟传感器读取
        vTaskDelay(pdMS_TO_TICKS(100));
        
        // 设置事件位
        xEventGroupSetBits(s_event_group, EVENT_BIT_SENSOR);
        
        // 主动让出 CPU,避免被低优先级任务抢占后延迟
        taskYIELD();
    }
}

// WiFi 任务,优先级 6(提高)
void wifi_task(void *arg) {
    EventBits_t bits;
    while (1) {
        // 等待事件位,清除该位
        bits = xEventGroupWaitBits(s_event_group, EVENT_BIT_SENSOR, pdTRUE, pdFALSE, portMAX_DELAY);
        if (bits & EVENT_BIT_SENSOR) {
            // 发送 WiFi 数据
            printf("WiFi sending data...\n");
        }
    }
}

// 日志任务,优先级 4(保持不变)
void log_task(void *arg) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(50));
        printf("Logging...\n");
    }
}

void app_main(void) {
    s_event_group = xEventGroupCreate();
    
    xTaskCreatePinnedToCore(sensor_task, "sensor", 2048, NULL, 5, NULL, tskNO_AFFINITY);
    xTaskCreatePinnedToCore(wifi_task, "wifi", 4096, NULL, 6, NULL, tskNO_AFFINITY);
    xTaskCreatePinnedToCore(log_task, "log", 2048, NULL, 4, NULL, tskNO_AFFINITY);
}

注意事项

  • 优先级调整需考虑 WiFi 协议栈内部任务(如 esp_event_task 优先级 18),用户任务优先级应低于 18,避免干扰协议栈。
  • 使用 taskYIELD() 只能缓解,不能根治,需结合优先级设计。
  • 若使用事件组,避免在中断中调用 xEventGroupSetBits,应使用 xEventGroupSetBitsFromISR
  • 在双核环境下,任务可能运行在不同核心,优先级反转可能涉及跨核调度,需使用 vTaskPrioritySet 动态调整时注意同步。
  • 建议使用 configUSE_PREEMPTIONconfigUSE_TIME_SLICING 配置,确保抢占式调度。

总结

ESP32 双核环境下的优先级反转问题隐蔽且影响大,事件组与 WiFi 协议栈共存时尤其需要警惕。通过系统化排查,结合优先级调整、主动让出 CPU 和合理使用同步机制,可有效避免问题。开发者应深入理解 FreeRTOS 调度机制,并在设计阶段考虑任务优先级关系,防患于未然。