Reduce SRAM Usage: PROGMEM Image Storage in Arduino
An Arduino Uno has 32KB of flash (program memory) and only 2KB of SRAM. A 128x64 mono bitmap is 1KB — half your SRAM gone if you forget PROGMEM. This post is the definitive guide to storing images (and any large constant data) in flash on AVR Arduinos, including the gotchas, the macros, and when modern boards make this all moot.
The Harvard architecture problem
AVR microcontrollers (ATmega328, ATmega2560, ATtiny family) use a Harvard architecture: flash and SRAM are physically separate memory spaces with separate address pointers. A "regular" pointer in C++ refers to SRAM. To read flash at runtime, you need special LPM (Load Program Memory) instructions, exposed via the pgm_read_* macros.
This is utterly unlike modern ARM (RP2040, STM32), Xtensa (ESP32), and RISC-V cores, which use a unified von Neumann memory map — flash is just an address range like everything else. On those boards, PROGMEM is a no-op kept for source compatibility.
What PROGMEM does
The PROGMEM attribute (defined as __attribute__((__progmem__)) in avr/pgmspace.h) tells avr-gcc: "place this data in the flash .progmem.data section, do not copy to SRAM at boot." Without it, every const array gets copied to SRAM during the C++ startup hook, even though you never write to it.
The correct declaration
For a bitmap:
const unsigned char myImage [] PROGMEM = {
0x00, 0xFF, 0x80, /* ... */
};
The order matters slightly. Both forms compile, but const ... PROGMEM is the canonical Adafruit/Arduino style and is what image2cpp emits.
Reading from PROGMEM at runtime
You cannot just dereference the pointer:
// WRONG on AVR — reads SRAM at the same address, gets garbage
unsigned char b = myImage[i];
// CORRECT on AVR
#include <avr/pgmspace.h>
unsigned char b = pgm_read_byte(&myImage[i]);
For different sizes:
| Macro | Type |
|---|---|
| pgm_read_byte | uint8_t |
| pgm_read_word | uint16_t |
| pgm_read_dword | uint32_t |
| pgm_read_float | float |
| pgm_read_ptr | pointer (rarely needed) |
Adafruit_GFX handles this for you
You don't write pgm_read_byte by hand for bitmap rendering — drawBitmap internally checks if the pointer is in PROGMEM and uses pgm_read_byte appropriately. The library docs note: "bitmap argument may reside in PROGMEM or RAM; the function correctly handles both." This is true on AVR. On non-AVR platforms it's irrelevant.
Sizing your image to fit flash
| Board | Flash | SRAM | Comment |
|---|---|---|---|
| ATtiny85 | 8KB | 512B | tight; ~6KB free for code+data |
| Arduino Uno (ATmega328) | 32KB | 2KB | 32x32 sprites comfortable; full-screen 128x64 mono OK; full-screen color too big |
| Arduino Mega (ATmega2560) | 256KB | 8KB | large mono images fine; small color images possible |
| RP2040 Pico | 2MB | 264KB | multiple full-screen RGB565 images fit |
| ESP32 | 4MB+ | 520KB | essentially unlimited image storage |
The "section .progmem.data is not within region 'text'" error
You see this at link time when your image array exceeds available flash. Solutions in order of preference:
- Reduce image resolution (the obvious fix)
- Switch from RGB565 to mono if visually acceptable
- Apply Floyd–Steinberg dithering and use mono
- Move from Uno to Mega (256KB flash) or migrate to ESP32 / RP2040
The 64KB barrier and __memx / pgm_read_far
On ATmega2560 (256KB flash), data above the first 64KB requires "far" addressing — regular pgm_read_byte only reaches the first 64KB. For arrays in the upper banks:
// Use uint_farptr_t and pgm_read_byte_far on ATmega2560
#include <avr/pgmspace.h>
const unsigned char hugeImage [] PROGMEM = { /* ... >64KB ... */ };
uint_farptr_t addr = pgm_get_far_address(hugeImage);
uint8_t b = pgm_read_byte_far(addr + i);
Adafruit_GFX does not handle far PROGMEM on Mega — if your bitmap is in the upper banks, drawBitmap will read garbage. Workaround: keep all image arrays in lower 64KB by placing them earlier in the source file.
F() macro for strings — same idea
Strings literals also live in SRAM by default. The Arduino F() macro forces them to flash:
Serial.println(F("This string lives in flash")); // 0 SRAM cost
Serial.println("This string lives in SRAM"); // wastes ~30 bytes
Same mechanism, different macro. The compiled F() string is read with __FlashStringHelper internally.
Finding hidden SRAM hogs
The Arduino IDE shows two numbers after compile: "Sketch uses N bytes of program storage" and "Global variables use N bytes of dynamic memory". The second is your SRAM baseline — before any runtime allocations. If it's already 70% of your chip's SRAM, you have unmarked-PROGMEM data:
- String literals not wrapped in
F() - Bitmap arrays missing
PROGMEM - Lookup tables (sin/cos, gamma) not in PROGMEM
- Global structs being default-initialized in SRAM
Quick audit: search your sketch for const arrays larger than 16 bytes — every one should have PROGMEM.
Why image2cpp's output is correct by default
image2cpp emits const unsigned char ... [] PROGMEM = ... — ready to paste into AVR sketches. On non-AVR boards, PROGMEM is silently ignored. There is no downside to keeping it in cross-platform code.
Modern boards: should you still bother?
Pragmatic take:
- If targeting AVR (Uno/Nano/Mega/ATtiny): always use PROGMEM for images, and double-check with the link-time SRAM report.
- If targeting RP2040, ESP32, STM32: use it for portability but it has zero effect at runtime.
- If you don't know your target: use it. Costs nothing, sometimes saves you.
Bottom line
On AVR, const PROGMEM is the difference between "compiles cleanly" and "stack collides with heap and the chip resets". image2cpp emits the right declaration; just don't strip the PROGMEM when copying. Generate your bitmap, paste it in, and the AVR linker will tell you immediately if you've blown the budget.
For a deeper memory-format breakdown, see our monochrome vs grayscale vs color post. And if your Arduino project deserves polished promo graphics, og.hjlabs.in ships favicons and OG previews in seconds.
Try image2cpp now — free, in browser
Drop in a PNG, JPG or BMP. Get a paste-ready C++ array for Arduino, ESP32 or RP2040. 100% client-side. Learn more about image2cpp or jump straight to the tool.
Open the converter →