Using PROGMEM on AVR Arduino Boards: Storing Constants in Flash to Free SRAM
Move constant strings and lookup tables to flash memory using PROGMEM and pgmspace.h, freeing scarce SRAM for runtime variables on AVR Arduino boards.
29 Aug 2025, 00:14 UTC

Why SRAM Runs Out on AVR Arduinos
If you've ever seen an Arduino Uno sketch crash after adding a few string literals or a modest lookup table, the culprit is often SRAM exhaustion. The ATmega328P on the Uno provides only 2 kilobytes of static memory, while flash memory holds 32 kilobytes—most of which goes unused when sketches only manipulate runtime data. Storing constant strings, font bitmaps, or calibration tables in flash instead of SRAM preserves the limited dynamic memory for stack and heap use, and PROGMEM is the AVR-specific attribute that makes this possible.
The PROGMEM Declaration Pattern
The PROGMEM keyword, defined in <avr/pgmspace.h>, places a variable in the .progmem.data section of the compiled binary, which resides in flash memory at link time. A declaration looks like const char welcome[] PROGMEM = "Welcome!";. The const qualifier is required; omitting it triggers a compiler warning and may cause the variable to be copied into SRAM at startup, defeating the purpose.
const char welcome_msg[] PROGMEM = "Hello, world!";Reading PROGMEM Data
Reading back a PROGMEM variable requires bus-cycle-aware macros because AVR flash and SRAM share the address space but use different bus cycles. pgm_read_byte(&welcome[0]) fetches a single byte, pgm_read_word(&table[2]) fetches a 16-bit value, and so on for pgm_read_dword and pgm_read_float. Direct indexing, such as welcome[0], reads from the wrong bus and produces garbage. The function-like macros handle the extra instruction needed to fetch from flash.
The F() Macro Shortcut
For string literals passed to Serial.print() or similar, the F() macro defined in WString.h (or <avr/pgmspace.h> on newer cores) transparently wraps the string in a FlashStringHelper object, and the Print class overloads retrieve it from flash without explicit pgm_read_* calls. Serial.print(F("Debug line")) keeps the literal out of SRAM automatically. This is the idiomatic shortcut for log messages and error text.
When PROGMEM Doesn't Help
On ARM-based Arduino boards such as the Due, Zero, MKR, Nano 33 IoT, or Portenta, the microcontroller has a unified address space where flash and RAM are mapped linearly. PROGMEM is defined as a no-op and the pgm_read_* macros become simple dereferences; code compiles but provides no SRAM savings. Similarly, compiler optimizations such as LTO or -Os may inline tiny PROGMEM arrays as immediate constants, effectively eliminating the flash read. Verification with avr-objdump -t firmware.elf | grep \.progmem or inspecting the .map file confirms whether the section exists and has size.
Concrete Example: String Table and Lookup Table
Consider a sketch that stores a 256-element sine‑lookup table and a dozen error messages. Without PROGMEM, the table alone consumes 512 bytes of the 2 KB SRAM, leaving little room for variables, String objects, or network buffers. With PROGMEM, the declaration const uint16_t sine_table[] PROGMEM = { 0, 257, 514, … }; places the array in flash. At runtime, each value is fetched with pgm_read_word(&sine_table[i]). The error messages can be emitted with Serial.print(F("Calibration failed")). The IDE runtime‑memory report typically shows roughly 512 fewer bytes used in dynamic memory, which can be the difference between a sketch that runs and one that resets.
Verification Without Guesswork
To confirm the SRAM savings without relying on intuition:
- Compile the sketch on an Uno and note the IDE line
'Global variables use X bytes (Y%) of dynamic memory'. - Replace the in‑SRAM array with a PROGMEM counterpart and recompile; the reported dynamic‑memory total should drop by the array's size (rounded to alignment).
- Run
avr-size -C firmware.elfand compare the.data+.bsscolumns (SRAM‑resident sections) between the two builds; the PROGMEM build should show a smaller sum. - Optionally, inspect the linker map generated with
-Wl,-Map,output.mapfor a.progmem.dataentry and its address range.
On ARM targets, the same steps will show no difference in .data + .bss because PROGMEM has no effect.
ISR and Concurrency Considerations
PROGMEM data cannot be modified at runtime. Flash writes on AVR require SPM instructions, bootloader context, or the EEPROM library, and each page erase (128 bytes on the ATmega328P) wears the flash endurance of approximately 10 000 cycles. Global PROGMEM arrays increase sketch size and may push the binary past the bootloader boundary—Optiboot on the Uno reserves the first 30 KB of flash, so a large PROGMEM table can cause linking errors. In ISR contexts, pgm_read_* macros are atomic for byte and word accesses, but multi‑byte struct reads are not; if consistency matters, disable interrupts briefly or copy the data to a local buffer before processing. The F() macro works only with string literals, not with String objects or char* variables allocated at runtime; those always occupy SRAM heap.
Takeaway
Use PROGMEM on AVR‑based Arduinos whenever you have constant strings, lookup tables, font data, or other read‑only constants that exceed a few dozen bytes. Pair declarations with the const qualifier, access values through the pgm_read_* macros (or the F() shortcut for Serial.print), and verify the SRAM impact with avr-size or the IDE memory report. On non‑AVR boards, PROGMEM is harmless but useless for memory savings; rely on const alone and let the linker place read‑only data in flash.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.