引言

ESP32 作为双核 MCU,其 Wi-Fi 协议栈运行在 Core 0 的 FreeRTOS 任务中,而用户应用任务通常分布在两个核心。当用户任务与 Wi-Fi 任务共享资源(如 SPI Flash、I2C 总线或自定义互斥锁)时,优先级反转可能导致 Wi-Fi 任务被低优先级任务长时间阻塞,进而引发吞吐量骤降。本文将通过一个典型场景,演示如何系统排查并修复该问题。

1. 问题现象与根因分析

1.1 现象描述

  • Wi-Fi 吞吐量从 5 Mbps 骤降至 0.5 Mbps,且波动明显。
  • 系统日志无报错,但 esp_wifi_get_ps 显示 Wi-Fi 处于活跃状态。
  • 通过 vTaskList 发现高优先级任务频繁阻塞。

1.2 根因:优先级反转

FreeRTOS 默认支持优先级继承,但 ESP-IDF 中 Wi-Fi 任务(优先级 23)与用户任务(如优先级 10)共享一个互斥锁时,若低优先级任务(优先级 5)持有锁,高优先级任务(优先级 20)等待,则低优先级任务会被临时提升至 20,但 Wi-Fi 任务(优先级 23)仍可能被阻塞,导致协议栈无法及时处理数据包。

更隐蔽的是,ESP32 双核下,若低优先级任务与 Wi-Fi 任务运行在不同核心,优先级继承无法跨核生效(FreeRTOS 的优先级继承仅限同核调度器),从而引发长时间反转。

2. 排查步骤

2.1 启用内核追踪

menuconfig 中开启:

Component config → FreeRTOS → Kernel → Tracing → FreeRTOS system view tracing

同时启用互斥锁追踪:

Component config → FreeRTOS → Kernel → Mutex → Include mutex priority inheritance

2.2 添加追踪代码

在共享资源访问处添加日志:

// 共享资源结构体
typedef struct {
    SemaphoreHandle_t lock;
    uint32_t data;
} shared_res_t;

void shared_res_write(shared_res_t *res, uint32_t val) {
    ESP_LOGI("SHARED", "Task %s trying to lock, prio=%d", pcTaskGetName(NULL), uxTaskPriorityGet(NULL));
    xSemaphoreTake(res->lock, portMAX_DELAY);
    ESP_LOGI("SHARED", "Task %s got lock, prio=%d", pcTaskGetName(NULL), uxTaskPriorityGet(NULL));
    // 模拟长时间操作
    vTaskDelay(pdMS_TO_TICKS(100));
    res->data = val;
    xSemaphoreGive(res->lock);
}

2.3 使用 SystemView 分析

将追踪数据导入 SystemView,观察任务状态:

  • 若发现 Wi-Fi 任务(wifi_task)处于 Blocked 状态,且等待的互斥锁被低优先级任务持有,则确认反转。
  • 检查核心 ID:若持有者运行在 Core 1,而 Wi-Fi 任务在 Core 0,则跨核反转。

3. 解决方案与代码示例

3.1 方案一:使用递归互斥锁(不推荐)

递归锁可避免同一任务重复获取,但无法解决跨核问题。

3.2 方案二:显式提升优先级(推荐)

在低优先级任务访问共享资源前,临时提升其优先级至 Wi-Fi 任务之上:

#define WIFI_TASK_PRIO 23
#define USER_TASK_PRIO 10
#define LOW_TASK_PRIO 5

void low_prio_task(void *arg) {
    shared_res_t *res = (shared_res_t*)arg;
    while(1) {
        // 提升优先级
        vTaskPrioritySet(NULL, WIFI_TASK_PRIO + 1);
        shared_res_write(res, 0x1234);
        // 恢复优先级
        vTaskPrioritySet(NULL, LOW_TASK_PRIO);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main() {
    shared_res_t res = { .lock = xSemaphoreCreateMutex(), .data = 0 };
    xTaskCreatePinnedToCore(low_prio_task, "low", 2048, &res, LOW_TASK_PRIO, NULL, 1);
    // 创建其他任务...
}

3.3 方案三:使用无锁设计或专用队列

对于 Wi-Fi 相关数据,使用 xQueueSend 代替共享内存,避免锁竞争。

4. 验证与注意事项

  • 修改后,用 vTaskList 观察任务状态,确认 Wi-Fi 任务不再长时间阻塞。
  • 吞吐量测试:使用 iperf 工具,连续测试 5 分钟,确保稳定。
  • 注意:提升优先级需谨慎,避免高优先级任务饥饿。
  • 跨核问题:若任务固定在不同核心,建议将共享资源访问任务绑定到同一核心。

5. 总结

优先级反转在双核环境下更隐蔽,通过内核追踪和日志分析可快速定位。推荐采用“临时提升优先级”或“队列通信”方案,并注意核心亲和性。掌握这些技巧,能显著提升 ESP32 应用的稳定性与性能。