← All articles · May 2, 2026 · pixel.hjlabs.in · Powered by image2cpp

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:

MacroType
pgm_read_byteuint8_t
pgm_read_worduint16_t
pgm_read_dworduint32_t
pgm_read_floatfloat
pgm_read_ptrpointer (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

BoardFlashSRAMComment
ATtiny858KB512Btight; ~6KB free for code+data
Arduino Uno (ATmega328)32KB2KB32x32 sprites comfortable; full-screen 128x64 mono OK; full-screen color too big
Arduino Mega (ATmega2560)256KB8KBlarge mono images fine; small color images possible
RP2040 Pico2MB264KBmultiple full-screen RGB565 images fit
ESP324MB+520KBessentially 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:

  1. Reduce image resolution (the obvious fix)
  2. Switch from RGB565 to mono if visually acceptable
  3. Apply Floyd–Steinberg dithering and use mono
  4. 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:

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:

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 →

More from the blog