引言

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. 确认任务优先级与核绑定

使用 uxTaskGetSystemStatevTaskList 打印任务状态,检查各任务的优先级和运行核。

void print_task_info(void) {
    char task_list[512];
    vTaskList(task_list);
    ESP_LOGI("TASK", "Task List:\n%s", task_list);
}

2. 监控事件组状态

在任务 B 中,定期打印事件组的值,确认事件位是否被设置。

EventBits_t bits = xEventGroupGetBits(event_group);
ESP_LOGI("EVENT", "Bits: 0x%x", bits);

3. 使用 Trace 工具

ESP-IDF 提供 SystemView 或 FreeRTOS 内核追踪,可以直观看到任务调度序列。

解决方案

方案一:调整任务优先级

将任务 B 的优先级提高到高于 WiFi 任务?不推荐,因为 WiFi 任务优先级是系统保留的,随意修改可能导致协议栈不稳定。

方案二:使用事件组 + 超时机制

在任务 B 中,使用带超时的等待,并增加重试逻辑,避免无限阻塞。

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。

xTaskCreatePinnedToCore(task_a, "TaskA", 4096, NULL, 5, &handle_a, 1);
xTaskCreatePinnedToCore(task_b, "TaskB", 4096, NULL, 3, &handle_b, 1);

方案四:使用互斥量替代事件组(如果场景允许)

如果同步逻辑简单,可以用二值信号量或互斥量,并启用优先级继承。

SemaphoreHandle_t sem = xSemaphoreCreateMutex();
// 在任务 A 中释放
xSemaphoreGive(sem);
// 在任务 B 中获取,带超时
if (xSemaphoreTake(sem, pdMS_TO_TICKS(1000)) == pdTRUE) {
    // 处理数据
}

完整代码示例

以下是一个修复后的示例,结合了核绑定和超时机制。

#include <stdio.h>
#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_FACILITYCONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS,方便打印任务状态。

总结

ESP32 双核环境下的优先级反转往往源于 WiFi 协议栈的高优先级抢占。通过合理绑定核、使用超时机制和监控任务状态,可以有效避免。本文提供的排查思路和代码示例,希望能帮助开发者快速定位类似问题,提升系统稳定性。