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.pydriver on the Pico filesystem (see below)
Wiring (Pico I2C0 defaults we will use)
| OLED | Pico | Notes |
|---|---|---|
| VCC | 3V3 | Do not use VBUS 5V unless the module is clearly 5V-tolerant and level-safe |
| GND | GND | Common ground |
| SDA | GP4 (pin 6) | I2C0 SDA |
| SCL | GP5 (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
freqto 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 | |
|---|---|---|
| RAM | 2 KB (tight) | 264 KB (comfortable) |
| Flash for frames | PROGMEM juggling | Normal arrays / files |
| Language | C++ sketches | Python |
| Startup | Upload sketch | Paste/run or main.py |
| Classroom fit | Classic electronics courses | CS / 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 →