ESP32基础知识-看门狗
目录
一、看门狗
看门狗(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)中调用 按钮按下 → 触发中断 → 执行handle() 同时,看门狗也在等待喂食
↓
看门狗超时 → 也触发复位
↓
系统重启,打印错误信息 |
| 实验二 |
优先级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)以等待串口硬件。阻塞行为会喂狗(重置看门狗计时器),所以不会超时重启。 |
| 实验四 |
优先级1+ 1号cpu |
这是ESP32双核的特殊性。任务看门狗默认只监控0号核心(Protocol CPU),而不监控1号核心(Application CPU)上的用户任务。因此,即使1号核心的任务死循环,也不会触发看门狗重启。 |
| 实验五 |
优先级0+ 0号cpu |
优先级为0。看门狗机制只监控优先级大于0的任务,因为优先级0的任务被认为是“空闲任务”,可以随时被更高优先级的任务抢占。系统默认允许优先级0的任务无限运行。 |
| 实验六 |
优先级1+ 0号cpu |
vTaskDelay()会让任务进入阻塞状态,主动让出CPU使用权。这个API调用会喂狗,所以看门狗计时器被重置,不会超时。 |
| 实验七 |
优先级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 |
在高占用任务中插入
|
良好 从根源上保证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中


更多推荐



所有评论(0)