Putting an ESP32 into deep sleep drops its current draw from roughly 80mA (active WiFi) to under 15µA. The CPU, most of the RAM, and the high-speed peripherals are powered down, leaving only the Real-Time Clock (RTC) controller, RTC memory, and specific RTC GPIO pads alive. But configuring the wake source correctly is where most DIY battery-powered builds fail. Pick the wrong wake mode, and your board will either fail to wake, instantly reset, or drain your 18650 cell in a week due to stray pull-up currents.

This guide cuts through the abstraction. We will select the exact wake source for your use case, wire a reliable external wake circuit on the ESP32-WROOM-32 DevKit V1, provide production-ready Arduino IDE code, and debug the most common silicon-level failures.

The ESP32 Deep Sleep Decision Matrix

The ESP32 supports four primary wake sources from deep sleep. Your choice dictates your circuit design, power budget, and code structure. Use this decision tree to terminate on a concrete pick.

Wake Source Best Use Case Typical Power Draw Hardware Limitation Verdict
Timer Periodic sensor logging (e.g., weather station every 15 mins) ~10µA None, purely internal. Choose for time-based polling.
EXT0 (External Single) Single button wake, single PIR motion sensor. ~15µA Restricted to RTC GPIO domain (e.g., 32, 33, 34, 35). DEFAULT PICK: Choose for single-event physical triggers.
EXT1 (External Multi) Multiple buttons, complex limit switch arrays. ~20µA Requires external pull-up/down resistors; internal pulls are disabled in EXT1. Choose only if you need >1 wake pin.
Touch Pad Capacitive touch buttons (classic ESP32 only). ~30µA Deprecated in ESP32-S3; susceptible to environmental noise/humidity. Avoid for new battery-powered designs.
The Concrete Pick: If you are building a battery-powered device triggered by a single physical action (a button press, a door reed switch, or a PIR sensor), terminate your decision here: use EXT0 on GPIO 33. It offers the best balance of low quiescent current, internal pull-up/pull-down support, and straightforward code.

Hardware Bill of Materials & Pin Mapping

To build a reliable EXT0 wake circuit, you need components that prevent floating pins and EMI-induced false wakes. A floating GPIO will pick up ambient RF noise and wake the ESP32 randomly, destroying your battery life.

Component Exact Variant / Spec Purpose
Microcontroller ESP32-WROOM-32 DevKit V1 (30-pin or 38-pin) Main processing and sleep controller.
Pull-down Resistor 10kΩ 1% Metal Film (1/4W) Holds GPIO 33 at a stable LOW to prevent phantom wakes.
Trigger Switch 6x6mm Momentary Tactile Switch Connects 3V3 to GPIO 33 to trigger the wake event.
Power Source 2x 18650 Li-ion in series (7.4V nominal) via buck converter to 5V Provides clean 5V to the DevKit VIN pin.

Pin Mapping Table

ESP32 Pin Connects To Notes
GPIO 33 Switch Pin 1 & 10kΩ Resistor Leg 1 Must be an RTC-capable pin. Input only.
3V3 Switch Pin 2 Provides the HIGH signal to trigger EXT0.
GND 10kΩ Resistor Leg 2 Completes the pull-down circuit.
VIN (5V) Buck Converter 5V Output Bypasses the onboard AMS1117 LDO for efficiency if feeding directly to 5V rail.

Wiring the External Wake Circuit

Follow these steps to wire the EXT0 circuit. Do not skip the pull-down resistor; relying solely on the ESP32's internal pull-downs in deep sleep can sometimes result in marginal noise immunity on long wire runs.

  1. Insert the 10kΩ resistor: Connect one leg to the ESP32 GND pin and the other leg to GPIO 33 on your breadboard.
  2. Wire the tactile switch: Connect one switch terminal to the ESP32 3V3 pin. Connect the opposite terminal to GPIO 33 (sharing the node with the 10kΩ resistor).
  3. Verify with a DMM: Before powering the ESP32, set your multimeter to resistance mode. Probe between GPIO 33 and GND. You should read exactly 10kΩ. Probe between 3V3 and GPIO 33; it should read open-loop (OL) until you press the button.
  4. Power up: Connect your 5V source to the VIN pin and GND. The ESP32 will boot, run the setup routine, and immediately enter deep sleep.

Complete Arduino IDE Code (Target: ESP32-WROOM-32)

This code targets the ESP32-WROOM-32 DevKit V1 using the official Arduino-ESP32 core. It uses RTC_DATA_ATTR to persist a boot counter across deep sleep cycles, configures EXT0 on GPIO 33, and includes explicit error handling for undefined wake reasons.

#include <Arduino.h>
#include <esp_sleep.h>

// Pin definitions - GPIO 33 is in the RTC domain
#define WAKE_PIN GPIO_NUM_33
#define WAKE_LEVEL 1 // 1 for HIGH (button press), 0 for LOW

// Variables in RTC memory survive deep sleep
RTC_DATA_ATTR int bootCount = 0;

void printWakeupReason() {
  esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason) {
    case ESP_SLEEP_WAKEUP_EXT0:
      Serial.println("[WAKE] Triggered by external signal on RTC GPIO (EXT0).");
      break;
    case ESP_SLEEP_WAKEUP_TIMER:
      Serial.println("[WAKE] Triggered by internal RTC timer.");
      break;
    case ESP_SLEEP_WAKEUP_UNDEFINED:
    default:
      Serial.println("[WAKE] Triggered by power-on or manual reset (EN pin).");
      break;
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000); // Allow time for Serial Monitor to attach

  Serial.println("\n--- ESP32 Deep Sleep EXT0 Demo ---");
  bootCount++;
  Serial.printf("Boot count: %d\n", bootCount);

  printWakeupReason();

  // Simulate work (e.g., reading a sensor, transmitting via WiFi)
  Serial.println("[WORK] Performing main task for 2 seconds...");
  delay(2000);

  // Configure EXT0 wake source
  // Parameters: GPIO number, wake level (1=HIGH, 0=LOW)
  esp_err_t err = esp_sleep_enable_ext0_wakeup(WAKE_PIN, WAKE_LEVEL);
  
  if (err != ESP_OK) {
    Serial.printf("[ERROR] Failed to configure EXT0 wake. Code: %d\n", err);
    Serial.println("[HALT] Pin is likely not an RTC GPIO. Resetting...");
    ESP.restart();
  }

  // Optional: Configure internal pull-down to supplement external resistor
  // rtc_gpio_pullup_dis(WAKE_PIN);
  // rtc_gpio_pulldown_en(WAKE_PIN);

  Serial.println("[SLEEP] Entering deep sleep now. Press button on GPIO 33 to wake.");
  Serial.flush(); // Ensure all serial data is transmitted before sleeping
  
  // Enter deep sleep - execution stops here and resumes in setup() on wake
  esp_deep_sleep_start();
}

void loop() {
  // This block is never reached in deep sleep implementations
  // esp_deep_sleep_start() halts execution before loop() is called
}

Debugging: "GPIO is not an RTC GPIO" & Wake Failures

When configuring deep sleep, the ESP-IDF framework is unforgiving about pin selection. If you attempt to use a pin that is not physically routed to the RTC controller on the silicon die, the system will reject it.

Exact Error String:
E (142) sleep: GPIO 16 is not an RTC GPIO

Ranked Causes for this Error

  1. Using a Digital-Only Pin: You passed a pin like GPIO 16 (TX2), GPIO 17 (RX2), or GPIO 5 to the esp_sleep_enable_ext0_wakeup() function. These pins are connected to the main digital matrix, not the ultra-low-power RTC island.
  2. Typo in Pin Definition: You defined #define WAKE_PIN 16 instead of GPIO_NUM_33 or 33.
  3. Using ESP32-C3 or ESP32-S3 Syntax on Classic ESP32: The RTC GPIO mappings differ wildly between the classic ESP32, the S3, and the C3. Code copied from an S3 tutorial will fail on a WROOM-32.

The First Three Things to Check When It Fails to Wake

If the code compiles, the board goes to sleep, but pressing the button does nothing, run this diagnostic path:

  • Check 1: Verify the RTC Domain. On the classic ESP32-WROOM-32, the valid RTC GPIOs are: 0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 39. Ensure your wake pin is strictly in this list. (Note: 34, 35, 36, 39 are input-only and lack internal pull-ups/downs, making them tricky for EXT0 without external resistors).
  • Check 2: Measure the Pin Voltage. Connect your DMM to GPIO 33 and GND. In the idle state, it should read 0.00V (held down by your 10kΩ resistor). When you press the button, it must snap to exactly 3.3V. If it only reaches 1.5V, your 3V3 rail is sagging, or the switch is faulty.
  • Check 3: Check for USB-UART Backfeed. If your measured sleep current is >1mA instead of ~15µA, the CP2102 or CH340 USB-to-Serial chip on the DevKit is backfeeding power through the TX/RX lines. For ultra-low-power deployments, you must physically deserial the USB chip or cut the PCB trace powering it, and power the ESP32 directly via the 3V3 pin.

Extending and Simplifying the Build

Once your baseline EXT0 wake circuit is stable, you can adapt the architecture to fit your specific project constraints.

How to Simplify (Lower Cost & Power)

If you do not need WiFi or Bluetooth and only need a low-power wake-and-read sensor node, drop the ESP32-WROOM-32 and switch to the ESP32-C3 SuperMini. The C3 is a single-core RISC-V chip that costs roughly $2.50 (compared to $5.00 for the WROOM). Its deep sleep current drops to an impressive ~5µA. However, the C3 lacks the EXT1 multi-pin wake feature, so you must stick to EXT0 or Timer wakes.

How to Extend (Add Sensor Logging)

To turn this into a data logger, wire a BME280 sensor to the standard I2C pins (GPIO 21 for SDA, GPIO 22 for SCL). Before calling esp_deep_sleep_start(), read the sensor data and append it to a file in the ESP32's SPIFFS/LittleFS partition. Because the RTC memory is limited to 8KB, you cannot store long logs in RTC_DATA_ATTR variables; you must use the flash filesystem. Ensure you call Wire.end() and explicitly set the I2C pins to INPUT mode before sleeping to prevent the I2C pull-up resistors from draining current through the sensor's VCC line.

For authoritative details on the silicon-level power domains and RTC memory retention limits, consult the Espressif ESP-IDF Sleep Modes API Guide. For practical Arduino core implementation nuances, the Random Nerd Tutorials ESP32 Deep Sleep Guide remains an excellent bench reference.