Chapter 3: RAM Management

The Stack

Every function call pushes a frame onto the stack. The frame contains:

When the function returns, the frame is popped. The stack grows downward (toward lower addresses) on both AVR and ESP32 (Xtensa).

Stack overflow occurs when the stack grows into the heap or BSS segment. On AVR, this corrupts data silently. On ESP32 (FreeRTOS), each task has its own stack and an overflow triggers a StackOverflow panic if CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is enabled.

Avoiding Stack Overflow

// ESP32: check how close to stack limit you got
UBaseType_t watermark = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("Stack high water mark: %u words remaining\n", watermark);

The Heap

Dynamic memory (malloc, new, Arduino String) comes from the heap, which grows upward. Fragmentation is the main hazard: after many alloc/free cycles, you may have plenty of total free bytes but no single contiguous block large enough for your allocation.

Best Practices


PROGMEM (AVR)

On AVR, the compiler places string literals and initialized arrays in SRAM by default. The PROGMEM attribute forces them into Flash, saving precious SRAM.

#include <avr/pgmspace.h>

const char msg[] PROGMEM = "Hello from Flash!";

void setup() {
    Serial.begin(9600);
    char buf[20];
    strcpy_P(buf, msg);   // copy from Flash to SRAM
    Serial.println(buf);
}

Reading back requires pgm_read_byte(), pgm_read_word(), or the _P variants of string functions (strcpy_P, strcmp_P, printf_P).

The F() Macro

The F() macro is the most common PROGMEM pattern. It keeps string literals in Flash without manual PROGMEM declarations:

Serial.println(F("This string stays in Flash"));

Without F(), every string literal is copied into SRAM at startup. A sketch with dozens of debug strings can easily waste hundreds of bytes.

PROGMEM Tables

Constant lookup tables (sine tables, font bitmaps, coefficient arrays) are prime candidates:

const uint8_t sineTable[256] PROGMEM = { 128, 131, 134, /* ... */ };

uint8_t val = pgm_read_byte(&sineTable[index]);

Measuring RAM Usage

At Compile Time (AVR)

The Arduino IDE reports SRAM usage after compilation, but this only accounts for global/static variables (BSS + Data segments). Stack and heap usage at runtime are not shown.

Use avr-size for a breakdown:

avr-size -C --mcu=atmega328p sketch.elf

At Runtime

See the freeRam() function from Chapter 2. Call it at key points in your code to track the minimum observed free RAM.

ESP32

// Log heap stats periodically
Serial.printf("Heap: %u free / %u total, min ever: %u\n",
    ESP.getFreeHeap(),
    ESP.getHeapSize(),
    ESP.getMinFreeHeap());

The getMinFreeHeap() value is the most useful — it tells you the worst-case low watermark since boot.


Static vs Dynamic Allocation

  Static/Global Stack (local) Heap (malloc/new)
Lifetime Program lifetime Function scope Until freed
Size known at compile time Yes Yes No
Fragmentation risk None None Yes
Good for Config, buffers Small temporaries Variable-size data

Rule of thumb for resource-constrained systems: allocate everything statically if you know the maximum size at compile time. Reserve dynamic allocation for cases where the size is truly unknown.


Previous: Memory Architecture Next: ROM and Flash Storage Home