128×64 OLED (SSD1306) with Raspberry Pi Pico using I2C in Wokwi – Complete Beginner Guide

OLED Display with Raspberry Pi Pico – I2C SSD1306 MicroPython Tutorial | MakeMindz
🖥️ Raspberry Pi Pico Tutorial

OLED Display Graphics & Animation with Raspberry Pi Pico

Wire a 128×64 SSD1306 OLED over I2C and program it with MicroPython. Display text, logos, and live timer animations — all simulated in Wokwi.

📡 I2C Protocol 🐍 MicroPython 🖥️ SSD1306 OLED 🖼️ FrameBuffer Graphics 🎞️ Animation 🔰 Beginner Friendly

Learn how to wire and program a 128×64 SSD1306 OLED display with the Raspberry Pi Pico using I2C communication in the Wokwi simulator. This hands-on tutorial covers displaying text, graphics, logos, and animations using MicroPython — perfect for IoT dashboards, digital clocks, and embedded display systems.

💡

What is an OLED Display?

An OLED (Organic Light Emitting Diode) display is a low-power screen that displays crisp text and graphics without any backlighting. The popular SSD1306 controller supports 128×64 and 128×32 resolutions over I2C or SPI.

SSD1306 128×64 — Interactive Preview

Why Use SSD1306 OLED?

Low Power
No backlight needed — ideal for battery projects
🔆
High Contrast
True black pixels with vivid white output
📡
Simple I2C
Only 2 signal wires — SCL + SDA
🌐
IoT Ready
Perfect for dashboards and sensor readouts
🛠

Components Used

🎛️
Raspberry Pi PicoMicrocontroller
🖥️
SSD1306 128×64 OLEDI2C Display
🔌
Jumper Wires4 wires needed
🧱
Mini BreadboardFor wiring
1

Create a New Project in Wokwi

  1. Go to wokwi.com and sign in
  2. Click New Project
  3. Select Raspberry Pi Pico
2

Add the OLED Display Component

  1. Click the blue "+" button in Wokwi
  2. Search for SSD1306 or OLED
  3. Select SSD1306 128×64 OLED Display
  4. Add it to the workspace

OLED Default Settings

Resolution: 128×64 px Interface: I2C Address: 0x3C
3

OLED Pin Configuration (I2C Mode)

OLED PinFunctionConnect To (Pico)Wire Colour
GND Ground GND.8 ⚫ Black
VCC Power (3.3V) 3V3_EN 🔴 Red
SCL I2C Clock GP27 🟣 Purple
SDA I2C Data GP26 🟠 Orange

Understanding I2C on Raspberry Pi Pico

The Raspberry Pi Pico has two I2C buses. This tutorial uses I2C1 on GP26/GP27:

I2C0
SDA GP0
SCL GP1
I2C1 ← Used Here
SDA GP26
SCL GP27
ℹ️
Alternative PinsYou can also use I2C1 on GP2 (SDA) / GP3 (SCL). All I2C buses share the same SSD1306 default address: 0x3C
4

Wire the Circuit — diagram.json

Copy the diagram.json below and paste it directly into the Wokwi diagram editor. This sets up the Pico, mini breadboard, and OLED with all connections pre-wired.

diagram.json
{
  "version": 1,
  "author": "MakeMindz",
  "editor": "wokwi",
  "parts": [
    {
      "type": "wokwi-breadboard-mini",
      "id": "bb1",
      "top": 0, "left": 0,
      "attrs": {}
    },
    {
      "type": "wokwi-pi-pico",
      "id": "pico",
      "top": 112, "left": 50,
      "rotate": 270,
      "attrs": { "env": "micropython-20220618-v1.19.1" }
    },
    {
      "type": "board-ssd1306",
      "id": "oled1",
      "top": 60, "left": 80,
      "attrs": {}
    }
  ],
  "connections": [
    [ "bb1:2b.f",  "bb1:2t.e",  "black",  [ "v0" ] ],
    [ "bb1:3b.f",  "bb1:3t.e",  "red",    [ "v0" ] ],
    [ "bb1:4b.f",  "bb1:4t.e",  "purple", [ "v0" ] ],
    [ "bb1:5b.f",  "bb1:5t.e",  "orange", [ "v0" ] ],
    [ "bb1:2t.d",  "bb1:12t.d", "black",  [ "v0" ] ],
    [ "bb1:3t.c",  "bb1:13t.c", "red",    [ "v0" ] ],
    [ "bb1:4t.b",  "bb1:14t.b", "purple", [ "v0" ] ],
    [ "bb1:5t.a",  "bb1:15t.a", "orange", [ "v0" ] ],
    [ "bb1:12t.e", "oled1:GND", "black",  [ "v0" ] ],
    [ "bb1:13t.e", "oled1:VCC", "red",    [ "v0" ] ],
    [ "bb1:14t.e", "oled1:SCL", "purple", [ "v0" ] ],
    [ "bb1:15t.e", "oled1:SDA", "orange", [ "v0" ] ],
    [ "pico:GND.8",  "bb1:2b.j",  "black",  [ "v0" ] ],
    [ "pico:3V3_EN", "bb1:3b.j",  "red",    [ "v0" ] ],
    [ "pico:GP27",   "bb1:4b.j",  "purple", [ "v-15", "h-38.25" ] ],
    [ "pico:GP26",   "bb1:5b.j",  "orange", [ "v-25", "h-36.92" ] ],
    [ "bb1:2t.a",   "bb1:17t.a", "black",  [ "v-7.93", "h144" ] ],
    [ "bb1:15t.d",  "bb1:16t.d", "orange", [ "v0" ] ]
  ],
  "dependencies": {}
}
How to useIn Wokwi, click the diagram.json tab, select all existing content, and paste this JSON in. The circuit will be wired automatically.
5

Add the SSD1306 Driver — ssd1306.py

In Wokwi, create a new file called ssd1306.py and paste the driver code below. This is the official MicroPython SSD1306 library from the MicroPython repository.

⚠️
ImportantThis file must be named exactly ssd1306.py — it is imported by your main.py. Create it as a separate file in the Wokwi project.
ssd1306.py — SSD1306 Driver
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
# Source: https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py

from micropython import const
import framebuf
import time

# Register definitions
SET_CONTRAST        = const(0x81)
SET_ENTIRE_ON       = const(0xA4)
SET_NORM_INV        = const(0xA6)
SET_DISP            = const(0xAE)
SET_MEM_ADDR        = const(0x20)
SET_COL_ADDR        = const(0x21)
SET_PAGE_ADDR       = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP       = const(0xA0)
SET_MUX_RATIO       = const(0xA8)
SET_COM_OUT_DIR     = const(0xC0)
SET_DISP_OFFSET     = const(0xD3)
SET_COM_PIN_CFG     = const(0xDA)
SET_DISP_CLK_DIV    = const(0xD5)
SET_PRECHARGE       = const(0xD9)
SET_VCOM_DESEL      = const(0xDB)
SET_CHARGE_PUMP     = const(0x8D)

class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width         = width
        self.height        = height
        self.external_vcc  = external_vcc
        self.pages         = self.height // 8
        self.buffer        = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,           # display off
            SET_MEM_ADDR, 0x00,         # horizontal addressing
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,
            SET_MUX_RATIO, self.height - 1,
            SET_COM_OUT_DIR | 0x08,
            SET_DISP_OFFSET, 0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            SET_DISP_CLK_DIV, 0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL, 0x30,
            SET_CONTRAST, 0xFF,
            SET_ENTIRE_ON,
            SET_NORM_INV,
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,            # display on
        ):
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self): self.write_cmd(SET_DISP | 0x00)
    def poweron(self):  self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0, x1 = 0, self.width - 1
        if self.width == 64:
            x0 += 32; x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0); self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0); self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c        = i2c
        self.addr       = addr
        self.temp       = bytearray(2)
        self.write_list = [b"\x40", None]
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)
6

Write main.py — MicroPython Code

This script initialises I2C1 on GP26/GP27, creates the OLED object, then runs three display functions: logo, text, and a live timer animation.

main.py — MicroPython
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import framebuf, sys
import utime

pix_res_x = 128
pix_res_y = 64

def init_i2c(scl_pin, sda_pin):
    # Initialise I2C1 at 200kHz on the specified pins
    i2c_dev  = I2C(1, scl=Pin(scl_pin), sda=Pin(sda_pin), freq=200000)
    i2c_addr = [hex(ii) for ii in i2c_dev.scan()]

    if not i2c_addr:
        print('No I2C Display Found')
        sys.exit()
    else:
        print("I2C Address      : {}".format(i2c_addr[0]))
        print("I2C Configuration: {}".format(i2c_dev))

    return i2c_dev

def display_logo(oled):
    # Display the Raspberry Pi logo using a FrameBuffer bitmap
    buffer = bytearray(
        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00"
        b"\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80"
        b"\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c"
        b"\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A"
        b"\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01"
        b"\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00"
        b"\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00"
        b"\x00\x00\x00\x00\x00\x00\x00"
    )
    fb = framebuf.FrameBuffer(buffer, 32, 32, framebuf.MONO_HLSB)
    oled.fill(0)
    oled.blit(fb, 96, 0)   # draw logo at top-right
    oled.show()

def display_text(oled):
    # Overlay text on the current screen
    oled.text("Raspberry Pi", 5, 5)
    oled.text("Pico", 5, 15)
    oled.show()

def display_anima(oled):
    # Live timer animation — updates every second
    start_time = utime.ticks_ms()

    while True:
        elapsed = (utime.ticks_diff(utime.ticks_ms(), start_time) // 1000) + 1

        # Clear only the timer line using a filled black rectangle
        oled.fill_rect(5, 40, oled.width - 5, 8, 0)

        oled.text("Timer:", 5, 30)
        oled.text(str(elapsed) + " sec", 5, 40)
        oled.show()
        utime.sleep_ms(1000)

def main():
    # Init I2C1: SCL=GP27, SDA=GP26
    i2c_dev = init_i2c(scl_pin=27, sda_pin=26)
    oled    = SSD1306_I2C(pix_res_x, pix_res_y, i2c_dev)

    display_logo(oled)    # Step 1: draw RPi logo
    display_text(oled)    # Step 2: overlay text
    display_anima(oled)   # Step 3: live timer loop

if __name__ == '__main__':
    main()

Code Breakdown

init_i2c()
Scans I2C bus, prints found address, returns I2C device object
display_logo()
Renders a 32×32 Raspberry Pi logo using MONO_HLSB FrameBuffer bitmap
display_text()
Writes "Raspberry Pi" and "Pico" text at (5,5) and (5,15)
display_anima()
Live timer loop updating once per second using fill_rect() to clear only the changing line
7

Run the Simulation

  1. Create ssd1306.py and paste the driver code
  2. Create / edit main.py and paste the main code
  3. Paste the diagram.json into the diagram editor
  4. Click the ▶ green Play button
  5. The OLED display will show the Raspberry Pi logo, then text, then a live timer
🔍
Serial MonitorThe I2C address will print to the serial console: I2C Address : 0x3c — confirming the display is connected correctly.
▶ Open Free Simulation on Wokwi Run the OLED display project instantly — no hardware required
🚀 Launch Simulation
🎓

What This Project Teaches

📡
I2C Basics
Two-wire serial communication with address scanning
🐍
MicroPython
Modular functions, imports, and while loops
🖼️
FrameBuffer
Drawing graphics and bitmaps in pixel memory
✏️
Text Rendering
Positioning characters with oled.text()
🎞️
Animation
Efficient partial screen updates with fill_rect()
📟
Embedded UI
Building display interfaces for IoT devices

Applications of OLED with Raspberry Pi Pico

🌐 IoT dashboards 📊 Sensor data display 🕐 Digital clocks 🎮 Mini game consoles 🦾 Robotics panels 🔬 Measurement tools 🏠 Smart home UI
🔰 Beginner Projects
⚙️ Intermediate Projects
🚀 Advanced Projects

Hands-on Electronics & MicroPython Tutorials for Makers, Students & Engineers

www.makemindz.com

Comments

try for free