What survives deep sleep
RAM is gone, the sketch restarts from the top, and the boot counter reads zero. Three places to leave yourself a note, with three different lifetimes and three different costs.
Three stores, three lifetimes
| Survives deep sleep | Survives hibernation | Survives a power cut | Cost to write | |
|---|---|---|---|---|
| Ordinary variable | no | no | no | free |
RTC_DATA_ATTR | yes | no | no | free |
| NVS / a file | yes | yes | yes | a flash write |
They look identical on a bench with a USB cable in, which is why this bug is usually found in the field.
RTC memory is the default answer
8 kB, no wear, free to write, and it holds exactly the things a duty-cycled sensor needs to carry across: a boot count, the last reading, the Wi-Fi channel and BSSID that worked last time, a flag saying the sensor is already calibrated.
Losing it costs you one slow cycle, not correctness. That is the test for whether something belongs here: if the answer to "what if this is missing" is "do the slow thing", RTC memory is right.
NVS for the things you cannot re-derive
Wi-Fi credentials, a device id, a calibration constant somebody measured with a reference instrument. Those must outlive a flat battery, and flash is the only place that does.
Flash also wears out. A write on every wake, at one wake a minute, is half a million cycles a year — well past what the part is rated for. The pattern that works is a running value in RTC memory, flushed to NVS every so often, and flushed once more before a deliberate shutdown.
Store values, not addresses
Anything in RTC memory is bytes that happen to be still there. A pointer from
the previous boot points into a heap that no longer exists, and a String or a
std::vector is a pointer wearing a nicer name. Plain integers, floats, fixed
arrays and structs of those survive; anything that allocates does not.
The code
Three counters, one sleep cycle. Only two of them mean anything after the wake, and only one of them survives the battery being changed.
#include <esp_sleep.h>
#include <Preferences.h>
int ramCount = 0; // gone every wake
RTC_DATA_ATTR int rtcCount = 0; // survives deep sleep
RTC_DATA_ATTR float lastReading = 0;
Preferences prefs;
void setup() {
Serial.begin(115200);
delay(100);
prefs.begin("boot", false);
int nvsCount = prefs.getInt("n", 0) + 1;
ramCount++;
rtcCount++;
Serial.printf("ram %d rtc %d nvs %d last %.1f\n",
ramCount, rtcCount, nvsCount, lastReading);
lastReading = 21.4;
// Write to flash only when it changed, and not on every wake.
if (rtcCount % 20 == 0) prefs.putInt("n", nvsCount);
prefs.end();
esp_sleep_enable_timer_wakeup(10ULL * 1000000);
esp_deep_sleep_start();
}
void loop() {}RTC_DATA_ATTR variables live in 8 kB of RTC slow memory. They survive deep sleep, are lost in hibernation, and are lost on any power cut or EN reset.
machine.RTC().memory() is the RTC-memory equivalent: up to 2 kB of bytes that survive deep sleep and nothing else.
import machine, json
rtc = machine.RTC()
try:
state = json.loads(rtc.memory() or b"{}")
except ValueError:
state = {}
state["boots"] = state.get("boots", 0) + 1
print("boot", state["boots"], "last", state.get("last"))
state["last"] = 21.4
rtc.memory(json.dumps(state))
machine.deepsleep(10_000)It stores bytes, so anything structured has to be packed. struct or a short json.dumps both work; json is easier to read in the monitor and costs a few hundred bytes of heap.
When it does not work
Something is doing a full reset rather than a wake. Pressing EN, a brown-out, a crash and re-uploading all clear RTC memory. esp_reset_reason() will say which — a real wake reports ESP_RST_DEEPSLEEP.
RTC memory is silicon, kept alive by the RTC power domain, and that domain has no power when the battery does not. Anything that must outlive a power cut belongs in NVS or a file.
Flash wear. NVS wear-levels, but a write on every wake is still tens of thousands of erase cycles a year. Keep the running value in RTC memory and flush it to NVS occasionally, which is what the modulo above does.
The heap does not survive, so an address from the last boot points at nothing. Store values, not references — and structs only if they contain no pointers.
The last option: a tiny second processor that stays awake for microamps, watches a pin or an ADC, and only wakes the real CPU when something is worth waking it for.
The ULP low-power core →Edit this page — content/esp32/what-survives-deep-sleep.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.