Raspberry Pi Pico: SSD1306 OLED Display (MicroPython)

This guide shows how to use the SSD1306 OLED display with the Raspberry Pi Pico programmed with MicroPython firmware. You’ll learn how to display text and other useful functions to interact with the OLED display.

Raspberry Pi Pico:SSD1306 OLED Display MicroPython

We have a similar tutorial using Raspberry Pi Pico with Arduino IDE: Raspberry Pi Pico: SSD1306 OLED Display (Arduino IDE).

Table of Contents

Prerequisites – MicroPython Firmware

To follow this tutorial you need MicroPython firmware installed in your Raspberry Pi Pico board. You also need an IDE to write and upload the code to your board.

The recommended MicroPython IDE for the Raspberry Pi Pico is Thonny IDE. Follow the next tutorial to learn how to install Thonny IDE, flash MicroPython firmware, and upload code to the board.

Introducing 0.96 inch OLED Display

The OLED display that we’ll use in this tutorial is the SSD1306 model: a monocolor, 0.96 inch display with 128×64 pixels as shown in the following figure.

0.96 inch OLED display with ESP32 ESP8266 Arduino

The OLED display doesn’t require backlight, which results in a very nice contrast in dark environments. Additionally, its pixels consume energy only when they are on, so the OLED display consumes less power when compared to other displays.

The model we’re using has four pins and communicates with any microcontroller using I2C communication protocol. There are models that come with an extra RESET pin or that communicate using SPI communication protocol.

Parts Required

Here’s a list of parts you need for this project:

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

OLED Display SSD1306 Pin Wiring

The OLED display uses I2C communication protocol, so wiring is pretty straightforward. You can use the following table as a reference. We’re using GPIO 4 (SDA) and GPIO 5 (SCL) as I2C pins, but you can use any other pair of I2C pins (make sure you check the Raspberry Pi Pico pinout diagram).

PinRaspberry Pi Pico
Vin3.3V(OUT)
GNDGND
SCLGPIO 5
SDAGPIO 4

You can also use the following schematic diagram as a reference.

Raspberry Pi Pico with OLED Display Diagram Fritzing

SSD1306 OLED Library

The library to write to the OLED display isn’t part of the standard MicroPython library by default. So, you need to upload the library to your Raspberry Pi Pico board.

#MicroPython SSD1306 OLED driver, I2C and SPI interfaces created by Adafruit

import time
import framebuf

# 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:
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        # Note the subclass must initialize self.framebuf to a framebuffer.
        # This is necessary because the underlying data buffer is different
        # between I2C and SPI implementations (I2C needs an extra byte).
        self.poweron()
        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.height == 32 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 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_framebuf()

    def fill(self, col):
        self.framebuf.fill(col)

    def pixel(self, x, y, col):
        self.framebuf.pixel(x, y, col)

    def scroll(self, dx, dy):
        self.framebuf.scroll(dx, dy)

    def text(self, string, x, y, col=1):
        self.framebuf.text(string, x, y, col)


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)
        # Add an extra byte to the data buffer to hold an I2C data/command byte
        # to use hardware-compatible I2C transactions.  A memoryview of the
        # buffer is used to mask this byte from the framebuffer operations
        # (without a major memory hit as memoryview doesn't copy to a separate
        # buffer).
        self.buffer = bytearray(((height // 8) * width) + 1)
        self.buffer[0] = 0x40  # Set first byte of data buffer to Co=0, D/C=1
        self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height)
        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_framebuf(self):
        # Blast out the frame buffer using a single I2C transaction to support
        # hardware I2C interfaces.
        self.i2c.writeto(self.addr, self.buffer)

    def poweron(self):
        pass


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.buffer = bytearray((height // 8) * width)
        self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height)
        super().__init__(width, height, external_vcc)

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

    def write_framebuf(self):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs.high()
        self.dc.high()
        self.cs.low()
        self.spi.write(self.buffer)
        self.cs.high()

    def poweron(self):
        self.res.high()
        time.sleep_ms(1)
        self.res.low()
        time.sleep_ms(10)
        self.res.high()

View raw code

Follow the next steps to upload the library file to the Raspberry Pi Pico.

1. Create a new file in Thonny IDE and copy the library code. The OLED library code can be found here.

SSD1306 micropython library file

2. Go to File > Save as and select Raspberry Pi Pico.

Save Files to Raspberry Pi Pico Thonny IDE

3. Name the file ssd1306.py and click OK to save the file on the Raspberry Pi Pico.

save the ssd1306 oled library micropython

And that’s it. The library was uploaded to your board. Now, you can use the library functionalities in your code by importing the library.

Finding the OLED I2C Address

By default, the OLED library we’re using will assume that your OLED I2C address is 0x3c. For most I2C SSD1306 displays, that will be their address. However, some displays might have a different address, so it’s important to check the I2C address before continuing.

I2C Scanner – MicroPython

Open Thonny IDE, or the IDE of your choice, and copy the following code.

# I2C Scanner MicroPython
from machine import Pin, SoftI2C

# You can choose any other combination of I2C pins
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

print('I2C SCANNER')
devices = i2c.scan()

if len(devices) == 0:
  print("No i2c device !")
else:
  print('i2c devices found:', len(devices))

  for device in devices:
    print("I2C hexadecimal address: ", hex(device))

View raw code

After copying the code:

  • connect the Raspberry Pi Pico to your computer, if it isn’t already;
  • make sure you have the OLED display properly connected to your board on the right I2C pins (SCL=GPIO5 ; SDA =GPIO4).
  • run the previous code (I2C scanner sketch). If you’re using Thonny IDE, you just need to click on the green run icon.

The I2C address of your display will be printed on the shell. In my case, it’s the default 0x3c. Yours might be different.

Write Text – OLED Display

The Adafruit library for the OLED display comes with several functions to write text. In this section, you’ll learn how to write text using the library functions.

The following sketch displays the Hello, world! message three times in the OLED display.

# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-ssd1306-oled-micropython/

from machine import Pin, SoftI2C
import ssd1306

#You can choose any other combination of I2C pins
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

oled.text('Hello, World 1!', 0, 0)
oled.text('Hello, World 2!', 0, 10)
oled.text('Hello, World 3!', 0, 20)

oled.show()

View raw code

Let’s take a quick look at how the code works.

Start by importing the necessary modules to interact with the GPIOs and send data to the OLED via I2C communication. You need to import the Pin and SoftI2C classes from the machine module.

from machine import Pin, SoftI2C

You also need to import the OLED library that you previously uploaded to the board as ssd1306.py file.

import ssd1306

We’ll use GPIO 4 (SDA) and GPIO 5 (SCL) to communicate with the display using I2C communication protocol, but you can use any other I2C pins.

#You can choose any other combination of I2C pins
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

Define the OLED width and height on the following variables:

oled_width = 128
oled_height = 64

After that, create an SSD1306_I2C object called oled. This object accepts the OLED width, height, and the I2C pins you defined earlier.

oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

By default, the code assumes your OLED display has the default 0x3c I2C address. If you have a different I2C address, change the previous line to:

oled = SSD1306_I2C(oled_width, oled_height, i2c, addr = YOUR_I2C_ADDRESS)

For example, if your I2C address is 0x3d, it will look as follows (you can check your I2C address here):

oled = SSD1306_I2C(oled_width, oled_height, i2c, addr = 0x3d)

After initializing the OLED display, you just need to use the text() function on the oled object to write text. After the text() function, you need to call the show() method to update the OLED.

oled.text('Hello, World 1!', 0, 0)
oled.text('Hello, World 2!', 0, 10)
oled.text('Hello, World 3!', 0, 20)
oled.show()

The text() method accepts the following arguments (in this order):

  • Message: must be of type String
  • X position: where the text starts
  • Y position: where the text is displayed vertically
  • Text color: it can be either black or white. The default color is white and this parameter is optional.
    • 0 = black
    • 1 = white

For example, the following line writes the ‘Hello, World 1!’ message in white color. The text starts on x = 0 and y = 0.

oled.text('Hello, World 1!', 0, 0)

The next line of code writes the text on the next line (y =10).

oled.text('Hello, World 2!', 0, 10)

Finally, for the changes to take effect, use the show() method on the oled object.

oled.show()

Demonstration

Save the code to your Raspberry Pi Pico board using Thonny IDE or any other MicroPython IDE of your choice.

Copy the code provided to a new file on Thonny IDE.

Raspberry Pi Pico OLED MicroPython code on Thonny IDE

With the code copied to the file, click on the Save icon. Then, select Raspberry Pi Pico.

Saving file to Raspberry Pi Pico MicroPython IDE

Save the file with the following name: main.py. Overwrite any existing files with the same name. Notice that you should also have the ssd1306.py file that you saved previously.

main.py thonny IDE with ssd1306 file

Note: When you name a file main.py, the Raspberry Pi Pico will run that file automatically on boot. If you call it a different name, it will still be saved on the board filesystem, but it will not run automatically on boot.

Reset your board (unplug and plug it into your computer). Click the little green button “Run Current Script” or press F5.

run micropython script press play button

The OLED should display the three “Hello World” messages that we’ve written on the code.

Raspberry Pi Pico Hello World OLED Display

Toubleshooting: if you get weird error messages and your OLED display doesn’t work, try to power it with 5V instead of 3V3. In that case, connect the VCC pin to the Raspberry Pi Pico VBUS pin.

Other OLED functions

The library provides other methods to interact with the OLED display.

Fill the screen

To fill the entire screen with white, use the fill() function as follows:

oled.fill(1)
oled.show()
oled fill white screen raspberry pi pico micropython

To clear the screen use the fill() method and pass 0 as argument. (Sets all pixels to black):

oled.fill(0)
oled.show()

Draw a pixel

To draw a pixel, use the pixel() method, followed by the show() method. The pixel() method accepts the following arguments:

  • X coordinate: pixel location horizontally
  • Y coordinate: pixel location vertically
  • Pixel color: can be black or white
    • 0 = black
    • 1 = white

For example, to draw a white pixel on the top left corner:

oled.pixel(0, 0, 1)
oled.show()
oled display a pixel raspberry pi pico micropython

Invert colors

You can also invert the OLED colors: white with black and vice versa, using the invert() method:

oled.invert(True)
oled invert colors raspberry pi pico micropython

To get back to the original colors, use:

oled.invert(False)

Displaying data from sensors

The text() function only accepts variables of type String as a message. Sensor readings are usually stored in int or float variables.

If you want to display sensor readings and they are stored in int or float variables, they should be converted to a String. To convert the data to a string you can use the str() function:

temperature = 12.34
temperature_string = str(temperature)

Then, you can display the temperature_string variable on the OLED using the text() and show() methods:

oled.text(temperature_string, 0, 0)
oled.show()

Wrapping Up

In this guide, we’ve shown you how to display text in the OLED display with the Raspberry Pi Pico programmed with MicroPython firmware. We’ve also shown you other useful functions to control the OLED display.

If you’re getting started with the Raspberry Pi Pico, you may also like reading some of the following tutorials:

Check out all our Raspberry Pi Pico Guides »

If you prefer to program the Raspberry Pi Pico using Arduino IDE, you can get started with the following tutorial:

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

20 thoughts on “Raspberry Pi Pico: SSD1306 OLED Display (MicroPython)”

  1. Hello, I asked this question on Nov 27, but I still don’t have an answer. My question is if you received my email for the query on the page where “Firebase: Control ESP32 GPIOs from Anywhere” is located. Thank you.

    Hi Sara, I have a problem with the Firebase_ESP_Client library, today I updated the FirebaseESP32 library.

    The problem is what the streamCallback(FirebaseStream) tells me “variable or field ‘streamCallback’ declared null
    56 | void streamCallback (FirebaseStream data)”

    It’s right?

    I use the code on this page and update the call with the new library.

    //#include <Firebase_ESP_Client.h>
    #include <FirebaseESP32.h>

    Thanks for your future response for me.

    Reply
  2. Hello Sara.
    thanks for another great tutorial
    It helped and inspired me to
    make this python script,
    which displays data from the bme280 and retrieves the time
    from an NTP server on first run and displays it.
    the script is updated every 5 seconds

    import machine
    import bme280
    import utime
    from machine import Pin, SoftI2C
    import ssd1306
    import network
    import socket
    import time
    import struct

    NTP_DELTA = 2208988800 – 3600
    host = “pool.ntp.org”

    led = Pin(‘LED’, Pin.OUT)

    ssid = ‘your ssid’
    password = ‘your password’

    def set_time():
    NTP_QUERY = bytearray(48)
    NTP_QUERY[0] = 0x1B
    addr = socket.getaddrinfo(host, 123)[0][-1]
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
    s.settimeout(1)
    res = s.sendto(NTP_QUERY, addr)
    msg = s.recv(48)
    finally:
    s.close()
    val = struct.unpack(“!I”, msg[40:44])[0]
    t = val – NTP_DELTA
    tm = time.gmtime(t)
    machine.RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))

    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(ssid, password)

    max_wait = 10
    while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
    break
    max_wait -= 1
    print(‘waiting for connection…’)
    time.sleep(1)

    if wlan.status() != 3:
    #raise RuntimeError(‘network connection failed’)
    led.on()
    print(‘not connected’)
    time.sleep(1)
    led.off()
    wlan.deinit()
    else:
    print(‘connected’)
    status = wlan.ifconfig()
    print( ‘ip = ‘ + status[0] )

    led.on()
    set_time()
    print(time.localtime())
    led.off()
    wlan.deinit()

    #You can choose any other combination of I2C pins
    i2c = machine.I2C(id=0, scl=machine.Pin(1), sda=machine.Pin(0))
    bme = bme280.BME280(i2c=i2c)

    i2c = SoftI2C(scl=Pin(5), sda=Pin(4))
    oled_width = 128
    oled_height = 64
    oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

    rtc=machine.RTC()
    running = True
    while running:
    try:
    #print(bme.values)
    oled.fill(0)
    oled.text(bme.values[0], 0, 0)
    oled.text(bme.values[2], 0, 10)
    oled.text(bme.values[1], 0, 20)

    timestamp=rtc.datetime()
    #timestring=”%04d-%02d-%02d %02d:%02d:%02d”%(timestamp[0:3] + timestamp[4:7])
    timestring=”%02d-%02d-%04d %02d:%02d”%(timestamp[2:3] + timestamp[1:2] +timestamp[0:1] + timestamp[4:6])
    oled.text(timestring, 0, 40)

    oled.show()

    utime.sleep(5)

    except KeyboardInterrupt:
    print(‘Program stopped’)
    running = False

    Reply
  3. Doesn’t work. I get the following errors:
    Traceback (most recent call last):
    File “”, line 9, in
    File “ssd1306.py”, line 116, in init
    File “ssd1306.py”, line 36, in init
    File “ssd1306.py”, line 61, in init_display
    File “ssd1306.py”, line 121, in write_cmd
    OSError: [Errno 19] ENODEV

    Not quite sure what it all means, but the oled display doesn’t turn on or display anything. Wondering if you’ve seen this before and have any help.
    Thanks.

    Reply
    • Hi.
      Check your I2C connections.
      Additionally, double-check your OLED I2C address.
      I’ve just added a section about it after realizing some people were having issues with it.
      Regards,
      Sara

      Reply
  4. hoping someone can help me understand this error message. I’ve loaded the library into the lib folder of the pico several different ways- manager as shown, from GitHub version, other tutorials copy and paste. Tried the whole MicroPython library changed, cables, added a sleep 200 ms without success. Nuked and reloaded several times by various methods. This is the best I get. I’m using the adafruit OLEDs so I’m sure I have the SSD and not the SH version. And I have changed cords and tried two different displays that both work on the Arduino and ESP32 sketches. I have placed the library in the lib/ folder( also tried it without a lib folder. Not running any other libraries on either of the picos I’ve tried.

    MPY: soft reboot
    Traceback (most recent call last):
    File “”, line 24, in
    File “/lib/ssd1306.py”, line 110, in init
    File “/lib/ssd1306.py”, line 36, in init
    File “/lib/ssd1306.py”, line 71, in init_display
    File “/lib/ssd1306.py”, line 115, in write_cmd
    OSError: [Errno 5] EIO

    for the most simple code:

    from machine import Pin, SoftI2C
    import ssd1306
    from time import sleep

    #You can choose any other combination of I2C pins
    i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

    also tried 0,1 and 2,3 as well

    oled_width = 128
    oled_height = 64
    oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

    oled.text(‘Hello, World 1!’, 0, 0)
    oled.text(‘Hello, World 2!’, 0, 10)
    oled.text(‘Hello, World 3!’, 0, 20)

    oled.show()
    oled.fill(0)
    oled.show()

    Reply
  5. It probably can’t help you,
    but are you sure you connected sda to gpio 4
    pin six from top and scl on gpio 5 pin 7 from top
    and + on 3v3 and not on 3v3_en

    Reply
  6. I solved it. Ran the I2C scanner, found my OLED is on 0x3d.
    I didn’t know how to specify the address which I guess in the example finds it by default.
    What worked for me is:
    oled = SSD1306_I2C(128,64,i2c,addr = 0x3d)

    I hope this spares others the pain I had. I thank you all for your help. Much appreciated.

    Reply
      • Sara, I have an additional tip. I have been using a 128 X64 OLED display by Adafruit. After much frustration, I discovered that the only way to get consistent response is to use the 5 volt VBus on the pico. I have read about 25 tutorials, stackover threads, MicroPython threads and none of them mentioned that. I was receiving both errno 15, errno 8 and the no device error aa well as a wide range of errors messages referring to the ssd1306 library. 5 Volts fixed them all !!

        Reply
        • Hi.
          I’ll add a note about that.
          My display works well with 3.3V, but other models might require 5V.
          Thanks for your contribution.
          Regards,
          Sara

          Reply
    • My I2C scanner keeps reporting that its on 60 (0x3c), but this address doesn’t work, and returns the “OSError: [Errno 5] EIO”. I even set up a script to try to initialize the oled object with every hex address from 0 to 5000 (arbitrary number, I have no idea how many addresses are even capable of supporting the oled), but none worked. I’m combing through the ssd1306 module to see if something’s misconfigured in it now.

      Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.