MicroPython Bitmap Display: Convert Images for ESP32/RP2040
MicroPython is the easy on-ramp to embedded development — no compile cycle, REPL on serial, batteries included. But the bitmap drawing path differs from Arduino. This post walks the MicroPython workflow: convert with image2cpp, embed as Python bytes, blit with the framebuf module on ESP32, RP2040 Pico, or any board running MicroPython.
The framebuf module: your only friend
MicroPython's built-in framebuf module is the universal pixel pusher. It owns a buffer of bytes, knows the format (mono / RGB565 / GS4 etc), and exposes blit(), fill(), text(), pixel(). Display drivers (ssd1306.py, st7789.py) inherit from FrameBuffer — so what you learn here applies to every display.
Convert with image2cpp — same as Arduino
The bytes are identical. Open pixel.hjlabs.in/converter, generate the array, but ignore the C++ wrapper — you only need the hex bytes. Use the "raw bytes" output if available, or copy the array contents and convert to Python.
From C array to Python bytes — one substitution
image2cpp gives you something like:
const unsigned char myImg [] PROGMEM = {
0x00, 0xFF, 0x80, 0x42, /* ... */
};
In Python, that becomes:
my_img = bytearray((
0x00, 0xFF, 0x80, 0x42, # ... same hex values
))
Or for a slightly cleaner form:
my_img = b'\x00\xff\x80\x42' # ... continuing
Wrap bytes as a FrameBuffer
import framebuf
# 32x32 monochrome bitmap (32*32/8 = 128 bytes)
my_img = bytearray((
0xff, 0xff, 0xff, 0xff, # row 0
0x80, 0x00, 0x00, 0x01, # row 1
# ... 32 rows total
))
fb = framebuf.FrameBuffer(my_img, 32, 32, framebuf.MONO_HLSB)
The format constants:
| Constant | Meaning | image2cpp setting |
|---|---|---|
| MONO_VLSB | 1-bit, vertical, LSB first | Vertical, LSB |
| MONO_HLSB | 1-bit, horizontal, LSB first (XBM) | XBM |
| MONO_HMSB | 1-bit, horizontal, MSB first | Horizontal MSB (Adafruit-compatible) |
| RGB565 | 16-bit color (2 bytes/pixel) | Arduino RGB565 |
| GS2_HMSB / GS4_HMSB / GS8 | 2/4/8-bit grayscale | Custom (less common) |
Display: SSD1306 on ESP32
Wire SSD1306 to ESP32 standard I2C (SDA=GPIO21, SCL=GPIO22). Install the ssd1306.py driver via mpremote mip install ssd1306 or copy from MicroPython's repo to the device.
from machine import Pin, I2C
import ssd1306
import framebuf
i2c = I2C(0, sda=Pin(21), scl=Pin(22))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
logo_bytes = bytearray((
# 128x64 = 1024 bytes from image2cpp
))
logo = framebuf.FrameBuffer(logo_bytes, 128, 64, framebuf.MONO_HMSB)
oled.fill(0)
oled.blit(logo, 0, 0)
oled.show()
Display: ST7789 on RP2040 Pico
For color images on a Pico, use Russ Hughes's excellent st7789_mpy driver (built into MicroPython firmware for many displays). Convert the image as RGB565 in image2cpp, then:
from machine import Pin, SPI
import st7789
spi = SPI(0, baudrate=40_000_000, sck=Pin(2), mosi=Pin(3))
tft = st7789.ST7789(spi, 240, 320,
reset=Pin(15, Pin.OUT), cs=Pin(5, Pin.OUT),
dc=Pin(4, Pin.OUT))
tft.init()
# RGB565 bytes for 64x64 sprite (8192 bytes)
sprite = bytearray((
# ...
))
tft.blit_buffer(sprite, 100, 80, 64, 64)
The blit_buffer method on st7789_mpy takes raw RGB565 bytes directly — the same bytes image2cpp emits.
Loading from filesystem instead of inlining
Hardcoding 8KB of hex into a .py file works but is ugly. Better: save the bytes as a binary file and load at runtime:
# conversion (one-time, on PC)
# Run image2cpp, copy hex array, then in Python:
import struct
hex_values = [0x00, 0xFF, /* ... */]
with open('logo.bin', 'wb') as f:
f.write(bytes(hex_values))
# Upload logo.bin to the board with mpremote
# at runtime, on the board
with open('logo.bin', 'rb') as f:
logo_bytes = f.read()
fb = framebuf.FrameBuffer(bytearray(logo_bytes), 128, 64, framebuf.MONO_HMSB)
oled.blit(fb, 0, 0)
oled.show()
Drawing GIF / animation frames
For sprite animation, store one bytes object per frame, advance with time.ticks_ms():
import time
frames = [frame0_bytes, frame1_bytes, frame2_bytes]
fbs = [framebuf.FrameBuffer(bytearray(f), 32, 32, framebuf.MONO_HMSB) for f in frames]
i = 0
last = time.ticks_ms()
while True:
if time.ticks_diff(time.ticks_ms(), last) > 100:
oled.fill(0)
oled.blit(fbs[i], 48, 16)
oled.show()
i = (i + 1) % len(fbs)
last = time.ticks_ms()
Memory constraints on RP2040 / ESP32
MicroPython on RP2040 leaves ~150KB heap free. ESP32 has ~100KB. A 320x240 RGB565 image is 153KB — that won't fit in heap on either. Strategies:
- Load image into a
bytearraystored in flash filesystem, then memory-map / stream - Use a smaller sprite layered onto a solid background
- Move to PSRAM-equipped boards (ESP32-WROVER) with 4MB+ external RAM
Common pitfalls
- Black or scrambled image. Wrong framebuf format. Try MONO_HMSB ↔ MONO_HLSB ↔ MONO_VLSB combinations.
- Image rotated or mirrored. Display rotation; check the driver's
rotationargument. - "MemoryError: memory allocation failed". Image too big for heap. Use bytes from flash filesystem.
- Slow animation.
oled.show()on I2C is the bottleneck (~30ms). Use SPI variant or partial updates.
Bottom line
MicroPython's framebuf consumes the same bytes as Adafruit_GFX — just wrap them in a FrameBuffer with the matching format constant. image2cpp generates the bytes, you copy-paste, you ship. Read our Arduino OLED tutorial for the C++ side — concepts transfer 1:1.
Streaming JSON from your MicroPython device? fmt.hjlabs.in formats and validates JSON in the browser.
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 →