# ESP32 双核环境下 FreeRTOS 任务与中断优先级嵌套导致死锁的排查方法 ## 一、问题背景 ESP32 搭载 Xtensa 双核处理器,FreeRTOS 默认支持对称多处理(SMP),任务可运行在任意核心上。当任务与中断(ISR)共享资源时,若优先级嵌套设计不当,极易引发死锁。典型症状:系统运行一段时间后无响应,看门狗超时复位,或低优先级任务永远得不到执行。 ## 二、死锁根因分析 ### 1. 双核调度特性 - 每个核心独立运行一个 FreeRTOS 调度器,但共享同一任务就绪列表。 - 任务可被迁移到另一个核心,但中断始终绑定在特定核心(ESP32 中中断可配置为任意核心)。 - 自旋锁(spinlock)用于保护内核数据结构,但用户代码中的临界区需显式处理。 ### 2. 优先级嵌套陷阱 FreeRTOS 中,中断优先级高于任务优先级。当 ISR 尝试获取一个被任务持有的互斥量(Mutex)时,若该任务被更高优先级任务抢占,则可能形成循环等待。 典型死锁场景: - 任务A(低优先级)持有互斥量M,正在访问共享外设。 - 中断ISR(高优先级)触发,尝试获取M,但M被A持有,ISR阻塞(FreeRTOS 中 ISR 不能阻塞,但若使用 `xQueueSendFromISR` 等可能导致任务级死锁)。 - 任务B(中优先级)就绪,抢占A,A无法释放M,ISR 等待,B 等待ISR结果,形成死锁。 ### 3. 双核加剧问题 - 任务A在核心0运行,ISR在核心1触发,核心1的ISR等待核心0的任务释放M,但核心0可能正在执行更高优先级任务,导致核心0无法调度A。 - 若两个核心同时进入临界区,且使用 `portENTER_CRITICAL` 未处理多核,会引发系统崩溃。 ## 三、排查方法 ### 1. 启用 FreeRTOS 调试功能 在 `sdkconfig` 中启用: - `CONFIG_FREERTOS_DEBUG_OCDAWARE`:支持 JTAG 调试。 - `CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE`:注册队列/互斥量便于查看。 - `CONFIG_FREERTOS_USE_TRACE_FACILITY`:启用跟踪。 ### 2. 添加死锁检测钩子 在 `FreeRTOSConfig.h` 中定义: ```c #define configUSE_MUTEX_DEADLOCK_DETECTION 1 ``` 并实现钩子函数: ```c void vApplicationMutexDeadlockDetected( Mutex_t *pxMutex ) { // 打印互斥量名称和持有者任务 ESP_LOGE("DEADLOCK", "Mutex %s held by %s", pcMutexName(pxMutex), pcTaskGetTaskName(pxMutex->u.xOwner)); // 可触发断点或重启 } ``` 注意:该功能需要 `configUSE_TRACE_FACILITY` 和 `configUSE_STATS_FORMATTING_FUNCTIONS`。 ### 3. 使用静态分析工具 - 使用 `FreeRTOS+Trace` 或 `SystemView` 记录任务状态和中断事件,分析时序。 - 在代码中定期打印任务状态: ```c void printTaskStats(void) { TaskStatus_t xTaskStatus; vTaskGetInfo(NULL, &xTaskStatus, pdTRUE, eInvalid); ESP_LOGI("TASK", "Task %s state=%d prio=%d", xTaskStatus.pcTaskName, xTaskStatus.eCurrentState, xTaskStatus.uxCurrentPriority); } ``` ### 4. 日志追踪法 在关键临界区前后添加日志,使用时间戳定位卡死点: ```c #define LOG_CRITICAL_ENTER(mutex) ESP_LOGI("LOCK", "Enter %s at %lld", mutex->name, esp_timer_get_time()) #define LOG_CRITICAL_EXIT(mutex) ESP_LOGI("LOCK", "Exit %s at %lld", mutex->name, esp_timer_get_time()) ``` ## 四、完整代码示例 以下示例演示一个典型死锁场景及修复方法。 ### 1. 错误示例(死锁) ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_log.h" static SemaphoreHandle_t mutex; static const char *TAG = "DEADLOCK"; // 模拟共享资源 void access_shared() { vTaskDelay(pdMS_TO_TICKS(100)); // 模拟耗时操作 } // 低优先级任务 void taskA(void *arg) { while (1) { xSemaphoreTake(mutex, portMAX_DELAY); ESP_LOGI(TAG, "TaskA acquired mutex"); access_shared(); xSemaphoreGive(mutex); vTaskDelay(pdMS_TO_TICKS(10)); } } // 中优先级任务 void taskB(void *arg) { while (1) { vTaskDelay(pdMS_TO_TICKS(50)); ESP_LOGI(TAG, "TaskB running"); // 模拟CPU密集型操作,抢占A for (int i = 0; i < 100000; i++) {} } } // 中断模拟(使用定时器) void timer_isr(void *arg) { // 尝试获取互斥量(错误:ISR中不能阻塞) if (xSemaphoreTakeFromISR(mutex, NULL) != pdTRUE) { ESP_DRAM_LOGE(TAG, "ISR failed to get mutex"); } else { ESP_DRAM_LOGI(TAG, "ISR got mutex"); xSemaphoreGiveFromISR(mutex, NULL); } } void app_main() { mutex = xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(taskA, "A", 2048, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(taskB, "B", 2048, NULL, 2, NULL, 1); // 创建定时器中断 esp_timer_create_args_t args = {.callback = timer_isr, .arg = NULL}; esp_timer_handle_t timer; esp_timer_create(&args, &timer); esp_timer_start_periodic(timer, 10000); // 10ms周期 } ``` 运行后,系统可能卡死,因为ISR中尝试获取互斥量,但互斥量被A持有,而A被B抢占,ISR无法完成。 ### 2. 修复方案 - 中断中不直接获取互斥量,而是通过队列通知任务。 - 使用二值信号量代替互斥量,并确保在ISR中只做非阻塞操作。 ```c // 修复后的ISR void timer_isr(void *arg) { // 发送通知给任务,让任务处理 BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(binarySem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } // 任务C处理共享资源 void taskC(void *arg) { while (1) { xSemaphoreTake(binarySem, portMAX_DELAY); // 安全访问共享资源 xSemaphoreTake(mutex, portMAX_DELAY); access_shared(); xSemaphoreGive(mutex); } } ``` ## 五、注意事项 - **中断中禁止调用阻塞API**:如 `xSemaphoreTake`,应使用 `FromISR` 结尾的函数。 - **多核临界区**:使用 `portENTER_CRITICAL` 时,需确保在同一个核心上进入和退出,否则使用 `spinlock` 或 `portENTER_CRITICAL_ISR`。 - **优先级设计**:避免任务优先级与中断优先级形成嵌套等待,可采用优先级继承(互斥量默认支持)。 - **使用看门狗**:启用任务看门狗,检测任务饿死。 - **测试覆盖**:进行压力测试,模拟中断高频触发场景。 ## 六、总结 ESP32 双核环境下的死锁排查需要结合 FreeRTOS 调度机制、中断行为和多核特性。通过启用调试钩子、日志追踪和静态分析,可以快速定位问题。核心原则是:中断中绝不阻塞,任务间共享资源使用互斥量并注意优先级翻转,多核访问使用自旋锁。希望本文的方法能帮助开发者高效解决此类问题。