Skip to content

soc_esp32::sleep

Low-power sleep: the RTC_CNTL sleep FSM, timer wake only.

Two modes, both woken by the RTC timer:

  • Light sleep pauses the CPU and gates its clock; SRAM and every register keep their contents, so execution resumes at the instruction after the one that asked to sleep. The kernel tick (Xtensa CCOUNT) stops with the CPU, so the caller must reconcile elapsed time on wake — see [elapsed_us_from_ticks] and the kernel’s power module.
  • Deep sleep powers the digital domain down. Only the RTC still has power, so nothing in SRAM survives and the timer wake arrives as a chip reset into the bootloader, not a return from this function.

esp-idf’s esp_light_sleep_start does a great deal more than the register sequence here: it suspends the flash, drops the PLL, switches the CPU to the RTC clock, and powers down the RF and memory domains for the lowest possible current. Every one of those is a way to hang a board that cannot wake, and none is needed for correctness — with the domains left powered the FSM still pauses the CPU and still wakes on the timer, it just draws more current than a tuned sleep would. This cut takes reliable wake over minimum current; that power-domain tuning is a documented follow-up.

What it does not skip is light_init — the power-up/wait counters and analog bias of rtc_sleep_init / rtc_sleep_low_init. Those are not a current optimisation: without them the FSM gates the CPU and the wake transition never completes, a silent hang (this was the #31 bug).

Timer only. RTC-GPIO, touch and ULP wake are separate pad/analog paths (esp_sleep_enable_ext1_wakeup, ..._touchpad_wakeup, ..._ulp_wakeup) and are deferred. A caller that needs them must not rely on this module.

The sequence follows esp-idf exactly, cited per step:

  • entry/poll: components/esp_hw_support/port/esp32/rtc_sleep.c (rtc_sleep_start)
  • timer arming: components/esp_hw_support/sleep_modes.c (timer_wakeup_prepare) and components/hal/esp32/include/hal/rtc_cntl_ll.h (rtc_cntl_ll_set_wakeup_timer)
  • deep-sleep domain power-down: rtc_sleep_init, the cfg.deep_slp branch
  • offsets/fields: components/soc/esp32/register/soc/rtc_cntl_reg.h
enum SleepError {
NoClock,
Timeout,
Rejected,
}

A sleep could not be entered or measured.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

unsafe fn deep_sleep(sleep_us: u64) -> Result<(), SleepError>

Deep sleep until the RTC timer fires, which arrives as a chip reset.

This does not return on success: powering the digital domain down loses the CPU and all of SRAM, so the timer wake restarts the chip through the bootloader exactly as a power-on would. Only RTC memory survives (this cut stores nothing there — a follow-up). A caller reaches the line after this only if the FSM rejected the sleep or timed out.

The single bit that distinguishes this from [light_sleep] is DG_WRAP_PD_EN (rtc_sleep_init, cfg.deep_slp); the aggressive analog and RF power-down esp-idf also does is deferred with the rest of the tuning.

Powers the CPU down. Everything in SRAM is lost. As [light_sleep], the timer wake must stay armed.

References: SleepError

fn elapsed_us_from_ticks(ticks: u64) -> u64

Slow-clock ticks to microseconds, rounded down. The inverse of [us_to_slowclk], used to turn a measured tick delta into elapsed time.

unsafe fn light_sleep(sleep_us: u64) -> Result<u64, SleepError>

Light sleep for approximately sleep_us microseconds, woken by the RTC timer.

Returns the measured elapsed time in microseconds — the slow-clock tick delta across the sleep, converted back — not the requested duration. The kernel feeds this to its tick reconciliation so the scheduler’s clock accounts for the paused interval. On a rejected sleep the return is roughly zero (the CPU never paused).

State is retained: on return, execution continues here and every register and all of SRAM hold what they did before.

Pauses this CPU. Must be called with a sane RTC clock; the timer wake is what guarantees return, so callers must not disable it. Do not call from the second core — this arms one shared FSM.

References: SleepError

fn us_to_slowclk(us: u64) -> u64

Microseconds to slow-clock ticks, rounded down.

Saturates rather than overflows: a u64 of microseconds times 150 000 would wrap past ~123 000 years, which no real sleep reaches, but a saturating multiply keeps a bad caller from arming a garbage comparator.