ElectronicZoologyfield notes from the garage
Memory • ESP32

Compression with PSRAM
Decompress Once, Access Freely

Board: ESP32 with PSRAM (AI Thinker, WROVER, S3)
Library: miniz
PSRAM required: Yes
ESP32 Memory Series - Step 9 of 9
⚠ Untested

The Approach

The previous guide covered compressing data in flash and decompressing it chunk by chunk into internal RAM. That works on any ESP32 but adds complexity - you have to process your data in pieces rather than having it all available at once.

If your board has PSRAM, you can do something much cleaner. Decompress everything into PSRAM at boot and access it freely for the rest of your program. No chunking, no decompressor state to manage - just a pointer to your data sitting ready in that big extra workbench.

Compress On Your Computer

The compression step is identical to the previous guide. Use the same Python script to compress your data and generate a PROGMEM header file.

import zlib, sys

with open(sys.argv[1], 'rb') as f:
    data = f.read()

compressed = zlib.compress(data, level=9)

print(f"// Original size: {len(data)} bytes")
print(f"// Compressed size: {len(compressed)} bytes")
print(f"const uint32_t originalSize = {len(data)};")
print(f"const uint32_t compressedSize = {len(compressed)};")
print("const uint8_t compressedData[] PROGMEM = {")
print(', '.join(f'0x{b:02x}' for b in compressed))
print("}")

Run it like this:

python3 compress.py myimage.bmp > image_data.h

Decompress Into PSRAM at Boot

#include <miniz.h>
#include "image_data.h"

uint8_t* imageBuffer = nullptr;

void setup() {
    Serial.begin(115200);
    delay(1000);

    if (!psramFound()) {
        Serial.println("No PSRAM found. Use the non-PSRAM version of this guide.");
        return;
    }

    // Allocate space in PSRAM for the decompressed data
    imageBuffer = (uint8_t*) ps_malloc(originalSize);
    if (imageBuffer == nullptr) {
        Serial.println("PSRAM allocation failed.");
        return;
    }

    // Copy compressed data from PROGMEM into a temporary internal RAM buffer
    uint8_t* compBuf = (uint8_t*) malloc(compressedSize);
    memcpy_P(compBuf, compressedData, compressedSize);

    // Decompress directly into PSRAM
    mz_ulong destLen = originalSize;
    int result = mz_uncompress(imageBuffer, &destLen, compBuf, compressedSize);

    free(compBuf);

    if (result != MZ_OK) {
        Serial.println("Decompression failed.");
        return;
    }

    Serial.println("Decompression complete.");
    Serial.printf("Free heap after decompression:  %u bytes\n", ESP.getFreeHeap());
    Serial.printf("Free PSRAM after decompression: %u bytes\n", ESP.getFreePsram());

    // imageBuffer is now a normal pointer to your full decompressed data in PSRAM
    Serial.printf("First byte: 0x%02x\n", imageBuffer[0]);
}

void loop() {
    // Access imageBuffer freely anywhere in your code
}

What Is Happening

At boot the compressed data is copied from PROGMEM into a small temporary internal RAM buffer just long enough to run the decompressor. The decompressor writes the full output directly into PSRAM. The temporary buffer is freed immediately after.

From that point on imageBuffer is just a normal pointer and you can read from it anywhere in your code without any chunking or decompressor overhead.

The result: Your flash budget stays small because the data is stored compressed. Your internal RAM stays free because the decompressed data lives in PSRAM. Your code stays simple because you have direct access to the whole dataset at once. Smaller on the outside, bigger on the inside.