OLED Animation Maker OLED Maker

MicroPython SSD1306 OLED Animation on Raspberry Pi Pico

Arduino is still the default in many schools, but the Raspberry Pi Pico + MicroPython combo is easier when students already know Python. The Pico has far more RAM than an Uno, so OLED animations stop being a PROGMEM puzzle and become a normal bytearray loop.

This guide covers I2C wiring, the official-style ssd1306 driver with framebuf, text, shapes, and a multi-frame animation. No photos — just pins, code, and the mistakes that waste a lab period.

What you need

  • Raspberry Pi Pico or Pico W
  • MicroPython UF2 already flashed
  • 0.96" I2C SSD1306 (3.3V-friendly — Pico IO is 3.3V)
  • Thonny or another MicroPython IDE
  • ssd1306.py driver on the Pico filesystem (see below)

Wiring (Pico I2C0 defaults we will use)

OLEDPicoNotes
VCC3V3Do not use VBUS 5V unless the module is clearly 5V-tolerant and level-safe
GNDGNDCommon ground
SDAGP4 (pin 6)I2C0 SDA
SCLGP5 (pin 7)I2C0 SCL

You can move to other I2C-capable pins; just change the constructor. If the screen stays blank, run an I2C scan in MicroPython before blaming the driver.

I2C scanner (MicroPython)

from machine import Pin, I2C
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
print("Devices:", [hex(a) for a in i2c.scan()])

Expect 0x3c (sometimes 0x3d). Empty list → wiring or power. Fix that first.

Driver file: ssd1306.py

Copy the standard MicroPython ssd1306.py (from the MicroPython repository drivers/display/ssd1306.py) onto the Pico as /ssd1306.py. Thonny: File → Save as → Raspberry Pi Pico. Without this file, import ssd1306 fails.

The driver subclasses framebuf.FrameBuffer, so you get text, pixel, line, rect, fill_rect, blit, etc.

Hello World

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import time

i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
oled = SSD1306_I2C(128, 64, i2c, addr=0x3c)

oled.fill(0)
oled.text("Hello Pico", 0, 0)
oled.text("SSD1306 OK", 0, 16)
oled.show()

while True:
    time.sleep(1)

Always call oled.show() after drawing — same idea as Arduino’s display.display().

Shapes and a simple HUD

oled.fill(0)
oled.rect(0, 0, 128, 64, 1)          # border
oled.fill_rect(4, 4, 40, 10, 1)     # header bar
oled.text("TEMP", 6, 5)               # note: text on filled bar needs inverse trick
# easier: text outside fill
oled.fill(0)
oled.rect(0, 0, 128, 64, 1)
oled.text("Temp C", 4, 4)
oled.text("24.6", 4, 24)
oled.hline(4, 40, 120, 1)
oled.fill_rect(4, 44, 72, 8, 1)     # fake bar
oled.show()

MicroPython’s built-in font is 8×8. For bigger digits, either scale by drawing blocks or blit a custom framebuffer glyph.

Multi-frame animation with bytearrays

Each 128×64 monochrome frame is 1024 bytes (MVLSB layout used by the driver). On Pico you can keep many frames in RAM. Example with three tiny programmatic frames (no external files):

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import framebuf
import time

i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)

def make_ball_frame(x):
    buf = bytearray(1024)
    fb = framebuf.FrameBuffer(buf, 128, 64, framebuf.MONO_VLSB)
    fb.fill(0)
    fb.text("Pico OLED", 0, 0)
    fb.fill_rect(x, 28, 12, 12, 1)
    return buf

frames = [make_ball_frame(x) for x in (10, 40, 70, 100, 70, 40)]

while True:
    for buf in frames:
        oled.blit(framebuf.FrameBuffer(buf, 128, 64, framebuf.MONO_VLSB), 0, 0)
        # Faster path: copy into oled buffer then show
        oled.show()
        time.sleep_ms(80)

Cleaner pattern used in production sketches: draw directly into oled each frame instead of prebuilding buffers, unless you need playback of imported GIF frames.

Playing exported animation frames

If you export MicroPython / framebuf data from oledanimationmaker.com, you typically get a list of bytearray literals or a flat bytes object plus frame count. Skeleton:

# frames_data = [ bytes([...]), bytes([...]), ... ]  # from exporter
FRAME_W, FRAME_H = 128, 64
FRAME_SIZE = FRAME_W * FRAME_H // 8

while True:
    for raw in frames_data:
        fb = framebuf.FrameBuffer(bytearray(raw), FRAME_W, FRAME_H, framebuf.MONO_VLSB)
        oled.fill(0)
        oled.blit(fb, 0, 0)
        oled.show()
        time.sleep_ms(100)

Confirm the exporter’s bit order matches MONO_VLSB. If the image looks sliced or mirrored, try the other mono formats only after checking width/height first — wrong size is the usual culprit.

Partial updates and speed

  • oled.show() sends the whole buffer over I2C — fine for 128×64.
  • Raise I2C freq to 400000 (shown above). Some modules tolerate 1_000_000; not all do.
  • Do not print() every frame to USB — it stalls animation in Thonny.
  • Sleep with time.sleep_ms, not busy loops.

Pico vs Arduino Uno for OLED animation

Uno + Arduino C++Pico + MicroPython
RAM2 KB (tight)264 KB (comfortable)
Flash for framesPROGMEM jugglingNormal arrays / files
LanguageC++ sketchesPython
StartupUpload sketchPaste/run or main.py
Classroom fitClassic electronics coursesCS / Python courses

Using main.py so it runs on power-up

Save your animation as main.py on the Pico. On reset it auto-runs. Keep a way to recover (hold BOOTSEL, reflash, or interrupt in Thonny) if an infinite loop locks USB — rare but teach students the escape hatch.

Troubleshooting

  • OSError on construct: wrong addr or no ACK — run scanner.
  • ImportError ssd1306: file not on device or wrong name/case.
  • Garbled image: SH1106 module or wrong framebuffer format.
  • Works in REPL once, fails as main.py: exception at import — wrap setup in try/except and show error text on OLED if you want self-debug.
  • Brownout when powering OLED from 3V3 pin with other loads: power OLED from a solid 3.3V supply and common GND.

Complete minimal animation project

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import time

i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)

x, dir_ = 0, 1
while True:
    oled.fill(0)
    oled.text("MicroPython", 0, 0)
    oled.fill_rect(x, 28, 16, 16, 1)
    oled.show()
    x += dir_ * 4
    if x < 0 or x > 112:
        dir_ = -dir_
    time.sleep_ms(40)

FAQ

Can I use CircuitPython instead?

Yes — APIs differ slightly (displayio). This article targets MicroPython’s ssd1306 + framebuf path.

Does Pico W change anything for OLED?

Same I2C pins work. WiFi uses power — watch brownouts on weak USB hubs.

Can I load frames from the filesystem?

Yes. Store raw 1024-byte files or a single blob and read with open(). Good for longer animations without huge main.py files.

Related

Export MicroPython-friendly frames

Build the animation visually, then adapt the byte arrays into the blit loop above.

Open the free tool →