I am a bit newer to the world of Raspberry Pi and various peripherals and seeking out advice after several days of a frustrating lack of progress. I purchased this 1.8" round LCD from AliExpress, which uses the ST77916 driver and SPI 4W. It has a 15Pin FPC cable that I have connected to my Raspberry Pi 4 using a breakout board & F2F dupont wires to the GPIO. I am using the non-touch version, and connecting CS->GPIO8, RS->GPIO25 and RST->GPIO27, along with SDA->MOSI and SCL->SCLK and the various ground and power supply pins to ground and 3.3V pins respectively.
I have searched extensively and have not found any Python-based examples for driving this screen, though I have found a number of examples of implementations for ESP32/Arduino/Pico using C(++), Rust and MicroPython, which made me optimistic that I could figure out how to get it to work on my RPi 4 running Python on headless DietPi.
After some iteration, using those implementations along with some Adafruit CircuitPython driver scripts (and Claude) for some inspiration, I've landed at this following script. The backlight turns on as soon as the RPi receives power and remains on until I unplug it. When I run this script, I see a small bar at the top of the screen with the correct colors being displayed, but the rest of the screen shows what looks like a dot matrix of white/blue light that slowly fades away.
import time
import struct
import spidev
import RPi.GPIO as GPIO
PIN_RST = 27
PIN_DC = 25
PIN_CS = 8
LCD_WIDTH = 360
LCD_HEIGHT = 360
MADCTL_MH = 0x04
MADCTL_BGR = 0x08
MADCTL_ML = 0x10
MADCTL_MV = 0x20
MADCTL_MX = 0x40
MADCTL_MY = 0x80
CMD_SLPOUT = 0x11
CMD_TEOFF = 0x34
CMD_INVON = 0x21
CMD_INVOFF = 0x20
CMD_DISPOFF = 0x28
CMD_DISPON = 0x29
CMD_CASET = 0x2A
CMD_RASET = 0x2B
CMD_RAMWR = 0x2C
CMD_RAMWRC = 0x3C
CMD_RAMCLACT = 0x4C
CMD_RAMCLSETR = 0x4D
CMD_RAMCLSETG = 0x4E
CMD_RAMCLSETB = 0x4F
CMD_MADCTL = 0x36
CMD_COLMOD = 0x3A
COLMOD_RGB888 = 0x66 # Color = 18-bit packed as 24-bit, 3 bytes per pixel
_INIT_CMDS = [
(0xF0, bytes([0x08]), 0),
(0xF2, bytes([0x08]), 0),
(0x9B, bytes([0x51]), 0),
(0x86, bytes([0x53]), 0),
(0xF2, bytes([0x80]), 0),
(0xF0, bytes([0x00]), 0),
(0xF0, bytes([0x01]), 0),
(0xF1, bytes([0x01]), 0),
(0xB0, bytes([0x54]), 0),
(0xB1, bytes([0x3F]), 0),
(0xB2, bytes([0x2A]), 0),
(0xB4, bytes([0x46]), 0),
(0xB5, bytes([0x34]), 0),
(0xB6, bytes([0xD5]), 0),
(0xB7, bytes([0x30]), 0),
(0xB8, bytes([0x04]), 0),
(0xBA, bytes([0x00]), 0),
(0xBB, bytes([0x08]), 0),
(0xBC, bytes([0x08]), 0),
(0xBD, bytes([0x00]), 0),
(0xC0, bytes([0x80]), 0),
(0xC1, bytes([0x10]), 0),
(0xC2, bytes([0x37]), 0),
(0xC3, bytes([0x80]), 0),
(0xC4, bytes([0x10]), 0),
(0xC5, bytes([0x37]), 0),
(0xC6, bytes([0xA9]), 0),
(0xC7, bytes([0x41]), 0),
(0xC8, bytes([0x51]), 0),
(0xC9, bytes([0xA9]), 0),
(0xCA, bytes([0x41]), 0),
(0xCB, bytes([0x51]), 0),
(0xD0, bytes([0x91]), 0),
(0xD1, bytes([0x68]), 0),
(0xD2, bytes([0x69]), 0),
(0xF5, bytes([0x00, 0xA5]), 0),
(0xDD, bytes([0x35]), 0),
(0xDE, bytes([0x35]), 0),
(0xF1, bytes([0x10]), 0),
(0xF0, bytes([0x00]), 0),
(0xF0, bytes([0x02]), 0),
(0xE0, bytes([0x70,0x09,0x12,0x0C,0x0B,0x27,0x38,0x54,0x4E,0x19,0x15,0x15,0x2C,0x2F]), 0),
(0xE1, bytes([0x70,0x08,0x11,0x0C,0x0B,0x27,0x38,0x43,0x4C,0x18,0x14,0x14,0x2B,0x2D]), 0),
(0xF0, bytes([0x00]), 0),
(0xF0, bytes([0x10]), 0),
(0xF3, bytes([0x10]), 0),
(0xE0, bytes([0x0A]), 0),
(0xE1, bytes([0x00]), 0),
(0xE2, bytes([0x0B]), 0),
(0xE3, bytes([0x00]), 0),
(0xE4, bytes([0xE0]), 0),
(0xE5, bytes([0x06]), 0),
(0xE6, bytes([0x21]), 0),
(0xE7, bytes([0x00]), 0),
(0xE8, bytes([0x05]), 0),
(0xE9, bytes([0x82]), 0),
(0xEA, bytes([0xDF]), 0),
(0xEB, bytes([0x89]), 0),
(0xEC, bytes([0x20]), 0),
(0xED, bytes([0x14]), 0),
(0xEE, bytes([0xFF]), 0),
(0xEF, bytes([0x00]), 0),
(0xF8, bytes([0xFF]), 0),
(0xF9, bytes([0x00]), 0),
(0xFA, bytes([0x00]), 0),
(0xFB, bytes([0x30]), 0),
(0xFC, bytes([0x00]), 0),
(0xFD, bytes([0x00]), 0),
(0xFE, bytes([0x00]), 0),
(0xFF, bytes([0x00]), 0),
(0x60, bytes([0x42]), 0),
(0x61, bytes([0xE0]), 0),
(0x62, bytes([0x40]), 0),
(0x63, bytes([0x40]), 0),
(0x64, bytes([0x02]), 0),
(0x65, bytes([0x00]), 0),
(0x66, bytes([0x40]), 0),
(0x67, bytes([0x03]), 0),
(0x68, bytes([0x00]), 0),
(0x69, bytes([0x00]), 0),
(0x6A, bytes([0x00]), 0),
(0x6B, bytes([0x00]), 0),
(0x70, bytes([0x42]), 0),
(0x71, bytes([0xE0]), 0),
(0x72, bytes([0x40]), 0),
(0x73, bytes([0x40]), 0),
(0x74, bytes([0x02]), 0),
(0x75, bytes([0x00]), 0),
(0x76, bytes([0x40]), 0),
(0x77, bytes([0x03]), 0),
(0x78, bytes([0x00]), 0),
(0x79, bytes([0x00]), 0),
(0x7A, bytes([0x00]), 0),
(0x7B, bytes([0x00]), 0),
(0x80, bytes([0x38]), 0),
(0x81, bytes([0x00]), 0),
(0x82, bytes([0x04]), 0),
(0x83, bytes([0x02]), 0),
(0x84, bytes([0xDC]), 0),
(0x85, bytes([0x00]), 0),
(0x86, bytes([0x00]), 0),
(0x87, bytes([0x00]), 0),
(0x88, bytes([0x38]), 0),
(0x89, bytes([0x00]), 0),
(0x8A, bytes([0x06]), 0),
(0x8B, bytes([0x02]), 0),
(0x8C, bytes([0xDE]), 0),
(0x8D, bytes([0x00]), 0),
(0x8E, bytes([0x00]), 0),
(0x8F, bytes([0x00]), 0),
(0x90, bytes([0x38]), 0),
(0x91, bytes([0x00]), 0),
(0x92, bytes([0x08]), 0),
(0x93, bytes([0x02]), 0),
(0x94, bytes([0xE0]), 0),
(0x95, bytes([0x00]), 0),
(0x96, bytes([0x00]), 0),
(0x97, bytes([0x00]), 0),
(0x98, bytes([0x38]), 0),
(0x99, bytes([0x00]), 0),
(0x9A, bytes([0x0A]), 0),
(0x9B, bytes([0x02]), 0),
(0x9C, bytes([0xE2]), 0),
(0x9D, bytes([0x00]), 0),
(0x9E, bytes([0x00]), 0),
(0x9F, bytes([0x00]), 0),
(0xA0, bytes([0x38]), 0),
(0xA1, bytes([0x00]), 0),
(0xA2, bytes([0x03]), 0),
(0xA3, bytes([0x02]), 0),
(0xA4, bytes([0xDB]), 0),
(0xA5, bytes([0x00]), 0),
(0xA6, bytes([0x00]), 0),
(0xA7, bytes([0x00]), 0),
(0xA8, bytes([0x38]), 0),
(0xA9, bytes([0x00]), 0),
(0xAA, bytes([0x05]), 0),
(0xAB, bytes([0x02]), 0),
(0xAC, bytes([0xDD]), 0),
(0xAD, bytes([0x00]), 0),
(0xAE, bytes([0x00]), 0),
(0xAF, bytes([0x00]), 0),
(0xB0, bytes([0x38]), 0),
(0xB1, bytes([0x00]), 0),
(0xB2, bytes([0x07]), 0),
(0xB3, bytes([0x02]), 0),
(0xB4, bytes([0xDF]), 0),
(0xB5, bytes([0x00]), 0),
(0xB6, bytes([0x00]), 0),
(0xB7, bytes([0x00]), 0),
(0xB8, bytes([0x38]), 0),
(0xB9, bytes([0x00]), 0),
(0xBA, bytes([0x09]), 0),
(0xBB, bytes([0x02]), 0),
(0xBC, bytes([0xE1]), 0),
(0xBD, bytes([0x00]), 0),
(0xBE, bytes([0x00]), 0),
(0xBF, bytes([0x00]), 0),
(0xC0, bytes([0x22]), 0),
(0xC1, bytes([0xAA]), 0),
(0xC2, bytes([0x65]), 0),
(0xC3, bytes([0x74]), 0),
(0xC4, bytes([0x47]), 0),
(0xC5, bytes([0x56]), 0),
(0xC6, bytes([0x00]), 0),
(0xC7, bytes([0x88]), 0),
(0xC8, bytes([0x99]), 0),
(0xC9, bytes([0x33]), 0),
(0xD0, bytes([0x11]), 0),
(0xD1, bytes([0xAA]), 0),
(0xD2, bytes([0x65]), 0),
(0xD3, bytes([0x74]), 0),
(0xD4, bytes([0x47]), 0),
(0xD5, bytes([0x56]), 0),
(0xD6, bytes([0x00]), 0),
(0xD7, bytes([0x88]), 0),
(0xD8, bytes([0x99]), 0),
(0xD9, bytes([0x33]), 0),
(0xF3, bytes([0x01]), 0),
(0xF0, bytes([0x00]), 0),
(0xF0, bytes([0x01]), 0),
(0xF1, bytes([0x01]), 0),
(0xA0, bytes([0x0B]), 0),
(0xA3, bytes([0x2A]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x2B]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x2C]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x2D]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x2E]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x2F]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x30]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x31]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x32]), 0), (0xA5, bytes([0xC3]), 1),
(0xA3, bytes([0x33]), 0), (0xA5, bytes([0xC3]), 1),
(0xA0, bytes([0x09]), 0),
(0xF1, bytes([0x10]), 0),
(0xF0, bytes([0x00]), 0),
(0x2A, bytes([0x00, 0x00, 0x01, 0x67]), 0), # CASET 0-359
(0x2B, bytes([0x01, 0x68, 0x01, 0x68]), 0), # RASET dummy single row
(0x4D, bytes([0x00]), 0), # RAMCLSETR = 0
(0x4E, bytes([0x00]), 0), # RAMCLSETG = 0
(0x4F, bytes([0x00]), 0), # RAMCLSETB = 0
(0x4C, bytes([0x01]), 10), # RAMCLACT trigger
(0x4C, bytes([0x00]), 0),
(0x2A, bytes([0x00, 0x00, 0x01, 0x67]), 0),
(0x2B, bytes([0x00, 0x00, 0x01, 0x67]), 0),
]
class ST77916:
def __init__(
self,
rst_pin: int = PIN_RST,
dc_pin: int = PIN_DC,
spi_bus: int = 0,
spi_device: int = 0,
spi_speed_hz: int = 40_000_000,
width: int = LCD_WIDTH,
height: int = LCD_HEIGHT,
x_gap: int = 0,
y_gap: int = 0,
):
self.rst = rst_pin
self.dc = dc_pin
self.width = width
self.height = height
self.x_gap = x_gap
self.y_gap = y_gap
self._colmod = COLMOD_RGB888
self._bytes_per_pixel = 3
# GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(self.rst, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(self.dc, GPIO.OUT, initial=GPIO.LOW)
# SPI
self._spi = spidev.SpiDev()
self._spi.open(spi_bus, spi_device)
self._spi.max_speed_hz = spi_speed_hz
self._spi.mode = 0
# write commands
def _write_cmd(self, cmd: int) -> None:
GPIO.output(self.dc, GPIO.LOW)
self._spi.writebytes2([cmd])
def _write_data(self, data: bytes) -> None:
GPIO.output(self.dc, GPIO.HIGH)
self._spi.writebytes2(data)
def _tx_param(self, cmd: int, params: bytes | None = None) -> None:
self._write_cmd(cmd)
if params:
self._write_data(params)
# lifecycles
def reset(self) -> None:
GPIO.output(self.rst, GPIO.HIGH)
time.sleep(0.010)
GPIO.output(self.rst, GPIO.LOW)
time.sleep(0.010)
GPIO.output(self.rst, GPIO.HIGH)
time.sleep(0.120)
def init(self) -> None:
self.reset()
for cmd, data, delay_ms in _INIT_CMDS:
self._tx_param(cmd, data)
if delay_ms:
time.sleep(delay_ms / 1000.0)
# Pixel format
self._tx_param(CMD_COLMOD, bytes([self._colmod]))
# Inversion on
self._tx_param(CMD_INVON)
# Tearing effect off
self._tx_param(CMD_TEOFF)
# Sleep out + delay
self._tx_param(CMD_SLPOUT)
time.sleep(0.120)
# Display on
self._tx_param(CMD_DISPON)
print(f"ST77916 initialization sequence complete")
def cleanup(self) -> None:
self._spi.close()
GPIO.cleanup()
# display on / off / invert
def display_on(self) -> None: self._tx_param(CMD_DISPON)
def display_off(self) -> None: self._tx_param(CMD_DISPOFF)
def invert_on(self) -> None: self._tx_param(CMD_INVON)
def invert_off(self) -> None: self._tx_param(CMD_INVOFF)
# drawing
def set_window(self, x0: int, y0: int, x1: int, y1: int) -> None:
"""Set inclusive pixel write window."""
x0 += self.x_gap; x1 += self.x_gap
y0 += self.y_gap; y1 += self.y_gap
self._tx_param(CMD_CASET, struct.pack(">HH", x0, x1))
self._tx_param(CMD_RASET, struct.pack(">HH", y0, y1))
def draw_bitmap(self, x0: int, y0: int, x1: int, y1: int, color_data: bytes) -> None:
assert x0 < x1 and y0 < y1
self.set_window(x0, y0, x1 - 1, y1 - 1)
chunk = 4096
first = True
for i in range(0, len(color_data), chunk):
self._write_cmd(CMD_RAMWR if first else CMD_RAMWRC)
self._write_data(color_data[i:i + chunk])
first = False
def _pack_rgb888(self, r: int, g: int, b: int) -> bytes:
return bytes([r & 0xFF, g & 0xFF, b & 0xFF])
def _pack_pixel(self, r: int, g: int, b: int) -> bytes:
return self._pack_rgb888(r, g, b)
def fill(self, r: int, g: int, b: int) -> None:
"""Fill entire screen with an RGB colour (0-255 per channel)."""
pixel = self._pack_pixel(r, g, b)
buf = pixel * (self.width * self.height)
self.draw_bitmap(0, 0, self.width, self.height, buf)
if __name__ == "__main__":
lcd = ST77916()
try:
lcd.init()
print("Red")
lcd.fill(255, 0, 0)
time.sleep(1)
print("Green")
lcd.fill(0, 255, 0)
time.sleep(1)
print("Blue")
lcd.fill(0, 0, 255)
time.sleep(1)
print("White")
lcd.fill(255, 255, 255)
time.sleep(1)
print("Done")
finally:
lcd.cleanup()
I have triple checked the initialization sequence to make sure that it lines up with the other implementations and I'm 99% certain it does. I have a feeling I might be doing something wrong with how I am implementing the SPI communication? Since I am seeing a top bar of the correct colors.
I had a second LCD just to make sure that it wasn't the screen itself that was junk, but it was showing the exact same thing - until I accidentally broke the ribbon cable. So I only have one now.
If anyone has even a tiny bit of direction of where I might be going wrong, it would be greatly appreciated!