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 中不要调用 printfmalloc 等非中断安全函数,否则会阻塞 TWDT。
  • 喂狗策略:任务中喂狗应放在循环末尾,且任务阻塞时间不能超过看门狗超时时间。
  • 多核调试:使用 xTaskGetCurrentTaskHandle()vTaskGetRunTimeStats() 辅助分析任务运行情况。
  • 测试环境:在修改中断优先级或核心绑定后,务必进行压力测试(如长时间运行、高频中断)。

六、总结

ESP32 双核环境下的 TWDT 与硬件定时器冲突,主要源于资源占用、中断优先级和核心调度。通过合理分配 Timer Group、降低中断优先级、明确任务核心绑定,并配合日志定位,可以快速解决此类问题。希望本文的排查思路和代码示例能帮助你构建更稳定的嵌入式系统。