Watchdog trên ESP32: vì sao mạch tự khởi động lại, đọc lý do reset và tự cứu khi chương trình treo
Watchdog là gì, task watchdog, interrupt watchdog, panic và brownout trên ESP32, in lý do khởi động lại bằng esp_reset_reason, đếm số lần khởi động, cho task tự đăng ký watchdog.
"Mạch ESP32 cứ chạy một lúc lại tự khởi động lại" là một trong những câu hỏi gặp nhiều nhất. Thủ phạm thường là watchdog, lỗi chương trình hoặc nguồn yếu, và ESP32 luôn ghi lại lần khởi động gần nhất là do đâu. Bài này giải thích watchdog là gì, ESP32 có những loại nào, cách in lý do khởi động lại ngay đầu chương trình, cách dùng task watchdog để mạch tự cứu khi bị treo, và cách phân biệt với sụt áp nguồn.
1. Watchdog là gì

Watchdog (chó canh cửa) là một bộ đếm ngược chạy độc lập với chương trình. Chương trình phải đều đặn "cho ăn" (đặt lại bộ đếm) để chứng tỏ mình vẫn chạy bình thường. Nếu chương trình bị kẹt trong một vòng lặp, chờ một thứ không bao giờ tới, hay bị lỗi làm đứng, việc cho ăn dừng lại; bộ đếm về 0 và watchdog khởi động lại chip.
Với thiết bị đặt ở xa (trạm đo ngoài vườn, bộ điều khiển trong tủ điện), khởi động lại tốt hơn nhiều so với đứng im mãi tới khi có người tới rút điện. Nhưng watchdog không sửa lỗi: nó chỉ đưa mạch về trạng thái ban đầu, lỗi vẫn còn và sẽ lặp lại.
2. Những thứ làm ESP32 tự khởi động lại

- Task watchdog (TWDT): theo dõi các task đã đăng ký. Mặc định trên Arduino ESP32 nó theo dõi task rỗi (IDLE) của nhân 0: nếu một task chạy liên tục không nhường quá 5 giây ở nhân 0, task rỗi không được chạy và watchdog báo lỗi.
loop()không được theo dõi trừ khi gọienableLoopWDT(). - Interrupt watchdog (IWDT): bảo đảm ngắt luôn được phục vụ. Hàm ngắt chạy quá lâu, hoặc chương trình tắt ngắt (đoạn khóa
portENTER_CRITICAL) quá 300 ms, là bị khởi động lại. - Panic: không phải watchdog mà là lỗi CPU: ghi vào con trỏ rỗng, chia số nguyên cho 0, tràn stack, gọi hàm không có trong IRAM khi đang ghi flash. Serial in "Guru Meditation Error" kèm địa chỉ lệnh lỗi.
- Brownout: điện áp nguồn của chip tụt dưới ngưỡng an toàn. Hay gặp nhất lúc bật Wi-Fi (dòng vọt lên vài trăm mA), đóng relay, chạy động cơ, hoặc dùng dây USB dài, mảnh.
3. In lý do khởi động lại
esp_reset_reason() trả về lý do của lần khởi động gần nhất: bật nguồn, lệnh ESP.restart(), panic, interrupt watchdog, task watchdog, thức dậy từ deep sleep, brownout. Chương trình trong bài in lý do này ngay đầu setup(). Thêm hai dòng này vào mọi dự án là cách nhanh nhất để biết mạch "tự reset" vì đâu.
Để đếm số lần khởi động, chương trình dùng biến RTC_NOINIT_ATTR: biến này nằm trong bộ nhớ RTC, không bị xóa khi chip khởi động lại bằng phần mềm hay watchdog, nhưng chứa giá trị rác khi vừa cấp điện. Vì vậy chương trình kiểm tra lý do là bật nguồn, hoặc giá trị "dấu hiệu" không khớp, thì đặt lại bộ đếm. Cách này dùng được để phát hiện mạch đang khởi động lại liên tục và chuyển sang chế độ an toàn (ví dụ tắt tính năng vừa cập nhật).
4. Chương trình
Chương trình đổi hạn của task watchdog thành 3 giây và bật chế độ khởi động lại khi quá hạn (trigger_panic = true), rồi tạo một task làm việc tự đăng ký với watchdog bằng esp_task_wdt_add(NULL) và cho ăn bằng esp_task_wdt_reset() mỗi vòng. Gõ một chữ trên Serial Monitor để thử từng loại:
T: task làm việc bị kẹt trong vòng lặp vô hạn, ngừng cho ăn → task watchdog.I: tắt ngắt rồi kẹt → interrupt watchdog sau khoảng 300 ms.P: ghi vào địa chỉ 0 → panic.R:ESP.restart()→ khởi động lại bằng lệnh.
// PYWORLD.VN - Watchdog trên ESP32: in lý do khởi động lại, đếm số lần khởi động, task watchdog 3 giây tự cứu khi chương trình treo
// Gõ trên Serial Monitor: T = task bị treo (task watchdog), I = treo khi tắt ngắt (interrupt watchdog), P = lỗi con trỏ (panic), R = khởi động lại bằng lệnh
// Chỉ cần ESP32, không cần thư viện ngoài. Mỗi lệnh thử đều làm chip khởi động lại: đó là điều bài muốn quan sát
#include <esp_task_wdt.h>
RTC_NOINIT_ATTR uint32_t dauHieu; // vùng RTC không bị xóa khi khởi động lại bằng phần mềm (mất khi rút điện)
RTC_NOINIT_ATTR uint32_t soLanKhoiDong;
const uint32_t DAU_HIEU = 0x50574C44;
TaskHandle_t taskLamViec;
volatile bool giaLapTreo = false;
const char *lyDo(esp_reset_reason_t r) {
switch (r) {
case ESP_RST_POWERON: return "cap dien (bat nguon)";
case ESP_RST_SW: return "lenh khoi dong lai (esp_restart)";
case ESP_RST_PANIC: return "loi chuong trinh (panic)";
case ESP_RST_INT_WDT: return "interrupt watchdog";
case ESP_RST_TASK_WDT: return "task watchdog";
case ESP_RST_WDT: return "watchdog khac";
case ESP_RST_DEEPSLEEP: return "thuc day tu deep sleep";
case ESP_RST_BROWNOUT: return "sut ap nguon (brownout)";
default: return "khac";
}
}
// Task đăng ký với task watchdog: mỗi vòng làm việc phải "cho ăn" (esp_task_wdt_reset), quá 3 giây không cho ăn là bị xử lý.
void lamViec(void *thamSo) {
esp_task_wdt_add(NULL);
uint32_t vong = 0;
for (;;) {
if (giaLapTreo) {
Serial.println("Task lam viec bi treo (vong lap khong thoat)...");
while (true) {} // giả lập lỗi: kẹt mãi, không bao giờ cho ăn
}
vong++;
if (vong % 10 == 0) Serial.printf("Lam viec binh thuong, vong %lu\n", (unsigned long)vong);
esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void setup() {
Serial.begin(115200);
delay(300);
esp_reset_reason_t r = esp_reset_reason();
if (r == ESP_RST_POWERON || dauHieu != DAU_HIEU) { // bật nguồn: vùng RTC chứa rác, khởi tạo lại
dauHieu = DAU_HIEU;
soLanKhoiDong = 0;
}
soLanKhoiDong++;
Serial.printf("\nKhoi dong lan %lu. Ly do: %s\n", (unsigned long)soLanKhoiDong, lyDo(r));
esp_task_wdt_config_t cauHinh = {
.timeout_ms = 3000, // 3 giây không cho ăn thì báo lỗi
.idle_core_mask = 1 << 0, // vẫn theo dõi task rỗi của nhân 0
.trigger_panic = true, // báo lỗi rồi khởi động lại, không chỉ in cảnh báo
};
esp_task_wdt_reconfigure(&cauHinh);
xTaskCreatePinnedToCore(lamViec, "lam viec", 4096, NULL, 1, &taskLamViec, 1);
Serial.println("Go T, I, P hoac R de thu");
}
void loop() {
if (!Serial.available()) {
vTaskDelay(pdMS_TO_TICKS(50));
return;
}
char c = toupper(Serial.read());
if (c == 'T') {
giaLapTreo = true;
} else if (c == 'I') {
Serial.println("Tat ngat roi treo: interrupt watchdog se khoi dong lai sau khoang 300 ms");
Serial.flush();
portDISABLE_INTERRUPTS();
while (true) {}
} else if (c == 'P') {
Serial.println("Ghi vao dia chi 0: chip bao loi (Guru Meditation) roi khoi dong lai");
Serial.flush();
volatile uint32_t *conTro = nullptr;
*conTro = 1;
} else if (c == 'R') {
Serial.println("Khoi dong lai bang lenh");
Serial.flush();
ESP.restart();
}
}
Mỗi lệnh thử đều làm chip khởi động lại, không hại gì tới mạch. Sau mỗi lần, dòng đầu tiên cho biết đúng lý do vừa xảy ra và số lần khởi động tăng thêm 1.
5. Kết quả mong đợi

Dòng task_wdt cho biết tên task không cho ăn đúng hạn: trong dự án thật, đó là manh mối để tìm vòng lặp bị kẹt. Với panic, Serial in thêm dãy địa chỉ (backtrace); công cụ "ESP Exception Decoder" đổi các địa chỉ đó thành tên hàm và dòng lệnh.
6. Dùng watchdog cho đúng
- Đừng tắt watchdog để "hết lỗi". Lỗi vẫn còn, chỉ là mạch đứng im thay vì tự khởi động lại.
- Vòng lặp tính toán dài: chèn
vTaskDelay(1)hoặcyield()định kỳ, không chỉ cho ăn watchdog. - Task quan trọng (đọc cảm biến, điều khiển) nên tự đăng ký watchdog: nếu nó kẹt, cả mạch khởi động lại thay vì chạy tiếp mà không làm việc chính.
- Chỉ cho ăn ở chỗ chứng tỏ việc chính đã làm xong, không đặt
esp_task_wdt_reset()trong một task phụ luôn chạy được. - Brownout: sửa nguồn chứ không tắt bộ phát hiện. Dùng dây USB ngắn, tốt; thêm tụ 470–1000 µF ở chân 5 V; relay, động cơ dùng nguồn riêng có GND chung.
7. Đọc nhanh lý do và cách xử lý
| Lý do in ra | Thường do | Cách xử lý |
|---|---|---|
| task watchdog | Vòng lặp dài không nhường; chờ mạng, chờ cảm biến không có thời hạn | Thêm thời hạn cho mọi lần chờ; vTaskDelay trong vòng lặp dài |
| interrupt watchdog | Hàm ngắt dài, in Serial trong ngắt, đoạn khóa dài | Ngắt chỉ dựng cờ; đoạn khóa vài lệnh |
| loi chuong trinh (panic) | Con trỏ sai, tràn stack task, chia cho 0 | Giải mã backtrace; tăng stack task; kiểm tra con trỏ |
| sut ap nguon (brownout) | Nguồn, dây USB kém; tải lớn dùng chung nguồn | Sửa nguồn, thêm tụ, tách nguồn tải |
| lenh khoi dong lai | Chương trình (hoặc thư viện, OTA) gọi ESP.restart() | Tìm chỗ gọi; bình thường sau khi cập nhật OTA |
| cap dien | Rút cắm nguồn, hoặc nguồn chập chờn mất hẳn | Kiểm tra dây nguồn, cổng cắm |
8. Xem thêm
- Deep Sleep trên ESP32: lý do "thức dậy từ deep sleep" và bộ nhớ RTC.
- Lưu dữ liệu khi mất điện bằng Preferences: khác với RTC, dữ liệu còn cả khi rút điện.
- Các bài FreeRTOS (task; queue, semaphore, mutex) và timer phần cứng trong cùng loạt ESP32 nâng cao.
Xem thêm