How to Interface Raspberry Pi Pico W with OLED Display using MicroPython?

Published  September 29, 2023   0
Raspberry Pi Pico W with OLED Display

Welcome to our blog, where we will take a deep dive into the world of OLED displays and the Raspberry Pi Pico W microcontroller. OLED (Organic Light Emitting Diode) displays are at the forefront of visual technology, offering brilliant colors, high contrast, and a wide viewing angle, making them perfect for a myriad of applications ranging from portable devices to wearable gadgets and beyond.

In this comprehensive guide, we will focus on one of the most versatile and popular microcontrollers, the Raspberry Pi Pico W, and explore how to interface it with OLED displays through the I2C communication protocol.

The components which we will require for this tutorial are:-

  • Raspberry Pi Pico board
  • OLED display module (with I2C interface)
  • Breadboard and jumper wires
  • Micro-USB cable for power

OLED Display Pinout

OLED Display Module Pinout

VCC Power supply voltage (usually 3.3V, check the specifications of your display).

GND Ground.

SDA I2C data line (Serial Data Line).

SCL I2C clock line (Serial Clock Line).

Commonly asked questions about OLED Displays

Can I use multiple OLED displays with Raspberry Pi Pico W simultaneously?

Yes, you can use multiple OLED displays simultaneously by connecting display to same or different i2c pins and using different I2C address for each display. If you want to use more than 2 oled displays. You can use a multiplexer ic like tca9548a. 

What communication protocol is commonly used to connect OLED displays?

OLED displays are commonly connected using the I2C (Inter-Integrated Circuit) communication protocol due to its simplicity and ease of use. I2C requires just two wires (SDA and SCL) and allows bidirectional communication with the Pico W acting as the master. Some OLED displays may also support SPI (Serial Peripheral Interface) as an alternative to I2C, offering faster data transfer rates for specific applications.

Are there any power-saving techniques or optimizations specific to OLED displays?

Yes, you can optimize power consumption with OLED displays by reducing the display refresh rate, dimming the screen during inactivity, and programmatically turning off the OLED display when not in use. Utilizing dark themes and optimizing graphics can further enhance power efficiency and extend the display's lifespan.

Circuit Diagram - Interfacing Raspberry Pi Pico W with OLED Display

 

Circuit Diagram - Interfacing Raspberry Pi Pico W with OLED Display

Connect the OLED display's VCC or power pin to the Raspberry Pi Pico's Vbus (pin 40).

Connect the OLED display's GND or ground pin to the Raspberry Pi Pico's GND pin (pin 38).

Connect the OLED display's SDA (Serial Data) pin to the Raspberry Pi Pico's GP6 pin (pin 4). This pin is used for data transfer in the I2C communication.

Connect the OLED display's SCL (Serial Clock) pin to the Raspberry Pi Pico's GP7 pin (pin 5). This pin is used for synchronizing data transfer in the I2C communication.

Programming Raspberry Pi Pico W with MicroPython

  1. First open the thonny ide.
  2. Download micropython firmware for raspberry pi pico W from official website.
  3. Go to tools> options>interpreter and select raspberry pi pico as interpreter.
  4. Copy paste the below code in thonny ide and save it as ssd1306.py in your board.

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)
  1. Copy paste the code (which is the helloworld code given below) in the thonny ide and save it in your board as main.py.

After saving the code, it will look like this.

Thonny IDE Pico W MicroPython Code

  1. Run the program

This is how the project will look at the end.

Raspberry Pi Pico W with OLED Display Circuit

Here’s the code explanation for displaying hello world on screen.

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

These lines import the necessary modules. machine is a module used for interacting with hardware on microcontrollers, Pin is used to control GPIO pins, SoftI2C is used for software-based I2C communication, ssd1306 is a driver for the SSD1306 OLED display, and sleep is used to introduce delays.

i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))

Here, a software-based I2C communication interface (SoftI2C) is created using the GPIO pins 5 (scl) and 4 (sda) for the communication.

pin = machine.Pin(16, machine.Pin.OUT)
pin.value(0)  
pin.value(1)  

This section sets up a GPIO pin 16 as an output (machine.Pin.OUT). It first sets the pin low (0) to reset the OLED display and then sets it high (1) to bring the OLED out of reset. This is a common practice in hardware design to ensure that components start in a known state.

oled_width = 128
oled_height = 64

These lines define the width and height of the OLED display in pixels.

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

Here, an instance of the SSD1306 driver is created using the SSD1306_I2C class. This associates the previously defined I2C interface with the OLED driver, allowing communication between the microcontroller and the display.

oled.fill(0)

This line clears the display by filling it with black pixels. The 0 argument indicates that black (off) pixels will be filled.

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

This line writes the text "Hello, World!" on the display starting from the top-left corner with an offset of 0 pixels from the left and 10 pixels from the top.

oled.show()

Finally, the show() method is called to update the OLED display with the content that was written using the previous commands.

Here’s the complete code for the same

import machine
from machine import Pin, SoftI2C
import ssd1306
from time import sleep
i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))
pin = machine.Pin(16, machine.Pin.OUT)
pin.value(0) 
pin.value(1) 
oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)
oled.fill(0)
oled.text('Hello, World!', 0, 10)
oled.show()

Here’s a secondary example with code for creating a square shape

Raspberry Pi Pico W with OLED Display Square Shape

Here’s the complete code for creating square shape

import machine
import ssd1306
from time import sleep
# Initialize the I2C communication
i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))
# Reset the OLED display (optional)
pin = machine.Pin(16, machine.Pin.OUT)
pin.value(0)  # Set GPIO16 low to reset OLED
pin.value(1)  # While OLED is running, set GPIO16 high
# Create an instance of the SSD1306 OLED driver
oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)
# Function to draw a square in the center
def draw_square():
    square_size = 20
    center_x = oled_width // 2
    center_y = oled_height // 2
    x0 = center_x - square_size // 2
    y0 = center_y - square_size // 2
    x1 = center_x + square_size // 2
    y1 = center_y + square_size // 2
    oled.rect(x0, y0, square_size, square_size, 1)
# Clear the OLED display
oled.fill(0)
# Draw a square in the center of the OLED
draw_square()
# Update the OLED display to show the text and square
oled.show()

And finally here’s the working animation of a rocket blast

Here’s the code for creating an animation of a rocket blast

import machine
import ssd1306
from time import sleep
import math
# Initialize the I2C communication
i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))
# Reset the OLED display (optional)
pin = machine.Pin(16, machine.Pin.OUT)
pin.value(0)  # Set GPIO16 low to reset OLED
pin.value(1)  # While OLED is running, set GPIO16 high
# Create an instance of the SSD1306 OLED driver
oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)
# Function to clear the OLED screen
def clear_screen():
    oled.fill(0)
    oled.show()
# Animation parameters
rocket_height = 20  # Make the rocket go higher
rocket_x = oled_width // 2
rocket_y = oled_height
# Function to draw a circle at the given position
def draw_circle(x, y, radius):
    for i in range(radius * 2):
        for j in range(radius * 2):
            distance = math.sqrt((i - radius) ** 2 + (j - radius) ** 2)
            if distance < radius:
                oled.pixel(x - radius + i, y - radius + j, 1)
# Function to display the bursting pattern
def display_burst_pattern(x, y, radius, iterations):
    for r in range(1, radius, max(1, int(radius/iterations))):
        clear_screen()
        draw_circle(x, y, r)
        oled.show()
        sleep(0.03)
# Main animation loop
try:
    while True:
        # Rocket animation: Move the rocket upwards
        for y in range(oled_height, oled_height - rocket_height, -1):
            clear_screen()
            oled.rect(rocket_x - 2, y - rocket_height, 4, rocket_height, 1)
            oled.show()
            sleep(0.1)
        # Burst animation: Display the bursting pattern
        burst_x = rocket_x
        burst_y = oled_height - rocket_height
        burst_radius = 40  # Make the bursts 5 times bigger
        burst_iterations = 20
        display_burst_pattern(burst_x, burst_y, burst_radius, burst_iterations)
except KeyboardInterrupt:
    clear_screen()

Projects using OLED Display

Creating Tetris Game with Arduino and OLED Display
Creating Tetris Game with Arduino and OLED Display

Dive into nostalgia with our DIY Tetris Game using Arduino and OLED Display. Learn coding, crafting, and electronics as we guide you through creating your own handheld classic. Join the fun and bring retro gaming to life in this exciting blog series.

How a KY-038 Sound Sensor works and how to Interface it with ESP32 using OLED?
How a KY-038 Sound Sensor works and how to Interface it with ESP32 using OLED?

Unlock the world of sound sensing technology with our guide to the KY-038 Sound Sensor. Discover its inner workings and learn how to seamlessly interface it with the ESP32 microcontroller. Elevate your projects by incorporating an OLED display for visual feedback. Dive into this hands-on tutorial where we break down complex concepts into simple steps. Join us and explore the exciting realm of sound-responsive innovations!

Interfacing OLED Display with ESP32
Interfacing OLED Display with ESP32

Step into the world of ESP32 and OLED integration. Our straightforward tutorial will walk you through the process, allowing you to effortlessly connect and display content on the OLED screen using the ESP32. No jargon, just simple steps to bring your project to life. Join us in exploring this exciting synergy between hardware components!

Code

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

 

//code for OLED with Pico

import machine

from machine import Pin, SoftI2C

import ssd1306

from time import sleep

i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))

pin = machine.Pin(16, machine.Pin.OUT)

pin.value(0)

pin.value(1)

oled_width = 128

oled_height = 64

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

oled.fill(0)

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

oled.show()

 

//Code for Square Shape in OLED

import machine

import ssd1306

from time import sleep

# Initialize the I2C communication

i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))

# Reset the OLED display (optional)

pin = machine.Pin(16, machine.Pin.OUT)

pin.value(0)  # Set GPIO16 low to reset OLED

pin.value(1)  # While OLED is running, set GPIO16 high

# Create an instance of the SSD1306 OLED driver

oled_width = 128

oled_height = 64

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

# Function to draw a square in the center

def draw_square():

    square_size = 20

    center_x = oled_width // 2

    center_y = oled_height // 2

    x0 = center_x - square_size // 2

    y0 = center_y - square_size // 2

    x1 = center_x + square_size // 2

    y1 = center_y + square_size // 2

    oled.rect(x0, y0, square_size, square_size, 1)

# Clear the OLED display

oled.fill(0)

# Draw a square in the center of the OLED

draw_square()

# Update the OLED display to show the text and square

oled.show()

 

//code for creating an animation of a rocket blast

import machine

import ssd1306

from time import sleep

import math

# Initialize the I2C communication

i2c = machine.SoftI2C(scl=machine.Pin(5), sda=machine.Pin(4))

# Reset the OLED display (optional)

pin = machine.Pin(16, machine.Pin.OUT)

pin.value(0)  # Set GPIO16 low to reset OLED

pin.value(1)  # While OLED is running, set GPIO16 high

# Create an instance of the SSD1306 OLED driver

oled_width = 128

oled_height = 64

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

# Function to clear the OLED screen

def clear_screen():

    oled.fill(0)

    oled.show()

# Animation parameters

rocket_height = 20  # Make the rocket go higher

rocket_x = oled_width // 2

rocket_y = oled_height

# Function to draw a circle at the given position

def draw_circle(x, y, radius):

    for i in range(radius * 2):

        for j in range(radius * 2):

            distance = math.sqrt((i - radius) ** 2 + (j - radius) ** 2)

            if distance < radius:

                oled.pixel(x - radius + i, y - radius + j, 1)

# Function to display the bursting pattern

def display_burst_pattern(x, y, radius, iterations):

    for r in range(1, radius, max(1, int(radius/iterations))):

        clear_screen()

        draw_circle(x, y, r)

        oled.show()

        sleep(0.03)

# Main animation loop

try:

    while True:

        # Rocket animation: Move the rocket upwards

        for y in range(oled_height, oled_height - rocket_height, -1):

            clear_screen()

            oled.rect(rocket_x - 2, y - rocket_height, 4, rocket_height, 1)

            oled.show()

            sleep(0.1)

        # Burst animation: Display the bursting pattern

        burst_x = rocket_x

        burst_y = oled_height - rocket_height

        burst_radius = 40  # Make the bursts 5 times bigger

        burst_iterations = 20

        display_burst_pattern(burst_x, burst_y, burst_radius, burst_iterations)

except KeyboardInterrupt:

    clear_screen()  

Have any question realated to this Article?

Ask Our Community Members