From 9256c874369b543c432c0c0fd22efaee9f615bdb Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 15 Dec 2020 11:41:23 +0000 Subject: [PATCH] 4-bit and ILI9341 support. Improve shared bus handling. Driver doc added. --- DRIVERS.md | 43 ++++++++++------------- README.md | 9 ++--- drivers/ili93xx/ili9341.py | 71 +++++++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 56 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index cd6bcf8..53cbbb1 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -219,18 +219,17 @@ Adafruit make several displays using this chip, for example `height` and `width` values. * `width=320` * `usd=False` Upside down: set `True` to invert display. - * `split=False` By default the entire display is refreshed by the `show` - method. A partial update may be specified for use with `uasyncio`. See below. * `init_spi=False` This optional arg enables flexible options in configuring - the SPI bus. The default assumes exclusive access to the bus with - `color_setup.py` initialising it. Those settings will be left in place. If a - callback function is passed, it will be called prior to each SPI bus write: - this is for shared bus applications. The callback will receive a single arg - being the SPI bus instance. In normal use it will be a one-liner or lambda - initialising the bus. A minimal example is this function: + 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) # Data sheet: max is 10MHz + spi.init(baudrate=10_000_000) ``` The ILI9341 class uses 4-bit color to conserve RAM. Even with this adaptation @@ -243,22 +242,18 @@ to use the `micropython.native` decorator. #### Use with uasyncio -A full refresh blocks for ~200ms. This may be unacceptable for some `uasyncio` -applications. The `split` constructor arg limits the number of display lines -which are updated at one time, reducing the blocking time. To use this, an -integer value of 2, 4, or 8 should be passed. For example to reduce blocking by -a factor of ~4 to 50ms the `split` constructor arg is set to 4. +A full refresh blocks for ~200ms. If this is acceptable, no special precautions +are required. However this period may be unacceptable for some `uasyncio` +applications. The driver provides an asynchronous `do_refresh(split=4)` method. +If this is run the display will regularly be refreshed, but will periodically +yield to the scheduler enabling other tasks to run. The arg determines the +number of times this will occur, so by default it will block for about 50ms. +A `ValueError` will result if `split` is not an integer divisor of the display +height. -For any value the following keeps the display updated: -```python -import uasyncio as asyncio -from gui.core.nanogui import refresh - -async def keep_refreshed(ssd): - while True: - refresh(ssd) # Blocks for a period defined by split - await asyncio.sleep_ms(0) -``` +An application using this should call `refresh(ssd, True)` once at the start, +then launch the `do_refresh` method. After that, no calls to `refresh` should +be made. See `gui/demos/scale_ili.py`. ###### [Contents](./DRIVERS.md#contents) diff --git a/README.md b/README.md index c156eea..8a90055 100644 --- a/README.md +++ b/README.md @@ -802,6 +802,7 @@ would present no problem. The ESP8266 is a minimal platform with typically 36.6KiB of free RAM. The framebuffer for a 128*128 OLED requires 16KiB of contiguous RAM (the display hardware uses 16 bit color but my driver uses an 8 bit buffer to conserve RAM). +The 4-bit driver halves this size. A further issue is that, by default, ESP8266 firmware does not support complex numbers. This rules out the plot module and the `Dial` widget. It is possible @@ -813,8 +814,8 @@ to create dynamic content, and the widgets themselves are relatively complex. I froze a subset of the `drivers` and the `gui` directories. A subset minimises the size of the firmware build and eliminates modules which won't compile due to the complex number issue. The directory structure in my frozen modules -directory matched that of the source. This is the structure of my frozen -directory: +directory matched that of the source. This was the structure of my frozen +directory before I added the 4 bit driver: ![Image](images/esp8266_tree.JPG) I erased flash, built and installed the new firmware. Finally I copied @@ -825,8 +826,8 @@ Both demos worked perfectly. I modified the demos to regularly report free RAM. `scale.py` reported 10480 bytes, `tbox.py` reported 10512 bytes, sometimes more, as the demo progressed. -In conclusion I think that applications of moderate complexity should be -feasible. +With the 4 bit driver `scale.py` reported 18112 bytes. In conclusion I think +that applications of moderate complexity should be feasible. ###### [Contents](./README.md#contents) diff --git a/drivers/ili93xx/ili9341.py b/drivers/ili93xx/ili9341.py index e4884bb..f7f329a 100644 --- a/drivers/ili93xx/ili9341.py +++ b/drivers/ili93xx/ili9341.py @@ -12,6 +12,7 @@ from time import sleep_ms import gc import framebuf +import uasyncio as asyncio @micropython.viper def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): @@ -20,7 +21,7 @@ def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): c = source[x] d = (c & 0xf0) >> 3 # 2* pointers (lut is 16 bit color) e = (c & 0x0f) << 1 - dest[n] = lut[d] + dest[n] = lut[d] # LS byte 1st n += 1 dest[n] = lut[d + 1] n += 1 @@ -34,24 +35,24 @@ class ILI9341(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 + # ILI9341 expects RGB order @staticmethod def rgb(r, g, b): - return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + 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=240, width=320, - usd=False, split=False, init_spi=False): + usd=False, init_spi=False): self._spi = spi self._cs = cs self._dc = dc self._rst = rst self.height = height self.width = width - if split and split not in (2, 4, 8): - raise ValueError('split must be 2, 4 or 8') self._spi_init = init_spi - self._lines = 0 if not split else height // split # For uasyncio use: show n lines only - self._line = 0 # Current line mode = framebuf.GS4_HMSB gc.collect() buf = bytearray(self.height * self.width // 2) @@ -84,7 +85,7 @@ class ILI9341(framebuf.FrameBuffer): else: self._wcd(b'\x36', b'\x28' if usd else b'\xe8') # MADCTL: RGB landscape mode self._wcd(b'\x37', b'\x00') # VSCRSADD Vertical scrolling start address - self._wcd(b'\x3a', b'\x55') # PIXFMT COLMOD: Pixel format + self._wcd(b'\x3a', b'\x55') # PIXFMT COLMOD: Pixel format 16 bits (MCU & interface) self._wcd(b'\xb1', b'\x00\x18') # FRMCTR1 Frame rate ctrl self._wcd(b'\xb6', b'\x08\x82\x27') # DFUNCTR self._wcd(b'\xf2', b'\x00') # ENABLE3G Enable 3 gamma ctrl @@ -93,7 +94,7 @@ class ILI9341(framebuf.FrameBuffer): self._wcd(b'\xe1', b'\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F') # GMCTRN1 self._wcmd(b'\x11') # SLPOUT Exit sleep sleep_ms(100) - self._wcmd(b'\x29') # DISPLAY_ON Display on + self._wcmd(b'\x29') # DISPLAY_ON sleep_ms(100) # Write a command. @@ -114,11 +115,8 @@ class ILI9341(framebuf.FrameBuffer): self._spi.write(data) self._cs(1) -# No. of lines buffered vs time. Tested in portrait mode 240 pixels/line. -# ESP32 at stock freq. -# 24 lines 171ms -# 2 lines 180ms -# 1 line 196ms +# Time (ESP32 stock freq) 196ms portrait, 185ms landscape. +# mem free on ESP32 43472 bytes (vs 110192) @micropython.native def show(self): clut = ILI9341.lut @@ -126,25 +124,42 @@ class ILI9341(framebuf.FrameBuffer): ht = self.height lb = self._linebuf buf = self._mvb - # Commands needed to start data write - #self._wcd(b'\x2a', *ustruct.pack(">HH", 0, self.width)) # SET_COLUMN - #self._wcd(b'\x2b', *ustruct.pack(">HH", 0, ht)) # SET_PAGE 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.width, 4, 'big')) # SET_COLUMN self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE self._wcmd(b'\x2c') # WRITE_RAM self._dc(1) self._cs(0) - if self._lines: - end = self._line + wd * self._lines - for start in range(self._line, end, wd): # For each line - _lcopy(lb, buf[start :], clut, wd) # Copy and map colors - self._spi.write(lb) - nxt = start + wd - self._line = nxt % wd*ht - 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) + 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) self._cs(1) + + async def do_refresh(self, split=4): + lines, mod = divmod(self.height, split) # Lines per segment + if mod: + raise ValueError('Invalid do_refresh arg.') + clut = ILI9341.lut + wd = self.width // 2 + ht = self.height + lb = self._linebuf + buf = self._mvb + while True: # Perform a refresh + # Commands needed to start data write + self._wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # SET_COLUMN + self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE + self._wcmd(b'\x2c') # WRITE_RAM + self._dc(1) + 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)