Ein Task-Watchdog-Reset auf einem ESP32 in einem MQTT-Projekt bedeutet fast immer, dass irgendein Code einen Task länger blockiert, als der Watchdog erlaubt. Dieser Artikel zeigt, wie Sie die task_wdt-Meldung lesen, benennt die üblichen Verdächtigen in MQTT-Sketches und liefert nicht blockierende Muster, die das Gerät ohne Resets verbunden halten.
Was der Task Watchdog tatsächlich tut
Auf dem ESP32 läuft FreeRTOS. Selbst in einem Arduino-Sketch laufen setup() und loop() in einem FreeRTOS-Task namens loopTask, WLAN und der TCP/IP-Stack laufen in eigenen Tasks, und jeder CPU-Kern hat einen Idle-Task, der läuft, wenn sonst nichts die CPU braucht.
Der Task Watchdog Timer (TWDT) überwacht eine Reihe angemeldeter Tasks. Jeder angemeldete Task muss den Watchdog regelmäßig „füttern". Versäumt einer das innerhalb des Timeouts, gibt der TWDT einen Fehler aus und löst je nach Konfiguration einen Panic mit Neustart des Chips aus.
Welche Tasks überwacht werden, hängt von Ihrer Umgebung ab:
| Umgebung | Standardmäßig überwacht | Typischer Timeout |
|---|---|---|
| ESP-IDF | Idle-Tasks beider Kerne (in menuconfig einstellbar) | 5 s |
| Arduino-ESP32 | Idle-Task auf Kern 0; loopTask nur, wenn Sie enableLoopWDT() aufrufen |
5 s |
| AsyncTCP (von AsyncMqttClient genutzt) | Sein async_tcp-Task, wenn mit aktivierter Watchdog-Option gebaut |
Übernimmt den TWDT-Timeout |
Der Idle-Task wird aus einem indirekten Grund überwacht: Kommt er nie zum Zug, belegt ein anderer Task auf diesem Kern die CPU, ohne sie abzugeben. Eine „IDLE0"-Meldung bedeutet also nicht, dass der Idle-Task defekt ist, sondern dass etwas anderes auf Kern 0 in einer Schleife kreist.
Daneben gibt es einen separaten Interrupt Watchdog, der auslöst, wenn Interrupts zu lange deaktiviert sind oder eine ISR zu lange läuft. Seine Meldung erwähnt „Interrupt wdt timeout" und hat andere Ursachen, meist im ISR-Code. In diesem Artikel geht es um den Task Watchdog.
Die Fehlermeldung lesen
Ein typischer Absturz sieht so aus:
E (25391) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (25391) task_wdt: - IDLE0 (CPU 0)
E (25391) task_wdt: Tasks currently running:
E (25391) task_wdt: CPU 0: mqttTask
E (25391) task_wdt: CPU 1: loopTask
E (25391) task_wdt: Aborting.
Lesen Sie sie in zwei Teilen. Die erste Liste sagt, wer ausgehungert wurde: hier der Idle-Task von Kern 0. Die Liste „currently running" sagt, wer die CPU in diesem Moment belegt hat: ein Task namens mqttTask auf Kern 0. Das ist Ihr Verdächtiger. Ist der ausgehungerte Task async_tcp, liegt das Problem in Code, der in einem AsyncTCP- oder AsyncMqttClient-Callback läuft. Ist es loopTask, hat Ihr loop() bei aktiviertem Loop-Watchdog blockiert.
Aktivieren Sie den Exception Decoder in Ihrem seriellen Monitor (in PlatformIO monitor_filters = esp32_exception_decoder), um die Backtrace-Adressen in Dateinamen und Zeilennummern zu übersetzen.
Die üblichen Verdächtigen bei ESP32-Task-Watchdog-MQTT-Abstürzen
Busy-Wait-Schleifen ohne Yield
Der klassische Übeltäter ist eine Warteschleife ohne Inhalt:
while (WiFi.status() != WL_CONNECTED) { } // gibt die CPU nie ab
while (!mqtt.connect(clientId)) { } // wiederholt mit voller Geschwindigkeit
In loop() auf Kern 1 geht das oft ohne Reset durch, weil der Arduino-Core den Idle-Task 1 standardmäßig nicht überwacht. Verschieben Sie denselben Code in einen an Kern 0 gebundenen Task oder aktivieren Sie den Loop-Watchdog, und nach fünf Sekunden folgt der Reset. Ein delay(10) oder vTaskDelay() in der Schleife behebt das Aushungern, nicht aber das Design: Das Gerät kann während des Wartens trotzdem nichts anderes tun.
Blockierende Connect-Aufrufe, die länger als der Timeout dauern
connect() von PubSubClient blockiert. Ist der Broker unerreichbar, können der zugrunde liegende TCP-Connect und das Warten auf CONNACK zusammen länger als fünf Sekunden dauern, besonders mit TLS, wo allein der Handshake bei schwachem Signal Sekunden brauchen kann. Eine Reconnect-Funktion, die connect() mehrmals hintereinander aufruft, vervielfacht das. Senken Sie den Socket-Timeout mit mqtt.setSocketTimeout(4) und versuchen Sie es pro Aufruf nur einmal.
Schwere Arbeit in MQTT-Callbacks
Bei AsyncMqttClient läuft onMessage im async_tcp-Task. Wenn Sie dort ein großes JSON-Dokument parsen, in den Flash schreiben, ein Display ansteuern oder delay() aufrufen, blockiert das den Netzwerkstack und kann den Watchdog dieses Tasks auslösen. Bei PubSubClient läuft der Callback innerhalb von mqtt.loop(), schwere Arbeit dort blockiert also stattdessen die Schleife. In beiden Fällen gilt: Payload kopieren, ein Flag setzen oder in eine Queue schieben und die Arbeit an anderer Stelle erledigen.
Lange blockierende Sensorabfragen und Flash-Schreibvorgänge
Manche Sensorbibliotheken fragen per Busy-Wait ab, und ein OTA-Download oder ein großer Flash-Schreibvorgang kann Sekunden dauern. In einem eigenen Task, der den Watchdog füttert, ist das in Ordnung, aber nicht in demselben Task, der MQTT bedient.
Reconnect-Stürme nach Netzverlust
Fällt das WLAN aus, ruft ein naiver Sketch WiFi.begin() und mqtt.connect() in einer engen Schleife direkt hintereinander auf. Jeder Aufruf blockiert ein wenig, die Schleife pausiert nie, und irgendwann löst der Watchdog aus. Exponentieller Backoff löst sowohl das Watchdog-Problem als auch die Last auf dem Broker.
Nicht blockierendes MQTT-Muster für loop()
Dieser Sketch hält loop() jederzeit kurz. Jeder Verbindungsversuch erfolgt höchstens einmal pro Backoff-Intervall, der Callback kopiert nur Daten, und die Arbeit läuft anschließend in loop(). Er verbindet sich mit dem öffentlichen MQTT-Broker broker.connectmq.com.
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* CMD_TOPIC = "connectmq/demo/b52e07/cmd";
const char* TELEMETRY_TOPIC = "connectmq/demo/b52e07/telemetry";
WiFiClient net;
PubSubClient mqtt(net);
String clientId;
volatile bool commandPending = false;
char commandBuf[128];
unsigned long nextMqttAttempt = 0;
unsigned long mqttBackoff = 1000;
unsigned long lastPublish = 0;
void onMessage(char* topic, byte* payload, unsigned int length) {
size_t n = min((size_t)length, sizeof(commandBuf) - 1);
memcpy(commandBuf, payload, n);
commandBuf[n] = '\0';
commandPending = true; // keine schwere Arbeit hier
}
void handleCommand(const char* cmd) {
Serial.printf("Handling command: %s\n", cmd);
// langsame Arbeit ist hier in Ordnung, solange sie unter dem Watchdog-Timeout bleibt
}
void maintainMqtt() {
if (WiFi.status() != WL_CONNECTED) return; // WLAN verbindet sich selbst neu
if (mqtt.connected()) { mqtt.loop(); return; }
if (millis() < nextMqttAttempt) return;
if (mqtt.connect(clientId.c_str())) {
mqtt.subscribe(CMD_TOPIC);
mqttBackoff = 1000;
} else {
nextMqttAttempt = millis() + mqttBackoff;
mqttBackoff = min(mqttBackoff * 2, 30000UL);
}
}
void setup() {
Serial.begin(115200);
enableLoopWDT(); // auch loop() überwachen, damit Blockaden beim Testen auffallen
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASS);
clientId = "esp32-wdt-" + String((uint32_t)ESP.getEfuseMac(), HEX);
mqtt.setServer("broker.connectmq.com", 1883);
mqtt.setSocketTimeout(4); // jeden blockierenden Connect deutlich unter 5 s halten
mqtt.setCallback(onMessage);
}
void loop() {
maintainMqtt();
if (commandPending) {
commandPending = false;
handleCommand(commandBuf);
}
if (mqtt.connected() && millis() - lastPublish > 10000) {
lastPublish = millis();
char payload[64];
snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"heap\":%u}",
millis() / 1000, ESP.getFreeHeap());
mqtt.publish(TELEMETRY_TOPIC, payload);
}
}
Der Arduino-Core füttert den Loop-Watchdog automatisch jedes Mal, wenn loop() zurückkehrt. Solange ein Durchlauf durch loop() deutlich unter dem Timeout bleibt, kostet das Aktivieren also nichts und deckt Regressionen früh auf. Ihn während der Entwicklung einzuschalten, ist eine günstige Methode, den blockierenden Aufruf zu finden, bevor er ins Feld gelangt.
MQTT in einem eigenen FreeRTOS-Task ausführen
Wenn Sie einen eigenen MQTT-Task bevorzugen, melden Sie ihn beim Watchdog an und setzen Sie ihn in jedem Durchlauf zurück. Die esp_task_wdt-API in ESP-IDF 5 (von Arduino-ESP32 3.x verwendet) sieht so aus:
#include <esp_task_wdt.h>
void mqttTask(void* arg) {
esp_task_wdt_add(NULL); // diesen Task überwachen
for (;;) {
maintainMqtt(); // muss schnell zurückkehren
esp_task_wdt_reset(); // den Watchdog füttern
vTaskDelay(pdMS_TO_TICKS(10)); // immer die CPU abgeben
}
}
void setup() {
// ... WLAN- und MQTT-Setup wie oben ...
xTaskCreatePinnedToCore(mqttTask, "mqttTask", 6144, NULL, 1, NULL, 1);
}
Halten Sie die Priorität niedrig (1 entspricht loopTask), sofern Sie keinen Grund für eine höhere haben; ein hochpriorer Task, der nie blockiert, hungert alles unter ihm aus. Binden Sie ihn an Kern 1, wenn Sie den Kern meiden wollen, auf dem das WLAN läuft. Wird der MQTT-Client aus mehr als einem Task verwendet, schützen Sie ihn mit einem Mutex, denn PubSubClient ist nicht threadsicher. Der Leitfaden zu ESP32 FreeRTOS MQTT behandelt Queues und Task-Design ausführlicher.
Sollten Sie den Watchdog-Timeout erhöhen?
Manchmal, aber nur als letzten Schritt. Ein höherer Timeout versteckt blockierenden Code, statt ihn zu beheben, und das Gerät erholt sich langsamer von einem echten Hänger. Berechtigte Fälle sind lange, unvermeidbare Vorgänge wie ein OTA-Flash-Schreibvorgang. In ESP-IDF 5 können Sie den TWDT zur Laufzeit neu konfigurieren:
esp_task_wdt_config_t cfg = {
.timeout_ms = 15000,
.idle_core_mask = (1 << 0), // IDLE0 weiter überwachen
.trigger_panic = true,
};
esp_task_wdt_reconfigure(&cfg);
Sauberer ist es bei einem einzelnen langen Vorgang, den Watchdog aus diesem heraus zu füttern, etwa aus einem OTA-Fortschritts-Callback, und den globalen Timeout unverändert zu lassen. Deaktivieren Sie den Watchdog in der Produktion nie vollständig. Er ist das Einzige, was ein im Feld in einem Deadlock festhängendes Gerät neu startet.
Checkliste zur Fehlerbehebung
- Lesen Sie ab, welcher Task ausgehungert wurde und welcher lief; dieses Paar zeigt Ihnen, wo Sie suchen müssen.
- Dekodieren Sie den Backtrace mit dem Exception Decoder, um die genaue Zeile zu erhalten.
- Durchsuchen Sie den Code nach
while-Schleifen ohnedelay(),yield()odervTaskDelay(). - Setzen Sie
mqtt.setSocketTimeout()auf wenige Sekunden und entfernen Sie Wiederholungsschleifen umconnect(). - Verlagern Sie JSON-Parsing, Flash-Schreibvorgänge und Display-Aktualisierungen aus MQTT-Callbacks heraus.
- Ergänzen Sie exponentiellen Backoff für WLAN- und MQTT-Reconnects.
- Aktivieren Sie beim Testen
enableLoopWDT(), damit Blockaden inloop()früh auffallen. - Beobachten Sie die Uptime des Geräts über MQTT: Ein Zähler, der immer wieder zurückspringt, deutet auf einen Watchdog- oder Brownout-Neustart hin.
uptime und esp_reset_reason() nach jedem Start zu veröffentlichen, ist eine einfache Gewohnheit, die unsichtbare Resets in Daten verwandelt. Bei Knoten in der Fertigung, die Maschinendaten für OEE oder Zustandsüberwachung liefern, gehören diese Werte neben die Prozessdaten in den Gerätezweig des Topic-Baums, damit Instandhaltung und IT instabile Geräte erkennen, bevor Datenlücken im MES auffallen. Viele Verbindungsprobleme, die wie Watchdog-Probleme aussehen, sind im Kern Netzwerkprobleme; wenn sich das Gerät gar nicht erst verbindet, gehen Sie zuerst durch, warum sich ein ESP32 nicht mit einem MQTT-Broker verbinden kann, und zur Wiederverbindungsstrategie lesen Sie den Leitfaden zum ESP32 MQTT Reconnect.
Überwachen Sie Ihre Geräte mit ConnectMQ
Der obige Test-Sketch läuft gegen den kostenlosen öffentlichen MQTT-Broker von ConnectMQ, der für Tests und Prototypen gedacht ist und sich perfekt eignet, um einen Reset am Schreibtisch nachzustellen. Für eine Flotte können Sie mit einem privaten Broker bei ConnectMQ die Telemetrie jedes Geräts im Live-Topic-Explorer verfolgen und Schwellenwert-Alarmregeln für Werte wie den freien Heap festlegen, sodass ein Gerät, das neu zu starten beginnt, schnell auffällt. Einen solchen Broker erstellen Sie im kostenlosen Serverless-Tarif unter app.connectmq.com, ohne Kreditkarte. Wenn Sie noch entscheiden, wo Sie Ihren Broker hosten, vergleicht der Leitfaden zum Cloud-MQTT-Broker die Optionen, und das ESP32-IoT-Tutorial zeigt ein vollständiges nicht blockierendes Geräteprojekt von Anfang bis Ende.
Häufig gestellte Fragen
Was bedeutet „Task watchdog got triggered" mit IDLE0?
Ein Task auf Kern 0 hat die CPU länger als den Timeout beschäftigt, ohne sie abzugeben, sodass der Idle-Task nie lief. Die Zeile „Tasks currently running" nennt den wahrscheinlichen Übeltäter.
Setzt delay() den Watchdog zurück?
delay() gibt die CPU an andere Tasks ab, wodurch der Idle-Task laufen kann; es verhindert also das Aushungern von IDLE. Einen Task, den Sie mit esp_task_wdt_add() angemeldet haben, füttert es jedoch nicht; dieser Task muss selbst esp_task_wdt_reset() aufrufen.
Warum löst AsyncMqttClient den async_tcp-Watchdog aus?
Seine Callbacks laufen im async_tcp-Task. Lange Arbeit dort blockiert den Task, halten Sie Callbacks daher kurz und übergeben Sie den Payload per Flag oder Queue an loop() oder einen separaten Task.
Ist es sicher, den Task Watchdog zu deaktivieren?
In der Produktion nicht. Er ist der Mechanismus, der ein Gerät aus einem Hänger befreit. Beheben Sie den blockierenden Code und erhöhen Sie den Timeout nur für wirklich lange Vorgänge.



