To the Home Page

Creating ST7565 driver for CircuitPython

Published on May 30, 2024 · Reading time: 6 minutes

If you just want to download the driver scroll to the very bottom of the page. Thank you.

Table of Contents

I’m working on a new quick project that requires a large display with the best sunlight visibility possible. IPS modules are pretty terrible in this regard, while cheap OLED modules are susceptible to burn-in.

After some research, I’ve found a cheap (€10) ST7567 transflective display module with a white backlight and dark font. Little had I known that the CircuitPython driver for this display is incompatible with the displayio compositor. It’s time to change it.

What is displayio? It’s the compositing system built into the CircuitPython, written in C. It offers excellent performance and a variety of first-party hardware-agnostic libraries for variable-width text, shapes, charts, basic UI components or even tilemaps. However, its most useful feature is the serial monitor, so you can see error messages and REPL prompts.

Setting up

First, let’s set up the hardware connections. The display has an 8-pin, 1.0 mm pitch FPC connector. The datasheet for the ST7567 controller states that the only required components are two capacitors, and they are both already present on the flex cable. I’ve received no documentation for the display, except for some technical parameters and a wiring diagram:

Pin Meaning
1 Chip Select
2 Data / Command
3 Reset
4 Data
5 Clock
6 3.3V
7 GND
8 GND

There are separate backlight pins as well (anode and cathode). The declared power consumption is 70 mA for the 5 built-in LEDs, so it’s necessary to add a basic transistor circuit. Do not drive so many diodes directly from the microcontroller’s digital pin!

I have installed CircuitPython 9.0.5 on Raspberry Pi Pico, copied the adafruit_framebuf.py and adafruit_st7565.py library files from the CircuitPython library bundle, and run the following script:

import board
import busio
import digitalio

import adafruit_st7565

spi = busio.SPI(board.GP18, MOSI=board.GP19)
dc = digitalio.DigitalInOut(board.GP20)
cs = digitalio.DigitalInOut(board.GP17)
reset = digitalio.DigitalInOut(board.GP21)

display = adafruit_st7565.ST7565(spi, dc, cs, reset)

ST7567 using framebuf with default settings
ST7567 using framebuf with default settings

It worked, but I could barely see what’s on the screen even at the minimum contrast setting, because everything was so dark. I’ve checked the technical parameters once again and found a solution. The recommended bias setting for this very display is 1/9, but it was initialized with the value of 1/7. Bingo!

There were also some artifacts near the right edge of the screen, and the text was incorrectly positioned. Luckily, I’ve already experienced that when working with cheap SH1106 OLED modules. For the displayio driver, you would set the colstart parameter. The framebuf ST7565 driver has an undocumented start_bytes variable that does the same.

This is the display configuration that worked for me:

display = adafruit_st7565.ST7565(spi, dc, cs, reset)
display.write_cmd(display.CMD_SET_BIAS_9)
display.contrast = 5
display.start_bytes = 4

ST7567 using framebuf with updated settings
ST7567 using framebuf with updated settings

Writing a displayio driver

People are frightened when someone starts a talk about drivers. This usually means your Windows computer is broken because newly installed drivers are not compatible with something else, or you need to update the damn drivers because the new AAA game does not work properly.

The displayio drivers are not scary at all! In fact, they’re pretty easy to understand. Most of the time, they only store an initialization sequence for the display and some helper functions. Let’s write a driver for the ST7565 display.

The initialization sequence is a list of commands that are necessary to set up a display. Such commands may require extra data, and sometimes you need to add a short delay here and there to let the display do its thing. CircuitPython developers came up with an interesting idea: encode everything as a bytearray and let the displayio do all the processing. Define a command, declare a number of parameters (and an optional delay by setting the highest bit to 1), list the parameters, and finally define the delay. Rinse and repeat.

Let’s rewrite the __init__ of the original driver. I had temporarily replaced CMD_SET_ALLPTS_NORMAL with CMD_SET_ALLPTS_ON so I could actually see that the display was initialized. I had also changed the bias and contrast values.

Original Init seq.
CMD_SET_BIAS_9 A2 00
CMD_SET_ADC_REVERSE A1 00
CMD_SET_COM_NORMAL C0 00
CMD_SET_DISP_START_LINE 40 00
CMD_SET_POWER_CONTROL | 0x4; time.sleep(0.05) 2C 80 32
CMD_SET_POWER_CONTROL | 0x6; time.sleep(0.05) 2E 80 32
CMD_SET_POWER_CONTROL | 0x7; time.sleep(0.01) 2F 80 0A
CMD_SET_RESISTOR_RATIO | 0x7 27 00
CMD_DISPLAY_ON AF 00
CMD_SET_ALLPTS_ON A5 00
CMD_SET_VOLUME_FIRST 81 00
CMD_SET_VOLUME_SECOND | 0x05 05 00

So this is the initialization sequence that should work:

import board
from busdisplay import BusDisplay
from busio import SPI
from displayio import release_displays, Group
from fourwire import FourWire

init_sequence = (
    b"\xA2\x00"
    b"\xA1\x00"
    b"\xC0\x00"
    b"\x40\x00"
    b"\x2C\x80\x32"
    b"\x2E\x80\x32"
    b"\x2F\x80\x0A"
    b"\x27\x00"
    b"\xAF\x00"
    b"\xA5\x00"
    b"\x81\x00"
    b"\x05\x00"
)

release_displays()

spi = SPI(board.GP18, MOSI=board.GP19)
bus = FourWire(spi, command=board.GP20, chip_select=board.GP17, reset=board.GP21)
display = BusDisplay(bus, init_sequence, width=128, height=64, rotation=0, colstart=4)

Except it didn’t.

I’ve looked at the source code of other displayio drivers and couldn’t figure out what’s wrong. The most suspicious code in the original ST7565 driver was the 0.5s reset command, but no other display required it.

Somehow, I saw some signs of life after I added more keyword arguments:

display = BusDisplay(bus, init_sequence, width=128, height=64, rotation=0, colstart=4,
                     data_as_commands=True, SH1107_addressing=True, color_depth=1)

I understand why data_as_commands may be necessary: no commands have any extra values, and the contrast command was split in two. But what about those other two kwargs? I’ve downloaded the CircuitPython source code, searched for their usages, and found no clues.

Okay, so what happens if I disable CMD_SET_ALLPTS_ON? Can I see the REPL? It looks like, I can’t. But since I use the SH1107_addressing kwarg, maybe the hint is in the SH1107 displayio driver?

# for sh1107 use column and page addressing.
#   lower column command = 0x00 - 0x0F
#   upper column command = 0x10 - 0x17
#   set page address     = 0xB0 - 0xBF (16 pages)

These are the same values as in the ST7565 framebuf driver. But I could be missing three more kwargs: grayscale, single_byte_bounds and pixels_in_byte_share_row. I’ve added the first one, and some junk has been displayed, with the screen content different on each keystroke while in REPL (look at the noise in the top-left corner of the display).

ST7567 using displayio with default settings
ST7567 using displayio with default settings

I’ve enabled the second one, and it did nothing.

I’ve enabled the third one, AND IT WORKS! Just as if I were using the framebuf driver, but with all the benefits of displayio.

ST7567 using displayio with updated settings
ST7567 using displayio with updated settings

Observations

It looks like many displays are similar to each other software-wise. The ST7565 (and ST7567) have the same buffer layout as the SH1106, SSD1306, KS0108 and so on, with one byte describing eight adjacent vertical pixels and eight 132px-wide pages. They all use a specific addressing scheme, where you need to run one command to specify the vertical position for the upcoming raw data but two commands for the horizontal position.

I’ve cleaned up the code and published a CircuitPython ST7565 displayio library on GitHub. It’s already available in the CircuitPython Community bundle, which means it can be installed by running the following command:

circup install displayio_st7565

Check out other blog posts: