sam.pikesley.org

Driving a tiny LCD

Over i2c on an ESP32

I bought one of these little colour LCD screens kind of on a whim (just after we got back from EMF), assuming it would do something useful out-of-the-box, but I couldn't make it do anything at all, so I chucked in a drawer and forgot about it.

However I recently had cause to start fiddling with ESPHome, which meant I had a couple of ESP32s knocking around, so I pulled out the screen to see if I could get it working over the Christmas break.

And of course I couldn't. I reached out to the nerds of Mastodon, got lots of great feedback, and now here we are.

Connecting it up

I used an ESP32 C3 Super Mini for this - I guess it will work on other ESP32s too, but you might need to change the data pins.

The screen has four wires, which I connected like:

screen esp32
5v 5v
GND GND
SCL Pin 8
SDA Pin 9

Running the demo

The following presumes that your device already has the micropython firmware on it, and that you can run mpremote.

Get the code

git clone https://github.com/pikesley/st7789v2-micropython.git
cd st7789v2-micropython

Configure your wifi secrets

You need a file in the root of the repo called secrets.py that looks like this:

SSID = "my-home-wifi"
KEY = "mysupersecretwifipassword"

Push the code

Connect your esp32 to your computer via USB, then run:

make push connect

This will (probably) copy the code across, then wait. If you hit ctrl-D, it will reboot, connect to your wifi, sync its time over NTP and start showing a clock:

clock

mpremote seems to be quite good at detecting connected devices and selecting the correct USB device, so if you've only got one ESP32 connected to your laptop, you're probably fine.

The code

There are a load of tests, which you can run on the Docker container:

make build
make run

and then

make

If the code has a slightly "bashed together over the Christmas holidays without much organisation" feel to it, that's because that's exactly what happened.

How does it work?

I had been using one of these little OLED screens on a Raspberry Pi project, where I was assembling images with Pillow and then throwing them at the screen. In a characteristic display of breathtaking naievety, I had assumed I could do something similar with this new screen. My optimism was wildly misplaced.

Let's talk about i2c

The screen (at least in the m5stack package I have) talks i2c. This is some low-level serial thing, which means we need to send raw bytes to hex addresses, something I have studiously avoided thinking about for many years.

Fortunately micropython has built-in i2c support, which makes it surprisingly easy to turn the screen up to full brightness by doing something like

from machine import Pin, SoftI2C
i2c = SoftI2C(sda=Pin(9), scl=Pin(8), freq=400000)

i2c.writeto_mem(0x3E, 0x22, bytearray([0xff]))

where

  • 0x3E is the device's i2c address (findable with i2c.scan())
  • 0x22 is the "set brightness" command, and
  • 0xff says "set the brightness to 255"

Drawing pictures

There are commands to draw individual pixels, rectangles, and even whole images, using 4 different colour depths (the first two of which were new to me):

  • rgb332, where a whole RGB colour fits into a single byte
  • rgb565, with 2 bytes for an RGB colour
  • rgb888, which is your convential 24-bit RGB triple, and
  • rgb8888, which is that, but with an alpha channel

The font

If you know me, you might know that I'm moderately obsessed with the Sinclair Spectrum character set, so obviously that was my choice for rendering here. The whole thing fits into a set of lists of lists of bytes, and with a little manipulation it's easy to scale it up.

Run-Length Encoding

The screen also supports the rendering of images compressed with run-length encoding, which is a surprisingly easy-to-implement lossless compression technique that I've tackled before.

Putting it all together

So each character of our string is

  • looked up in the character-set, and
  • scaled up

Then

  • the characters are joined together horizontally
  • the whole thing is colourised,
  • run-length encoded, and then
  • the resulting bytes are made available from a generator

My early bumblings didn't bother with a generator and just attempted to yeet the entire list at the rendering tools, but it's remarkably easy to make your tiny microcontroller run out of memory, so we're doing it this way.

Using the tool

To actually write some text to the screen, you just do something like this:

from st7789v2.screen import screen

screen.write_text(
    "Hello World!",
    x="centered",
    y="centered",
    colour=255,  # unsurprisingly, this is white in rgb332
    scale_factor=1,
)

Packaging

I have attempted to structure this like a micropython package, but the contents of package.json are an absolute guess based on stuff I found elsewhere.

Next steps

I want to work out how to reduce a PNG or something to just a series of bytes, run-length encode them, then display them. Thus far this has led only to a lot of swearing.


These comments are generated from replies to this Mastodon post