目录

一、看门狗

二、根据实验观察现象

三、原因分析

四、看门狗的分类

五、看门狗机制

六、规避机制

1、中断看门狗的规避机制

2、任务看门狗的规避机制

2.1 0号CPU空闲任务看门狗规避机制

2.2 1号CPU空闲任务看门狗规避机制

七、为任务增加看门狗机制

八、RTOS的其他任务函数

   1、获取任务运行在哪个CPU上

    2、获取任务的名字

    3、获取任务的优先级

    4、esp32内置task


一、看门狗

       看门狗(Watchdog Timer,简称WDT)是一个硬件计时器【倒计时计时器】,用于检测和恢复系统故障。它的工作原理就像一个"电子保安":

看门狗 = 一个倒计时器 + 一个复位机制

正常情况:程序定期"喂狗"(重置计时器)→ 系统正常运行
异常情况:程序忘记"喂狗"(计时器归零)→ 系统自动复位重启

重置计时器即重置计时器为最大值

二、根据实验观察现象

      下面给给出6个实验,根据实验观察现象 。

      实验一

需求:

假设现在我们要基于中断来实现一个需求,有一个计数器,初始值为0,每次按下按钮开关并释放时,计数值加1,需要定期通过串口打印计数器的当前值。

       使用中断完成需求代码:

#include <Arduino.h>

int count = 0;            // 按钮计数器
long last_press_time = 0; // 上一次按下按钮的时间,用于按钮消抖

void handle()
{
  // 两次按下的时间要大于500ms才认为是一次有效的操作
  if (millis() - last_press_time > 500)
  {
    count++;                    // 计数器加一
    Serial.println(count);      // 把当前计数值通过串口打印出来
    delay(8000);
    last_press_time = millis(); // 记录本次按钮按下的时间
  }
}

void setup()
{
  Serial.begin(115200);
  // 开启23号引脚的中断
  attachInterrupt(digitalPinToInterrupt(23), handle, RISING);
}

void loop()
{
}

       电路搭建如下:

        运行结果如下:

Description

      实验二

需求

基于Freertos,创建一个任务,该任务不断对一个计数器进行累加,并且该任务绑定到0号CPU,任务优先级为1

       使用任务函数完成需求代码:

#include <Arduino.h>

volatile uint64_t number = 0;

void count(void *ptr)
{
  while (1)
  {
    number++;
  }
}

void setup()
{
  Serial.begin(115200);
  // 任务优先级为1,绑定到0号CPU
  xTaskCreatePinnedToCore(count, "count", 2048, nullptr, 1, NULL, 0);
}

void loop()
{
}

       运行结果如下:

Description

      实验三

需求:

修改实验二的任务代码,每次对number累加后,通过串口输出

        完成需求代码,

void count(void *ptr)
{
  while (1)
  {
    number++;
    Serial.println(number);
  }
}

        运行结果:

  代码正常运行,计数器持续累加,esp32并未重启。

      实验四

需求:

保持实验二中的代码不变,仅修改创建任务的逻辑,将任务绑定到1号CPU

void setup()
{
  Serial.begin(115200);
  // 任务优先级为1,绑定到1号CPU
  xTaskCreatePinnedToCore(count, "count", 2048, nullptr, 1, NULL, 1);
}

        运行结果:

  代码正常运行,计数器持续累加,esp32并未重启。

      实验五

需求:

保持实验二中的代码不变,仅修改创建任务的逻辑,将任务优先级调整成0,仍然绑定到0号CPU上

void setup()
{
  Serial.begin(115200);
  // 任务优先级为0,绑定到0号CPU
  xTaskCreatePinnedToCore(count, "count", 2048, nullptr, 0, NULL, 0);
}

        运行结果:

  代码正常运行,计数器持续累加,esp32并未重启。

      实验六

需求:
保持实验二中的代码不变,仅修改任务逻辑,每次计数器+1后,通过vTaskDelay函数延迟1个tick

void count(void *ptr)
{
  while (1)
  {
    number++;
    vTaskDelay(1);
  }
}

    运行结果:

  代码正常运行,计数器持续累加,esp32并未重启。

      实验七

需求:

保持实验二中的代码不变,仅修改任务逻辑,每次计数器+1后,通过vTaskSuspend()函数让任务挂起,让随后又通过vTaskResume函数让任务继续运行

void count(void *ptr)
{
  while (1)
  {
    number++;
    vTaskSuspend(NULL);
    vTaskResume(NULL);
  }
}

    运行结果:

  代码正常运行,计数器持续累加,esp32并未重启。

注意:

  • vTaskSuspend这个函数的作用是让一个任务暂停,参数为NULL表示暂停当前任务;

  • vTaskResume这个函数的作用是让一个任务继续执行,参数为NULL表示恢复当前任务。

三、原因分析

        在使用中断函数、任务创建函数【未进行任何内容输出】,则会导致esp32重启。

        在使用任务创建函数【Serial输出、指定优先级、指定CPU、vTaskSuspend  +  vTaskResume】则不会导致esp32重启。

实验一 中断+delay

在中断服务函数(ISR)中调用delay(3000)导致FreeRTOS调度器断言失败,同时CPU被长期占用无法喂狗触发看门狗复位,双重故障最终导致系统重启。

按钮按下 → 触发中断 → 执行handle()
                           ↓
                    delay(3000) 被调用
                           ↓
              delay() → vTaskDelay()
                           ↓
         vTaskDelay()检查当前上下文
                           ↓
         发现当前在中断上下文(不是任务)
                           ↓
    尝试挂起"当前任务" → 但当前不是任务
                           ↓
        调度器状态变得不一致
                           ↓
     FreeRTOS检测到异常 → 触发断言
                           ↓
         assert失败 → 系统复位

   同时,看门狗也在等待喂食
             ↓
    看门狗超时 → 也触发复位
             ↓
    系统重启,打印错误信息
实验二

number++(死循环)

优先级1+ 0号cpu

0号CPU上默认运行着空闲任务(Idle Task,优先级0),但没有用户任务时,看门狗机制在单核模式下有时会不生效。

但是,优先级为1>0,执行count任务。这个任务一直霸占0号CPU时,0号CPU上的空闲任务得不到运行,即看门狗得不到投喂,系统仍然会因看门狗而重启。

实验三 number++ + Serial.println()优先级1+ 0号cpu 这是关键Serial.println()内部涉及互斥量(Mutex)硬件FIFO等待,当串口发送缓冲区满时,任务会进入阻塞状态(Blocked)以等待串口硬件。阻塞行为会喂狗(重置看门狗计时器),所以不会超时重启。
实验四

number++(死循环)

优先级1+ 1号cpu

这是ESP32双核的特殊性。任务看门狗默认只监控0号核心(Protocol CPU),而不监控1号核心(Application CPU)上的用户任务。因此,即使1号核心的任务死循环,也不会触发看门狗重启。
实验五

number++(死循环)

优先级0+ 0号cpu

优先级为0。看门狗机制只监控优先级大于0的任务,因为优先级0的任务被认为是“空闲任务”,可以随时被更高优先级的任务抢占。系统默认允许优先级0的任务无限运行。
实验六

number++(死循环)

优先级1+ 0号cpu

vTaskDelay()会让任务进入阻塞状态,主动让出CPU使用权。这个API调用会喂狗,所以看门狗计时器被重置,不会超时。
实验七

number++ + vTaskSuspend(NULL) + vTaskResume(NULL)

优先级1+ 0号cpu

虽然vTaskSuspend(NULL)挂起自己,但紧接着vTaskResume(NULL)又立即恢复自己。关键点在于:这两个API都会触发任务调度器的上下文切换,并且挂起/恢复操作会涉及系统内核的调度逻辑,这同样起到了“喂狗”的效果,重置了看门狗计时器。

    在上面实验中遇到的主要是任务看门狗、中断看门狗、系统看门狗。

四、看门狗的分类

    看门狗的核心作用:故障检测、系统恢复

    看门狗的分类:中断看门狗、任务看门狗、系统看门狗。

    中断看门狗(IWDT)

中断看门狗,简称IWDT(interrupt watch dof timer):

它是用于监控有没有哪一个中断处理程序执行时间过长(默认时间为800ms),如果时间过长,直接重启esp32。

   任务看门狗(TWDT)

任务看门狗,简称TWDT(task watch dog timer):

它用来看管freertos中的任务,被它看管的任务,如果长时间(超时时间为5秒)得不到CPU资源,没机会运行,它将重启esp32。        

五、看门狗机制

      中断看门狗机制:默认开启,所以我们在编写中断处理函数时,一定要让函数逻辑尽可能简单。

      任务看门狗机制:esp32有0号CPU和1号CPU,在每一个CPU上都有一个默认的任务,这个任务的优先级非常低(为0),并且它的作用有如下几个:

  • 当esp32处于空闲状态时,自动降低功耗,增加续航时间;
  • 当自己CPU下有任务被删除时,主动清理相关内存资源;
  • 重置看门狗定时器

       所以默认情况下,esp32为0号CPU上的空闲任务添加了看门狗监管逻辑,如果CPU0上的空闲任务,在5秒钟之内获取不到CPU资源,没有机会运行,它将触发看门狗重启机制,导致esp32重启。

六、规避机制

1、中断看门狗的规避机制

       对于中断看门狗来说,要避免esp32不被重启,需要尽可能降低中断处理函数的复杂度,让中断函数尽快运行结束(<800ms)。

  • 中断处理函数中的逻辑尽量简单快捷,时间<800ms。
  • 将复杂的延长时间的逻辑放到loop函数中执行。

    案例:

  • 每次只在中断处理函数中对计数器进行累加(这个操作非常轻量快速)
  • 定期通过串口打印计数器的逻辑可以放到loop函数中
#include <Arduino.h>



int count = 0;            // 按钮计数器

long last_press_time = 0; // 上一次按下按钮的时间,用于按钮消抖



void handle()

{

  // 两次按下的时间要大于500ms才认为是一次有效的操作

  if (millis() - last_press_time > 500)

  {

    count++;                    // 计数器加一

    last_press_time = millis(); // 记录本次按钮按下的时间

  }

}



void setup()

{

  Serial.begin(115200);

  // 开启23号引脚的中断

  attachInterrupt(digitalPinToInterrupt(23), handle, RISING);

}



void loop()

{

  Serial.println(count); // 把当前计数值通过串口打印出来

  delay(1000);

}

2、任务看门狗的规避机制

2.1 0号CPU空闲任务看门狗规避机制
禁用看门狗 调用 disableCore0WDT() 直接关闭硬件定时器,彻底取消监控机制。 极差(饮鸩止渴)系统失去最后一道防线,一旦任务死循环或死锁,将直接导致设备硬死机,且无任何恢复手段。
降低任务优先级 降低任务优先级与空闲任务相同优先级相同均为0,使二者按时间片轮转调度。 中等 若任务存在偶尔超时,IDLE可及时喂狗;但若任务持续高强度运算,仍可能饿死IDLE导致复位。
主动让出CPU

在高占用任务中插入 

vTaskDelay() 等阻塞函数,主动释放CPU控制权。

良好 从根源上保证IDLE任务能够定期获得CPU时间,喂狗稳定,系统鲁棒性高。

禁用看门狗

============================================================

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器
void cal(void *ptr)
{
  while (1)
  {
    count++;
  }
  vTaskDelete(NULL);
}

void setup()
{
  Serial.begin(115200);
  disableCore0WDT(); // 禁用CPU0上的看门狗机制
  // 任务优先级为1,分配到CPU0
  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 0);
}

void loop()
{
}

降低任务优先级

============================================================

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器
void cal(void *ptr)
{
  while (1)
  {
    count++;
  }
  vTaskDelete(NULL);
}

void setup()
{
  Serial.begin(115200);

  // 任务优先级为1,分配到CPU0
  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 0, NULL, 0);
}

void loop()
{
}

主动让出CPU

============================================================

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器
void cal(void *ptr)
{
  while (1)
  {
    count++;
    vTaskDelay(1); // 主动休眠1ms,让出CPU给IDLE任务
  }
  vTaskDelete(NULL);
}

void setup()
{
  Serial.begin(115200);

  // 任务优先级为1,分配到CPU0
  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 0);
}

void loop()
{
}

2.2 1号CPU空闲任务看门狗规避机制

       实验一:

       将任务运行在 1号CPU上,任务执行的优先级为0

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器

void cal(void *ptr)

{

  while (1)

  {

    count++;

  }

  vTaskDelete(NULL);

}

void setup()

{

  Serial.begin(115200);

  // 将任务创建在CPU1上,并且上一行启用了CPU1上的空闲任务看门狗,这个程序将会触发任务看门狗

  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 0, NULL, 1);

}

void loop()

{

}

       

       运行结果没有重启esp32,因为cal任务和空闲任务的优先级都是0。

      实验二:

       将任务运行在 1号CPU上,任务执行的优先级为1,这样 1号CPU空闲任务没有机会运行。

       xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 1);

       运行结果没有重启esp32。

        默认1号cpu上的空闲任务【优先级为0】没有被看门狗看管,如果需要启动则使用 enableCore1WDT(); // 启用用CPU1上的看门狗机制。

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器

void cal(void *ptr)

{

  while (1)

  {

    count++;

  }

  vTaskDelete(NULL);

}

void setup()

{

  Serial.begin(115200);

  enableCore1WDT();

  // 将任务创建在CPU1上,并且上一行启用了CPU1上的空闲任务看门狗,这个程序将会触发任务看门狗

  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 1);

}

void loop()

{

}

         开启后,Esp32进行了重启。

      观察:

      使用之前的规避机制,在任务中增加主动让出CPU查看效果。

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器

void cal(void *ptr)

{

  while (1)

  {

    count++;

    vTaskDelay(1);

  }

  vTaskDelete(NULL);

}

void setup()

{

  Serial.begin(115200);

  enableCore1WDT();

  // 将任务创建在CPU1上,并且上一行启用了CPU1上的空闲任务看门狗,这个程序将会触发任务看门狗

  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 1);

}

void loop()

{

}

      观察后,仍然进行了重启,并提示在CPU1号上有一个任务loopTask 即 loop 任务。底层自动运行了一个 loop 任务,这个任务一直占用CPU1号。因此导致看门狗进行重启。

     因此需要在 loop  中释放 CPU。

#include <Arduino.h>

volatile uint64_t count = 0; // 创建一个计数器

void cal(void *ptr)

{

  while (1)

  {

    count++;

    vTaskDelay(1);

  }

  vTaskDelete(NULL);

}

void setup()

{

  Serial.begin(115200);

  enableCore1WDT();

  // 将任务创建在CPU1上,并且上一行启用了CPU1上的空闲任务看门狗,这个程序将会触发任务看门狗

  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 1);

}

void loop()

{

  vTaskDelay(1);

}

      运行结果未进行重启esp32

七、为任务增加看门狗机制

      需要引入头文件【引入看门狗相关函数的文件】:esp_task_wdt.h 

      在代码中使用 esp_task_wdt_add() 函数申请使用看门狗进行看护。

#include <Arduino.h>

#include <esp_task_wdt.h>

volatile uint64_t count = 0; // 创建一个计数器

void cal(void *ptr)

{

  esp_task_wdt_add(NULL); // 申请看门狗看护本任务

  while (1)

  {

    count++;

    esp_task_wdt_reset(); // 重置倒计时定时器

  }

  vTaskDelete(NULL);

}

void setup()

{

  Serial.begin(115200);

  // 将任务创建在CPU1上,并且上一行启用了CPU1上的空闲任务看门狗,这个程序将会触发任务看门狗

  xTaskCreatePinnedToCore(cal, "cal", 2048, nullptr, 1, NULL, 1);

}

void loop()

{

  vTaskDelay(1);

}

八、RTOS的其他任务函数

   1、获取任务运行在哪个CPU上

int xPortGetCoreID(void)
该函数没有参数,就是返回调用者所在任务运行在哪个CPU上,返回值为0或者1

    2、获取任务的名字

 char * pcTaskGetName( TaskHandle_t xTaskToQuery )

 参数为任务句柄,如果填NULL,则获取当前任务的名称,返回值为一个字符串

    3、获取任务的优先级

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask )

参数为任务句柄,如果填NULL,则获取当前任务的优先级,返回值为一个整数

    4、esp32内置task

  • 两个CPU上默认各有一个空闲任务,名叫Idle task,优先级最低,都为0
  • 在CPU1上默认有一个loopTask,就是Arduino中的setup和loop函数所在的任务,优先级为1,创建该函数的源码在文件.platformio\packages\framework-arduinoespressif32\cores\esp32\main.cpp

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐