Skip to content

08 · Memory & Power Basics

Two resources define what an embedded system can do: memory (a few KB to a few hundred KB, fixed forever at design time) and power (mains, USB, or a battery that must last). This module maps out the three kinds of memory in your chip, shows how to measure what you're using, and introduces the ESP32's deep sleep — the difference between a battery project lasting hours and lasting months.

The three memories

Memory Uno (ATmega328P) ESP32 Survives power-off? Holds
Flash 32 KB 4 MB (typical) Yes Your program, constants
SRAM 2 KB ~520 KB No Variables, stack, heap
EEPROM / NVS 1 KB EEPROM NVS (in flash) Yes Settings, counters — data you write at runtime
  • Flash is written when you upload; your code executes from it, and it keeps the program through power cycles. Wears out after ~10k–100k erase cycles — irrelevant for uploads, very relevant if firmware logs data to it (Level 2 topic).
  • SRAM is working memory — every variable, the call stack, the heap. It's the scarcest resource on small chips, and running out doesn't give a tidy error: the stack silently collides with your data, corrupting either — the classic cause of random resets and impossible bugs.
  • EEPROM/NVS is small persistent storage your program can write: calibration values, a WiFi password, a boot counter.

The compile summary you saw in module 1 reports the first two. Watch the RAM number: past ~75% on an Uno, stack collisions become a real risk.

Measuring free memory at runtime

On the ESP32 it's built in:

void loop() {
  Serial.print(F("free heap: "));
  Serial.println(ESP.getFreeHeap());       // bytes currently available
  Serial.print(F("min free ever: "));
  Serial.println(ESP.getMinFreeHeap());    // low-water mark since boot — the number to watch
  delay(5000);
}

getMinFreeHeap() is the diagnostic gold: a steadily falling value is a memory leak (often String churn — module 2) and predicts exactly when the device will die. Log it in anything meant to run for weeks.

On the Uno there's no API, but the classic trick measures the gap between the heap and the stack:

int freeRam() {
  extern int __heap_start, *__brkval;
  int v;
  return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}

You don't need to understand the internals yet — call it, log it, watch the trend, exactly as with the ESP32 version.

Persistent data

Classic Arduino — the EEPROM library reads/writes single bytes or whole structs:

#include <EEPROM.h>

uint32_t bootCount;
EEPROM.get(0, bootCount);        // read 4 bytes at address 0
bootCount++;
EEPROM.put(0, bootCount);        // write back (put skips unchanged bytes)

ESP32 — EEPROM is emulated; the native mechanism is NVS (non-volatile storage), a key-value store used via Preferences:

#include <Preferences.h>
Preferences prefs;

void setup() {
  Serial.begin(115200);
  prefs.begin("app", false);                          // namespace, read-write
  uint32_t boots = prefs.getUInt("boots", 0) + 1;     // default 0 if absent
  prefs.putUInt("boots", boots);
  prefs.end();
  Serial.printf("boot number %u\n", boots);
}

Remember flash wear: save when a value changes (or rarely), never unconditionally in loop().

Power: why sleep matters

Rough ESP32 numbers tell the whole story:

State Current (order of magnitude)
Active, WiFi transmitting 150–250 mA
Active, WiFi off 30–50 mA
Deep sleep ~10 µA

A 2000 mAh battery lasts about two days at 40 mA — or years at 10 µA. Battery devices therefore live a duty-cycled life: wake, measure, transmit, sleep. The fraction of time awake sets the battery life.

ESP32 deep sleep

In deep sleep the CPU and RAM power off; only the ultra-low-power RTC domain keeps running. Waking is a rebootsetup() runs again — so state you need across sleeps goes in NVS or in RTC memory:

const uint64_t SLEEP_S = 30;

RTC_DATA_ATTR uint32_t wakeCount = 0;    // RTC_DATA_ATTR: survives deep sleep
                                         // (but not power-off — that's what NVS is for)

void setup() {
  Serial.begin(115200);
  wakeCount++;

  Serial.printf("Wake #%u, cause %d\n", wakeCount, esp_sleep_get_wakeup_cause());

  // ... read sensor, log or transmit the value ...

  Serial.printf("Sleeping %llu s...\n", SLEEP_S);
  Serial.flush();                                    // let the UART finish printing!
  esp_sleep_enable_timer_wakeup(SLEEP_S * 1000000ULL);  // microseconds
  esp_deep_sleep_start();                            // never returns
}

void loop() {}                                       // never reached

Run it in Wokwi (ESP32 template — the simulator supports deep sleep): the serial monitor shows a boot banner and an incrementing wake count every 30 simulated seconds. Note the structure — everything happens in setup(), because every wake is a fresh boot.

Timer wakeup is one of several sources: esp_sleep_enable_ext0_wakeup(pin, level) wakes on a GPIO (a button, a sensor's alarm output), and sources can be combined — esp_sleep_get_wakeup_cause() tells you which one fired.

How It Actually Works

Why SRAM corruption causes "impossible" bugs: on both AVR and ESP32, the stack (function call frames, local variables, return addresses) and the heap (dynamic allocations, including every hidden String allocation) grow toward each other from opposite ends of the same physical SRAM address range — the stack grows downward from high addresses, the heap grows upward from just above your global variables. There is no hardware guard page in between on these small chips (unlike an MMU-equipped application processor). When they collide, a function call's return address or a local variable literally gets overwritten by heap data (or vice versa) — the CPU doesn't detect this as an error at all, it just executes whatever garbage address it finds when the function returns, which is why the symptom is a spontaneous reset (jumping to an invalid address trips the watchdog or triggers a fault) rather than a clean error message.

Why flash "wears out": flash memory stores bits by trapping electrical charge on a floating gate inside each memory cell, insulated by a thin oxide layer. Writing a 10 bit requires an erase-then-program cycle that forces charge across that oxide layer via quantum tunneling; each cycle physically stresses and gradually degrades the oxide's insulating ability until, after tens of thousands of cycles, it can no longer reliably hold charge — a hardware failure mode, not a software one. This is exactly why this module is careful to say "write on change, not per loop": every prefs.putUInt()/EEPROM.put() call that changes the underlying flash sector consumes part of its finite lifetime, and code that saves every second instead of every hour can wear out a sector in weeks.

What actually happens in deep sleep: an ESP32 in deep sleep powers down the CPU cores, most SRAM, and most peripherals entirely — literally cutting their supply rails — leaving only a small, separately-powered RTC domain containing a low-power timer, a handful of RTC GPIOs, and a tiny slice of RTC memory alive. RTC_DATA_ATTR places a variable's storage in that surviving RTC memory region instead of ordinary SRAM, specifically so it isn't wiped when the rest of SRAM loses power. Waking is indistinguishable from a power-on reset from the CPU's point of view — the boot ROM runs, app_main/setup() runs from scratch — except the boot ROM first checks esp_sleep_get_wakeup_cause() (a status register the RTC domain sets before waking the rest of the chip) so your code can tell "fresh power-on" from "woke from a timer/GPIO." This full power-down of the CPU is exactly what gets deep sleep down to ~10 µA — there is simply no clocked digital logic left running to consume current, only quiescent leakage in the RTC domain.

Cheat sheet

Concept Detail
Flash / SRAM / EEPROM-NVS Program (persistent) / variables (volatile, scarce) / runtime settings (persistent)
Out of SRAM No error — stack corrupts data → random resets
ESP.getFreeHeap() / getMinFreeHeap() Current / worst-case free RAM; falling min = leak
EEPROM.get/put(addr, var) AVR persistent storage
Preferences (NVS) ESP32 key-value persistent store — preferred
Flash wear ~10k–100k erase cycles — write on change, not per loop
Deep sleep current ~10 µA vs 30–250 mA active — the battery-life lever
esp_sleep_enable_timer_wakeup(µs) + esp_deep_sleep_start() Timed sleep; wake = reboot into setup()
RTC_DATA_ATTR Variable survives deep sleep (not power loss)
Serial.flush() before sleep Otherwise the last log lines are cut off

Exercise

Build a Wokwi ESP32 sketch simulating a battery sensor node: on each wake, increment a counter in NVS (so it survives even full power-off, unlike RTC_DATA_ATTR — keep both and print both to see the difference), read a potentiometer as a stand-in sensor, print boot number, both counters, the reading, and ESP.getFreeHeap(), then deep-sleep 15 s. Let it run several cycles and confirm: NVS counter always climbs, RTC counter climbs per sleep cycle, heap stays flat. Bonus: estimate battery life at 2000 mAh if the node is awake 200 ms at 45 mA per cycle and asleep at 10 µA otherwise — first for a 15 s cycle, then for 10 min.