Intermediate7 min readESP32

ESP32 Deep Sleep Basics: Timer and Button Wakeup

Put an ESP32 into deep sleep, wake it with a timer or a button, keep data across sleeps with RTC_DATA_ATTR, and see why dev boards draw far more than the datasheet says.

In deep sleep the ESP32 switches off its CPUs, its main RAM, and the Wi-Fi and Bluetooth radio. Only a small always-on section, the RTC domain, keeps running: a low-power timer, a few kilobytes of RTC memory, and, if you ask for it, the circuitry that watches a pin for a wakeup signal. Deep sleep is the main tool for making a battery-powered ESP32 last. This article shows the standard pattern with the ESP32 Arduino core 3.x: wake on a timer or a button, count wakeups across sleeps, and find out why your wakeup happened.

Waking up is a reboot

This is the single most important thing to understand. When the ESP32 wakes from deep sleep, it does not continue after the line that put it to sleep. It boots from scratch: the bootloader runs, global variables get their initial values again, and setup() starts from the top. Everything in normal RAM is gone.

So a deep-sleep sketch does all its work in setup():

  1. Find out why it woke up.
  2. Do the work (read a sensor, send a message).
  3. Configure the wakeup sources.
  4. Call esp_deep_sleep_start(), which never returns.

loop() is never reached. It still has to exist, because the Arduino core calls it and the sketch won't link without it, but it stays empty.

Wiring

For the button wakeup, connect a push button between GPIO33 and 3V3. The sketch enables the pin's internal pull-down resistor, which holds the pin LOW while the chip sleeps. Pressing the button pulls it HIGH, and that wakes the chip.

The sketch

// Deep sleep with timer wakeup and ext0 (button) wakeup.
// Target: "ESP32 Dev Module", ESP32 Arduino core 3.x.
#include "driver/rtc_io.h"
 
const uint64_t SLEEP_SECONDS = 30;            // timer wakeup interval
const gpio_num_t WAKE_PIN = GPIO_NUM_33;      // must be an RTC GPIO
 
// Lives in RTC memory, so it survives deep sleep (but not a power cut or reset).
RTC_DATA_ATTR int bootCount = 0;
 
void printWakeupReason() {
  esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
 
  switch (cause) {
    case ESP_SLEEP_WAKEUP_TIMER:
      Serial.println("Wakeup cause: timer");
      break;
    case ESP_SLEEP_WAKEUP_EXT0:
      Serial.println("Wakeup cause: button on GPIO33 (ext0)");
      break;
    case ESP_SLEEP_WAKEUP_UNDEFINED:
      Serial.println("Wakeup cause: none - this was a power-on or reset, not a wakeup");
      break;
    default:
      Serial.print("Wakeup cause: other (");
      Serial.print((int)cause);
      Serial.println(")");
      break;
  }
}
 
void setup() {
  Serial.begin(115200);
  delay(200);  // let the USB-serial link settle so the first lines aren't lost
 
  bootCount++;
  Serial.print("Boot number: ");
  Serial.println(bootCount);
  printWakeupReason();
 
  // ---- Do your work here: read a sensor, send data, etc. ----
 
  // Wake source 1: the RTC timer. The argument is in microseconds.
  esp_sleep_enable_timer_wakeup(SLEEP_SECONDS * 1000000ULL);
 
  // Wake source 2: ext0, wake when WAKE_PIN goes HIGH (1) or LOW (0).
  esp_sleep_enable_ext0_wakeup(WAKE_PIN, 1);
  // Hold the pin LOW during sleep with the RTC pull-down, so it only
  // goes HIGH when the button connects it to 3V3.
  rtc_gpio_pullup_dis(WAKE_PIN);
  rtc_gpio_pulldown_en(WAKE_PIN);
 
  Serial.print("Going to sleep for ");
  Serial.print((unsigned long)SLEEP_SECONDS);
  Serial.println(" seconds, or until the button is pressed");
  Serial.flush();  // wait until the text has actually been sent
 
  esp_deep_sleep_start();  // never returns
}
 
void loop() {
  // Never reached: the chip sleeps at the end of setup() and reboots on wakeup.
}

Upload it and open the Serial Monitor at 115200. The first boot reports "none" as the wakeup cause, because it came from the upload's reset. After that, the board wakes every 30 seconds with cause "timer" and the boot number goes up by one each time. Press the button and it wakes immediately with cause "ext0".

Tip

A sleeping board sometimes doesn't respond to an upload. If that happens, hold BOOT, tap EN, release BOOT, and upload again. This puts the chip into download mode no matter what the sketch does.

How each piece works

Finding out why you woke up

esp_sleep_get_wakeup_cause() returns which source ended the sleep. The values used here:

ValueMeaning
ESP_SLEEP_WAKEUP_TIMERThe RTC timer ran out.
ESP_SLEEP_WAKEUP_EXT0The ext0 pin reached the chosen level.
ESP_SLEEP_WAKEUP_UNDEFINEDNot a wakeup at all: first power-on, the EN button, or the reset after an upload.

That last value is useful. It lets a sketch tell "fresh start, run first-time setup" apart from "routine wakeup".

Keeping data across sleeps with RTC_DATA_ATTR

A normal global like int bootCount = 0; resets to 0 on every wakeup, because waking up is a reboot. RTC_DATA_ATTR places the variable in RTC memory instead, which stays powered during deep sleep. Its initializer (= 0) only applies on a fresh start.

Things to know:

  • It is lost on a power cut, and the EN button or an upload resets it to its initial value too.
  • RTC memory is only a few kilobytes. Use it for counters, the last reading, or "send only every tenth wakeup" logic.
  • Store plain values and fixed-size char arrays. A String or any object that allocates memory keeps its contents on the normal heap, which doesn't survive sleep.

Timer wakeup and the ULL suffix

esp_sleep_enable_timer_wakeup() takes microseconds as a 64-bit number. Write the multiplication with a ULL suffix (1000000ULL) so the compiler does it in 64 bits. With plain int arithmetic, 3600 * 1000000 (one hour) overflows the 32-bit range, and you get a sleep time that has nothing to do with what you asked for. In this sketch SLEEP_SECONDS is already uint64_t, so the suffix is belt and braces. In your own code it may be the only thing preventing the bug.

The RTC timer runs from an internal oscillator by default, so long sleeps drift. If you need real clock time, sync it over the network (NTP) while awake, rather than counting sleep intervals.

ext0 wakeup

ext0 watches one pin and wakes the chip when that pin reaches a level: 1 for HIGH, 0 for LOW. Details:

  • Only RTC GPIOs work: 0, 2, 4, 12–15, 25–27 and 32–39. GPIO33 is a good choice because it isn't a strapping pin.
  • GPIO34–39 have no internal pull resistors, so rtc_gpio_pulldown_en() can't help there. Use an external resistor instead.
  • It is level-triggered. If the button is held down when the sketch goes to sleep, the chip wakes again immediately.
  • ext0 keeps the RTC peripherals powered during sleep, which costs a little extra current. To wake on any of several pins, use ext1 (esp_sleep_enable_ext1_wakeup()), which takes a bitmask of RTC GPIOs.

When both sources are enabled, whichever fires first wins.

One more thing that surprises people: don't count on an output pin keeping its level during deep sleep. If an LED or relay must stay on or off while the chip sleeps, it needs an explicit hold, such as rtc_gpio_hold_en() on an RTC GPIO.

Why your dev board still draws milliamps

The ESP32 datasheet quotes deep-sleep current for the chip in the microamp range. Put a multimeter on a typical dev board and you'll measure much more, often in the milliamp range. Nothing is wrong with the sleep code. The extra current comes from the parts of the board that don't sleep:

  • The voltage regulator. Many boards, especially low-cost DEVKIT V1 clones, use a linear regulator such as the AMS1117. Its own quiescent current is in the milliamp range, which is far more than the sleeping ESP32 needs.
  • The USB-serial chip. On many boards the CP2102 or CH340 runs from the same 3.3 V supply and keeps drawing current whether or not a USB cable is plugged in.
  • The power LED is lit all the time.
  • Everything you connected. A sensor breakout keeps drawing current unless you switch its power off, and a pull-up resistor draws current whenever its pin is held at the opposite level.

To measure, put a multimeter in series with the battery, not the USB cable. When the board runs from USB, the USB-serial chip and the regulator are fully active, so the reading says nothing about battery life. Many inline USB power meters also can't resolve currents this small.

For real battery life you need hardware designed for it: a board with a low-quiescent-current regulator, no always-on LED, and a USB chip that turns off when unplugged, or a bare module with your own power circuit. Power sensors from a GPIO pin or through a switch so you can turn them off before sleeping.

Next steps

Stuck on a step?

Describe your board and what you see; the assistant has this page as context.

Ask the assistant

Keep going