# ESP32 多核环境下 FreeRTOS 任务与中断优先级嵌套导致死锁的排查方法 ## 一、问题背景 ESP32 采用双核 Xtensa LX6 处理器,FreeRTOS 默认支持对称多处理(SMP),任务可运行在任意核心上。当任务与中断服务程序(ISR)共享资源时,若优先级嵌套处理不当,极易引发死锁。典型症状:系统运行一段时间后卡死,看门狗超时复位,或低优先级任务永远得不到执行。 ## 二、死锁原理分析 ### 1. 多核环境下的竞争条件 在单核 MCU 中,任务切换发生在中断或系统节拍中,临界区只需屏蔽中断即可保证原子性。但在 ESP32 双核上,两个核心并行执行,若任务 A 在 Core 0 持有锁,任务 B 在 Core 1 同时请求同一锁,就会产生竞争。FreeRTOS 使用自旋锁(spinlock)保护内部数据结构,但用户代码中的互斥量(Mutex)并不自动屏蔽中断,因此需要额外处理。 ### 2. 优先级嵌套死锁场景 - 场景:高优先级任务 H 等待互斥量 M,而 M 被低优先级任务 L 持有。L 在持有 M 期间被中断 ISR 打断,ISR 中又尝试获取另一个互斥量 N,而 N 被 H 持有(H 此时在等待 M)。 - 结果:H 等待 M,L 等待 ISR 完成,ISR 等待 N,N 被 H 持有,形成循环等待。 - 多核加剧:若 H 和 L 运行在不同核心,ISR 可能绑定在某个核心,导致调度器无法通过优先级抢占打破僵局。 ### 3. FreeRTOS 优先级机制 - 任务优先级:0(最低)到 configMAX_PRIORITIES-1(最高),数字越大优先级越高。 - 中断优先级:ESP32 使用 1-7 级,数字越大优先级越高。中断可以抢占任务,但 ISR 中不能调用阻塞 API(如 `xSemaphoreTake` 带超时),否则会触发断言或死锁。 ## 三、排查流程 ### 1. 复现与日志 - 在死锁发生前打印关键状态:每个任务的状态(运行、阻塞、就绪)、持有的互斥量、等待的互斥量。 - 使用 `vTaskList` 和 `vTaskGetRunTimeStats` 输出任务信息。 - 开启 FreeRTOS 的跟踪宏:`configUSE_TRACE_FACILITY` 和 `configUSE_STATS_FORMATTING_FUNCTIONS`。 ### 2. 检查优先级配置 - 确认所有互斥量是否设置了优先级继承(`xSemaphoreCreateMutex` 默认支持)。 - 检查中断优先级是否高于可延迟中断(如 `portMAX_DELAY` 在 ISR 中禁用)。 - 使用 `ESP_INTR_FLAG_LEVEL` 明确中断级别,避免嵌套。 ### 3. 审查临界区 - 在任务中,使用 `taskENTER_CRITICAL` 和 `taskEXIT_CRITICAL` 保护共享资源,但注意这些宏在 SMP 下会关闭当前核心的中断并获取自旋锁,可能导致其他核心等待。 - 避免在临界区中调用任何可能阻塞的函数。 ### 4. 使用死锁检测工具 - 开启 `configUSE_MUTEX_DEADLOCK_DETECTION`(需自定义实现),或使用 `xSemaphoreGetMutexHolder` 查询持有者。 - 利用 ESP-IDF 的 `esp_cpu_dump` 或 JTAG 调试器查看当前任务栈和 PC。 ## 四、代码示例与修复 ### 1. 问题代码(死锁触发) ```c // 共享互斥量 SemaphoreHandle_t mutexA, mutexB; // 高优先级任务 H void taskH(void *arg) { while (1) { xSemaphoreTake(mutexA, portMAX_DELAY); // 等待 A // 处理... xSemaphoreGive(mutexA); vTaskDelay(10); } } // 低优先级任务 L void taskL(void *arg) { while (1) { xSemaphoreTake(mutexB, portMAX_DELAY); // 持有 B // 模拟中断触发 vTaskDelay(1); xSemaphoreGive(mutexB); vTaskDelay(100); } } // 中断服务程序(假设绑定 Core 1) void IRAM_ATTR isrHandler(void *arg) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreTakeFromISR(mutexB, &xHigherPriorityTaskWoken); // 尝试获取 B,但被 L 持有 // 若 B 被 L 持有,这里会死等(实际上 ISR 中不能阻塞,但错误代码可能使用轮询) portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } ``` ### 2. 修复方案 - **方案一:统一资源访问顺序**。所有任务和 ISR 按相同顺序获取互斥量,避免循环等待。 - **方案二:使用队列代替互斥量**。ISR 中通过 `xQueueSendFromISR` 发送事件,任务中处理,避免 ISR 直接获取锁。 - **方案三:设置互斥量超时**。在任务中使用 `xSemaphoreTake(mutex, pdMS_TO_TICKS(100))`,超时后释放已持有的锁并重试。 - **方案四:调整优先级**。将持有互斥量的任务优先级临时提升(优先级继承),或使用 `xSemaphoreCreateRecursiveMutex` 支持递归获取。 修复后的代码示例: ```c // 任务 L 中,先获取 A 再获取 B,保持一致顺序 void taskL(void *arg) { while (1) { if (xSemaphoreTake(mutexA, pdMS_TO_TICKS(50)) == pdTRUE) { if (xSemaphoreTake(mutexB, pdMS_TO_TICKS(50)) == pdTRUE) { // 临界区 xSemaphoreGive(mutexB); } xSemaphoreGive(mutexA); } vTaskDelay(100); } } // ISR 中只发送通知,不获取锁 void IRAM_ATTR isrHandler(void *arg) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(semaphoreNotify, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } ``` ## 五、注意事项 - **ISR 中严禁阻塞**:任何 `portMAX_DELAY` 或轮询等待都会导致系统崩溃。 - **多核亲和性**:使用 `xTaskCreatePinnedToCore` 将关键任务固定到特定核心,减少竞争。 - **优先级反转**:FreeRTOS 互斥量默认支持优先级继承,但仅限任务间,不涉及中断。 - **调试技巧**:在死锁发生时,通过 `esp_backtrace` 打印调用栈,或使用 `gdb` 连接 JTAG 查看任务状态。 - **测试覆盖**:使用 `stress test` 长时间运行,并随机触发中断,以暴露潜在死锁。 ## 六、总结 ESP32 多核环境下的死锁排查需要结合 FreeRTOS 调度原理和硬件特性。通过系统化检查优先级配置、临界区使用和资源获取顺序,配合日志和工具,可以快速定位问题。建议在设计阶段就遵循“统一顺序、避免中断持锁、使用队列通信”的原则,从根源上减少死锁风险。