# ESP32 双核环境下 FreeRTOS 任务看门狗与硬件定时器冲突的排查方法 ## 一、问题现象与背景 在 ESP32 双核(PRO_CPU 和 APP_CPU)上使用 FreeRTOS 时,开发者常遇到以下现象: - 系统周期性复位,`esp_task_wdt` 打印 `Task watchdog got triggered`。 - 硬件定时器中断服务函数(ISR)不执行,或执行后任务卡死。 - 单独运行定时器或看门狗时正常,但合并后崩溃。 这些问题的根源在于 ESP32 的硬件定时器(Timer Group 0/1)与 FreeRTOS 任务看门狗(Task Watchdog Timer, TWDT)共享了部分硬件资源,且中断优先级和 CPU 核心分配不当会加剧冲突。 ## 二、原理剖析 ### 1. 任务看门狗(TWDT)工作机制 ESP-IDF 中的任务看门狗基于硬件定时器(默认使用 Timer Group 0 的某个通道),它周期性地检查注册的任务是否在超时时间内喂狗(`esp_task_wdt_reset`)。如果某个任务因死循环或阻塞过久未喂狗,TWDT 会触发中断并复位系统。 关键点:TWDT 使用的中断优先级通常为 1(最低),且运行在 PRO_CPU 上。 ### 2. 硬件定时器(Timer Group) ESP32 有两个 Timer Group,每组有两个 64 位定时器。开发者常使用 `timer_init` 配置定时器,并注册中断回调。硬件定时器中断优先级可配置为 1~3(ESP32 中优先级 1 最低,3 最高)。 ### 3. 冲突根因 - **资源占用**:TWDT 默认占用 Timer Group 0 的某个定时器通道,若开发者同时使用同一组定时器,会导致配置冲突或中断互相干扰。 - **中断优先级**:若硬件定时器中断优先级高于 TWDT,且 ISR 执行时间过长,会阻塞 TWDT 的中断处理,导致看门狗误触发。 - **CPU 核心绑定**:FreeRTOS 任务可绑定到特定核心,而 TWDT 中断只在 PRO_CPU 上触发。若某个任务绑定在 APP_CPU 且长时间运行,但喂狗操作在 PRO_CPU 上,可能因调度延迟导致超时。 ## 三、排查步骤 ### 步骤 1:确认 TWDT 使用的定时器资源 在 `sdkconfig` 中检查: ```c CONFIG_ESP_TASK_WDT_TIMER_GROUP=0 // 默认使用 Timer Group 0 CONFIG_ESP_TASK_WDT_TIMER_INDEX=0 // 使用该组的第 0 个定时器 ``` 若你的硬件定时器也使用 Timer Group 0,请改为 Timer Group 1,或调整 TWDT 的定时器索引。 ### 步骤 2:检查中断优先级 确保硬件定时器中断优先级不高于 TWDT(优先级 1)。建议将硬件定时器中断优先级设为 1,并保持 ISR 简短。 ```c // 定时器配置示例 timer_config_t config = { .divider = 80, // 1MHz 计数 .counter_dir = TIMER_COUNT_UP, .counter_en = TIMER_PAUSE, .alarm_en = TIMER_ALARM_EN, .auto_reload = true, }; timer_init(TIMER_GROUP_1, TIMER_0, &config); timer_set_counter_value(TIMER_GROUP_1, TIMER_0, 0); timer_set_alarm_value(TIMER_GROUP_1, TIMER_0, 1000000); // 1s timer_enable_intr(TIMER_GROUP_1, TIMER_0); timer_isr_callback_add(TIMER_GROUP_1, TIMER_0, timer_isr, NULL, 1); // 优先级 1 ``` ### 步骤 3:验证 CPU 核心绑定 使用 `xTaskCreatePinnedToCore` 创建任务时,明确指定核心。若任务需要长时间运行,建议绑定到与 TWDT 不同的核心,并确保喂狗操作在任务内完成。 ```c void task_function(void *arg) { while (1) { // 业务逻辑 esp_task_wdt_reset(); // 喂狗 vTaskDelay(pdMS_TO_TICKS(100)); } } // 绑定到 APP_CPU (核心 1) xTaskCreatePinnedToCore(task_function, "task", 4096, NULL, 5, NULL, 1); ``` ### 步骤 4:使用日志定位 开启 TWDT 调试日志: ```c esp_task_wdt_config_t twdt_config = { .timeout_ms = 5000, .idle_core_mask = (1 << 0) | (1 << 1), // 两个核心的空闲任务都喂狗 .trigger_panic = true, }; esp_task_wdt_init(&twdt_config); esp_task_wdt_add(NULL); // 添加当前任务 ``` 当触发时,日志会打印所有未喂狗的任务名称和核心,据此判断冲突任务。 ## 四、完整代码示例 以下示例演示如何正确配置硬件定时器(使用 Timer Group 1)并注册任务看门狗,避免冲突。 ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_task_wdt.h" #include "driver/timer.h" // 定时器 ISR 回调(简短执行) bool timer_isr_cb(void *args) { // 处理定时事件,例如翻转 GPIO // 注意:不要做耗时操作,如 printf return true; // 返回 true 以重新使能报警 } void setup_timer() { timer_config_t config = { .divider = 80, .counter_dir = TIMER_COUNT_UP, .counter_en = TIMER_PAUSE, .alarm_en = TIMER_ALARM_EN, .auto_reload = true, }; timer_init(TIMER_GROUP_1, TIMER_0, &config); timer_set_counter_value(TIMER_GROUP_1, TIMER_0, 0); timer_set_alarm_value(TIMER_GROUP_1, TIMER_0, 1000000); // 1s timer_enable_intr(TIMER_GROUP_1, TIMER_0); timer_isr_callback_add(TIMER_GROUP_1, TIMER_0, timer_isr_cb, NULL, 1); // 优先级 1 timer_start(TIMER_GROUP_1, TIMER_0); } void worker_task(void *arg) { // 注册任务到看门狗 esp_task_wdt_add(NULL); while (1) { // 模拟业务处理 vTaskDelay(pdMS_TO_TICKS(100)); esp_task_wdt_reset(); // 喂狗 } } void app_main() { // 初始化 TWDT,超时 5 秒 esp_task_wdt_config_t twdt_config = { .timeout_ms = 5000, .idle_core_mask = (1 << 0) | (1 << 1), .trigger_panic = true, }; esp_task_wdt_init(&twdt_config); // 配置硬件定时器(使用 Timer Group 1) setup_timer(); // 创建任务,绑定到 APP_CPU xTaskCreatePinnedToCore(worker_task, "worker", 4096, NULL, 5, NULL, 1); } ``` ## 五、注意事项 - **避免使用 Timer Group 0**:除非你明确知道 TWDT 已禁用或改用其他组,否则硬件定时器优先使用 Timer Group 1。 - **ISR 保持简短**:硬件定时器 ISR 中不要调用 `printf`、`malloc` 等非中断安全函数,否则会阻塞 TWDT。 - **喂狗策略**:任务中喂狗应放在循环末尾,且任务阻塞时间不能超过看门狗超时时间。 - **多核调试**:使用 `xTaskGetCurrentTaskHandle()` 和 `vTaskGetRunTimeStats()` 辅助分析任务运行情况。 - **测试环境**:在修改中断优先级或核心绑定后,务必进行压力测试(如长时间运行、高频中断)。 ## 六、总结 ESP32 双核环境下的 TWDT 与硬件定时器冲突,主要源于资源占用、中断优先级和核心调度。通过合理分配 Timer Group、降低中断优先级、明确任务核心绑定,并配合日志定位,可以快速解决此类问题。希望本文的排查思路和代码示例能帮助你构建更稳定的嵌入式系统。