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
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
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
- First open the thonny ide.
- Download micropython firmware for raspberry pi pico W from official website.
- Go to tools> options>interpreter and select raspberry pi pico as interpreter.
- 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)
- 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.
- Run the program
This is how the project will look at the end.
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
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
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.
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!
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!
//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()