ESP32双核下FreeRTOS任务看门狗(TWDT)误触发:从IDLE任务饿死到Core亲和性配置

1. 问题现象与背景

在ESP32双核开发中,许多开发者遇到过这样的场景:系统运行一段时间后突然重启,串口打印Task watchdog got triggered,提示某个任务(通常是IDLE)长时间未喂狗。即使代码中并未显式启用TWDT,ESP-IDF默认在启动时初始化并启用任务看门狗,监控每个CPU核心的IDLE任务。一旦IDLE任务得不到调度,TWDT便会触发panic。

本文从IDLE任务饿死的根因出发,剖析TWDT工作机制,并给出基于CPU亲和性配置的解决方案。

2. TWDT与IDLE任务的关系

2.1 任务看门狗工作原理

ESP-IDF的TWDT(Task Watchdog Timer)是一个硬件定时器,用于监控任务是否正常运行。默认配置下:

  • 监控每个CPU核心的IDLE任务(IDLE0和IDLE1)。
  • 超时时间默认5秒(可配置)。
  • 若IDLE任务在超时时间内未运行(即未“喂狗”),TWDT触发中断并打印错误信息,最终重启系统。

2.2 IDLE任务的作用

FreeRTOS要求每个核心至少有一个空闲任务,用于:

  • 回收已删除任务的内存(清理TCB和栈)。
  • 进入低功耗模式(若启用tickless idle)。
  • 维持系统调度器正常运行。

IDLE任务优先级为0(最低),当有更高优先级任务就绪时,IDLE任务无法运行。

2.3 IDLE任务饿死的典型原因

  • 高优先级任务忙循环:任务中无阻塞调用(如vTaskDelay),一直占用CPU。
  • 中断风暴:高频中断导致任务无法调度。
  • 错误的任务亲和性配置:所有高优先级任务绑定到同一核心,导致该核心IDLE任务饿死,而另一核心空闲。

3. 复现IDLE任务饿死

以下代码创建一个高优先级任务,绑定到Core 1,且内部无阻塞,导致Core 1的IDLE1任务无法运行,TWDT触发。

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

void busy_task(void *pvParameters) {
    while (1) {
        // 忙等待,无阻塞
        volatile int i = 0;
        for (i = 0; i < 1000000; i++);
    }
}

void app_main() {
    // 默认TWDT已启用,监控IDLE任务
    xTaskCreatePinnedToCore(busy_task, "busy_task", 4096, NULL, 10, NULL, 1);
}

运行后,约5秒后串口输出:

E (5000) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (5000) task_wdt:  - IDLE1 (CPU 1)
E (5000) task_wdt: Tasks currently running:
E (5000) task_wdt: CPU 0: IDLE0
E (5000) task_wdt: CPU 1: busy_task

4. 解决方案:CPU亲和性与TWDT配置

4.1 合理分配任务到不同核心

避免所有高优先级任务集中在一个核心。使用xTaskCreatePinnedToCore的最后一个参数指定核心(0或1),或使用tskNO_AFFINITY让调度器自动分配。

4.2 为忙任务添加阻塞或主动喂狗

若任务必须长时间运行,可定期调用vTaskDelay(1)让出CPU,或使用esp_task_wdt_reset()手动喂狗(需先将任务添加到TWDT监控)。

4.3 调整TWDT超时或禁用IDLE监控

  • 初始化时配置超时时间:esp_task_wdt_init(10, true)(10秒超时,panic使能)。
  • 禁用IDLE任务监控:esp_task_wdt_delete(xTaskGetIdleTaskHandleForCPU(0))(不推荐,仅调试用)。

5. 完整示例:双核任务与TWDT和谐共处

以下代码创建两个任务,分别绑定到Core 0和Core 1,每个任务定期阻塞,确保IDLE任务有机会运行。

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

void task_core0(void *pvParameters) {
    while (1) {
        printf("Task on Core 0 running\n");
        vTaskDelay(pdMS_TO_TICKS(1000)); // 阻塞1秒,让出CPU
    }
}

void task_core1(void *pvParameters) {
    while (1) {
        printf("Task on Core 1 running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main() {
    // 可选:调整TWDT超时为10秒
    esp_task_wdt_init(10, true);

    xTaskCreatePinnedToCore(task_core0, "task_core0", 4096, NULL, 5, NULL, 0);
    xTaskCreatePinnedToCore(task_core1, "task_core1", 4096, NULL, 5, NULL, 1);
}

此时代码正常运行,IDLE任务定期获得调度,TWDT不会误触发。

6. 注意事项

  • 默认TWDT监控IDLE任务:ESP-IDF默认启用,且监控两个核心的IDLE任务,务必确保每个核心都有机会运行IDLE。
  • 避免在中断中长时间处理:中断服务程序应尽量短小,必要时使用中断延迟处理。
  • 谨慎使用vTaskSuspendAll:挂起调度器会阻止任务切换,导致IDLE饿死。
  • 调试时临时禁用TWDT:可通过menuconfig关闭CONFIG_ESP_TASK_WDT,但量产固件应保持启用。
  • 使用esp_task_wdt_add监控自定义任务:若需监控关键任务,可将其加入TWDT,并定期调用esp_task_wdt_reset()。

通过理解TWDT与IDLE任务的关系,合理配置任务优先级和CPU亲和性,可有效避免看门狗误触发,提升系统稳定性。