How to Connect OLED Display to Raspberry Pi Pico in Wokwi: Complete I2C Tutorial

 

This tutorial shows you how to wire and program a 128x64 OLED display (SSD1306) with Raspberry Pi Pico using I2C communication in the Wokwi simulator. Perfect for displaying text, graphics, and sensor data!

What is an OLED Display?

An OLED (Organic LED) display is a low-power screen that can show text and graphics. The common SSD1306 controller supports 128x64 or 128x32 pixel resolution and uses I2C or SPI communication. OLED displays don't need backlighting, making them energy-efficient and easy to read.

Getting Started with Wokwi

Step 1: Open Wokwi and Create New Project

  • Go to https://wokwi.com
  • Click "New Project"
  • Select "Raspberry Pi Pico" as your board

Step 2: Add the OLED Display Component

Add OLED Display:

  • Click the blue "+" button
  • Search for "SSD1306" or "OLED"
  • Select "SSD1306 128x64 OLED Display"
  • Click to add it to your workspace
  • The display will appear on your canvas

Configure Display (Optional):

  • Click on the OLED display
  • In properties panel, you can verify:
    • Resolution: 128x64 pixels
    • Interface: I2C
    • I2C Address: Usually 0x3C or 0x3D

Step 3: Understanding OLED Display Pins

The SSD1306 OLED display has 4 I2C pins:

  • VCC/VDD - Power supply (3.3V or 5V)
  • GND - Ground
  • SCL - I2C Clock line
  • SDA - I2C Data line

Some displays may have additional pins for SPI mode (ignore these for I2C).

Step 4: Wire the Circuit in Wokwi

Based on your diagram, make these connections:

Power Connections:

  • VCC on OLED3V3 on Raspberry Pi Pico (Red wire)
  • GND on OLEDGND on Raspberry Pi Pico (Black wire)
ssd103.py
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
#
# library taken from repository at:
# 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)

# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
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,  # off
            # Address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # Resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # Column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # Scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # Timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # Display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # Output follows RAM contents
            SET_NORM_INV,  # Not inverted
            # Charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,
        ):  # 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 = 0
        x1 = self.width - 1
        if self.width == 64:
            # Displays with width of 64 pixels are shifted by 32
            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]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        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)

# Only required for SPI version (not covered in this project)
class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs

        self.res(1)
        time.sleep_ms(1)
        self.res(0)
        time.sleep_ms(10)
        self.res(1)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)

    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

I2C Communication Connections:

  • SCL on OLEDGP1 (I2C0 SCL) on Pico (Yellow/Orange wire)
  • SDA on OLEDGP0 (I2C0 SDA) on Pico (Purple/Pink wire)

Wiring Steps in Wokwi:

  1. Click VCC pin on OLED → drag to 3V3 on Pico
  2. Click GND pin on OLED → drag to GND on Pico
  3. Click SCL pin on OLED → drag to GP1 on Pico
  4. Click SDA pin on OLED → drag to GP0 on Pico
Diagram.json:

{
  "version": 1,
  "author": "Anderson Costa",
  "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:9", "bb1:4b.j", "purple", [ "v0" ] ],
    [ "pico:8", "bb1:5b.j", "orange", [ "v0" ] ],
    [ "bb1:2t.a", "bb1:17t.a", "black", [ "v-7.93", "h144" ] ],
    [ "bb1:15t.d", "bb1:16t.d", "orange", [ "v0" ] ],
    [ "pico:3V3_EN", "bb1:3b.j", "red", [ "v0" ] ],
    [ "pico:GND.8", "bb1:2b.j", "black", [ "v0" ] ],
    [ "pico:GP27", "bb1:4b.j", "purple", [ "v-15", "h-38.25" ] ],
    [ "pico:GP26", "bb1:5b.j", "orange", [ "v-25", "h-36.92" ] ]
  ],
  "dependencies": {}
}

Step 5: Understanding I2C Pin Options

The Raspberry Pi Pico has two I2C buses:

  • I2C0: GP0 (SDA), GP1 (SCL) - Used in this tutorial
  • I2C1: GP2 (SDA), GP3 (SCL) - Alternative option

You can use either bus, just update your code accordingly.

Step 6: Write the Basic Display Code

In Wokwi, the SSD1306 library is built-in. Create this code:\


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):
    # Initialize I2C device
    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 on the OLED
    buffer = bytearray(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
    fb = framebuf.FrameBuffer(buffer, 32, 32, framebuf.MONO_HLSB)
   
    oled.fill(0)
    oled.blit(fb, 96, 0)
    oled.show()

def display_text(oled):
    # Display text on the OLED
    oled.text("Raspberry Pi", 5, 5)
    oled.text("Pico", 5, 15)
    oled.show()

def display_anima(oled):
    # Display a simple timer animation on the OLED
    start_time = utime.ticks_ms()

    while True:
        elapsed_time = (utime.ticks_diff(utime.ticks_ms(), start_time) // 1000) + 1
       
        # Clear the specific line by drawing a filled black rectangle
        oled.fill_rect(5, 40, oled.width - 5, 8, 0)

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

def main():
    i2c_dev = init_i2c(scl_pin=27, sda_pin=26)
    oled = SSD1306_I2C(pix_res_x, pix_res_y, i2c_dev)
    display_logo(oled)
    display_text(oled)
    display_anima(oled)

if __name__ == '__main__':
    main()






Comments