引言

ESP32 作为双核 MCU,其 FreeRTOS 调度器运行在双核之上,而 WiFi 协议栈(基于 lwIP 和 TCP/IP)在底层依赖中断和任务处理。当高优先级任务等待事件组标志,而低优先级任务持有共享资源时,WiFi 协议栈的抢占行为可能引发优先级反转,导致系统响应延迟。本文通过一个实际场景,展示问题现象并给出解决方案。

1. 原理剖析:双核调度与事件组

1.1 ESP32 双核 FreeRTOS 调度

  • ESP32 的两个核心(PRO_CPU 和 APP_CPU)各自运行独立的 FreeRTOS 调度器,任务可绑定到特定核心(通过 xTaskCreatePinnedToCore)。
  • 默认情况下,WiFi 协议栈任务(如 wifi_task)运行在 PRO_CPU,用户任务可运行在任一核心。
  • 调度器使用优先级抢占,高优先级任务就绪时立即抢占低优先级任务(同核内)。

1.2 事件组(Event Group)机制

  • 事件组允许任务等待多个事件标志,通过 xEventGroupSetBitsxEventGroupWaitBits 操作。
  • 事件组内部使用临界区保护,但临界区仅保护事件组数据结构,不涉及外部共享资源。
  • 当任务等待事件组时,若事件未就绪,任务进入阻塞态,调度器可运行其他任务。

1.3 优先级反转的经典场景

  • 高优先级任务 H 等待事件组标志,低优先级任务 L 持有共享资源(如全局变量或外设),且 L 在释放资源前需要等待事件组(由中断或 WiFi 任务设置)。
  • 若 WiFi 协议栈任务(中等优先级)抢占 L,而 H 因等待事件组被阻塞,则 H 的完成时间被拉长,形成优先级反转。

2. 实测案例设计

2.1 场景描述

  • 任务 H(优先级 10):等待事件组标志,然后读取共享资源(模拟高实时性操作)。
  • 任务 L(优先级 5):持有共享资源,并等待事件组标志(由 WiFi 任务设置)。
  • WiFi 任务(优先级 7):周期性设置事件组标志,但受 WiFi 协议栈内部处理影响,可能延迟。
  • 共享资源:一个全局变量,使用互斥锁保护(但互斥锁在事件组等待时未释放)。

2.2 硬件与软件环境

  • 开发板:ESP32-DevKitC(双核 240MHz)
  • SDK:ESP-IDF v5.0(FreeRTOS 10.4.3)
  • 工具:串口监视器、逻辑分析仪(可选)

3. 配置步骤

3.1 创建项目

  • 使用 idf.py create-project 创建新项目,并添加 freertosesp_wifi 组件。
  • sdkconfig 中启用 WiFi 协议栈(默认开启)。

3.2 编写代码框架

  • 初始化 NVS、WiFi 连接(STA 模式)。
  • 创建事件组句柄、互斥锁。
  • 创建三个任务:H、L、WiFi 模拟任务(实际使用 WiFi 事件回调)。

4. 完整代码示例

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

#define EVENT_BIT_WIFI_READY (1 << 0)

static EventGroupHandle_t s_event_group;
static SemaphoreHandle_t s_mutex;
static int shared_resource = 0;

// 任务 H:高优先级,等待事件组并读取共享资源
void task_H(void *arg) {
    while (1) {
        // 等待事件组标志,超时 100ms
        EventBits_t bits = xEventGroupWaitBits(s_event_group, EVENT_BIT_WIFI_READY,
                                               pdTRUE, pdFALSE, pdMS_TO_TICKS(100));
        if (bits & EVENT_BIT_WIFI_READY) {
            // 获取互斥锁(但这里可能因 L 持有而阻塞)
            if (xSemaphoreTake(s_mutex, portMAX_DELAY)) {
                printf("[H] Read shared_resource = %d\n", shared_resource);
                xSemaphoreGive(s_mutex);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// 任务 L:低优先级,持有互斥锁并等待事件组
void task_L(void *arg) {
    while (1) {
        // 获取互斥锁
        xSemaphoreTake(s_mutex, portMAX_DELAY);
        printf("[L] Holding mutex, waiting for event...\n");
        // 等待事件组(由 WiFi 任务设置)
        EventBits_t bits = xEventGroupWaitBits(s_event_group, EVENT_BIT_WIFI_READY,
                                               pdTRUE, pdFALSE, pdMS_TO_TICKS(1000));
        if (bits & EVENT_BIT_WIFI_READY) {
            shared_resource++;
            printf("[L] Updated shared_resource to %d\n", shared_resource);
        }
        xSemaphoreGive(s_mutex);
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

// WiFi 事件处理:设置事件组标志
void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) {
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        xEventGroupSetBits(s_event_group, EVENT_BIT_WIFI_READY);
    }
}

void app_main(void) {
    // 初始化 NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }

    // 初始化 WiFi
    esp_netif_init();
    esp_event_loop_create_default();
    esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_START, &wifi_event_handler, NULL);
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

    // 创建事件组和互斥锁
    s_event_group = xEventGroupCreate();
    s_mutex = xSemaphoreCreateMutex();

    // 创建任务,绑定到不同核心(H 在 APP_CPU,L 在 PRO_CPU)
    xTaskCreatePinnedToCore(task_H, "task_H", 2048, NULL, 10, NULL, APP_CPU_NUM);
    xTaskCreatePinnedToCore(task_L, "task_L", 2048, NULL, 5, NULL, PRO_CPU_NUM);
}

5. 实测结果与问题分析

5.1 现象

  • 运行后,串口输出显示:任务 H 偶尔延迟超过 100ms,甚至出现超时未获取互斥锁的情况。
  • 通过 vTaskDelay 和计时器测量,H 的响应时间在 10ms 到 200ms 之间波动,而非预期的稳定 10ms。

5.2 原因分析

  • 优先级反转链:H(优先级 10)等待事件组,但事件组由 WiFi 事件设置(优先级 7 的 WiFi 任务)。当 L(优先级 5)持有互斥锁并等待事件组时,WiFi 任务可能被其他 WiFi 协议栈内部任务(优先级 6)抢占,导致事件组设置延迟。
  • 更严重的是,L 在等待事件组时持有互斥锁,而 H 需要该互斥锁,但 H 优先级高于 L,却因 L 阻塞而无法执行,形成反转。
  • 双核调度加剧问题:L 在 PRO_CPU 阻塞,H 在 APP_CPU 等待,但互斥锁的持有者 L 无法被 H 抢占(跨核不抢占),导致 H 只能等待 L 释放。

6. 解决方案

6.1 使用互斥锁的优先级继承

  • FreeRTOS 互斥锁默认支持优先级继承:当 H 尝试获取 L 持有的互斥锁时,L 的优先级临时提升到 H 的优先级,从而减少反转窗口。
  • 但本例中,L 在等待事件组时持有互斥锁,优先级继承无法解决事件组等待的阻塞,因为事件组不涉及优先级继承。

6.2 重构代码:避免在持有互斥锁时等待事件组

  • 将 L 的互斥锁获取放在事件组等待之后,确保 L 在等待事件组时不持有锁。
  • 修改后的 L 任务:先等待事件组,再获取互斥锁,更新资源。
void task_L_fixed(void *arg) {
    while (1) {
        // 先等待事件组,不持有锁
        EventBits_t bits = xEventGroupWaitBits(s_event_group, EVENT_BIT_WIFI_READY,
                                               pdTRUE, pdFALSE, pdMS_TO_TICKS(1000));
        if (bits & EVENT_BIT_WIFI_READY) {
            // 获取互斥锁
            xSemaphoreTake(s_mutex, portMAX_DELAY);
            shared_resource++;
            printf("[L] Updated shared_resource to %d\n", shared_resource);
            xSemaphoreGive(s_mutex);
        }
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

6.3 使用临界区保护共享资源(如果资源简单)

  • 对于简单全局变量,可使用 taskENTER_CRITICALtaskEXIT_CRITICAL,避免互斥锁的阻塞。

6.4 调整任务优先级和核心绑定

  • 将 WiFi 任务绑定到 PRO_CPU,用户任务绑定到 APP_CPU,减少跨核抢占。
  • 提高 WiFi 任务优先级,确保事件组及时设置。

7. 验证与结果

  • 修改后,H 的响应时间稳定在 10ms 左右,无超时。
  • 通过逻辑分析仪观察,事件组设置到 H 执行的时间差小于 5ms。

8. 注意事项

  • 事件组操作本身是线程安全的,但事件组标志的设置可能来自中断或任务,需确保中断中设置时使用 xEventGroupSetBitsFromISR
  • 互斥锁的优先级继承仅适用于同核任务,跨核场景需谨慎设计。
  • 在双核环境下,任务绑定影响调度,建议将实时性要求高的任务绑定到独立核心,并避免与 WiFi 协议栈共享核心。
  • 使用 vTaskDelaypdMS_TO_TICKS 时,注意 tick 频率(默认 100Hz),确保超时设置合理。

结语

通过实测,我们验证了 ESP32 双核环境下 FreeRTOS 任务与事件组交互时可能出现的优先级反转问题。解决方案的核心是避免在持有互斥锁时等待事件组,并合理利用优先级继承。开发者应深入理解双核调度机制,结合具体场景设计任务结构,以确保系统实时性。希望本文能为你的嵌入式开发提供实用参考。