Waking on a timer
The simplest wake source there is, and the one most battery projects use. Two numbers decide how long the battery lasts, and the sleep current is not one of them.
The two numbers
Slide the sleep interval and watch the battery life move a little. Slide the awake time and watch it move a lot.
That is the whole lesson of battery work on this chip, and it goes the opposite way to everybody's instinct. The sleep current gets all the attention because it is the number in the datasheet; the awake time gets none because it is not printed anywhere and has to be measured.
Where the awake time goes
For a typical Wi-Fi sensor, roughly:
| Step | Time |
|---|---|
Boot to setup() | 300 ms |
| Wi-Fi join, full scan and DHCP | 2–4 s |
| Wi-Fi join, stored channel and BSSID, static IP | 400–900 ms |
| Read a sensor | 10–800 ms, depending on the sensor |
| Post a reading over HTTP | 100 ms |
| Post a reading over HTTPS, cold | 500–900 ms |
The gap between the two Wi-Fi rows is the single biggest saving available in most projects, and it costs a few bytes in RTC memory.
Everything runs in setup
A deep sleep wake is a boot. loop() is not where the work goes, because the
board is not going to reach a second pass — the sketch is one cycle of the
duty cycle, and the last line of setup() is the sleep call.
Leave yourself a way out. A pin checked at the top that skips the sleep when grounded turns "the board is unreachable for ten minutes at a time" back into something you can debug.
The code
There is no loop here worth speaking of. The board wakes, does its job in setup, and sleeps again — the sketch is the awake half of one cycle.
#include <esp_sleep.h>
RTC_DATA_ATTR int boots = 0; // survives deep sleep
void setup() {
Serial.begin(115200);
delay(100);
boots++;
Serial.printf("boot %d, awake at %lu ms\n", boots, millis());
// ... take a reading, send it, whatever this board is for ...
Serial.printf("slept and woke in %lu ms of awake time\n", millis());
Serial.flush();
esp_sleep_enable_timer_wakeup(10ULL * 1000000); // note the ULL
esp_deep_sleep_start();
}
void loop() {}The argument is microseconds and it is 64-bit. Writing 30 * 1000000 in 32-bit arithmetic overflows past about 35 minutes and gives you a much shorter sleep than you asked for.
machine.deepsleep takes milliseconds. Everything above the call runs on every wake, because a wake is a fresh boot.
import machine, time
if machine.reset_cause() == machine.DEEPSLEEP_RESET:
print("woke from sleep")
else:
print("cold boot")
start = time.ticks_ms()
# ... take a reading, send it ...
print("awake for", time.ticks_diff(time.ticks_ms(), start), "ms")
machine.deepsleep(10_000) # millisecondsThere is no way back from deepsleep in code. If you need a way to interrupt the cycle for debugging, check a pin at the top of main.py and skip the sleep when it is grounded.
When it does not work
32-bit overflow. esp_sleep_enable_timer_wakeup takes microseconds in a 64-bit argument, and 3600 * 1000000 computed as int wraps. Use the ULL suffix or a uint64_t.
That is deep sleep working. RAM is gone and execution restarts at setup — there is no resume. Anything that has to carry across goes in RTC memory or in NVS.
Almost always the awake half. A Wi-Fi join with DHCP is two to four seconds at 120 mA, which dwarfs an hour of sleep at 10 µA. Time the awake portion before optimising the sleep.
The dev board, not the chip. USB-serial chip, power LED and regulator quiescent current together are far larger than the chip asleep. A bare module or a board with those parts removed is what the datasheet figures assume.
The other reason to wake: something happened. Three functions, and the pins each of them will accept.
Waking on a pin or a touch →Edit this page — content/esp32/timer-wake-up.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.