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:
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 withi2c.scan()
)0x22
is the "set brightness" command, and0xff
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.