Raspberry Pi Pico I2C OLED Display (SSD1306) with MicroPython

A small display turns a Pico project into a standalone device: no laptop, no serial monitor, just readings on a screen. The 0.96 inch 128x64 OLED modules based on the SSD1306 controller are cheap, bright, need no backlight and talk over I2C with only two signal wires. In this tutorial you will wire one to the Pico, confirm it on the bus, install the MicroPython driver, and build a screen that shows text, shapes and a live bar graph of an analog input.

I2C in Brief

I2C is a two-wire bus: SDA carries data and SCL carries the clock generated by the controller (the Pico). Many devices can share the same two wires because each has a 7-bit address. Both lines are open-drain, which means devices only pull them low and pull-up resistors bring them high. Almost every SSD1306 breakout already includes these pull-ups, so you usually do not need to add any.

The Pico has two hardware I2C controllers, I2C0 and I2C1, and each can be routed to several pin pairs. You must use a valid SDA/SCL pair for the controller you choose.

ControllerSDA pinsSCL pins
I2C0GP0, GP4, GP8, GP12, GP16, GP20GP1, GP5, GP9, GP13, GP17, GP21
I2C1GP2, GP6, GP10, GP14, GP18, GP26GP3, GP7, GP11, GP15, GP19, GP27

Wiring the OLED

OLED pinPico pinPhysical pin
GNDGND8
VCC3V3(OUT)36
SCLGP5 (I2C0 SCL)7
SDAGP4 (I2C0 SDA)6

Check the order of the header on your module. Some boards print VCC first and GND second; reversing power can destroy the display. The potentiometer from the ADC tutorial stays on GP26 to provide live data for the bar graph.

Step 1: Scan the Bus

Before writing display code, confirm the Pico can see the module. Type this in the Thonny Shell:

>>> from machine import Pin, I2C
>>> i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
>>> [hex(a) for a in i2c.scan()]
['0x3c']

Most SSD1306 modules answer at 0x3C; some use 0x3D, selected by a solder jumper on the back. An empty list means a wiring fault: swapped SDA and SCL, wrong pins for the controller, or no power to the module.

Step 2: Install the ssd1306 Driver

The driver is not built into the firmware. It is a single file, ssd1306.py, from the official micropython-lib collection. Install it in one of these ways:

  • In Thonny, open Tools > Manage packages, search for ssd1306, and install it to the Pico.
  • With the mpremote command-line tool: mpremote mip install ssd1306.
  • On a Pico W already connected to Wi-Fi: import mip then mip.install('ssd1306').
  • Or copy ssd1306.py manually to the board's lib folder using Thonny's Files pane.

Step 3: Text, Shapes and a Live Bar Graph

oled_demo.py - splash screen, then live voltage readout and bar graph on an SSD1306
# oled_demo.py - SSD1306 128x64 OLED on I2C0 (GP4 = SDA, GP5 = SCL)
from machine import Pin, I2C, ADC
import ssd1306
import time

WIDTH, HEIGHT = 128, 64

i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
if 0x3C not in i2c.scan():
    raise OSError('OLED not found at 0x3C - check wiring')

oled = ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c, addr=0x3C)
pot = ADC(26)

# ---- Splash screen ----
oled.fill(0)                          # clear the buffer (0 = black)
oled.rect(0, 0, WIDTH, HEIGHT, 1)     # border
oled.text('Raspberry Pi', 16, 16, 1)  # built-in 8x8 font
oled.text('Pico + OLED', 20, 32, 1)
oled.show()                           # send the buffer to the display
time.sleep(2)

# ---- Live screen ----
history = []                          # last readings for the graph
while True:
    raw = pot.read_u16()
    volts = raw * 3.3 / 65535

    history.append(volts)
    if len(history) > WIDTH:
        history.pop(0)

    oled.fill(0)
    oled.text('ADC0: {:.2f} V'.format(volts), 0, 0, 1)

    # Horizontal bar: full width = 3.3 V
    bar = int(volts / 3.3 * (WIDTH - 2))
    oled.rect(0, 12, WIDTH, 10, 1)
    oled.fill_rect(1, 13, bar, 8, 1)

    # Scrolling line graph in the lower 40 pixels (y from 24 to 63)
    for x in range(1, len(history)):
        y0 = 63 - int(history[x - 1] / 3.3 * 39)
        y1 = 63 - int(history[x] / 3.3 * 39)
        oled.line(x - 1, y0, x, y1, 1)

    oled.show()
    time.sleep_ms(100)

How the Code Works

SSD1306_I2C subclasses MicroPython's framebuf.FrameBuffer. Every drawing call, such as text, rect, fill_rect, line and pixel, changes a 1 KB buffer in the Pico's RAM (128 x 64 pixels at 1 bit each is 8192 bits). Nothing appears on the screen until show() sends that whole buffer over I2C. At 400 kHz this takes only a few milliseconds, so redrawing the full screen ten times a second is easy.

  • Coordinates start at (0, 0) in the top-left corner; x increases to the right and y downward.
  • The built-in font is 8 x 8 pixels, so a 128 x 64 screen fits 16 characters on each of 8 lines.
  • Colour 1 lights a pixel and 0 clears it. On two-colour modules the top 16 rows are yellow and the rest blue; this is fixed in the glass, not set in code.
  • oled.contrast(0-255) changes brightness, oled.invert(1) swaps black and white, and oled.poweroff() blanks the panel to save power and reduce burn-in.

The history list keeps the last 128 readings, one per pixel column, and pop(0) drops the oldest so the graph scrolls left. Each reading is scaled so that 0 V sits on the bottom row and 3.3 V reaches row 24, just under the bar, which keeps the three screen areas from overlapping.

Troubleshooting

  • OSError: [Errno 5] EIO: the device did not acknowledge. Recheck wiring, the address and that the module has power.
  • ImportError: no module named 'ssd1306': the driver file is not on the Pico. Install it as described above.
  • Display shows noise or is shifted by two pixels: the module uses an SH1106 controller (common on 1.3 inch displays), which needs an SH1106 driver instead.
  • Only the top half works: you created the object as 128 x 32 for a 128 x 64 panel, or the reverse.
  • Long wires cause errors: reduce freq to 100000 or shorten the wires.

Summary

You connected an SSD1306 OLED to I2C0 on GP4 and GP5, verified its 0x3C address with i2c.scan(), installed the micropython-lib driver and drew text, rectangles and a scrolling graph using the framebuffer API. Remember that drawing happens in RAM and show() updates the panel. The same I2C bus can carry more devices, such as the BME280 sensor mentioned in the next tutorial, as long as their addresses differ.