From 7ed8f7d627ce92b721a1cf597180f3f2f751e3b1 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 13 Jan 2023 16:43:03 +0000 Subject: [PATCH] ILI9486 update. --- DRIVERS.md | 51 +++++++++++++----- drivers/ili94xx/ili9486.py | 97 +++++++++++++++++----------------- setup_examples/ili9486_pico.py | 4 +- 3 files changed, 87 insertions(+), 65 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index 2b55106..57063fc 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -638,18 +638,41 @@ If your display shows garbage, check the following (I have seen both): ## 3.4 Driver for ILI9486 -This was tested with +The ILI9486 supports displays of up to 480x320 pixels which is large by +microcontroller standards. Even with 4-bit color the frame buffer requires +76,800 bytes. On a Pico `nanogui` works fine, but `micro-gui` fails to +compile unless frozen bytecode is used, in which case it runs with about 75K of +free RAM. + +The driver aims to work with any ILI9486, however [this display](https://www.waveshare.com/product/3.5inch-RPi-LCD-A.htm), a -480x320 color LCD designed for the Raspberry Pi. Note that even with 4-bit -color the display buffer is 76,800 bytes. On a Pico `nanogui` works fine, but -`micro-gui` fails to compile unless frozen bytecode is used, in which case it -runs with about 75K free RAM. +480x320 color LCD designed for the Raspberry Pi, has special hardware. Rather +than driving the ILI9486 via SPI, it uses SPI to fill a shift register, copying +the data to the chip using a parallel interface. The driver is designed to work +with both types of hardware. -See [nanogui setup](https://github.com/peterhinch/micropython-nano-gui/blob/master/setup_examples/ili9486_pico.py) -and [microgui setup](https://github.com/peterhinch/micropython-micro-gui/blob/main/setup_examples/ili9486_pico.py) -for examples of initialisation files. +##### Generic display wiring -##### Wiring +Testing was done with a Pico, using the following setup files: +[nanogui setup](https://github.com/peterhinch/micropython-nano-gui/blob/master/setup_examples/ili9486_pico.py) +and [microgui setup](https://github.com/peterhinch/micropython-micro-gui/blob/main/setup_examples/ili9486_pico.py). +These use the following pinout: + +| Pico pin | GPIO | Display | Signal | +|:---------|:-----|:--------|:--------| +| 40 | n/a | | Vbus 5V | +| 36 | n/a | | 3.3V | +| 3,8,36.. | n/a | Gnd | Gnd | +| 9 | 6 | SCLK | | +| 10 | 7 | MOSI | | +| 11 | 8 | DC | | +| 12 | 9 | RST | | +| 14 | 10 | CS | | + +Please check the power requirements of the display board, which may require a +5V or a 3.3V supply. + +##### Waveshare PI HAT wiring This shows the Raspberry Pi connector looking at the underside of the board with the bulk of the board to the right. This was tested with a Pi Pico. @@ -697,11 +720,11 @@ def spi_init(spi): spi.init(baudrate=10_000_000) ``` -The ILI9486 class uses 4-bit color to conserve RAM. Even with this adaptation -the buffer size is 76.85KiB. See [Color handling](./DRIVERS.md#11-color-handling) -for details of the implications of 4-bit color. On the Pico with the display -driver loaded there was 85KiB free RAM running `nano-gui` but `micro-gui` ran -out of RAM.. +The ILI9486 class uses 4-bit color to conserve RAM. See +[Color handling](./DRIVERS.md#11-color-handling) for the implications of 4-bit +color. On the Pico with the display driver loaded there was 85KiB free RAM +running `nano-gui`. To run `micro-gui` it was necessary to run the GUI as +frozen bytecode, when it ran with 75K of free RAM. The driver uses the `micropython.viper` decorator. If your platform does not support this, the Viper code will need to be rewritten with a substantial hit diff --git a/drivers/ili94xx/ili9486.py b/drivers/ili94xx/ili9486.py index f615acc..20431d1 100644 --- a/drivers/ili94xx/ili9486.py +++ b/drivers/ili94xx/ili9486.py @@ -4,11 +4,14 @@ # Copyright (c) Peter Hinch 2022 # Released under the MIT license see LICENSE -# Inspired by @brave-ulysses https://github.com/micropython/micropython/discussions/10404 +# Much help provided by @brave-ulysses in this thread +# https://github.com/micropython/micropython/discussions/10404 for the special handling +# required by the Waveshare Pi HAT. -# Design note. I could not find a way to do landscape display at the chip level -# without a nasty hack. Consequently this driver uses portrait mode at chip level, -# with rotation performed in the driver. +# This driver configures the chip in portrait mode with rotation performed in the driver. +# This is done to enable default values to be used for the Column Address Set and Page +# Address Set registers. This avoids having to use commands with multi-byte data values, +# which would necessitate special code for the Waveshare Pi HAT (see DRIVERS.md). from time import sleep_ms import gc @@ -18,21 +21,22 @@ from drivers.boolpalette import BoolPalette # Portrait mode @micropython.viper -def _lcopy(dest:ptr16, source:ptr8, lut:ptr16, length:int): +def _lcopy(dest: ptr16, source: ptr8, lut: ptr16, length: int): # rgb565 - 16bit/pixel n = 0 for x in range(length): c = source[x] dest[n] = lut[c >> 4] # current pixel n += 1 - dest[n] = lut[c & 0x0f] # next pixel + dest[n] = lut[c & 0x0F] # next pixel n += 1 + # FB is in landscape mode, hence issue a column at a time to portrait mode hardware. @micropython.viper -def _lscopy(dest:ptr16, source:ptr8, lut:ptr16, ch:int): - col = ch & 0x1ff # Unpack (viper 4 parameter limit) - height = (ch >> 9) & 0x1ff +def _lscopy(dest: ptr16, source: ptr8, lut: ptr16, ch: int): + col = ch & 0x1FF # Unpack (viper 4 parameter limit) + height = (ch >> 9) & 0x1FF wbytes = ch >> 19 # Width in bytes is width // 2 # rgb565 - 16bit/pixel n = 0 @@ -40,7 +44,7 @@ def _lscopy(dest:ptr16, source:ptr8, lut:ptr16, ch:int): idx = col >> 1 # 2 pixels per byte for _ in range(height): if clsb: - c = source[idx] & 0x0f + c = source[idx] & 0x0F else: c = source[idx] >> 4 dest[n] = lut[c] # 16 bit transfer of rightmost 4-bit pixel @@ -58,7 +62,7 @@ class ILI9486(framebuf.FrameBuffer): # ILI9486 expects RGB order. 8 bit register writes require padding @staticmethod def rgb(r, g, b): - return (r & 0xf8) | (g & 0xe0) >> 5 | (g & 0x1c) << 11 | (b & 0xf8) << 5 + return (r & 0xF8) | (g & 0xE0) >> 5 | (g & 0x1C) << 11 | (b & 0xF8) << 5 # Transpose width & height for landscape mode def __init__(self, spi, cs, dc, rst, height=320, width=480, usd=False, init_spi=False): @@ -71,12 +75,12 @@ class ILI9486(framebuf.FrameBuffer): self._long = max(height, width) # Physical dimensions of screen and aspect ratio self._short = min(height, width) self._spi_init = init_spi - pmode = framebuf.GS4_HMSB - self.palette = BoolPalette(pmode) + mode = framebuf.GS4_HMSB + self.palette = BoolPalette(mode) gc.collect() buf = bytearray(height * width // 2) self._mvb = memoryview(buf) - super().__init__(buf, width, height, pmode) # Logical aspect ratio + super().__init__(buf, width, height, mode) # Logical aspect ratio self._linebuf = bytearray(self._short * 2) # Hardware reset @@ -89,64 +93,63 @@ class ILI9486(framebuf.FrameBuffer): self._lock = asyncio.Lock() # Send initialization commands - self._wcmd(b'\x01') # SWRESET Software reset + self._wcmd(b"\x01") # SWRESET Software reset sleep_ms(100) - self._wcmd(b'\x11') # sleep out + self._wcmd(b"\x11") # sleep out sleep_ms(20) - self._wcd(b'\x3a', b'\x55') # interface pixel format - self._wcd(b'\x36', b'\x48' if usd else b'\x88') # MADCTL: RGB portrait mode - self._wcmd(b'\x11') # sleep out - self._wcmd(b'\x29') # display on - - # Write data. - def _wdata(self, data): - self._dc(1) - self._cs(0) - self._spi.write( data ) - self._cs(1) + self._wcd(b"\x3a", b"\x55") # interface pixel format + # 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 + 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._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._spi.write(command) self._cs(1) self._dc(1) self._cs(0) - self._spi.write( data ) + self._spi.write(data) self._cs(1) - @micropython.native + # @micropython.native # Made almost no difference to timing def show(self): # Physical display is in portrait mode clut = ILI9486.lut lb = self._linebuf buf = self._mvb - if self._spi_init: # A callback was passed self._spi_init(self._spi) # Bus may be shared - # Commands needed to start data write - self._wcd(b'\x2a', int.to_bytes(self._short -1, 4, 'big')) # SET_COLUMN works 0 .. width - self._wcd(b'\x2b', int.to_bytes(self._long -1, 4, 'big')) # SET_PAGE ht - self._wcmd(b'\x2c') # WRITE_RAM + self._wcmd(b"\x2c") # WRITE_RAM self._dc(1) self._cs(0) if self.width < self.height: # Portrait 214ms on RP2 120MHz, 30MHz SPI clock wd = self.width // 2 ht = self.height - for start in range(0, wd*ht, wd): # For each line - _lcopy(lb, buf[start :], clut, wd) # Copy and map colors + 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: # Landscpe 264ms on RP2 120MHz, 30MHz SPI clock - cargs = (self.height << 9) + (self.width << 18) # Viper 4-arg limit - for col in range(self.width -1, -1, -1): # For each column of landscape display - _lscopy(lb, buf, clut, col + cargs) # Copy and map colors + width = self.width + wd = width - 1 + cargs = (self.height << 9) + (width << 18) # Viper 4-arg limit + 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) @@ -154,31 +157,28 @@ class ILI9486(framebuf.FrameBuffer): async with self._lock: lines, mod = divmod(self._long, split) # Lines per segment if mod: - raise ValueError('Invalid do_refresh arg.') + raise ValueError("Invalid do_refresh arg.") clut = ILI9486.lut lb = self._linebuf buf = self._mvb - self._wcd(b'\x2a', int.to_bytes(self._short -1, 4, 'big')) # SET_COLUMN works 0 .. width - self._wcd(b'\x2b', int.to_bytes(self._long -1, 4, 'big')) # SET_PAGE ht - self._wcmd(b'\x2c') # WRITE_RAM + self._wcmd(b"\x2c") # WRITE_RAM self._dc(1) if self.width < self.height: # Portrait: write sets of rows wd = self.width // 2 - ht = self.height 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 :], clut, wd) # Copy and map colors + _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 + sc = self.width - 1 # Start and end columns ec = sc - lines # End column for _ in range(split): # For each segment if self._spi_init: # A callback was passed @@ -191,4 +191,3 @@ class ILI9486(framebuf.FrameBuffer): ec -= lines self._cs(1) # Allow other tasks to use bus await asyncio.sleep_ms(0) - diff --git a/setup_examples/ili9486_pico.py b/setup_examples/ili9486_pico.py index a61d973..8536fab 100644 --- a/setup_examples/ili9486_pico.py +++ b/setup_examples/ili9486_pico.py @@ -11,8 +11,8 @@ import gc from drivers.ili94xx.ili9486 import ILI9486 as SSD pdc = Pin(8, Pin.OUT, value=0) -pcs = Pin(9, Pin.OUT, value=1) -prst = Pin(10, Pin.OUT, value=1) +prst = Pin(9, Pin.OUT, value=1) +pcs = Pin(10, Pin.OUT, value=1) spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=30_000_000) gc.collect() # Precaution before instantiating framebuf ssd = SSD(spi, pcs, pdc, prst)