Raspberry Pi Pico With SSD1306 OLED Display – MicroPython Example

In this tutorial, we will learn to interface Raspberry Pi Pico with an SSD1306 OLED display easily. MicroPython code is used to interface the two devices.

Many kinds of displays are used in microcontroller projects, such as e-ink displays, LCDs, 7-segment displays, and OLED displays. OLED displays are convenient as they have a fast refresh rate, better visibility, low power consumption, and high pixel density. Moreover, they can be easily interfaced using the I2C and SPI communication interfaces.

Raspberry Pi Pico OLED Display Interfacing tutorial.
Raspberry & SSD1306 OLED display connected using I2C interface

Overview of SSD1306 OLED Display

SSD1306 is a controller chip that can control 128×64 dot matrix diode displays. 128 and 64 refer to the horizontal and vertical pixels respectively i.e. there are 128 pixels horizontally on the display and 64 pixels vertically, arranged in a rectangular matrix.

ssd1306 diagram

Features of SSD1306:

  • Power Supply: 3V – 5V.
  • On-chip low-power RC oscillator.
  • Communication interface: Parallel interface, 3/4 wire SPI, I2C interface.
  • Programmable Frame Rate.
  • Operating temperature range: -40°C to 85°C.

Prerequisites & Setup

  • A Raspberry Pi Pico or Pico W.
  • 4 Pin I2C OLED display module (size: 0.96inch/2.44cm; 128×64).
  • Breadboard and connecting wires.

Your Raspberry Pi Pico needs to be flashed with a MicroPython UF2 file to program it in MicroPython. You can read our getting started guide for Raspberry Pi Pico where we show all the steps required to start programming Raspberry Pi Pico & Pico W. If you are using macOS, follow our guide to program Raspberry Pi Pico on macOS using Thonny IDE.

I2C is a communication protocol commonly used in embedded systems. It can facilitate communication between multiple devices using just a two-wire bus. Read more about I2C and how we can use the I2C in Pi Pico in our article Raspberry Pi Pico I2C Communication Guide.

I2C Pins in Raspberry Pi Pico to Interface OLED

The Raspberry Pi Pico has two I2C ports. You can access these ports with various GPIO pins. Take a look at the pin diagram below.

raspberry pi pico w pinout
Raspberry Pi Pico Pin Diagram. Source: Datasheet

As seen in the pinout diagram, many pins function as SDA and SCL. These pins are connected to either of the two I2C ports internally. We can choose any of these pins to interface with the OLED display.

raspberry pi pico I2C pinout
Raspberry Pi Pico I2C pinout

I2C requires 4 wires for communication- Positive voltage, Ground, SDA, and SCL. SDA stands for Serial Data. This wire sends the bits to communicate between two or more entities. SCL stands for Serial Clock. This wire is responsible for the synchronization of data.

Here we shall use the pin GPIO 0 as SDA and GPIO 1 as SCL.

Advertisement

Wiring Raspberry Pi Pico W with SSD1306 OLED Display

Connect your Raspberry Pi Pico/Pico W to the SSD1306 OLED display module as shown below.

Schematic of Raspberry Pi Pico with SSD1306 OLED display module

Pin-to-Pin connection details:

Raspberry Pi Pico PinOLED Display Pin
Pin 1 (GPIO0)4(SDA)
Pin 2 (GPIO1)3(SCL)
Pin 36(3.3 Volts Out)2(VDD)
Pin 38(GND)1(GND)
Table: Wiring between Raspberry Pi Pico and OLED display

Installation of MicroPython OLED Library

To communicate with the OLED display, we need to send certain commands with precise delays between the commands. Thankfully, there is a library that takes care of all the commands.

Upload the following code to Raspberry Pi Pico and save it as ssd1306.py.

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces

from micropython import const
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)

# 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)


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
        import time

        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)

If you are using the Thonny IDE, here are the steps to upload a library or any script to Raspberry Pi Pico:

1. With all connections done according to the schematic, connect the Pico to your computer using a USB cable. Open Thonny IDE and set the interpreter to use MicroPython on Raspberry Pi Pico.

Thonny IDE select interpreter as MicroPython Raspberry Pi Pico

2. Go to File>New in Thonny IDE to create a new project. 

3. Paste your code into the new project.

4. Click on File>Save as and select the save location as Raspberry Pi Pico.

Thonny Save to

5. Save the code with a proper filename. Save as ssd1306.py for this example.

MicroPython Code to Interface Raspberry Pi Pico with OLED

Let us now write a simple script to interface the OLED display with Raspberry Pi Pico. This script will make function calls to the ssd1306.py library we installed earlier.

Copy and paste the following code and save it as main.py on Raspberry Pi Pico.

# Source: Electrocredible.com, Language: MicroPython

# Import necessary modules
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import time
# Pin numbers for I2C communication
sda_pin=0
scl_pin=1
# Display dimensions
WIDTH =128 
HEIGHT= 64
# Set up I2C communication
i2c=I2C(0,scl=scl_pin,sda=sda_pin,freq=200000)
time.sleep(0.1)
# Initialize SSD1306 display with I2C interface
oled = SSD1306_I2C(WIDTH,HEIGHT,i2c)
while True:
    # Clear the display
    oled.fill(0)
    # text, x-position, y-position
    oled.text("Hello,", 42, 20) 
    oled.text("World !", 38, 40)
    # Show the updated display
    oled.show()
Code language: Python (python)

Run the code(Press the F5 keyboard shortcut in Thonny IDE to run code). Your OLED display should now display the words as shown in the image below.

SSD1306 OLED display output

If you want to display dynamic text or values, such as data from sensors, be sure to type oled.fill(0) at the beginning or end of the main loop. This will clear the display every time the main loop runs. If you don’t clear the display, the characters will overlap each other and the text will be incomprehensible.

Scrolling Text Animation

The framebuf module of MicroPython comes with a built-in method to display scrolling text. We can scroll text by using oled.scroll(xstep, ystep) method. It takes two arguments. The arguments specify the number of pixels by which the text will be shifted on the x-axis and y-axis respectively.

Here, the code scrolls the text 1 position to the left by setting ‘xstep’ to -1. We repeat this in a loop 128 times by setting the loop range to ‘WIDTH’.

Upload the following script to scroll a line of text:

# Source: Electrocredible.com, Language: MicroPython
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import time
WIDTH =128 
HEIGHT= 64
i2c=I2C(0,scl=Pin(1),sda=Pin(0),freq=200000)
time.sleep(0.1)
oled = SSD1306_I2C(WIDTH,HEIGHT,i2c)
while True:
    oled.text("Scrolling text",10, 32)
    for i in range (WIDTH):
        oled.show()
        oled.scroll(-1,0)
        time.sleep(0.01)Code language: Python (python)

Demonstration

Due to the frame rate of the camera, the screen refresh rate is visible in the video but is invisible to the human eye.

Display Graphics on OLED Display

The framebuf module has some useful prebuilt functions to display simple Bitmap graphics on an OLED display.

Horizontal Line

The oled.hline(x, y, w, c) method takes four parameters. The x-position of the starting end of the line is the argument ‘x’, the y-position is ‘y’, ‘w’ is the width of the line, and ‘c’ should be set to ‘1’ for monochrome display. Enter the following code in your main program to see the results.

oled.text("Horizontal Line", 0, 0)
oled.hline(0,32,128,1)
oled.show()Code language: Python (python)

Vertical Line

The oled.vline(x, y, w, c) method works in the same way as the one above. Here ‘w’ indicates the vertical length of the line in pixels.

oled.text("Vertical Line", 0, 0)
oled.vline(64,20,40,1)
oled.show()Code language: Python (python)

Rectangle

Use the function oled.rect(x, y, w, h, c) to display a rectangle. x,y is the position of the pixel on the top-left corner, and w and h denote the width and height of the rectangle respectively.

oled.text("Rectangle", 0, 0)
oled.rect(32,16,64,32,1)
oled.show()Code language: Python (python)

You can also display a filled rectangle by using the oled.fill_rect(x, y, w, h, c) method.

oled.text("Filled Rectangle", 0, 0)
oled.fill_rect(32, 16, 64, 32, 1)
oled.show()Code language: Python (python)

Troubleshooting: “OSError: [Errno 5] EIO” & “[Errno 110] ETIMEDOUT”

This errors are commonly encountered while interfacing SSD1306 display using MicroPython. Here are some possible solutions:

  • This issue is often caused by wrong or loose wire connections. I faced this issue because of a defective jumper wire.
  • This problem may also occur if your OLED module does not have pull-up resistors for the SDA and SCL lines. Manually pull the SDA and SCK pins to the 3.3 Volts using 4.7K or 10K pull-up resistors.
  • Sometimes, just disconnecting and reconnecting the USB cable solves the issue.
  • Adding a small delay after initializing the i2c instance also helps in avoiding this issue.

Wrapping Up

If you want to experiment further, here is a project that uses an OLED display: Raspberry Pi Pico DHT22(AM2302) Interfacing Tutorial. Please comment below if you have any queries regarding this Raspberry Pi Pico OLED display tutorial. Thanks for reading and have a fun time making projects!

Also Read: Interface Raspberry Pi Pico W With TM1637 4-Digit 7-Segment Display


Posted

in

by

Comments

2 responses to “Raspberry Pi Pico With SSD1306 OLED Display – MicroPython Example”

  1. junzo yoshida Avatar
    junzo yoshida

    Raspberry Pi Pico の「OSError: [Errno 5] EIO

    thanks your advice
    but….
    troubled now

  2. K. Berkmann Avatar
    K. Berkmann

    Great website, thank you.
    The hint to use 10k pullup resistors saved my problem with the OSError: [Errno 5] .

    Unfortunately the text size on the OLED is very small and there seems to be no possibility to make it larger with this library, but this is another story …

Leave a Reply

Your email address will not be published. Required fields are marked *