diff --git a/drivers/ili94xx/ili9488.py b/drivers/ili94xx/ili9488.py new file mode 100644 index 0000000..0b3dc3d --- /dev/null +++ b/drivers/ili94xx/ili9488.py @@ -0,0 +1,323 @@ +# ILI9488 nano-gui driver for ili9488 displays + +### Based on ili9486.py by Peter Hinch. +### Retaining his copyright +# +# Copyright (c) Peter Hinch 2022-2025 +# Released under the MIT license see LICENSE + +# +# Note: If your hardware uses the ILI9488 parallel interface +# you will likely be better off using the ili9486 driver. +# It will send 2 bytes per pixel which will run faster. +# +# You must use this driver only when using the ILI9488 SPI +# interface. It will send 3 bytes per pixel. +# + +from time import sleep_ms +import gc +import framebuf +import asyncio +from drivers.boolpalette import BoolPalette + + +# Portrait mode greyscale +@micropython.viper +def _lcopy_gs(dest: ptr8, source: ptr8, length: int) : + # rgb666 - 18bit/pixel + n: int = 0 + x: int = 0 + while x < length: + c : uint = source[x] + # Store the index in the 4 high order bits + p : uint = c & 0xF0 # current pixel + q : uint = c << 4 # next pixel + + dest[n] = p + n += 1 + dest[n] = p + n += 1 + dest[n] = p + n += 1 + + dest[n] = q + n += 1 + dest[n] = q + n += 1 + dest[n] = q + n += 1 + + x += 1 + + +# Portrait mode color +@micropython.viper +def _lcopy(dest: ptr8, source: ptr8, lut: ptr8, length: int) : + # rgb666 - 18bit/pixel + n: int = 0 + x: int = 0 + while x < length: + c : uint = source[x] + p : uint = 3 * (c >> 4) # current pixel + q : uint = 3 * (c & 0x0F) # next pixel + + dest[n] = lut[p] + n += 1 + dest[n] = lut[p+1] + n += 1 + dest[n] = lut[p+2] + n += 1 + + dest[n] = lut[q] + n += 1 + dest[n] = lut[q+1] + n += 1 + dest[n] = lut[q+2] + n += 1 + + x += 1 + +# FB is in landscape mode greyscale +@micropython.viper +def _lscopy_gs(dest: ptr8, source: ptr8, ch: int) : + col = ch & 0x1FF # Unpack (viper old 4 parameter limit) + height = (ch >> 9) & 0x1FF + wbytes = ch >> 19 # Width in bytes is width // 2 + # rgb666 - 18bit/pixel + n = 0 + clsb = col & 1 + idx = col >> 1 # 2 pixels per byte + while height: + if clsb : + c = source[idx] << 4 + else : + c = source[idx] & 0xf0 + dest[n] = c + n += 1 + dest[n] = c + n += 1 + dest[n] = c + n += 1 + idx += wbytes + height -= 1 + +# FB is in landscape mode color, hence issue a column at a time to portrait mode hardware. +@micropython.viper +def _lscopy(dest: ptr8, source: ptr8, lut: ptr8, ch: int) : + col = ch & 0x1FF # Unpack (viper old 4 parameter limit) + height = (ch >> 9) & 0x1FF + wbytes = ch >> 19 # Width in bytes is width // 2 + # rgb666 - 18bit/pixel + n = 0 + clsb = col & 1 + idx = col >> 1 # 2 pixels per byte + while height: + if clsb: + c = 3 * (source[idx] & 0x0F) + else: + c = 3 * (source[idx] >> 4) + dest[n] = lut[c] + n += 1 + dest[n] = lut[c+1] + n += 1 + dest[n] = lut[c+2] + n += 1 + idx += wbytes + height -= 1 + + +class ILI9488(framebuf.FrameBuffer): + + # A lookup table with 3 bytes per entry formatted for direct send to ILI9488 + lut = bytearray(48) + + COLOR_INVERT = 0 + + # Convert r, g, b in range 0-255 to a 16 bit colour value + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + # ILI9488 expects RGB order. 8 bit register writes require padding + @classmethod + def rgb(cls, r, g, b): + r_device = cls.COLOR_INVERT ^ (r & 0xfc) + g_device = cls.COLOR_INVERT ^ (g & 0xfc) + b_device = cls.COLOR_INVERT ^ (b & 0xfc) + return (r_device << 16) | (g_device << 8) | b_device + + # Transpose width & height for landscape mode + def __init__( + self, spi, cs, dc, rst, height=320, width=480, usd=False, mirror=False, init_spi=False + ): + self._spi = spi + self._cs = cs + self._dc = dc + self._rst = rst + self.lock_mode = False # If set, user lock is passed to .do_refresh + self.height = height # Logical dimensions for GUIs + self.width = width + self._long = max(height, width) # Physical dimensions of screen and aspect ratio + self._short = min(height, width) + self._spi_init = init_spi + self._gscale = False # Interpret buffer as index into color LUT + self.mode = framebuf.GS4_HMSB + self.palette = BoolPalette(self.mode) + gc.collect() + buf = bytearray(height * width // 2) + self.mvb = memoryview(buf) + super().__init__(buf, width, height, self.mode) # Logical aspect ratio + self._linebuf = bytearray(self._short * 3) + + # 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() + # Send initialization commands + + self._wcmd(b"\x01") # SWRESET Software reset + sleep_ms(100) + self._wcmd(b"\x11") # sleep out + sleep_ms(20) + self._wcd(b"\x3a", b"\x66") # interface pixel format 18 bits per pixel + # Normally use defaults. This allows it to work on the Waveshare board with a + # shift register. If size is not 320x480 assume no shift register. + # Default column address start == 0, end == 0x13F (319) + if self._short != 320: # Not the Waveshare board: no shift register + self._wcd(b"\x2a", int.to_bytes(self._short - 1, 4, "big")) + # Default page address start == 0 end == 0x1DF (479) + if self._long != 480: + self._wcd(b"\x2b", int.to_bytes(self._long - 1, 4, "big")) # SET_PAGE ht + # self._wcd(b"\x36", b"\x48" if usd else b"\x88") # MADCTL: RGB portrait mode + madctl = 0x48 if usd else 0x88 + if mirror: + madctl ^= 0x80 + self._wcd(b"\x36", madctl.to_bytes(1, "big")) # MADCTL: RGB portrait mode + self._wcmd(b"\x11") # sleep out + 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 + + # @micropython.native # Made almost no difference to timing + def show(self): # Physical display is in portrait mode + clut = ILI9488.lut + lb = self._linebuf + buf = self.mvb + cm = self._gscale # color False, greyscale True + 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) + if self.width < self.height: # Portrait 300 ms on ESP32 240MHz, 30MHz SPI clock + wd = self.width // 2 + ht = self.height + if cm : + for start in range(0, wd * ht, wd): # For each line + _lcopy_gs(lb, buf[start:], wd) # Copy greyscale + self._spi.write(lb) + else : + for start in range(0, wd * ht, wd): # For each line + _lcopy(lb, buf[start:], clut, wd) # Copy and map colors + self._spi.write(lb) + else: # Landscape 330 ms on ESP32 240MHz, 30MHz SPI clock + width = self.width + wd = width - 1 + cargs = (self.height << 9) + (width << 18) # Viper 4-arg limit + if cm : + for col in range(width): # For each column of landscape display + _lscopy_gs(lb, buf, wd - col + cargs) # Copy greyscale + self._spi.write(lb) + else : + for col in range(width): # For each column of landscape display + _lscopy(lb, buf, clut, wd - col + cargs) # Copy and map colors + self._spi.write(lb) + + self._cs(1) + + def short_lock(self, v=None): + if v is not None: + self.lock_mode = v # If set, user lock is passed to .do_refresh + return self.lock_mode + + # nanogui apps typically call with no args. ugui and tgui pass split and + # may pass a Lock depending on lock_mode + async def do_refresh(self, split=4, elock=None): + if elock is None: + elock = asyncio.Lock() + async with self._lock: + lines, mod = divmod(self._long, split) # Lines per segment + if mod: + raise ValueError("Invalid do_refresh arg.") + clut = ILI9488.lut + lb = self._linebuf + buf = self.mvb + cm = self._gscale # color False, greyscale True + self._wcmd(b"\x2c") # WRITE_RAM + self._dc(1) + if self.width < self.height: # Portrait: write sets of rows + wd = self.width // 2 + line = 0 + for _ in range(split): # For each segment + async with elock: + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._cs(0) + if cm: + for start in range(wd * line, wd * (line + lines), wd): # For each line + _lcopy_gs(lb, buf[start:], wd) # Copy and greyscale + self._spi.write(lb) + else : + for start in range(wd * line, wd * (line + lines), wd): # For each line + _lcopy(lb, buf[start:], clut, wd) # Copy and map colors + self._spi.write(lb) + + line += lines + self._cs(1) # Allow other tasks to use bus + await asyncio.sleep_ms(0) + else: # Landscape: write sets of cols. lines is no. of cols per segment. + cargs = (self.height << 9) + (self.width << 18) # Viper 4-arg limit + sc = self.width - 1 # Start and end columns + ec = sc - lines # End column + for _ in range(split): # For each segment + async with elock: + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._cs(0) + if cm : + for col in range(sc, ec, -1): # For each column of landscape display + _lscopy_gs(lb, buf, col + cargs) # Copy and map colors + self._spi.write(lb) + else : + for col in range(sc, ec, -1): # For each column of landscape display + _lscopy(lb, buf, clut, col + cargs) # Copy and map colors + self._spi.write(lb) + + sc -= lines + ec -= lines + self._cs(1) # Allow other tasks to use bus + await asyncio.sleep_ms(0) diff --git a/gui/core/writer.py b/gui/core/writer.py index 48f5e14..90096f0 100644 --- a/gui/core/writer.py +++ b/gui/core/writer.py @@ -263,10 +263,23 @@ class CWriter(Writer): return c if not 0 <= idx <= 15: raise ValueError('Color nos must be 0..15') - x = idx << 1 - ssd.lut[x] = c & 0xff - ssd.lut[x + 1] = c >> 8 - return idx + + if len(ssd.lut) == 32 : + # ssd supports 2 byte color + x = idx << 1 + ssd.lut[x] = c & 0xff + ssd.lut[x + 1] = c >> 8 + return idx + elif len(ssd.lut) == 48 : + # ssd supports 3 byte color + x = idx * 3 + ssd.lut[x] = c >> 16 + ssd.lut[x + 1] = (c >> 8) & 0xff + ssd.lut[x + 2] = c & 0xff + return idx + else : + raise TypeError("lut of %d bytes not supported" % (len(ssd.lut))) + def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True): if not hasattr(device, 'palette'): diff --git a/setup_examples/ili9488_esp32.py b/setup_examples/ili9488_esp32.py new file mode 100644 index 0000000..91a44f6 --- /dev/null +++ b/setup_examples/ili9488_esp32.py @@ -0,0 +1,40 @@ +# +# Setup for ThingPulse Grande Kit +# +# Has ILI9488 display running on an ESP32-WROVER-E +# +# Released under the MIT License (MIT). See LICENSE. + +from micropython import const + +# +# Pin assignments here are for the Grande Kit values from schematic: +# https://thingpulse.com/wp-content/uploads/2022/10/Schematic_Color-Kit-Grande_2023-01-14-pdf.jpg +# You will need to customize for your board. + +LCD_DC = const(2) +LCD_CS = const(15) +LCD_CLK = const(5) +LCD_MOSI = const(18) +LCD_MISO = const(19) +LCD_BackLight = const(32) +LCD_RST = const(4) + +from machine import Pin, SPI, freq +from drivers.ili94xx.ili9488 import ILI9488 as SSD + +# Screen configuration +# (Create and export an SSD instance) + +prst = Pin(LCD_RST, Pin.OUT, value=1) +pdc = Pin(LCD_DC, Pin.OUT, value=1) +pcs = Pin(LCD_CS, Pin.OUT, value=1) + +# turn on back light +backlight=Pin(LCD_BackLight, Pin.OUT, value=1) + +# Use SPI bus 1, 30 Mhz is maximum speed. +spi = SPI(1, 30_000_000, sck=Pin(LCD_CLK), mosi=Pin(LCD_MOSI, Pin.OUT), miso=Pin(LCD_MISO, Pin.OUT)) + +# Precaution before instantiating framebuf +ssd = SSD(spi, height=480, width=320, dc=pdc, cs=pcs, rst=prst, usd=False) diff --git a/setup_examples/ili9488_pico.py b/setup_examples/ili9488_pico.py new file mode 100644 index 0000000..8121585 --- /dev/null +++ b/setup_examples/ili9488_pico.py @@ -0,0 +1,39 @@ +# +# Setup for ILI9488 interfaced to Raspberry PI Pico 2 and Pico +# +# Released under the MIT License (MIT). See LICENSE. + +from micropython import const + +# Modify these Pin assignments to match your hardware. + +# Simple GPIO's +LCD_DC = 'GPIO21' # Pin 27 +LCD_RST = 'GPIO22' # Pin 29 +LCD_CS = 'GPIO27' # Pin 32 +LCD_BackLight = 'GPIO28' # Pin 34 + +# SPI pins + +LCD_CLK = 'GPIO18' # Pin 24 +LCD_MOSI = 'GPIO19' # Pin 25 +LCD_MISO = 'GPIO16' # Pin 21 + +from machine import Pin, SPI +from drivers.ili94xx.ili9488 import ILI9488 as SSD + +# Screen configuration +# (Create and export an SSD instance) + +prst = Pin(LCD_RST, Pin.OUT, value=1) +pdc = Pin(LCD_DC, Pin.OUT, value=1) +pcs = Pin(LCD_CS, Pin.OUT, value=1) + +# turn on back light +backlight=Pin(LCD_BackLight, Pin.OUT, value=1) + +# Use SPI bus 0, 24 Mhz is maximum speed on PICO +spi = SPI(0, 24_000_000, sck=Pin(LCD_CLK), mosi=Pin(LCD_MOSI, Pin.OUT), miso=Pin(LCD_MISO, Pin.OUT)) + +# Precaution before instantiating framebuf +ssd = SSD(spi, height=480, width=320, dc=pdc, cs=pcs, rst=prst, usd=False)