OLED display

The SSD1306 OLED display is a cheap and popular display. It is, for example, the display that is used on the Makerfabs ESP32 board.

In this tutorial we will write text on it, and draw some icons.

Prerequisites

This tutorial assumes that you have done the i2c tutorial.

We recommend to have read the packages tutorial, since we are going to use a package for the SSD1306 driver.

Setup

Connect the display to the ESP32 board as follows:

  • VCC/VIN to 3V3
  • GND to GND
  • SCL to pin 25
  • SDA to pin 26

Many boards that have an SSD1306 display integrated (like the Makerfabs MaESP Oled or the Wemos Lolin) have the display connected to pins 4 (SDA) and 5 (SCL). If you use one of these boards you have to change the code below to use these pins.

Makerfabs ESP32 board with integrated SSD1306 display
Makerfabs ESP32 board with integrated SSD1306 display

The Heltec boards with integrated displays, use the following pins:

  • v2: SDA=4, SCL=15
  • v3: SDA=17, SCL=18

Note that we use the Adafruit 128x32 display in the following diagrams. The connections are the same for 128x64 displays.

Wiring diagram for an SSD1306 display
Wiring diagram for an SSD1306 display
Schematics for an SSD1306 display
Schematics for an SSD1306 display

Packages

The driver for the SSD1306 is not part of the standard Toit distribution, but must be downloaded as package. See the packages tutorial for details.

We are using the ssd1306 package. To install it, run the following command in your project directory:

jag pkg install github.com/toitware/toit-ssd1306@v2

You can probably just write jag pkg install ssd1306, but the full ID together with the version is more explicit, and will make sure you get the right package.

Similarly, install the pixel-display package to provide convenience methods to draw on the display:

jag pkg install toitware/toit-pixel-display@v2

For information about how to use the display libraries, see the display documentation.

In this tutorial, we are just going to show a few examples.

Text

Start a ssd1306_text.toit program and watch it with Jaguar.

Enter the following program:

import font show *
import gpio
import i2c
import pixel-display show *
import pixel-display.two-color show *
import ssd1306 show *

current-date:
  now := Time.now.local
  return "$now.year-$(%02d now.month)-$(%02d now.day)"

current-time:
  now := Time.now.local
  return "$(%02d now.h):$(%02d now.m):$(%02d now.s)"

main:
  sda := gpio.Pin 26
  scl := gpio.Pin 25
  frequency := 400_000

  bus := i2c.Bus --sda=sda --scl=scl --frequency=frequency

  devices := bus.scan
  if not devices.contains Ssd1306.I2C-ADDRESS:
    throw "No SSD1306 display found"

  device := bus.device Ssd1306.I2C-ADDRESS
  driver := Ssd1306.i2c device
  display := PixelDisplay.two-color driver
  display.background = BLACK

  sans := Font.get "sans10"
  [
    Label --x=30 --y=20 --text="Toit",
    Label --x=30 --y=40 --id="date",
    Label --x=30 --y=60 --id="time",
  ].do: display.add it

  STYLE ::= Style
      --type-map={
          "label": Style --font=sans --color=WHITE,
      }
  display.set-styles [STYLE]

  date/Label := display.get-element-by-id "date"
  time/Label := display.get-element-by-id "time"
  while true:
    date.text = current-date
    time.text = current-time
    display.draw
    sleep --ms=250

As a first step we create an i2c bus object with: i2c.Bus --sda=sda --scl=scl --frequency=frequency. This calls the constructor of the Bus class, passing in the named arguments. The display supports the "fast" mode of i2c which is why we can set the frequency to 400KHz (and not just 100KHz). The code would work the same way with 100KHz. Usually, 700kHz works too, and makes for slightly faster updates.

We then instantiate an Ssd1306 object by calling its i2c constructor. As argument it receives the i2c device with ID Ssd1306.I2C-ADDRESS. The value of Ssd1306.I2C-ADDRESS is equal to 60, thus identifying the display on the bus.

With the driver object we can then build a display object which gives us some useful methods to operate the display. We can set the background color to BLACK, and then use the WHITE constant to write later.

Writing text is done by adding Label elements to the display, then styling them with a Style object. The style object sets the font and color for the Label elements.

Note that operations on the display don't immediately interact with the physical display but build up a drawing queue/graph instead. Only with display.draw is the content constructed and sent to the physical display.

This has two major advantages. We can reuse the graph and simply update the parts that change. In our example we have a while-true loop that just updates the objects we received from the get-element-by-id calls. We could also call move-to on the object to move it somewhere.

The program writes 3 lines:

  • Toit,
  • the date in ISO 8601 format, and
  • the time.

Time

By default the device starts with a time set to 1970. You can use the ntp package to synchronize the device's time.

See this example.

Icons

As the name implies, it is possible to draw individual pixels onto a display. However, in many cases, icons are completely sufficient.

Toit provides a package with icons from the Pictogrammers project. To use them, install the pictogrammers_icons:

jag pkg install github.com/toitware/toit-icons-pictogrammers@v1

Start a new file and save it as display_icon.toit. Use jag watch to run it whenever it is saved.

Enter the following code into the new file:

import pictogrammers-icons.size-48 as icons
import gpio
import i2c
import pixel-display show *
import pixel-display.two-color show *
import ssd1306 show *

get-display -> PixelDisplay:
  sda := gpio.Pin 26
  scl := gpio.Pin 25
  frequency := 400_000

  bus := i2c.Bus --sda=sda --scl=scl --frequency=frequency

  devices := bus.scan
  if not devices.contains Ssd1306.I2C-ADDRESS:
    throw "No SSD1306 display found"

  device := bus.device Ssd1306.I2C-ADDRESS
  driver := Ssd1306.i2c device
  return PixelDisplay.two-color driver

main:
  display := get-display
  display.background = BLACK

  icon := Label --x=0 --y=50 --icon=icons.HUMAN-SCOOTER --color=WHITE
  display.add icon
  while true:
    80.repeat:
      icon.move-to it 50
      display.draw
      sleep --ms=1
    sleep --ms=2_000

The code starts again by creating the display. This time we moved the display-creation code into its own function (get-display which returns a PixelDisplay).

If your display is only a 32-line display you need to add the argument --height=32 to the driver creation. In some cases you also need to play with the following arguments:

  • --flip, to flip the display vertically,
  • --inverse, to invert black and white,
  • --layout=XXX where XXX is one of LAYOUT-SEQUENTIAL, LAYOUT-SEQUENTIAL-SWITCHED, LAYOUT-ALTERNATED, or LAYOUT-ALTERNATED-SWITCHED.

For this simple example, we don't create a style object. We just pass the color to the constructor of the Label.

We then add the icon-label to the display and start a loop. For each iteration, the position is updated before every draw. This makes the icon move from left to right over the display.

You can browse the icons at: https://materialdesignicons.com/.

Often, devices are used for temperature measurements, in which case icons that start with icons.WEATHER_ are interesting. Use the completion to see which icons exist.

Exercises

As long as the connections were done correctly you can't damage your hardware by changing your program.

  • Change the text.
  • Fix the device's internal time before showing anything on the display. See the ntp package and the corresponding example.
  • Invert the colors, so that the background is white and the text is black.