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 中检查:
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 简短。
// 定时器配置示例
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 不同的核心,并确保喂狗操作在任务内完成。
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 调试日志:
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)并注册任务看门狗,避免冲突。
#include <stdio.h>
#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、降低中断优先级、明确任务核心绑定,并配合日志定位,可以快速解决此类问题。希望本文的排查思路和代码示例能帮助你构建更稳定的嵌入式系统。