MicroPython: SSD1306 OLED Display Scroll Functions and Draw Shapes (ESP32/ESP8266)

This guide shows additional functions to control an OLED display with MicroPython using the ESP32 or ESP8266. You’ll learn how to scroll the entire screen horizontally and vertically and how to draw shapes.

MicroPython: SSD1306 OLED Display Scroll Functions and Draw Shapes ESP32 ESP8266

We recommend that you follow this getting started guide for the OLED display with MicroPython, first: MicroPython OLED Display with ESP32 and ESP8266.

Prerequisites

To follow this tutorial you need MicroPython firmware installed in your ESP32 or ESP8266 boards. You also need an IDE to write and upload the code to your board. We suggest using Thonny IDE or uPyCraft IDE:

Learn more about MicroPython: MicroPython Programming with ESP32 and ESP8266 eBook

Parts Required

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

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!

Schematic – ESP32

Follow the next schematic diagram if you’re using an ESP32 board:

OLED display SSD1306 with ESP32 MicroPython schematic diagram circuit I2C SCL SDA

Recommended reading: ESP32 Pinout Reference Guide

Schematic – ESP8266 NodeMCU

Follow the next schematic diagram if you’re using an ESP8266 NodeMCU board:

OLED display SSD1306 with ESP8266 NodeMCU MicroPython schematic diagram circuit I2C SCL SDA

Recommended reading: ESP8266 Pinout Reference Guide

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 ESP32/ESP8266 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 set of instructions for the IDE you’re using:

  • A. Upload OLED library with uPyCraft IDE
  • B. Upload OLED library with Thonny IDE

A. Upload OLED library with uPyCraft IDE

This section shows how to upload a library using uPyCraft IDE. If you’re using Thonny IDE, read the next section.

1. Create a new file by pressing the New File button.

2. Copy the OLED library code into that file. The OLED library code can be found here.

Note: the SSD1306 OLED display library was built by Adafruit and will no longer
be updated. At the moment, it works fine. However, we’ll update this guide if we
find a similar library that works as well as this one.

3. After copying the code, save the file by pressing the Save button.

4. Call this new file “ssd1306.py” and press ok.

import library upycraft ide save file

5. Click the Download and Run button.

The file should be saved on the device folder with the name “ssd1306.py” as
highlighted in the following figure.

import save library uPyCraft IDE script file

Now, you can use the library functionalities in your code by importing the library.

B. Upload OLED library with Thonny IDE

If you’re using Thonny IDE, follow the next steps:

1. Copy the library code to a new file. The OLED library code can be found here.

2. Save that file as ssd1306.py.

3. Go to Device Upload current script with the current name

thonny ide upload library file script micropython

And that’s it. The library was uploaded to your board. To make sure that it was uploaded successfully, in the Shell you can type:

%lsdevice

It should return the files currently saved on your board. One of them should be the ssd1306.py file.

show device folder thonny ide

After uploading the library to your board, you can use the library functionalities in your code by importing the library.

MicroPython OLED Scroll Functions

The ss1306.py library comes with a scroll(x, y) function. It scroll x number of pixels to the right and y number of pixels down.

Scroll OLED Screen Horizontally

Sometimes you want to display different screens on the OLED display. For example, the first screen shows sensor readings, and the second screen shows GPIO states.

Scroll in horizontally

The following function scroll_in_screen(screen) scrolls the content of an entire screen (right to left).

def scroll_in_screen(screen):
  for i in range (0, oled_width+1, 4):
    for line in screen:
      oled.text(line[2], -oled_width+i, line[1])
    oled.show()
    if i!= oled_width:
      oled.fill(0)

This function accepts as argument a list of lists. For example:

screen1 = [[0, 0 , screen1_row1], [0, 16, screen1_row2], [0, 32, screen1_row3]]

Each list of the list contains the x coordinate, the y coordinate and the message [x, y, message].

As an example, we’ll display three rows on the first screen with the following messages.

screen1_row1 = "Screen 1, row 1"
screen1_row2 = "Screen 1, row 2"
screen1_row3 = "Screen 1, row 3"

Then, to make your screen scrolling from left to right, you just need to call the scroll_in_screen() function and pass as argument the list of lists:

scroll_in_screen(screen1)

You’ll get something as follows:


Scroll out horizontally

To make the screen scroll out, you can use the scroll_out_screen(speed) function that scrolls the entire screen out of the OLED. It accepts as argument a number that controls the scrolling speed. The speed must be a divisor of 128 (oled_width)

def scroll_out_screen(speed):
  for i in range ((oled_width+1)/speed):
    for j in range (oled_height):
      oled.pixel(i, j, 0)
    oled.scroll(speed,0)
    oled.show()

Now, you can use both functions to scroll between screens. For example:

scroll_in_screen(screen1)
scroll_out_screen(4)
scroll_in_screen(screen2)
scroll_out_screen(4)

Continuous horizontal scroll

If you want to scroll the screen in and out continuously, you can use the scroll_screen_in_out(screen) function instead.

def scroll_screen_in_out(screen):
  for i in range (0, (oled_width+1)*2, 1):
    for line in screen:
      oled.text(line[2], -oled_width+i, line[1])
    oled.show()
    if i!= oled_width:
      oled.fill(0)

You can use this function to scroll between screens, or to scroll the same screen over and over again.

scroll_screen_in_out(screen1)
scroll_screen_in_out(screen2)
scroll_screen_in_out(screen3)

Scroll OLED Screen Vertically

We also created similar functions to scroll the screen vertically.

Scroll in vertically

The scroll_in_screen_v(screen) scrolls in the content of the entire screen.

def scroll_in_screen_v(screen):
  for i in range (0, (oled_height+1), 1):
    for line in screen:
      oled.text(line[2], line[0], -oled_height+i+line[1])
    oled.show()
    if i!= oled_height:
      oled.fill(0)

Scroll out vertically

You can use the scroll_out_screen_v(speed) function to scroll out the screen vertically. Similarly to the horizontal function, it accepts as argument, the scrolling speed that must be a number divisor of 64 (oled_height).

def scroll_out_screen_v(speed):
  for i in range ((oled_height+1)/speed):
    for j in range (oled_width):
      oled.pixel(j, i, 0)
    oled.scroll(0,speed)
    oled.show()

Continuous vertical scroll

If you want to scroll the screen in and out vertically continuously, you can use the scroll_in_out_screen_v(screen) function.

def scroll_screen_in_out_v(screen):
  for i in range (0, (oled_height*2+1), 1):
    for line in screen:
      oled.text(line[2], line[0], -oled_height+i+line[1])
    oled.show()
    if i!= oled_height:
      oled.fill(0)

Scroll OLED Screen MicroPython Script

The following script applies all the functions we’ve described previously. You can upload the following code to your board to see all the scrolling effects.

# Complete project details at https://RandomNerdTutorials.com/micropython-ssd1306-oled-scroll-shapes-esp32-esp8266/

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

# ESP32 Pin assignment
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))

# ESP8266 Pin assignment
#i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

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

screen1_row1 = "Screen 1, row 1"
screen1_row2 = "Screen 1, row 2"
screen1_row3 = "Screen 1, row 3"

screen2_row1 = "Screen 2, row 1"
screen2_row2 = "Screen 2, row 2"

screen3_row1 = "Screen 3, row 1"

screen1 = [[0, 0 , screen1_row1], [0, 16, screen1_row2], [0, 32, screen1_row3]]
screen2 = [[0, 0 , screen2_row1], [0, 16, screen2_row2]]
screen3 = [[0, 40 , screen3_row1]]

# Scroll in screen horizontally from left to right
def scroll_in_screen(screen):
  for i in range (0, oled_width+1, 4):
    for line in screen:
      oled.text(line[2], -oled_width+i, line[1])
    oled.show()
    if i!= oled_width:
      oled.fill(0)

# Scroll out screen horizontally from left to right
def scroll_out_screen(speed):
  for i in range ((oled_width+1)/speed):
    for j in range (oled_height):
      oled.pixel(i, j, 0)
    oled.scroll(speed,0)
    oled.show()

# Continuous horizontal scroll
def scroll_screen_in_out(screen):
  for i in range (0, (oled_width+1)*2, 1):
    for line in screen:
      oled.text(line[2], -oled_width+i, line[1])
    oled.show()
    if i!= oled_width:
      oled.fill(0)

# Scroll in screen vertically
def scroll_in_screen_v(screen):
  for i in range (0, (oled_height+1), 1):
    for line in screen:
      oled.text(line[2], line[0], -oled_height+i+line[1])
    oled.show()
    if i!= oled_height:
      oled.fill(0)

# Scroll out screen vertically
def scroll_out_screen_v(speed):
  for i in range ((oled_height+1)/speed):
    for j in range (oled_width):
      oled.pixel(j, i, 0)
    oled.scroll(0,speed)
    oled.show()

# Continous vertical scroll
def scroll_screen_in_out_v(screen):
  for i in range (0, (oled_height*2+1), 1):
    for line in screen:
      oled.text(line[2], line[0], -oled_height+i+line[1])
    oled.show()
    if i!= oled_height:
      oled.fill(0)

while True:

  # Scroll in, stop, scroll out (horizontal)
  scroll_in_screen(screen1)
  sleep(2)
  scroll_out_screen(4)

  scroll_in_screen(screen2)
  sleep(2)
  scroll_out_screen(4)

  scroll_in_screen(screen3)
  sleep(2)
  scroll_out_screen(4)

  # Continuous horizontal scroll
  scroll_screen_in_out(screen1)
  scroll_screen_in_out(screen2)
  scroll_screen_in_out(screen3)

  # Scroll in, stop, scroll out (vertical)
  scroll_in_screen_v(screen1)
  sleep(2)
  scroll_out_screen_v(4)

  scroll_in_screen_v(screen2)
  sleep(2)
  scroll_out_screen_v(4)

  scroll_in_screen_v(screen3)
  sleep(2)
  scroll_out_screen_v(4)

  # Continuous verticall scroll
  scroll_screen_in_out_v(screen1)
  scroll_screen_in_out_v(screen2)
  scroll_screen_in_out_v(screen3)

View raw code

MicroPython OLED Draw Shapes

To draw shapes on the OLED display using MicroPython we’ll use the Adafruit_GFX MicroPython Library.

Adafruit GFX Library

To draw shapes on the OLED display, we’ll use the Adafruit GFX Library. This library isn’t part of the standard MicroPython library by default. So, you need to upload the library to your ESP32/ESP8266 board.

# Port of Adafruit GFX Arduino library to MicroPython.
# Based on: https://github.com/adafruit/Adafruit-GFX-Library
# Author: Tony DiCola (original GFX author Phil Burgess)
# License: MIT License (https://opensource.org/licenses/MIT)

class GFX:

    def __init__(self, width, height, pixel, hline=None, vline=None):
        # Create an instance of the GFX drawing class.  You must pass in the
        # following parameters:
        #  - width = The width of the drawing area in pixels.
        #  - height = The height of the drawing area in pixels.
        #  - pixel = A function to call when a pixel is drawn on the display.
        #            This function should take at least an x and y position
        #            and then any number of optional color or other parameters.
        #  You can also provide the following optional keyword argument to
        #  improve the performance of drawing:
        #  - hline = A function to quickly draw a horizontal line on the display.
        #            This should take at least an x, y, and width parameter and
        #            any number of optional color or other parameters.
        #  - vline = A function to quickly draw a vertical line on the display.
        #            This should take at least an x, y, and height paraemter and
        #            any number of optional color or other parameters.
        self.width = width
        self.height = height
        self._pixel = pixel
        # Default to slow horizontal & vertical line implementations if no
        # faster versions are provided.
        if hline is None:
            self.hline = self._slow_hline
        else:
            self.hline = hline
        if vline is None:
            self.vline = self._slow_vline
        else:
            self.vline = vline

    def _slow_hline(self, x0, y0, width, *args, **kwargs):
        # Slow implementation of a horizontal line using pixel drawing.
        # This is used as the default horizontal line if no faster override
        # is provided.
        if y0 < 0 or y0 > self.height or x0 < -width or x0 > self.width:
            return
        for i in range(width):
            self._pixel(x0+i, y0, *args, **kwargs)

    def _slow_vline(self, x0, y0, height, *args, **kwargs):
        # Slow implementation of a vertical line using pixel drawing.
        # This is used as the default vertical line if no faster override
        # is provided.
        if y0 < -height or y0 > self.height or x0 < 0 or x0 > self.width:
            return
        for i in range(height):
            self._pixel(x0, y0+i, *args, **kwargs)

    def rect(self, x0, y0, width, height, *args, **kwargs):
        # Rectangle drawing function.  Will draw a single pixel wide rectangle
        # starting in the upper left x0, y0 position and width, height pixels in
        # size.
        if y0 < -height or y0 > self.height or x0 < -width or x0 > self.width:
            return
        self.hline(x0, y0, width, *args, **kwargs)
        self.hline(x0, y0+height-1, width, *args, **kwargs)
        self.vline(x0, y0, height, *args, **kwargs)
        self.vline(x0+width-1, y0, height, *args, **kwargs)

    def fill_rect(self, x0, y0, width, height, *args, **kwargs):
        # Filled rectangle drawing function.  Will draw a single pixel wide
        # rectangle starting in the upper left x0, y0 position and width, height
        # pixels in size.
        if y0 < -height or y0 > self.height or x0 < -width or x0 > self.width:
            return
        for i in range(x0, x0+width):
            self.vline(i, y0, height, *args, **kwargs)

    def line(self, x0, y0, x1, y1, *args, **kwargs):
        # Line drawing function.  Will draw a single pixel wide line starting at
        # x0, y0 and ending at x1, y1.
        steep = abs(y1 - y0) > abs(x1 - x0)
        if steep:
            x0, y0 = y0, x0
            x1, y1 = y1, x1
        if x0 > x1:
            x0, x1 = x1, x0
            y0, y1 = y1, y0
        dx = x1 - x0
        dy = abs(y1 - y0)
        err = dx // 2
        ystep = 0
        if y0 < y1:
            ystep = 1
        else:
            ystep = -1
        while x0 <= x1:
            if steep:
                self._pixel(y0, x0, *args, **kwargs)
            else:
                self._pixel(x0, y0, *args, **kwargs)
            err -= dy
            if err < 0:
                y0 += ystep
                err += dx
            x0 += 1

    def circle(self, x0, y0, radius, *args, **kwargs):
        # Circle drawing function.  Will draw a single pixel wide circle with
        # center at x0, y0 and the specified radius.
        f = 1 - radius
        ddF_x = 1
        ddF_y = -2 * radius
        x = 0
        y = radius
        self._pixel(x0, y0 + radius, *args, **kwargs)
        self._pixel(x0, y0 - radius, *args, **kwargs)
        self._pixel(x0 + radius, y0, *args, **kwargs)
        self._pixel(x0 - radius, y0, *args, **kwargs)
        while x < y:
            if f >= 0:
                y -= 1
                ddF_y += 2
                f += ddF_y
            x += 1
            ddF_x += 2
            f += ddF_x
            self._pixel(x0 + x, y0 + y, *args, **kwargs)
            self._pixel(x0 - x, y0 + y, *args, **kwargs)
            self._pixel(x0 + x, y0 - y, *args, **kwargs)
            self._pixel(x0 - x, y0 - y, *args, **kwargs)
            self._pixel(x0 + y, y0 + x, *args, **kwargs)
            self._pixel(x0 - y, y0 + x, *args, **kwargs)
            self._pixel(x0 + y, y0 - x, *args, **kwargs)
            self._pixel(x0 - y, y0 - x, *args, **kwargs)

    def fill_circle(self, x0, y0, radius, *args, **kwargs):
        # Filled circle drawing function.  Will draw a filled circule with
        # center at x0, y0 and the specified radius.
        self.vline(x0, y0 - radius, 2*radius + 1, *args, **kwargs)
        f = 1 - radius
        ddF_x = 1
        ddF_y = -2 * radius
        x = 0
        y = radius
        while x < y:
            if f >= 0:
                y -= 1
                ddF_y += 2
                f += ddF_y
            x += 1
            ddF_x += 2
            f += ddF_x
            self.vline(x0 + x, y0 - y, 2*y + 1, *args, **kwargs)
            self.vline(x0 + y, y0 - x, 2*x + 1, *args, **kwargs)
            self.vline(x0 - x, y0 - y, 2*y + 1, *args, **kwargs)
            self.vline(x0 - y, y0 - x, 2*x + 1, *args, **kwargs)

    def triangle(self, x0, y0, x1, y1, x2, y2, *args, **kwargs):
        # Triangle drawing function.  Will draw a single pixel wide triangle
        # around the points (x0, y0), (x1, y1), and (x2, y2).
        self.line(x0, y0, x1, y1, *args, **kwargs)
        self.line(x1, y1, x2, y2, *args, **kwargs)
        self.line(x2, y2, x0, y0, *args, **kwargs)

    def fill_triangle(self, x0, y0, x1, y1, x2, y2, *args, **kwargs):
        # Filled triangle drawing function.  Will draw a filled triangle around
        # the points (x0, y0), (x1, y1), and (x2, y2).
        if y0 > y1:
            y0, y1 = y1, y0
            x0, x1 = x1, x0
        if y1 > y2:
            y2, y1 = y1, y2
            x2, x1 = x1, x2
        if y0 > y1:
            y0, y1 = y1, y0
            x0, x1 = x1, x0
        a = 0
        b = 0
        y = 0
        last = 0
        if y0 == y2:
            a = x0
            b = x0
            if x1 < a:
                a = x1
            elif x1 > b:
                b = x1
            if x2 < a:
                a = x2
            elif x2 > b:
                b = x2
            self.hline(a, y0, b-a+1, *args, **kwargs)
            return
        dx01 = x1 - x0
        dy01 = y1 - y0
        dx02 = x2 - x0
        dy02 = y2 - y0
        dx12 = x2 - x1
        dy12 = y2 - y1
        if dy01 == 0:
            dy01 = 1
        if dy02 == 0:
            dy02 = 1
        if dy12 == 0:
            dy12 = 1
        sa = 0
        sb = 0
        if y1 == y2:
            last = y1
        else:
            last = y1-1
        for y in range(y0, last+1):
            a = x0 + sa // dy01
            b = x0 + sb // dy02
            sa += dx01
            sb += dx02
            if a > b:
                a, b = b, a
            self.hline(a, y, b-a+1, *args, **kwargs)
        sa = dx12 * (y - y1)
        sb = dx02 * (y - y0)
        while y <= y2:
            a = x1 + sa // dy12
            b = x0 + sb // dy02
            sa += dx12
            sb += dx02
            if a > b:
                a, b = b, a
            self.hline(a, y, b-a+1, *args, **kwargs)
            y += 1

View raw code

Follow the previous instructions on how to install a library, but for the GFX library. Save the GFX library file as gfx.py. Then, you can use the library functionalities by importing the library in your code.

In summary, here’s how to draw shapes. First, you need to include the ssd1306 and gfx libraries as well as the Pin and SoftI2C modules.

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

Then, define the pins for the ESP32.

i2c = SoftI2C(scl=Pin(22), sda=Pin(21))

If you’re using an ESP8266, use the following pins instead:

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

We’re using a 128×64 OLED display. If you’re using an OLED display with different dimensions, change that on the following lines:

oled_width = 128
oled_height = 64

Create an ss1306 object called oled.

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

Then, we need to create a gfx object to draw shapes. In this case, it’s called graphics. It takes as arguments, the width and height of the drawing area. In this case, we want to draw in the entire OLED, so we pass the OLED width and height. We should also pass as argument one function of our display that draws pixels, in our case is oled.pixel.

graphics = gfx.GFX(oled_width, oled_height, oled.pixel)

Then, you can use the drawing functions we’ll show you next to display shapes.

Draw a Line

ESP32 ESP8266 Arduino OLED Display Line

Use the line(x0, y0, x1, y1, color) method on the gfx object to create a line. The (x0, y0) coordinates indicate the start of the line, and the (x1, y1) coordinates indicate where the line ends. You always need to call oled.show() to actually display the shapes on the OLED. Here’s an example:

graphics.line(0, 0, 127, 20, 1)
oled.show()

Rectangle

ESP32 ESP8266 Arduino OLED Display Rectangle

To draw a rectangle, you can use the rect(x0, y0, width, height, color) method on the gfx object. The (x0, y0) coordinates indicate the top left corner of the rectangle. Then, you need to specify the width, height and color of the rectangle. For example:

graphics.rect(10, 10, 50, 30, 1)
oled.show()

Filled Rectangle

ESP32 ESP8266 Arduino OLED Display Filled

You can use the fill_rect(x0, y0, width, height, color) method to draw a filled rectangle. This method accepts the same arguments as drawRect().

graphics.rect(10, 10, 50, 30, 1)
oled.show()

Circle

ESP32 ESP8266 Arduino OLED Display Circle

Draw a circle using the circle(x0, y0, radius, color) method. The (x0, y0) coordinates indicate the center of the circle. Here’s an example:

graphics.circle(64, 32, 10, 1)
oled.show()

Filled Circle

ESP32 ESP8266 Arduino OLED Display Circle Filled

Draw a filled circle using the fill_circle(x0, y0, radius, color) method.

graphics.fill_circle(64, 32, 10, 1)
oled.show()

Triangle

ESP32 ESP8266 Arduino OLED Display Triangle

There’s also a method to draw a triangle: triangle(x0, y0, x1, y1, x2, y2, color). This method accepts as arguments the coordinates of each corner and the color.

graphics.triangle(10,10,55,20,5,40,1)
oled.show()

Filled Triangle

ESP32 ESP8266 Arduino OLED Display Triangle Filled

Use the fill_triangle(x0, y0, x1, y1, x2, y2, color) method to draw a filled triangle.

graphics.fill_triangle(10,10,55,20,5,40,1)
oled.show()

MicroPython Script – Draw Shapes

The following script implements all the drawing methods shown previously.

# Complete project details at https://RandomNerdTutorials.com/micropython-ssd1306-oled-scroll-shapes-esp32-esp8266/

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

# ESP32 Pin assignment
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))

# ESP8266 Pin assignment
#i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

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

graphics = gfx.GFX(oled_width, oled_height, oled.pixel)

while True:

  graphics.line(0, 0, 127, 20, 1)
  oled.show()
  sleep(1)
  oled.fill(0)

  graphics.rect(10, 10, 50, 30, 1)
  oled.show()
  sleep(1)
  oled.fill(0)

  graphics.fill_rect(10, 10, 50, 30, 1)
  oled.show()
  sleep(1)
  oled.fill(0)


  graphics.circle(64, 32, 10, 1)
  oled.show()
  sleep(1)
  oled.fill(0)

  graphics.fill_circle(64, 32, 10, 1)
  oled.show()
  sleep(1)
  oled.fill(0)

  graphics.triangle(10,10,55,20,5,40,1)
  oled.show()
  sleep(1)
  oled.fill(0)

  graphics.fill_triangle(10,10,55,20,5,40,1)
  oled.show()
  sleep(1)
  oled.fill(0)

View raw code

Wrapping Up

In this tutorial you’ve learned how to use more advanced functions to scroll the OLED screen and draw shapes using MicroPython with the ESP32 or ESP8266. To draw shapes you need to import the Adafruit GFX MicroPython library.

We hope you’ve found this tutorial useful. If this is your first time dealing with the OLED display using MicroPython, we recommend following the getting started guide first:

We have a similar tutorials, but using Arduino IDE:

If you want to learn more about MicroPython, check our eBook:



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!

13 thoughts on “MicroPython: SSD1306 OLED Display Scroll Functions and Draw Shapes (ESP32/ESP8266)”

    • Hi.
      You just need to insert your variables in the content of the rows. For example:

      while True:
      temp = randint(1,20)
      hum = randint (50, 100)

      screen1_row1 = “Temperature: ” + str(temp)
      screen1_row2 = “Humidity: ” + str(hum)

      screen1 = [[0, 0 , screen1_row1], [0, 16, screen1_row2], [0, 32, screen1_row3]]

      # Scroll in, stop, scroll out (horizontal)
      scroll_in_screen(screen1)
      sleep(2)
      scroll_out_screen(4)

      In this case, the temperature and humidity are random numbers, they should be replaced with the actual values.
      As you can see, you just need to insert the variables in the content of the rows, and then, create the screen list.

      I hope this is clear.
      Regards,
      Sara

      Reply
  1. Thanks a lot for this tutorial this is exactly what I need for my projects and also for the coding club I go to with kids learning code. Thanks again ! At the club we love micropython.

    Reply
  2. Hello,
    I’m getting error message:
    Traceback (most recent call last):
    File “”, line 15, in
    File “ssd1306.py”, line 110, in init
    File “ssd1306.py”, line 36, in init
    File “ssd1306.py”, line 71, in init_display
    File “ssd1306.py”, line 115, in write_cmd
    OSError: [Errno 110] ETIMEDOUT
    I remember it used to run without problem the first time. I did not change anything in the code. Please help. Thanks.

    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.