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!