引言

ESP32 作为双核 MCU,其 FreeRTOS 支持对称多处理(SMP),任务可运行在任意核心上。然而,当多个任务共享资源(如互斥锁、信号量)时,优先级反转问题可能悄然发生,尤其在 Wi-Fi 协议栈任务(如 wifi_task)与用户任务竞争 CPU 时,会导致 Wi-Fi 断流、连接超时等异常。本文通过一个实际案例,讲解如何识别和解决这类问题。

优先级反转原理

优先级反转是指高优先级任务因等待低优先级任务持有的资源而被中优先级任务抢占,导致高优先级任务被“卡住”的现象。经典场景如下:

  • 任务 A(高优先级)等待任务 C(低优先级)持有的互斥锁。
  • 任务 B(中优先级)不访问该资源,但持续占用 CPU。
  • 任务 C 因被 B 抢占而无法释放锁,A 一直阻塞。

在单核环境中,FreeRTOS 通过优先级继承机制缓解此问题:当高优先级任务等待互斥锁时,持有锁的任务临时提升到高优先级,从而快速释放锁。但在 ESP32 双核环境下,情况更复杂:

  • 两个核心独立调度,任务可并行运行。
  • 若任务 B 运行在另一个核心上,即使 C 提升了优先级,B 仍可能占用另一个核心,导致 C 无法及时释放锁(若 C 被 B 抢占)。
  • 此外,Wi-Fi 协议栈任务通常运行在核心 0,用户任务可能运行在核心 1,跨核竞争加剧了不确定性。

案例:Wi-Fi 断流现象

某项目使用 ESP32 作为 IoT 网关,通过 Wi-Fi 上传传感器数据。系统创建了三个任务:

  • wifi_task(优先级 10):处理 Wi-Fi 事件和 TCP/IP 协议栈。
  • sensor_task(优先级 8):读取传感器并发送数据。
  • log_task(优先级 5):打印日志到串口。

运行一段时间后,Wi-Fi 频繁断流,重连耗时数秒。通过日志发现,wifi_task 长时间未执行,而 log_task 占用大量 CPU。初步怀疑是优先级反转,但单核继承机制似乎未生效。

排查步骤

1. 确认任务调度情况

使用 vTaskList()vTaskGetRunTimeStats() 打印任务状态和 CPU 占用率。在断流时,观察 wifi_task 的状态是否为 Blocked,以及 log_task 的 CPU 占用率是否异常高。

// 打印任务列表
void print_task_stats(void) {
    char buffer[512];
    vTaskList(buffer);
    printf("Task List:\n%s\n", buffer);
}

2. 检查共享资源

检查所有任务是否使用互斥锁或信号量。本例中,sensor_tasklog_task 共享一个 UART 输出锁,用于保护串口打印。

SemaphoreHandle_t uart_mutex;

void log_message(const char *msg) {
    xSemaphoreTake(uart_mutex, portMAX_DELAY);
    printf("%s\n", msg);
    xSemaphoreGive(uart_mutex);
}

3. 复现与监控

在断流时,使用 esp_cpu_get_cycle_count() 记录各任务执行时间,或使用 tracealyzer 等工具。发现 log_task 在持有 UART 锁时,被 sensor_task 抢占(因为 sensor_task 优先级更高),而 log_task 被抢占后无法释放锁,导致 wifi_task 等待该锁(尽管 wifi_task 并不直接使用 UART,但可能通过其他共享资源间接关联)。

实际上,wifi_task 内部会调用 esp_event 发送事件,而事件处理函数中可能调用了 log_message,因此 wifi_task 也等待 UART 锁。

4. 验证优先级反转

log_task 获得锁后,人为提高其优先级(模拟继承),观察断流是否消失。若消失,则确认问题。

// 临时提升优先级
vTaskPrioritySet(log_task_handle, 10);

解决方案

1. 使用互斥锁而非二值信号量

FreeRTOS 互斥锁(xSemaphoreCreateMutex)支持优先级继承,而二值信号量不支持。确保所有共享资源使用互斥锁。

uart_mutex = xSemaphoreCreateMutex();

2. 避免在临界区中执行耗时操作

log_message 中的 printf 可能阻塞,应尽量缩短锁持有时间。例如,将日志字符串先存入缓冲区,再统一输出。

void log_message(const char *msg) {
    xSemaphoreTake(uart_mutex, portMAX_DELAY);
    // 仅复制到缓冲区,不直接打印
    strncpy(log_buffer, msg, sizeof(log_buffer));
    xSemaphoreGive(uart_mutex);
    // 在锁外打印
    printf("%s\n", log_buffer);
}

3. 任务优先级调整

适当降低 log_task 的优先级,或提高 wifi_task 的优先级,确保 Wi-Fi 任务优先执行。但需注意,优先级过高可能影响其他实时任务。

4. 使用任务通知替代信号量

对于简单的标志传递,使用任务通知(xTaskNotify)比信号量更轻量,且不会引起优先级反转。

完整代码示例

以下是一个修复后的关键代码片段:

// 创建互斥锁
SemaphoreHandle_t uart_mutex;

void app_main() {
    uart_mutex = xSemaphoreCreateMutex();
    
    // 创建任务
    xTaskCreatePinnedToCore(wifi_task, "wifi", 4096, NULL, 10, &wifi_task_handle, 0);
    xTaskCreatePinnedToCore(sensor_task, "sensor", 2048, NULL, 8, NULL, 1);
    xTaskCreatePinnedToCore(log_task, "log", 2048, NULL, 5, &log_task_handle, 1);
}

// 日志函数(优化后)
void log_message(const char *msg) {
    static char buffer[128];
    xSemaphoreTake(uart_mutex, portMAX_DELAY);
    strncpy(buffer, msg, sizeof(buffer) - 1);
    xSemaphoreGive(uart_mutex);
    printf("%s\n", buffer);
}

// 任务函数
void wifi_task(void *arg) {
    while (1) {
        // 处理 Wi-Fi 事件
        log_message("Wi-Fi event");
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}

void sensor_task(void *arg) {
    while (1) {
        // 读取传感器
        log_message("Sensor data");
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

void log_task(void *arg) {
    while (1) {
        // 定期输出日志
        log_message("Log task running");
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

注意事项

  • 在 ESP32 双核环境下,任务优先级反转可能跨核发生,务必使用互斥锁并开启优先级继承。
  • 避免在锁内调用阻塞函数(如 printfvTaskDelay),否则会放大问题。
  • 使用 vTaskListvTaskGetRunTimeStats 定期监控任务状态,便于早期发现异常。
  • 如果 Wi-Fi 断流问题依然存在,考虑检查电源、射频干扰等其他因素,但优先级反转是常见原因之一。

总结

通过分析优先级反转原理,结合 ESP32 双核特性,我们定位并解决了 Wi-Fi 断流问题。核心在于使用互斥锁、缩短临界区、合理调整优先级。嵌入式开发中,多任务并发问题往往隐蔽,掌握系统化排查方法至关重要。希望本文能帮助你在类似项目中少走弯路。