diff --git a/DISPLAYS.md b/DISPLAYS.md index e2bb330..1f2cc2d 100644 --- a/DISPLAYS.md +++ b/DISPLAYS.md @@ -13,28 +13,29 @@ owing to their long update time. Size is diagonal in inches. C/M/GS color/monochrome/greyscale. Width and height are pixels. -| Size | Width | Height | Tech | Driver | Description | Notes | -|:------:|:-----:|:------:|:------|:--------------|:---------------------------|:------| -| 0.96C | 94 | 64 | OLED | [SSD1331][1d] | [Adafruit 684][1m] | | -| 1.12GS | 96 | 96 | OLED | [SSD1327][11d]| [Seeed 104030011][21m] | Obsolescent | -| 1.27C | 128 | 96 | OLED | [SSD1351][2d] | [Adafruit 1673][2m] | | -| 1.5C | 128 | 128 | OLED | [SSD1351][2d] | [Adafruit 1431][3m] | | -| 1.44C | 128 | 128 | TFT | [ST7735R][4d] | [Adafruit 2088][5m] | | -| 1.5C | 160 | 128 | TFT | [ST7735R][4d] | [Adafruit 358][6m] | | -| 1.3C | 240 | 240 | TFT | [ST7789][5d] | [Adafruit 4313][7m] | | -| 1.5GS | 128 | 128 | OLED | [SSD1327][11d]| [Waveshare 13992][20m] | | -| 2.0C | 320 | 240 | TFT | [ST7789][5d] | [Waveshare Pico LCD 2][18m]| For Pi Pico | -| 1.54C | 240 | 240 | TFT | [ST7789][5d] | [Adafruit 3787][8m] | | -| 1.14C | 240 | 135 | TFT | [ST7789][5d] | [T-Display][9m] | ESP32 with attached display | -| 2.8C | 320 | 240 | TFT | [ST7789][5d] | [Waveshare pico 2.8][10m] | Display for Pi Pico | -| 1.14C | 240 | 135 | TFT | [ST7789][5d] | [Waveshare pico 1.14][11m] | For Pi Pico. Buttons good for micro-gui | -| 3.2C | 320 | 240 | TFT | [ILI9341][6d] | [Adafruit 1743][12m] | Big display. eBay equivalents work here. | -| 2.9M | 296 | 128 | eInk | [UC8151D][7d] | [Adafruit 4262][13m] | Flexible ePaper display | -| 2.9M | 296 | 128 | eInk | [UC8151D][7d] | [Adafruit 4777][15m] | FeatherWing ePaper display | -| 4.2M | 400 | 300 | eInk | [WS][10d] | [Waveshare pico 4.2][19m] | Pico, Pico W plug in. Other hosts via cable | -| 2.7M | 274 | 176 | eInk | [HAT][8d] | [Waveshare HAT][14m] | HAT designed for Raspberry Pi, repurposed. | -| 2.7M | 400 | 240 | Sharp | [Sharp][9d] | [Adafruit 4694][16m] | Micropower monochrome display. | -| 1.3M | 168 | 144 | Sharp | [Sharp][9d] | [Adafruit 3502][17m] | Ditto | +| Size | Width | Height | Tech | Driver | Description | Notes | +|:------:|:-----:|:------:|:------|:--------------|:----------------------------|:------| +| 0.96C | 94 | 64 | OLED | [SSD1331][1d] | [Adafruit 684][1m] | | +| 1.12GS | 96 | 96 | OLED | [SSD1327][11d]| [Seeed 104030011][21m] | Obsolescent | +| 1.27C | 128 | 96 | OLED | [SSD1351][2d] | [Adafruit 1673][2m] | | +| 1.5C | 128 | 128 | OLED | [SSD1351][2d] | [Adafruit 1431][3m] | | +| 1.44C | 128 | 128 | TFT | [ST7735R][4d] | [Adafruit 2088][5m] | | +| 1.5C | 160 | 128 | TFT | [ST7735R][4d] | [Adafruit 358][6m] | | +| 1.3C | 240 | 240 | TFT | [ST7789][5d] | [Adafruit 4313][7m] | | +| 1.5GS | 128 | 128 | OLED | [SSD1327][11d]| [Waveshare 13992][20m] | | +| 2.0C | 320 | 240 | TFT | [ST7789][5d] | [Waveshare Pico LCD 2][18m] | For Pi Pico | +| 1.54C | 240 | 240 | TFT | [ST7789][5d] | [Adafruit 3787][8m] | | +| 1.14C | 240 | 135 | TFT | [ST7789][5d] | [T-Display][9m] | ESP32 with attached display | +| 2.8C | 320 | 240 | TFT | [ST7789][5d] | [Waveshare pico 2.8][10m] | Display for Pi Pico | +| 1.14C | 240 | 135 | TFT | [ST7789][5d] | [Waveshare pico 1.14][11m] | For Pi Pico. Buttons good for micro-gui | +| 3.2C | 320 | 240 | TFT | [ILI9341][6d] | [Adafruit 1743][12m] | Big display. eBay equivalents work here. | +| 3.5C | 480 | 320 | TFT | [ILI9486][12d]| [Waveshare Rpi 3.5 LCD][22m]| Pi HAT. Many pixels. Needs plenty of RAM. | +| 2.9M | 296 | 128 | eInk | [UC8151D][7d] | [Adafruit 4262][13m] | Flexible ePaper display | +| 2.9M | 296 | 128 | eInk | [UC8151D][7d] | [Adafruit 4777][15m] | FeatherWing ePaper display | +| 4.2M | 400 | 300 | eInk | [WS][10d] | [Waveshare pico 4.2][19m] | Pico, Pico W plug in. Other hosts via cable | +| 2.7M | 274 | 176 | eInk | [HAT][8d] | [Waveshare HAT][14m] | HAT designed for Raspberry Pi, repurposed. | +| 2.7M | 400 | 240 | Sharp | [Sharp][9d] | [Adafruit 4694][16m] | Micropower monochrome display. | +| 1.3M | 168 | 144 | Sharp | [Sharp][9d] | [Adafruit 3502][17m] | Ditto | ## Displays using compatible drivers @@ -89,6 +90,7 @@ simple. See [this doc](./DRIVERS.md#7-writing-device-drivers) for details. [9d]: https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md#4-drivers-for-sharp-displays [10d]: https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md#53-waveshare-400x300-pi-pico-display [11d]: https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md#23-drivers-for-ssd1327 +[12d]: https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md#34-driver-for-ili9486 [1m]: https://www.adafruit.com/product/684 [2m]: https://www.adafruit.com/product/1673 @@ -111,3 +113,4 @@ simple. See [this doc](./DRIVERS.md#7-writing-device-drivers) for details. [19m]: https://thepihut.com/collections/epaper-displays-for-raspberry-pi/products/4-2-e-paper-display-module-for-raspberry-pi-pico-black-white-400x300 [20m]: https://www.waveshare.com/product/ai/displays/oled/1.5inch-oled-module.htm?___SID=U [21m]: https://www.seeedstudio.com/Grove-OLED-Display-1-12.html?queryID=080778ddd8f54df96ca0e016af616327&objectID=1763&indexName=bazaar_retailer_products +[22m]: https://www.waveshare.com/product/3.5inch-rpi-lcd-a.htm diff --git a/DRIVERS.md b/DRIVERS.md index 4cbb368..5c36d2f 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -40,6 +40,7 @@ access via the `Writer` and `CWriter` classes is documented      3.3.2 [Waveshare Pico Res Touch](./DRIVERS.md#332-waveshare-pico-res-touch)      3.3.3 [Waveshare Pico LCD 2](./DRIVERS.md#333-waveshare-pico-lcd-2)      3.3.4 [Troubleshooting](./DRIVERS.md#334-troubleshooting) + 3.4 [3.4 Driver for ILI9486](./DRIVERS.md#34-driver-for-ili9486) Very large display needs plenty of RAM 4. [Drivers for sharp displays](./DRIVERS.md#4-drivers-for-sharp-displays) Large low power monochrome displays 4.1 [Display characteristics](./DRIVERS.md#41-display-characteristics)      4.1.1 [The VCOM bit](./DRIVERS.md#411-the-vcom-bit) @@ -635,6 +636,71 @@ If your display shows garbage, check the following (I have seen both): * `height` and `width` not matching the choice of `LANDSCAPE` or `PORTRAIT` display mode. +## 3.4 Driver for ILI9486 + +This was tested with +[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` runs out of RAM. + +##### 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. + +Connections may be adapted for other MicroPython targets. The board may be +powered from 5V or 3.3V: there is a regulator on board. + +| Pico | | L | R | | Pico | +|:-----|:-----|:--:|:--:|:-----|:-----| +| Vin | VIN | 2 | 1 | 3V3 | | +| | | 4 | 3 | | | +| | | 6 | 5 | | | +| | | 8 | 7 | | | +| | | 10 | 9 | GND | Gnd | +| | | 12 | 11 | | | +| | | 14 | 13 | | | +| | | 16 | 15 | | | +| 17 | DC | 18 | 17 | | | +| | | 20 | 19 | MOSI | 3 | +| 7 | RST | 22 | 21 | | | +| 14 | CS | 24 | 23 | SCLK | 6 | +| | | 25 | 26 | | | + +#### ILI486 Constructor args: + * `spi` An initialised SPI bus instance. The device can support clock rates of + upto 10MHz. + * `cs` An initialised output pin. Initial value should be 1. + * `dc` An initialised output pin. Initial value should be 0. + * `rst` An initialised output pin. Initial value should be 1. + * `height=320` Display dimensions in pixels. For portrait mode exchange + `height` and `width` values. + * `width=480` + * `usd=False` Upside down: set `True` to invert display. + * `init_spi=False` This optional arg enables flexible options in configuring + the SPI bus. The default assumes exclusive access to the bus. In this normal + case, `color_setup.py` initialises it and the settings will be left in place. + If the bus is shared with devices which require different settings, a callback + function should be passed. It will be called prior to each SPI bus write. The + callback will receive a single arg being the SPI bus instance. It will + typically be a one-liner or lambda initialising the bus. A minimal example is + this function: +```python +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 driver uses the `micropython.viper` decorator. If your platform does not +support this, comment it out and remove the type annotations. You may be able +to use the `micropython.native` decorator. + ###### [Contents](./DRIVERS.md#contents) # 4. Drivers for sharp displays diff --git a/drivers/ili94xx/ili9486.py b/drivers/ili94xx/ili9486.py new file mode 100644 index 0000000..f615acc --- /dev/null +++ b/drivers/ili94xx/ili9486.py @@ -0,0 +1,194 @@ +# ILI9486 nano-gui driver for ili9486 displays +# As with all nano-gui displays, touch is not supported. + +# Copyright (c) Peter Hinch 2022 +# Released under the MIT license see LICENSE + +# Inspired by @brave-ulysses https://github.com/micropython/micropython/discussions/10404 + +# 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. + +from time import sleep_ms +import gc +import framebuf +import uasyncio as asyncio +from drivers.boolpalette import BoolPalette + +# Portrait mode +@micropython.viper +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 + 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 + wbytes = ch >> 19 # Width in bytes is width // 2 + # rgb565 - 16bit/pixel + n = 0 + clsb = col & 1 + idx = col >> 1 # 2 pixels per byte + for _ in range(height): + if clsb: + c = source[idx] & 0x0f + else: + c = source[idx] >> 4 + dest[n] = lut[c] # 16 bit transfer of rightmost 4-bit pixel + n += 1 # 16 bit + idx += wbytes + + +class ILI9486(framebuf.FrameBuffer): + + lut = bytearray(32) + + # 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 + # 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 + + # Transpose width & height for landscape mode + def __init__(self, spi, cs, dc, rst, height=320, width=480, usd=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._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) + gc.collect() + buf = bytearray(height * width // 2) + self._mvb = memoryview(buf) + super().__init__(buf, width, height, pmode) # Logical aspect ratio + self._linebuf = bytearray(self._short * 2) + + # 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'\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) + + # 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) + + @micropython.native + 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._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 + 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 + self._spi.write(lb) + self._cs(1) + + async def do_refresh(self, split=4): + async with self._lock: + lines, mod = divmod(self._long, split) # Lines per segment + if mod: + 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._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 + 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 + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._cs(0) + 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/setup_examples/ili9486_pico.py b/setup_examples/ili9486_pico.py new file mode 100644 index 0000000..55bcf95 --- /dev/null +++ b/setup_examples/ili9486_pico.py @@ -0,0 +1,18 @@ +# ili9486_pico.py Customise for your hardware config and rename + +# Released under the MIT License (MIT). See LICENSE. + +# ILI9486 on Pi Pico +# See DRIVERS.md for wiring details. + +from machine import Pin, SPI +import gc + +from drivers.ili94xx.ili9486 import ILI9486 as SSD + +pdc = Pin(17, Pin.OUT, value=0) +pcs = Pin(14, Pin.OUT, value=1) +prst = Pin(7, Pin.OUT, value=1) +spi = SPI(0, sck=Pin(6), mosi=Pin(3), miso=Pin(4), baudrate=30_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst)