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.

The Exact Error String:
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:

  1. Blocking Infinite Loops: A while(!Serial) or while(WiFi.status() != WL_CONNECTED) loop that lacks a yield() or delay() call inside the block.
  2. I2C Bus Hangs: The Wire library waits indefinitely for an ACK from a sensor. If the I2C lines are floating or lack pull-up resistors, the ESP32 hangs on the Wire.endTransmission() call until the WDT trips.
  3. 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

Difficulty: Beginner-Intermediate | Time: 20 Minutes | Target Board: ESP32-WROOM-32 DevKit V1 (30-pin variant)

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 GPIOBME280 PinNotes
GPIO 21SDAI2C Data (Requires 4.7kΩ pull-up to 3.3V)
GPIO 22SCLI2C Clock (Requires 4.7kΩ pull-up to 3.3V)
3V3VINDo not use 5V; BME280 logic is strictly 3.3V
GNDGNDCommon ground reference
Bench Insight: Never rely on the ESP32's internal I2C pull-ups. They are roughly 45kΩ, which is far too weak for 400kHz I2C speeds. The bus capacitance will round off the signal edges, causing the 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:

  1. 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.
  2. Check for Blocking Network Calls: Functions like WiFi.begin() or HTTPClient.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.
  3. Audit Custom FreeRTOS Task Priorities: If you created a custom task using xTaskCreate with a priority higher than 1 (the default priority of the Arduino loopTask), and that task lacks a vTaskDelay(), it will starve the main loop. The main loop will never get CPU time to call yield(), 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.