ESP32双核下FreeRTOS任务与WiFi协议栈抢CPU导致看门狗复位的定位方法

一、问题现象与背景

在ESP32上同时跑WiFi和高负载用户任务时,常见如下现象:

  • 设备运行数秒到数分钟后突然重启,串口打印 Task watchdog got triggered 或 Interrupt wdt timeout on CPU0/CPU1。
  • 复位原因寄存器显示 RTCWDT_RTC_RESET 或 TG0WDT_SYS_RESET。
  • 降低用户任务频率或优先级后问题消失,但无法解释根因。

ESP32是双核(PRO_CPU / APP_CPU),WiFi协议栈默认绑定在CPU0,并以较高优先级运行。若用户任务长期占用CPU0且不让出,协议栈任务得不到调度,看门狗便会复位系统。

二、原理:双核调度与看门狗

  • 任务看门狗(TWDT):默认监控IDLE任务与部分系统任务。IDLE任务长时间得不到运行,TWDT触发。
  • 中断看门狗(IWDT):监控中断是否长时间关闭,常见于临界区过长或关中断操作。
  • WiFi协议栈:wifi、tiT、pp 等任务优先级通常在18~23,绑定CPU0。用户任务若设为同等或更高优先级并死循环,会直接饿死协议栈。
  • 双核亲和性:xTaskCreatePinnedToCore 可指定核心。把计算密集任务放到CPU1,能显著降低与协议栈的冲突。

关键结论:看门狗复位往往不是看门狗本身的问题,而是CPU被长期占用或中断被长时间关闭的征兆。

三、定位步骤

步骤1:确认复位原因

读取复位原因,区分是任务看门狗、中断看门狗还是 panic:

#include "esp_system.h"

void print_reset_reason(void)
{
    esp_reset_reason_t r = esp_reset_reason();
    switch (r) {
    case ESP_RST_TASK_WDT:  printf("TASK_WDT\n");  break;
    case ESP_RST_INT_WDT:   printf("INT_WDT\n");   break;
    case ESP_RST_PANIC:     printf("PANIC\n");     break;
    case ESP_RST_POWERON:   printf("POWERON\n");   break;
    default:                printf("OTHER:%d\n", r); break;
    }
}

步骤2:开启看门狗打印与任务快照

在 menuconfig 中打开:

  • Component config -> ESP System Settings -> Panic handler behaviour -> Print registers and halt
  • Component config -> FreeRTOS -> Enable FreeRTOS trace facility(可选)

复位时串口会打印各任务状态(Running/Ready/Blocked)及栈回溯,重点看哪个任务长期处于Running。

步骤3:测量各任务CPU占用

用 vTaskGetRunTimeStats 获取各任务运行时间占比,快速找出“吃CPU大户”:

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

void cpu_usage_report(void)
{
    static char buf[1024];
    vTaskGetRunTimeStats(buf);
    printf("%s\n", buf);
}

需在 menuconfig 中开启 FreeRTOS -> Generate Run Time Stats,并配置高精度计时器。

步骤4:检查任务优先级与核心绑定

  • 用户计算任务优先级不要高于WiFi任务(一般≤10)。
  • 用 xTaskCreatePinnedToCore(..., 1) 绑定到CPU1。
  • 长循环中插入 vTaskDelay(1) 或 taskYIELD(),给同核低优先级任务让出时间。

步骤5:检查临界区与关中断

  • 避免在 portENTER_CRITICAL 内做耗时操作。
  • 避免长时间 taskDISABLE_INTERRUPTS()。
  • 使用 esp_timer 或队列代替忙等。

四、完整示例:安全的双核任务结构

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

static const char *TAG = "DUAL_CORE";

/* 计算密集任务:绑定CPU1,低优先级,主动让出 */
static void cpu_task(void *arg)
{
    while (1) {
        volatile uint32_t s = 0;
        for (uint32_t i = 0; i < 100000; i++) s += i;
        vTaskDelay(pdMS_TO_TICKS(10));   /* 关键:让出CPU */
    }
}

/* 监控任务:周期性打印CPU占用 */
static void monitor_task(void *arg)
{
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(5000));
        static char buf[1024];
        vTaskGetRunTimeStats(buf);
        ESP_LOGI(TAG, "\n%s", buf);
    }
}

void app_main(void)
{
    print_reset_reason();

    wifi_init_sta();  /* 用户自行实现WiFi初始化 */

    xTaskCreatePinnedToCore(cpu_task, "cpu_task", 4096, NULL,
                            5, NULL, 1);   /* CPU1,优先级5 */
    xTaskCreatePinnedToCore(monitor_task, "monitor", 4096, NULL,
                            3, NULL, 0);   /* CPU0,低优先级 */
}

五、注意事项

  • 不要盲目提高看门狗超时:延长超时只是掩盖问题,根因仍是CPU饥饿。
  • WiFi任务不要随意改优先级:协议栈内部有时序假设,改动易引入丢包或断连。
  • 双核共享资源要加锁:跨核访问同一变量使用 portMUX_TYPE 或原子操作。
  • 日志本身也耗CPU:高频 ESP_LOGI 会加剧抢占,定位阶段可降低日志等级。
  • 区分TWDT与IWDT:前者多为任务饿死,后者多为关中断过长,排查方向不同。
  • 复现时记录任务快照:在复位打印中保存各任务状态,比事后猜测更可靠。

按上述流程,通常能在10分钟内定位到是哪个任务长期占用CPU0或哪段临界区过长,从而针对性修复,而不是反复调整看门狗参数。