目录
一、看门狗
看门狗(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()
{
}
电路搭建如下:

运行结果如下:

实验二
需求:
基于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()
{
}
运行结果如下:

实验三
需求:
修改实验二的任务代码,每次对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函数中
cs#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 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()
{
}