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.
xTaskCreate(fn, "name", 8192, NULL, 1, NULL)uxTaskGetStackHighWaterMark(NULL) to measure peak stack usage// 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);
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.
String concatenation in Arduino sketches on small AVR boards — use char[] insteadheap_caps_malloc(size, MALLOC_CAP_8BIT) lets you choose which memory region to allocate fromOn 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 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.
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]);
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
See the freeRam() function from Chapter 2. Call it at key points in your code to track the minimum observed free RAM.
// 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/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 |