From 60994e1f60819f5c24af378e0a366f0e89f8808d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 23 May 2024 18:51:27 +0100 Subject: [PATCH] Add 8-bit gc9a01 driver. --- drivers/gc9a01/gc9a01.py | 1 - drivers/gc9a01/gc9a01_8_bit.py | 216 +++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 drivers/gc9a01/gc9a01_8_bit.py diff --git a/drivers/gc9a01/gc9a01.py b/drivers/gc9a01/gc9a01.py index 95fc7e8..dbb4b09 100644 --- a/drivers/gc9a01/gc9a01.py +++ b/drivers/gc9a01/gc9a01.py @@ -172,7 +172,6 @@ class GC9A01(framebuf.FrameBuffer): self._spi.write(data) self._cs(1) - # @micropython.native # Made almost no difference to timing def show(self): # Physical display is in portrait mode clut = GC9A01.lut lb = self._linebuf diff --git a/drivers/gc9a01/gc9a01_8_bit.py b/drivers/gc9a01/gc9a01_8_bit.py new file mode 100644 index 0000000..6539f8b --- /dev/null +++ b/drivers/gc9a01/gc9a01_8_bit.py @@ -0,0 +1,216 @@ +# gc9a01_8_bit.py nano-gui driver for gc9a01 displays using 8 bit pixels +# Default args are for a 240*240 (typically circular) display. This will result +# in a 57,600 byte frame buffer. + +# Copyright (c) Peter Hinch 2024 +# Released under the MIT license see LICENSE + +from time import sleep_ms +import gc +import framebuf +import asyncio +from drivers.boolpalette import BoolPalette + +# Initialisation ported from Russ Hughes' C driver +# https://github.com/russhughes/gc9a01_mpy/ +# Based on a ST7789 C driver: https://github.com/devbis/st7789_mpy +# Many registers are undocumented. Lines initialising them are commented "?" +# in cases where initialising them seems to have no effect. + +# Datasheet 7.3.4 allows scl <= 100MHz +# Waveshare touch board https://www.waveshare.com/wiki/1.28inch_Touch_LCD has CST816S touch controller +# Touch controller uses I2C + +# g4 g3 g2 b7 b6 b5 b4 b3 r7 r6 r5 r4 r3 g7 g6 g5 +@micropython.viper +def _lcopy(dest: ptr16, source: ptr8, length: int, gscale: bool): + # rgb565 - 16bit/pixel + n: int = 0 + while length: + c = source[n] + if gscale: # Source byte holds 8-bit greyscale + # dest rrrr rggg gggb bbbb + dest[n] = (c & 0xF1) | (c >> 5) | ((c & 0x1C) << 11) | ((c & 0xF1) << 5) + else: # Source byte holds 8-bit rrrgggbb + # dest 000b b000 rrr0 0ggg + dest[n] = (c & 0xE0) | ((c & 0x1C) >> 2) | ((c & 0x03) << 11) + n += 1 + length -= 1 + + +class GC9A01(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to an 8 bit colour value + # rrrgggbb. Converted to 16 bit on the fly. + # GC9A01 expects RGB order. + @staticmethod + def rgb(r, g, b): + return (r & 0xE0) | ((g >> 3) & 0x1C) | (b >> 6) + + def __init__( + self, + spi, + cs, + dc, + rst, + height=240, + width=240, + lscape=False, + usd=False, + mirror=False, + init_spi=False, + ): + self._spi = spi + self._cs = cs + self._dc = dc + self._rst = rst + self.height = height # Logical dimensions for GUIs + self.width = width + self._spi_init = init_spi + self._gscale = False # Interpret buffer as rrrgggbb color + mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. + self.palette = BoolPalette(mode) + gc.collect() + buf = bytearray(height * width) # Frame buffer + self._mvb = memoryview(buf) + super().__init__(buf, width, height, mode) + self._linebuf = bytearray(width * 2) # Line buffer (16-bit colors) + + # Hardware reset + self._rst(0) + sleep_ms(50) + self._rst(1) + sleep_ms(50) + if self._spi_init: # A callback was passed + self._spi_init(spi) # Bus may be shared + self._lock = asyncio.Lock() # Prevent concurrent refreshes. + sleep_ms(100) + self._wcd(b"\x2a", int.to_bytes(width - 1, 4, "big")) + # Default page address start == 0 end == 0xEF (239) + self._wcd(b"\x2b", int.to_bytes(height - 1, 4, "big")) # SET_PAGE ht + # **** Start of opaque chip setup **** + self._wcmd(b"\xEF") # Inter register enable 2 + self._wcd(b"\xEB", b"\x14") # ? + self._wcmd(b"\xFE") # Inter register enable 1 + self._wcmd(b"\xEF") # Inter register enable 2 + self._wcd(b"\xEB", b"\x14") # ? + self._wcd(b"\x84", b"\x40") # ? + self._wcd(b"\x85", b"\xFF") # ? + self._wcd(b"\x87", b"\xFF") # ? + self._wcd(b"\x86", b"\xFF") # ? + self._wcd(b"\x88", b"\x0A") # ? + self._wcd(b"\x89", b"\x21") # ? + self._wcd(b"\x8A", b"\x00") # ? + self._wcd(b"\x8B", b"\x80") # ? + self._wcd(b"\x8C", b"\x01") # ? + self._wcd(b"\x8D", b"\x01") # ? + self._wcd(b"\x8E", b"\xFF") # ? + self._wcd(b"\x8F", b"\xFF") # ? + self._wcd(b"\xB6", b"\x00\x00") # Display function control + self._wcd(b"\x3A", b"\x55") # COLMOD + self._wcd(b"\x90", b"\x08\x08\x08\x08") # ? + self._wcd(b"\xBD", b"\x06") # ? + self._wcd(b"\xBC", b"\x00") # ? + self._wcd(b"\xFF", b"\x60\x01\x04") # ? + self._wcd(b"\xC3", b"\x13") # Vreg1a voltage Control + self._wcd(b"\xC4", b"\x13") # Vreg1b voltage Control + self._wcd(b"\xC9", b"\x22") # Vreg2a voltage Control + self._wcd(b"\xBE", b"\x11") # ? + self._wcd(b"\xE1", b"\x10\x0E") # ? + self._wcd(b"\xDF", b"\x21\x0c\x02") # ? + self._wcd(b"\xF0", b"\x45\x09\x08\x08\x26\x2A") # Gamma + self._wcd(b"\xF1", b"\x43\x70\x72\x36\x37\x6F") # Gamma + self._wcd(b"\xF2", b"\x45\x09\x08\x08\x26\x2A") # Gamma + self._wcd(b"\xF3", b"\x43\x70\x72\x36\x37\x6F") # Gamma + self._wcd(b"\xED", b"\x1B\x0B") # ? + self._wcd(b"\xAE", b"\x77") # ? + self._wcd(b"\xCD", b"\x63") # ? + self._wcd(b"\x70", b"\x07\x07\x04\x0E\x0F\x09\x07\x08\x03") # ? + self._wcd(b"\xE8", b"\x34") # Frame rate / dot inversion + self._wcd(b"\x62", b"\x18\x0D\x71\xED\x70\x70\x18\x0F\x71\xEF\x70\x70") # ? + self._wcd(b"\x63", b"\x18\x11\x71\xF1\x70\x70\x18\x13\x71\xF3\x70\x70") # ? + self._wcd(b"\x64", b"\x28\x29\xF1\x01\xF1\x00\x07") # ? + self._wcd(b"\x66", b"\x3C\x00\xCD\x67\x45\x45\x10\x00\x00\x00") # Undoc but needed + self._wcd(b"\x67", b"\x00\x3C\x00\x00\x00\x01\x54\x10\x32\x98") # Undoc but needed + self._wcd(b"\x74", b"\x10\x85\x80\x00\x00\x4E\x00") # ? + self._wcd(b"\x98", b"\x3e\x07") # ? + self._wcmd(b"\x35") # Tearing effect line on + self._wcmd(b"\x21") # Display inversion on ??? + self._wcmd(b"\x11") + sleep_ms(120) + # ************************* + + # madctl reg 0x36 p127 6.2.18. b0-2 == 0. b3: color output BGR RGB/ + # b4 == 0 + # d5 row/col exchange + # d6 col address order + # d7 row address order + if lscape: + madctl = 0x28 if usd else 0xE8 # RGB landscape mode + else: + madctl = 0x48 if usd else 0x88 # RGB portrait mode + if mirror: + madctl ^= 0x80 + self._wcd(b"\x36", madctl.to_bytes(1, "big")) # MADCTL: RGB portrait mode + self._wcmd(b"\x29") # display on + + # Write a command. + def _wcmd(self, command): + self._dc(0) + self._cs(0) + self._spi.write(command) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, command, data): + self._dc(0) + self._cs(0) + self._spi.write(command) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(data) + self._cs(1) + + def greyscale(self, gs=None): + if gs is not None: + self._gscale = gs + return self._gscale + + def show(self): # Physical display is in portrait mode + lb = self._linebuf + buf = self._mvb + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._wcmd(b"\x2c") # WRITE_RAM + self._dc(1) + self._cs(0) + wd = self.width + ht = self.height + cm = self._gscale # color False, greyscale True + for start in range(0, wd * ht, wd): # For each line + _lcopy(lb, buf[start:], wd, cm) # Copy and map colors + self._spi.write(lb) + self._cs(1) + + async def do_refresh(self, split=4): + async with self._lock: + lines, mod = divmod(self.height, split) # Lines per segment + if mod: + raise ValueError("Invalid do_refresh arg.") + lb = self._linebuf + buf = self._mvb + self._wcmd(b"\x2c") # WRITE_RAM + self._dc(1) + wd = self.width + cm = self._gscale # color False, greyscale True + line = 0 + for _ in range(split): # For each segment + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._cs(0) + for start in range(wd * line, wd * (line + lines), wd): # For each line + _lcopy(lb, buf[start:], wd, cm) # Copy and map colors + self._spi.write(lb) + line += lines + self._cs(1) # Allow other tasks to use bus + await asyncio.sleep_ms(0)