# 引言 ESP32 作为一款双核 MCU,其 Wi-Fi/BT 协议栈运行在 PRO_CPU(协议 CPU)上,且具有比用户任务更高的抢占优先级。当用户任务使用 FreeRTOS 事件组进行同步时,若 Wi-Fi 中断或协议栈任务抢占 CPU 核心,可能导致事件组等待任务被无限期阻塞,形成优先级反转。本文通过一个实测案例,揭示这一现象并给出解决方案。 # 原理分析 ## 双核调度与 Wi-Fi 抢占 ESP32 的两个核心(PRO_CPU 和 APP_CPU)独立运行 FreeRTOS 调度器。Wi-Fi 协议栈任务(如 `wifi_task`)被固定到 PRO_CPU,且优先级通常为 23(高于大多数用户任务)。当 Wi-Fi 活动频繁(如大量数据收发)时,PRO_CPU 上的用户任务可能被长时间抢占。 ## 事件组与优先级反转 事件组(Event Group)是 FreeRTOS 提供的同步机制,允许任务等待多个事件位。当任务 A 等待事件组,而任务 B(高优先级)由于 Wi-Fi 抢占无法及时设置事件位时,任务 A 会一直阻塞。更严重的是,若任务 B 本身也在等待另一个事件组,而该事件组由低优先级任务 C 设置,则形成链式优先级反转。 # 实测环境 - 硬件:ESP32-DevKitC V4(双核 240MHz) - 软件:ESP-IDF v5.0,FreeRTOS 10.4.3 - 测试场景:创建三个任务(高、中、低优先级),使用事件组同步,同时开启 Wi-Fi 连接并持续传输 UDP 数据。 # 配置步骤 1. 初始化 NVS 和 Wi-Fi,连接 AP。 2. 创建事件组句柄。 3. 创建三个任务: - `high_task`(优先级 10):等待事件位 0x01,设置后执行短暂计算。 - `mid_task`(优先级 5):等待事件位 0x02,设置后模拟中等负载。 - `low_task`(优先级 2):设置事件位 0x01 和 0x02,模拟低优先级工作。 4. 在 Wi-Fi 接收回调中,触发 UDP 数据接收,增加协议栈负载。 # 完整代码示例 ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_wifi.h" #include "esp_event.h" #include "nvs_flash.h" #define EVENT_BIT_HIGH (1 << 0) #define EVENT_BIT_MID (1 << 1) static EventGroupHandle_t event_group; void low_task(void *arg) { while (1) { vTaskDelay(pdMS_TO_TICKS(100)); // 模拟低优先级工作 xEventGroupSetBits(event_group, EVENT_BIT_HIGH | EVENT_BIT_MID); printf("Low task set bits\n"); } } void mid_task(void *arg) { while (1) { xEventGroupWaitBits(event_group, EVENT_BIT_MID, pdTRUE, pdFALSE, portMAX_DELAY); printf("Mid task got bit, doing work...\n"); vTaskDelay(pdMS_TO_TICKS(50)); // 模拟中等负载 } } void high_task(void *arg) { while (1) { xEventGroupWaitBits(event_group, EVENT_BIT_HIGH, pdTRUE, pdFALSE, portMAX_DELAY); printf("High task got bit, doing critical work...\n"); vTaskDelay(pdMS_TO_TICKS(10)); // 模拟关键计算 } } void wifi_udp_task(void *arg) { // 创建 UDP socket 并持续接收数据,增加 Wi-Fi 负载 // 此处省略具体实现,假设已连接并接收数据 while (1) { // 接收数据并处理 vTaskDelay(pdMS_TO_TICKS(10)); } } void app_main() { nvs_flash_init(); esp_netif_init(); esp_event_loop_create_default(); wifi_init_sta(); // 初始化并连接 Wi-Fi event_group = xEventGroupCreate(); xTaskCreatePinnedToCore(low_task, "low", 2048, NULL, 2, NULL, tskNO_AFFINITY); xTaskCreatePinnedToCore(mid_task, "mid", 2048, NULL, 5, NULL, tskNO_AFFINITY); xTaskCreatePinnedToCore(high_task, "high", 2048, NULL, 10, NULL, tskNO_AFFINITY); xTaskCreatePinnedToCore(wifi_udp_task, "udp", 4096, NULL, 8, NULL, PRO_CPU_NUM); } ``` # 实测结果与分析 - 在 Wi-Fi 空闲时,三个任务按预期顺序执行,事件组正常触发。 - 当 Wi-Fi 持续接收 UDP 数据时,`high_task` 和 `mid_task` 的响应时间显著增加,最严重时 `high_task` 的等待时间从预期的 100ms 增加到 500ms 以上。 - 原因:Wi-Fi 协议栈任务(优先级 23)在 PRO_CPU 上频繁抢占,而 `low_task` 可能被调度到 PRO_CPU,导致其设置事件位的操作被延迟。同时,`mid_task` 和 `high_task` 可能运行在 APP_CPU,但事件组操作涉及临界区,若临界区被 Wi-Fi 中断打断,也会造成阻塞。 # 优化策略 - **固定任务核心**:将关键任务固定到 APP_CPU,避免与 Wi-Fi 协议栈争抢 PRO_CPU。 - **提高任务优先级**:将 `low_task` 优先级提升至高于 Wi-Fi 协议栈(不推荐,可能影响系统稳定性),或使用中断级事件组设置。 - **使用二值信号量代替事件组**:信号量操作在中断中更高效,且可避免多事件位等待的复杂性。 - **减少临界区时间**:避免在临界区中执行耗时操作,使用 `portENTER_CRITICAL` 时尽量缩短。 # 注意事项 - 事件组操作不是完全中断安全的,`xEventGroupSetBitsFromISR` 需在中断中调用,但普通任务调用会进入临界区。 - 双核下,任务优先级是全局的,但调度器在每个核心独立运行,需注意任务亲和性。 - 实测中,Wi-Fi 负载越高,优先级反转越严重,建议在设计中预留裕量。 # 总结 ESP32 双核环境下,Wi-Fi 协议栈的高优先级抢占会引发 FreeRTOS 事件组的优先级反转,导致任务响应延迟。通过合理分配任务核心、调整优先级和使用更轻量的同步机制,可以有效缓解问题。开发者应结合具体应用场景进行实测,避免理论上的优先级设计在实际中失效。