If you have ever stared at a serial monitor watching your ESP32 abruptly reboot with a cryptic rst:0xc (SW_CPU_RESET) message, you have met the Task Watchdog Timer (TWDT). Unlike simpler 8-bit microcontrollers, the ESP32 runs a full FreeRTOS operating system under the hood. The WiFi and Bluetooth stacks require constant CPU time to process packets and maintain connections. When you write a blocking while() loop or a heavy computational routine in your loop() function, you starve the RTOS idle task. The watchdog assumes the system has locked up and triggers a hardware reset.
The direct fix is the yield() function. Calling yield() inside your loops passes execution to the FreeRTOS idle task, feeding the watchdog timer and allowing the wireless stacks to breathe. Below, we break down the exact error strings, the hardware reasons behind I2C hangs, and a complete, robust sensor polling build that will not crash your ESP32.
The Anatomy of an ESP32 Watchdog Panic
When the TWDT triggers, the ESP32 dumps a specific stack trace to the serial port. Recognizing this exact string saves hours of misdiagnosing hardware faults.
E (6014) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (6014) task_wdt: - loopTask (CPU 1)
E (6014) task_wdt: Tasks currently running in here:
E (6014) task_wdt: xTaskPinnedCore: 0
E (6014) task_wdt: Aborting.
rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
Here are the ranked causes for this panic, from most to least common on the bench:
- Blocking Infinite Loops: A
while(!Serial)orwhile(WiFi.status() != WL_CONNECTED)loop that lacks ayield()ordelay()call inside the block. - I2C Bus Hangs: The
Wirelibrary waits indefinitely for an ACK from a sensor. If the I2C lines are floating or lack pull-up resistors, the ESP32 hangs on theWire.endTransmission()call until the WDT trips. - Heavy Math in loop(): Large buffer copies, unoptimized floating-point FFTs, or driving WS2812 LED strips via bit-banging (without using the RMT peripheral) taking longer than the default 5-second WDT timeout.
Project Build: Robust I2C Sensor Polling Without WDT Crashes
This build demonstrates how to safely poll an I2C sensor while maintaining a WiFi connection in the background, utilizing proper yield() placement and hardware bus stabilization.
Parts List
- Microcontroller: ESP32-WROOM-32 DevKit V1 (30-pin, e.g., HiLetgo or NodeMCU-32S)
- Sensor: Adafruit BME280 I2C Temperature/Humidity/Pressure Sensor (Adafruit 2652)
- Resistors: 2x 4.7kΩ through-hole resistors (for I2C pull-ups)
- Wiring: Half-size breadboard and 22 AWG solid core jumper wires
Pin Mapping Table
| ESP32 GPIO | BME280 Pin | Notes |
|---|---|---|
| GPIO 21 | SDA | I2C Data (Requires 4.7kΩ pull-up to 3.3V) |
| GPIO 22 | SCL | I2C Clock (Requires 4.7kΩ pull-up to 3.3V) |
| 3V3 | VIN | Do not use 5V; BME280 logic is strictly 3.3V |
| GND | GND | Common ground reference |
Wire library to hang and trigger a WDT panic. Always use external 4.7kΩ resistors tied to the 3.3V rail.
Complete Compilable Code
This code targets the ESP32 Arduino Core (v2.x or v3.x). It requires the Adafruit_BME280 and Adafruit_Unified_Sensor libraries installed via the Library Manager.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// --- Pin Definitions for ESP32-WROOM-32 DevKit V1 (30-pin) ---
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BME280 bme;
unsigned long lastReadTime = 0;
const unsigned long readInterval = 2000; // Read every 2 seconds
void setup() {
Serial.begin(115200);
// CRITICAL: Feed WDT while waiting for Serial monitor to connect
while(!Serial) {
yield();
}
Serial.println("ESP32 Yield & WDT Safe Sensor Polling");
// Initialize I2C with explicit pins
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
Wire.setClock(400000); // 400kHz fast mode
// Error handling for sensor initialization
if (!bme.begin(0x77)) { // Adafruit breakout default is usually 0x77
Serial.println("ERROR: Could not find BME280. Check wiring and I2C pull-ups!");
while (1) {
yield(); // Prevent WDT panic in infinite error loop
}
}
Serial.println("BME280 initialized successfully.");
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - lastReadTime >= readInterval) {
lastReadTime = currentMillis;
// Read sensors (Wire library handles I2C transactions)
float temp = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F;
float altitude = bme.readAltitude(SEALEVELPRESSURE_HPA);
// Simulate heavy processing that might take a few milliseconds
for(int i=0; i<1000; i++) {
volatile float dummy = sqrt(temp * pressure);
yield(); // Yield inside heavy math loops to feed WDT
}
Serial.printf("Temp: %.2f C | Hum: %.2f %% | Press: %.2f hPa | Alt: %.2f m\n",
temp, humidity, pressure, altitude);
}
// CRITICAL: Yield at the end of every loop iteration.
// This allows the FreeRTOS idle task to run and feed the TWDT.
yield();
}
First Three Things to Check When the WDT Fails
If you have implemented yield() but are still seeing the SW_CPU_RESET traceback, the issue is likely hardware-level or tied to task starvation. Run through this diagnostic sequence:
- Verify I2C/SPI Pull-ups and Wiring: If a sensor is disconnected or missing pull-ups, the
Wire.endTransmission()function can block indefinitely. Use a multimeter to verify continuity on SDA/SCL and check for 3.3V on both lines when idle. - Check for Blocking Network Calls: Functions like
WiFi.begin()orHTTPClient.GET()can block the main thread if the router is unresponsive. Ensure you are using non-blocking WiFi event handlers (WiFi.onEvent()) or wrapping network calls in a dedicated FreeRTOS task with its own timeout logic. - Audit Custom FreeRTOS Task Priorities: If you created a custom task using
xTaskCreatewith a priority higher than1(the default priority of the ArduinoloopTask), and that task lacks avTaskDelay(), it will starve the main loop. The main loop will never get CPU time to callyield(), resulting in a WDT panic on Core 1.
Extending and Simplifying the Build
Depending on your project constraints, you can either simplify your codebase or scale it up for multi-core processing.
How to Simplify
If you do not need precise microsecond timing, replace custom millis() polling loops with the standard delay() function. In the ESP32 Arduino Core, delay() is not a dumb busy-wait; it is implemented as vTaskDelay(), which inherently yields to the RTOS and feeds the watchdog. Calling delay(10) is functionally safer for beginners than writing a custom timer loop and forgetting the yield() call.
How to Extend
For production IoT devices, move sensor polling off the main loop() entirely. The ESP32 has two cores: Core 0 handles the WiFi/BLE stack, and Core 1 runs the Arduino loop(). You can extend this build by creating a dedicated FreeRTOS task pinned to Core 0 to read sensors via I2C, passing the data to Core 1 via a thread-safe FreeRTOS Queue (xQueueSend). This ensures that a flaky I2C bus will never crash your main WiFi telemetry loop. For deeper architectural guidance, consult the Espressif WDT API Reference and the Arduino-ESP32 Core Documentation.
FAQ: ESP32 Yield and Background Tasks
Does delay() call yield() on the ESP32?
Yes. In the ESP32 Arduino core, delay() is mapped to the FreeRTOS vTaskDelay() function. When you call delay(100), the RTOS puts the current task into the Blocked state for 100 milliseconds, allowing the idle task to run, which feeds the Task Watchdog Timer and processes WiFi events. However, delayMicroseconds() does not yield; it is a busy-wait loop and will trigger a WDT panic if used for long durations.
Why does my ESP32 disconnect from WiFi during long calculations?
The WiFi stack runs as a background task on Core 0. If your code on Core 1 (or a high-priority task on Core 0) hogs the CPU without calling yield(), the WiFi task cannot process incoming beacon frames from your router. The router assumes the ESP32 has dropped off the network and severs the connection. Inserting yield() inside your calculation loops ensures the WiFi task gets the CPU cycles it needs to maintain the association.
Can I disable the Task Watchdog Timer entirely?
Technically, yes, by calling disableCore0WDT() and disableCore1WDT() in your setup function, or by altering the menuconfig settings in ESP-IDF. However, this is strongly discouraged. Disabling the WDT masks underlying architectural flaws in your code. If a sensor bus hangs or a memory allocation fails, your ESP32 will freeze indefinitely instead of rebooting and recovering. Fix the blocking code with yield() or hardware timeouts instead of disabling the safety net.
What is the difference between yield() and vTaskDelay() in FreeRTOS?
yield() (which maps to portYIELD()) tells the RTOS scheduler to immediately switch to another ready task of equal or higher priority. It does not guarantee a specific time delay; if no other tasks are ready, it returns instantly. vTaskDelay() puts the task to sleep for a specific number of RTOS ticks (e.g., vTaskDelay(1) sleeps for at least 1ms). For simply feeding the watchdog in a tight loop, yield() is sufficient and faster. For pacing sensor reads, vTaskDelay() is the correct tool.






