ESP32双核任务看门狗超时:从任务饥饿到TWDT触发路径的逐层定位

一、问题背景:TWDT超时为何难定位

ESP32的任务看门狗(Task Watchdog Timer,TWDT)默认监控IDLE任务与用户注册任务。当被监控任务在设定周期内未“喂狗”,TWDT会打印各CPU当前运行任务与调用栈,随后复位。

难点在于:超时点往往不是根因点。真正的问题可能是某高优先级任务长期占用CPU(任务饥饿)、临界区过长、或双核间共享资源竞争导致低优先级喂狗任务得不到调度。

二、TWDT与调度器原理速览

  • TWDT由定时器组驱动,周期默认5s,可配置。
  • 每个被监控任务需周期性调用esp_task_wdt_reset()。
  • IDLE任务默认被监控:若某CPU的IDLE任务长时间得不到运行,说明该核被高优先级任务占满。
  • FreeRTOS为抢占式调度,同优先级时间片轮转;高优先级任务不阻塞则低优先级永远得不到CPU。
  • 双核下xTaskCreatePinnedToCore决定任务绑定核,跨核竞争会放大饥饿概率。

三、触发路径逐层拆解

  1. 应用层:某任务进入长循环或忙等,未调用阻塞API。
  2. 调度层:该任务优先级高于喂狗任务,且不主动让出CPU,喂狗任务处于Ready但不可运行。
  3. TWDT层:定时器到期,检查各监控任务时间戳,发现超时。
  4. 异常层:打印Task watchdog got triggered及各核任务列表,触发panic复位。

关键判据:日志中若显示某核当前任务为业务任务而非IDLE,且喂狗任务在同一核处于Ready态,基本可判定为任务饥饿。

四、配置步骤

  1. menuconfig中开启Component config → ESP System Settings → Task Watchdog,设置周期。
  2. 初始化TWDT并添加需监控的任务句柄。
  3. 在监控任务循环内调用复位接口。
  4. 保留IDLE监控以检测CPU饥饿。

五、完整代码示例

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_task_wdt.h"
#include "esp_log.h"

static const char *TAG = "TWDT_DEMO";
static TaskHandle_t s_feed_task = NULL;

// 喂狗任务:低优先级,绑定CPU0
static void feed_task(void *arg)
{
    esp_task_wdt_add(NULL); // 监控自身
    while (1) {
        esp_task_wdt_reset();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// 饥饿制造任务:高优先级忙等,绑定同一CPU
static void hog_task(void *arg)
{
    ESP_LOGW(TAG, "hog task start, will starve feed task");
    volatile uint32_t x = 0;
    while (1) {
        x++; // 忙等,不让出CPU
        if ((x & 0xFFFFFF) == 0) {
            ESP_LOGI(TAG, "hog alive");
        }
    }
}

void app_main(void)
{
    esp_task_wdt_config_t cfg = {
        .timeout_ms = 5000,
        .idle_core_mask = (1 << 0), // 监控CPU0的IDLE
        .trigger_panic = true,
    };
    ESP_ERROR_CHECK(esp_task_wdt_init(&cfg));

    xTaskCreatePinnedToCore(feed_task, "feed", 2048, NULL, 3, &s_feed_task, 0);
    vTaskDelay(pdMS_TO_TICKS(100));
    // 优先级5高于feed的3,且忙等不阻塞 → feed饥饿
    xTaskCreatePinnedToCore(hog_task, "hog", 2048, NULL, 5, NULL, 0);
}

运行后约5s触发TWDT,日志会显示CPU0当前任务为hog,feed处于Ready,IDLE未运行,直接印证任务饥饿路径。

六、定位技巧

  • 看日志中CPU0/CPU1当前任务名与状态,区分“忙等”与“阻塞”。
  • 用uxTaskGetSystemState()周期性打印各任务运行计数,观察喂狗任务计数是否停滞。
  • 检查临界区:portENTER_CRITICAL内禁止长耗时操作。
  • 双核场景确认喂狗任务与业务任务是否同核,必要时分离到不同核。

七、注意事项

  • 喂狗任务优先级不宜过低,否则易被业务任务饿死;也不宜过高,避免掩盖真实阻塞。
  • 忙等循环中插入vTaskDelay(0)或taskYIELD()可让出CPU,但治标不治本,应重构为事件驱动。
  • 临界区与关中断时间必须远小于TWDT周期。
  • 调试阶段可先设trigger_panic=false仅打印,便于抓取现场。
  • 生产环境建议结合esp_task_wdt_add精确监控关键任务,而非仅依赖IDLE监控。