Using an OLED Display with MicroPython on ESP32

Bidut Sharkar Shemanto - Jun 20 - - Dev Community

Using an OLED Display with MicroPython on ESP32

Introduction

In this tutorial, we will learn how to interface an OLED display with an ESP32 microcontroller using MicroPython. OLED displays are great for displaying text and simple graphics, making them ideal for various projects. We will use the SSD1306 OLED driver for this tutorial.

Prerequisites

Before we dive into the code, ensure you have the following:

  • ESP32 microcontroller
  • SSD1306 OLED display
  • Breadboard and jumper wires
  • MicroPython installed on the ESP32
  • Thonny IDE or any other suitable IDE for writing and uploading MicroPython code

SSD1306 OLED Driver

First, let's look at the SSD1306 OLED driver module. This module handles the communication with the OLED display and provides functions to draw text and graphics.

ssd1306.py Module Code

This module handles the low-level operations of the SSD1306 OLED display.



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


Enter fullscreen mode Exit fullscreen mode

main.py Code

The main script imports the SSD1306_I2C class from the ssd1306.py module and uses it to display text on the OLED.



# code written by Shemanto Sharkar (let's connect on LinkedIn: https://www.linkedin.com/in/shemanto/)
# step-1: importing necessary modules
from machine import Pin, I2C
import ssd1306

# step-2: telling ESP32 where our sensor's data pin is connected
i2c = I2C(0, scl=Pin(22), sda=Pin(21))

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

# step-3: reading data continuously inside loop
while True:
  try:
    oled.text('Hello World!', 10, 10)      
    oled.show()

  except OSError as e: # Error Handling  
    print("Error Data")


Enter fullscreen mode Exit fullscreen mode

Detailed Code Breakdown

  1. Importing Necessary Modules: ```python

from machine import Pin, I2C
import ssd1306

   - `from machine import Pin, I2C`: Imports the `Pin` and `I2C` classes from the `machine` module.
   - `import ssd1306`: Imports the `ssd1306` module for OLED display control.

2. **Setting Up the I2

C Interface:**
   ```python


   i2c = I2C(0, scl=Pin(22), sda=Pin(21))


Enter fullscreen mode Exit fullscreen mode
  • i2c = I2C(0, scl=Pin(22), sda=Pin(21)): Initializes the I2C interface on the ESP32 with GPIO 22 as the clock line and GPIO 21 as the data line.
  1. Setting Up the OLED Display: ```python

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

   - Initializes the OLED display with a width of 128 pixels and a height of 64 pixels using the I2C interface.

4. **Displaying Text on the OLED:**
   ```python


   while True:
     try:
       oled.text('Hello World!', 10, 10)
       oled.show()

     except OSError as e:
       print("Error Data")


Enter fullscreen mode Exit fullscreen mode
  • while True: Starts an infinite loop to continuously display data.
  • oled.text('Hello World!', 10, 10): Displays the text "Hello World!" at coordinates (10, 10) on the OLED.
  • oled.show(): Updates the OLED display with the new data.
  • except OSError as e: Catches any errors that occur during the display process and prints an error message.

Diagram

Here’s a diagram illustrating the connections:



ESP32 Microcontroller:
----------------------
        ___________
       |           |
       |           |
       |    21     |--------> OLED (SDA)
       |           |
       |    22     |--------> OLED (SCL)
       |___________|
         |
         |
       GND
       VCC (3.3V)


Enter fullscreen mode Exit fullscreen mode

Connections:

  • Connect the VCC pin of the OLED to the 3.3V pin of the ESP32.
  • Connect the GND pin of the OLED to the GND pin of the ESP32.
  • Connect the SDA pin of the OLED to GPIO 21 of the ESP32.
  • Connect the SCL pin of the OLED to GPIO 22 of the ESP32.

Conclusion

By following this tutorial, you will be able to display text on an OLED using an ESP32 microcontroller running MicroPython. This basic setup can be extended for various applications like displaying sensor data, system status, and more. Happy coding!

If you have any questions or need further assistance, feel free to reach out on LinkedIn: Shemanto Sharkar.

. . . . . . . . . . . . . .