引言
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 设置BIT0和BIT1。 - Wi-Fi 任务:默认优先级 23,持续收发 UDP 数据包。
-
测试结果
- 无 Wi-Fi 负载时,
Task_A响应时间 < 1ms。 - 有 Wi-Fi 负载时,
Task_A响应时间抖动至 20~50ms,甚至出现 100ms 以上的峰值。 - 通过
vTaskGetRunTimeStats()观察,Task_B占用大量 CPU 时间,但Task_A被阻塞在事件组等待上。
根因分析
- 事件组内部互斥锁:FreeRTOS 事件组使用临界区保护,但 Wi-Fi 任务在持有内部锁时可能被更高优先级的硬件中断打断,导致临界区时间延长。
-
优先级反转:
Task_C设置事件位时,Task_B(优先级 10)先获得事件组控制权,执行耗时操作。此时Task_A(优先级 5)虽已就绪,但被Task_B阻塞。若 Wi-Fi 任务(优先级 23)抢占Task_B,则Task_A的等待时间进一步拉长。 -
双核调度差异:Wi-Fi 任务固定在 Core 0,而用户任务默认可在任意核运行。若
Task_A和Task_B被调度到不同核,事件组操作需跨核同步,增加锁竞争。
规避方案
方案一:使用事件组位掩码 + 任务通知
将事件组替换为任务通知(xTaskNotifyWait),利用通知值作为位掩码,避免全局锁。
// 任务 A 等待 BIT0
uint32_t notify_value;
xTaskNotifyWait(0, BIT0, ¬ify_value, portMAX_DELAY);
// 任务 C 发送通知
xTaskNotify(TaskA_handle, BIT0, eSetBits);
- 优点:任务通知无锁,速度更快。
- 缺点:仅支持单任务等待,不适合多任务同步。
方案二:核绑定 + 优先级继承
将关键任务绑定到同一核心,并启用优先级继承(configUSE_MUTEXES 和 configPRIO_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, ¬ify_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, ¬ify_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_timer或vTaskGetRunTimeStats()精确测量响应时间,避免使用millis()等不精确函数。
总结
ESP32 双核环境下,Wi-Fi 协议栈的高优先级任务会显著加剧 FreeRTOS 事件组的优先级反转。通过实测对比,任务通知 + 核绑定方案能有效规避该问题,将响应时间从 100ms 级降至 1ms 级。开发者应根据实际需求选择合适方案,并在设计时考虑任务优先级、核分配和同步机制的综合影响。