The Arduino PROGMEM tutorial addresses one of the most common causes of mysterious crashes and instability in Arduino projects: running out of SRAM. The Arduino Uno has only 2 KB of SRAM — and a surprisingly large chunk of it can be consumed by string constants defined in your code. PROGMEM (Program Memory) is a mechanism to store constant data in the much larger flash memory (32 KB on the Uno) instead of SRAM, freeing precious RAM for variables, buffers, and function call stacks. This guide covers everything from basic string storage to lookup tables and practical large-project strategies.
Table of Contents
- Why SRAM Is So Scarce and Why PROGMEM Matters
- Basic PROGMEM Usage: Storing Strings
- The F() Macro: The Easy Way
- Reading Data Back from PROGMEM
- Arrays and Lookup Tables in PROGMEM
- PROGMEM String Tables
- Checking Available SRAM at Runtime
- Frequently Asked Questions
Why SRAM Is So Scarce and Why PROGMEM Matters
When your Arduino sketch starts, all global variables — including string literals — are copied from flash into SRAM. This happens automatically at boot. Consider a sketch with ten error messages like "Error: sensor not responding", "Initialising SD card...", and so on. Each character takes one byte of SRAM. Ten messages averaging 30 characters each consume 300 bytes of SRAM — 15% of the Uno’s entire 2 KB budget — just for text that is never modified.
When SRAM runs out, the symptoms are subtle and maddening: sketches that compile and upload successfully but behave erratically, crash randomly, or produce garbage output. The heap (dynamic allocation) and stack (function calls) grow toward each other, and when they collide, undefined behaviour results — often silently.
PROGMEM solves this by keeping string constants in flash, reading them byte-by-byte only when needed. The trade-off: reading from flash requires special functions and adds a tiny amount of code complexity. For most projects with more than a few dozen bytes of string constants, this trade-off is absolutely worth it.
Basic PROGMEM Usage: Storing Strings
To store a string in flash memory, use the PROGMEM keyword along with the PGM_P or char type in program memory:
#include <avr/pgmspace.h>
// Store a string in flash (program memory)
const char welcome_msg[] PROGMEM = "Welcome to Zbotic Arduino Project!";
const char error_msg[] PROGMEM = "Error: Sensor not found. Check wiring.";
const char ready_msg[] PROGMEM = "System ready. Waiting for input...";
void setup() {
Serial.begin(9600);
}
Important rules for PROGMEM variables:
- Must be declared as
const— PROGMEM data is read-only. - Must be declared at global or static scope — not inside functions on the stack.
- The
PROGMEMattribute goes after the type, before the variable name, or as shown above. - You cannot read PROGMEM data with normal pointer dereferences — use the special
pgm_read_*functions.
The F() Macro: The Easy Way
For Serial.print() statements specifically, Arduino provides the elegantly simple F() macro that wraps a string literal to be stored in flash:
// Without F() — string stored in SRAM (wastes RAM)
Serial.println("System initialising..."); // Bad
// With F() — string stored in flash (saves RAM)
Serial.println(F("System initialising...")); // Good
The F() macro works with any function that accepts a __FlashStringHelper* argument — and all Arduino’s print functions (Serial.print, lcd.print, Serial.println) support it directly. This is the single most impactful and easiest PROGMEM optimisation available, and you should use it habitually for all string literals passed to print functions.
Replace all these:
Serial.println("Initialising...");
Serial.println("SD card OK");
Serial.println("Sensor error!");
Serial.println("Loop started");
With these:
Serial.println(F("Initialising..."));
Serial.println(F("SD card OK"));
Serial.println(F("Sensor error!"));
Serial.println(F("Loop started"));
On a typical logging sketch with 20 such messages, this single change can save 300–500 bytes of SRAM with zero functional change.
Reading Data Back from PROGMEM
To use a PROGMEM string, you must copy it to a SRAM buffer first using strcpy_P(), or read it character by character using pgm_read_byte():
#include <avr/pgmspace.h>
const char error_msg[] PROGMEM = "Error: Sensor not found!";
void printProgmemString(const char* str_P) {
char c;
while ((c = pgm_read_byte(str_P++)) != 0) {
Serial.write(c);
}
Serial.println();
}
void setup() {
Serial.begin(9600);
// Method 1: Print character by character
printProgmemString(error_msg);
// Method 2: Copy to buffer then use
char buffer[32];
strcpy_P(buffer, error_msg); // Copies from flash to SRAM buffer
Serial.println(buffer); // Now usable as normal string
// Method 3: Use Serial.print's built-in PROGMEM support
Serial.println((__FlashStringHelper*) error_msg);
}
Key PROGMEM reading functions from <avr/pgmspace.h>:
pgm_read_byte(addr)— reads one byte (char or uint8_t) from flashpgm_read_word(addr)— reads two bytes (int or uint16_t) from flashpgm_read_dword(addr)— reads four bytes (long or uint32_t) from flashpgm_read_float(addr)— reads four bytes as float from flashstrcpy_P(dest, src_P)— copy PROGMEM string to SRAM bufferstrcat_P(dest, src_P)— concatenate PROGMEM string onto SRAM stringstrcmp_P(str, str_P)— compare SRAM string with PROGMEM stringstrlen_P(str_P)— get length of PROGMEM string without copying
Arrays and Lookup Tables in PROGMEM
PROGMEM is not limited to strings — numeric arrays, sine wave tables, font bitmaps, and any constant data can live in flash. This is particularly useful for lookup tables used in signal processing or graphics:
#include <avr/pgmspace.h>
// Sine wave lookup table (256 values, 0–255 representing 0–1)
const uint8_t sine_table[256] PROGMEM = {
128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 162, 165, 167,
170, 173, 176, 179, 182, 185, 188, 190, 193, 196, 198, 201, 203, 206,
// ... (full 256 values)
128
};
// Temperature calibration offsets for 8 sensors
const float cal_offsets[8] PROGMEM = {
0.12, -0.08, 0.23, -0.15, 0.04, -0.21, 0.18, -0.06
};
void loop() {
// Read from sine table
uint8_t phase = 64; // Quarter-wave
uint8_t amp = pgm_read_byte(&sine_table[phase]);
// Read float from calibration table
float offset = pgm_read_float(&cal_offsets[2]); // Sensor 3's offset
}
Note the & operator before the array element — you are passing the flash address of the element to pgm_read_*, not the value. This is a common mistake that leads to subtle bugs: pgm_read_byte(sine_table[phase]) would try to read from the address equal to the table value, not the address of the element. Always use pgm_read_byte(&array[index]).
PROGMEM String Tables
When you need a table of multiple strings — error messages, menu items, day/month names — a string table in PROGMEM is the efficient solution. This requires an array of PROGMEM pointers to PROGMEM strings (a pointer-to-pointer in flash):
#include <avr/pgmspace.h>
// Individual strings in PROGMEM
const char day0[] PROGMEM = "Sunday";
const char day1[] PROGMEM = "Monday";
const char day2[] PROGMEM = "Tuesday";
const char day3[] PROGMEM = "Wednesday";
const char day4[] PROGMEM = "Thursday";
const char day5[] PROGMEM = "Friday";
const char day6[] PROGMEM = "Saturday";
// Table of pointers to PROGMEM strings — also in PROGMEM
const char* const day_table[] PROGMEM = {
day0, day1, day2, day3, day4, day5, day6
};
void printDay(uint8_t day_num) {
// Two-step read: first read the pointer from flash, then read the string
PGM_P p = (PGM_P) pgm_read_word(&day_table[day_num]);
char c;
while ((c = pgm_read_byte(p++)) != 0) {
Serial.write(c);
}
}
void setup() {
Serial.begin(9600);
printDay(3); // Prints "Wednesday"
Serial.println();
}
The double PROGMEM dereference (pgm_read_word() to get the address, then pgm_read_byte() to read the string) is the standard pattern. On 32-bit Arduino boards (Zero, Due, Nano 33 series), pointers are 4 bytes — use pgm_read_dword() or just pgm_read_ptr() which is architecture-aware.
Checking Available SRAM at Runtime
Before and after applying PROGMEM optimisations, measure the actual SRAM usage. Add this function to your sketch:
int freeRam() {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
void setup() {
Serial.begin(9600);
Serial.print(F("Free SRAM: "));
Serial.print(freeRam());
Serial.println(F(" bytes"));
}
This function returns the number of free bytes between the heap (top of dynamic allocation) and the stack (bottom of current call stack). A value below 200–300 bytes is dangerous — the sketch is at risk of stack-heap collision. Below 100 bytes and crashes are nearly certain under any non-trivial code path.
Arduino IDE 2.x also shows SRAM usage in the compiler output after a successful build (look for “Global variables use X bytes of dynamic memory”). This reports the minimum SRAM used by global variables, but does not account for stack depth or heap fragmentation at runtime.
Frequently Asked Questions
Does PROGMEM work on all Arduino boards?
PROGMEM is an AVR-specific feature — it applies to Arduino boards using AVR microcontrollers: Uno, Mega, Nano, Leonardo, Pro Mini, and Nano Every. ARM-based boards (Arduino Due, Zero, Nano 33 BLE Sense, Nano RP2040 Connect) store constants in flash automatically and don’t require PROGMEM. The F() macro is defined as a no-op on ARM boards for code compatibility, but it doesn’t save RAM there.
Why do I get an error when trying to print a PROGMEM string directly?
PROGMEM pointers cannot be used where a standard char* is expected because the data lives in a different address space. Use (__FlashStringHelper*) cast with Serial.print, or use strcpy_P() to copy to a SRAM buffer first. The compiler will often warn (but not error) when you pass a PROGMEM pointer where a regular pointer is expected — watch for these warnings.
Can I use the String class with PROGMEM data?
You can construct a String from PROGMEM data: String s = String((__FlashStringHelper*) my_pgm_str); — but this defeats part of the purpose since String allocates heap memory. In memory-constrained AVR sketches, avoid the String class entirely. Use character arrays (char buf[32]), strcpy_P(), strtok(), and sprintf() for string manipulation.
How much flash memory does a PROGMEM string use compared to SRAM?
Exactly the same number of bytes — one byte per character plus a null terminator. A 30-character string uses 31 bytes whether stored in SRAM or PROGMEM. The difference is WHERE those bytes live: without PROGMEM they occupy precious SRAM; with PROGMEM they consume flash (of which there is 16–256x more depending on the board), freeing SRAM for runtime data.
I moved my strings to PROGMEM but my sketch still crashes. What else could be using SRAM?
Other SRAM consumers beyond string literals include: local variables in deeply-nested function calls (stack depth), dynamic memory allocation with malloc() or new, the String class (avoid it entirely on AVR), large buffers like char buf[512] on the stack, and interrupt service routines that maintain their own stack frame. Check freeRam() at multiple points in your code to find where SRAM drops below safe levels.
Mastering PROGMEM is a rite of passage for serious Arduino developers. Once you internalise the habit of storing constants in flash — especially the effortless F() macro for all Serial.print strings — you will find your projects become dramatically more stable, and you will have SRAM headroom for the features that actually matter.
Take your Arduino skills further. Browse our complete Arduino boards and accessories at Zbotic — including the Uno, Mega, Nano Every, and complete starter kits, shipped fast across India at competitive prices.
Add comment