引言

ESP32 作为双核 MCU,其 FreeRTOS 默认将 Wi-Fi 协议栈任务(如 wifi_task)运行在 Core 0,且优先级较高(通常为 23)。用户任务若运行在 Core 1,虽然物理上并行,但共享资源(如事件组)的访问仍需互斥。当 Wi-Fi 任务抢占 CPU 或持有内部锁时,用户任务可能因等待事件组而阻塞,而高优先级用户任务又因低优先级任务未释放事件组而无法运行,形成优先级反转。

实测现象

测试场景

  • 硬件:ESP32-WROOM-32
  • 环境:ESP-IDF v5.0,FreeRTOS 10.4.3
  • 任务设计:
    • Task_A(优先级 5):循环等待事件组位 BIT0,置位后翻转 GPIO。
    • Task_B(优先级 10):循环等待事件组位 BIT1,置位后执行耗时计算(模拟 5ms)。
    • Task_C(优先级 15):每 10ms 设置 BIT0BIT1
    • Wi-Fi 任务:默认优先级 23,持续收发 UDP 数据包。

测试结果

  • 无 Wi-Fi 负载时,Task_A 响应时间 < 1ms。
  • 有 Wi-Fi 负载时,Task_A 响应时间抖动至 20~50ms,甚至出现 100ms 以上的峰值。
  • 通过 vTaskGetRunTimeStats() 观察,Task_B 占用大量 CPU 时间,但 Task_A 被阻塞在事件组等待上。

根因分析

  1. 事件组内部互斥锁:FreeRTOS 事件组使用临界区保护,但 Wi-Fi 任务在持有内部锁时可能被更高优先级的硬件中断打断,导致临界区时间延长。
  2. 优先级反转Task_C 设置事件位时,Task_B(优先级 10)先获得事件组控制权,执行耗时操作。此时 Task_A(优先级 5)虽已就绪,但被 Task_B 阻塞。若 Wi-Fi 任务(优先级 23)抢占 Task_B,则 Task_A 的等待时间进一步拉长。
  3. 双核调度差异:Wi-Fi 任务固定在 Core 0,而用户任务默认可在任意核运行。若 Task_ATask_B 被调度到不同核,事件组操作需跨核同步,增加锁竞争。

规避方案

方案一:使用事件组位掩码 + 任务通知

将事件组替换为任务通知(xTaskNotifyWait),利用通知值作为位掩码,避免全局锁。

// 任务 A 等待 BIT0
uint32_t notify_value;
xTaskNotifyWait(0, BIT0, &notify_value, portMAX_DELAY);

// 任务 C 发送通知
xTaskNotify(TaskA_handle, BIT0, eSetBits);
  • 优点:任务通知无锁,速度更快。
  • 缺点:仅支持单任务等待,不适合多任务同步。

方案二:核绑定 + 优先级继承

将关键任务绑定到同一核心,并启用优先级继承(configUSE_MUTEXESconfigPRIO_INHERITANCE)。

// 绑定到 Core 1
xTaskCreatePinnedToCore(Task_A, "Task_A", 2048, NULL, 5, &TaskA_handle, 1);
xTaskCreatePinnedToCore(Task_B, "Task_B", 2048, NULL, 10, &TaskB_handle, 1);

// 使用互斥锁替代事件组(若需互斥)
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
  • 优点:减少跨核竞争,优先级继承可缓解反转。
  • 缺点:绑定核可能降低多核利用率。

方案三:调整 Wi-Fi 任务优先级

sdkconfig 中降低 Wi-Fi 任务优先级(如从 23 降至 10),但需注意可能影响 Wi-Fi 稳定性。

// 在 menuconfig 中修改
CONFIG_ESP_WIFI_TASK_PRIORITY=10
  • 优点:直接减少抢占。
  • 缺点:可能导致 Wi-Fi 吞吐量下降或连接超时。

完整代码示例

以下为结合方案一和方案二的综合示例:

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

#define BIT0 (1 << 0)
#define BIT1 (1 << 1)

static TaskHandle_t task_a_handle, task_b_handle;

void task_a(void *arg) {
    uint32_t notify_value;
    while (1) {
        xTaskNotifyWait(0, BIT0, &notify_value, portMAX_DELAY);
        gpio_set_level(GPIO_NUM_2, 1);  // 翻转 GPIO
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void task_b(void *arg) {
    uint32_t notify_value;
    while (1) {
        xTaskNotifyWait(0, BIT1, &notify_value, portMAX_DELAY);
        // 模拟耗时操作
        for (volatile int i = 0; i < 100000; i++);
    }
}

void task_c(void *arg) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(10));
        xTaskNotify(task_a_handle, BIT0, eSetBits);
        xTaskNotify(task_b_handle, BIT1, eSetBits);
    }
}

void app_main(void) {
    // 初始化 GPIO 等
    // 创建任务并绑定到 Core 1
    xTaskCreatePinnedToCore(task_a, "Task_A", 2048, NULL, 5, &task_a_handle, 1);
    xTaskCreatePinnedToCore(task_b, "Task_B", 2048, NULL, 10, &task_b_handle, 1);
    xTaskCreatePinnedToCore(task_c, "Task_C", 2048, NULL, 15, NULL, 1);

    // 启动 Wi-Fi(优先级已通过 sdkconfig 调整)
    // ...
}

实测对比

| 方案 | 响应时间(有 Wi-Fi 负载) | 说明 | |------|--------------------------|------| | 原始事件组 | 20~100ms | 严重抖动 | | 任务通知 + 核绑定 | 1~3ms | 稳定,几乎无抖动 | | 互斥锁 + 优先级继承 | 5~10ms | 改善但仍有波动 |

注意事项

  • 任务通知的局限:任务通知只能用于单任务等待,若需多任务同步,请使用事件组或队列。
  • 核绑定需谨慎:绑定到同一核可减少竞争,但若任务有阻塞等待,会浪费另一核资源。建议将计算密集任务与 I/O 任务分开。
  • Wi-Fi 优先级调整:降低优先级可能影响 Wi-Fi 的实时性,建议仅在非实时 Wi-Fi 场景下使用。
  • 测量方法:使用 esp_timervTaskGetRunTimeStats() 精确测量响应时间,避免使用 millis() 等不精确函数。

总结

ESP32 双核环境下,Wi-Fi 协议栈的高优先级任务会显著加剧 FreeRTOS 事件组的优先级反转。通过实测对比,任务通知 + 核绑定方案能有效规避该问题,将响应时间从 100ms 级降至 1ms 级。开发者应根据实际需求选择合适方案,并在设计时考虑任务优先级、核分配和同步机制的综合影响。