From e1a61b67e80e6e074e2592ea58171dbc354da160 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 25 Mar 2021 10:18:45 +0000 Subject: [PATCH] ST7789 add uasyncio support. --- DRIVERS.md | 32 +++++++++----- color_setup/ssd7789.py | 7 ++-- drivers/st7789/st7789_4bit.py | 49 ++++++++++++++-------- gui/demos/{scale_ili.py => scale_async.py} | 3 +- 4 files changed, 59 insertions(+), 32 deletions(-) rename gui/demos/{scale_ili.py => scale_async.py} (94%) diff --git a/DRIVERS.md b/DRIVERS.md index fb4077f..93d6138 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -350,13 +350,14 @@ 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. +number of times in a frame where 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 `height` passed to the constructor. 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`. +be made. See `gui/demos/scale_async.py`. The initial synchronous `refresh` call +will block for the full refresh period. Another option to reduce blocking is overclocking the SPI bus. @@ -376,9 +377,6 @@ The response may be of interest. ## 3.3 Drivers for ST7789 -**UNDER DEVELOPMENT** -Initial testing on Adafruit display looks good. - These displays tend to be physically small with a high pixel density. The chip supports up to 240x320 displays. The Adafruit units tested are 240x240. To keep the buffer size down, the driver uses 4-bit color with dynamic conversion to 16 @@ -394,7 +392,7 @@ uses the same CircuitPython driver so can be expected to work. The `color_setup.py` file should initialise the SPI bus with a baudrate of 30_000_000. Args `polarity`, `phase`, `bits`, `firstbit` are defaults. Hard or soft SPI may be used but hard may be faster. 30MHz is a conservative value: see -below. +below. An example file for the Pi Pico is in `color_setup/ssd7789.py`. #### ST7789 Constructor args: * `spi` An initialised SPI bus instance. The chip supports clock rates of upto @@ -453,8 +451,22 @@ On Adafruit displays, combinations that don't produce mirror images are: #### Use with uasyncio -Running the SPI bus at 60MHz a refresh blocks for 83ms. Considering whether to -offer a solution as per ili9341. +Running the SPI bus at 60MHz a refresh blocks for 83ms. If this is acceptable, +no special precautions are required. This period may be unacceptable for some +`uasyncio` applications. Some may use lower SPI baudrates either for electrical +reasons or where the host cannot support high speeds. + +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 +in a frame where this will occur, so by default it will block for about 15ms. A +`ValueError` will result if `split` is not an integer divisor of the `height` +passed to the constructor. + +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_async.py`. The initial synchronous `refresh` call +will block for the full refresh period. ###### [Contents](./DRIVERS.md#contents) diff --git a/color_setup/ssd7789.py b/color_setup/ssd7789.py index 824c489..0e0e612 100644 --- a/color_setup/ssd7789.py +++ b/color_setup/ssd7789.py @@ -26,15 +26,14 @@ from machine import Pin, SPI import gc -# *** Choose your color display driver here *** - -from drivers.st7789.st7789_4bit import ST7789 as SSD, PORTRAIT, USD +from drivers.st7789.st7789_4bit import ST7789 as SSD, PORTRAIT, USD, REFLECT pdc = Pin(13, Pin.OUT, value=0) # Arbitrary pins pcs = Pin(14, Pin.OUT, value=1) prst = Pin(15, Pin.OUT, value=1) gc.collect() # Precaution before instantiating framebuf -spi = SPI(1, 60_000_000, sck=Pin(10), mosi=Pin(11), miso=Pin(8)) +# Conservative low baudrate. Can go to 62.5MHz. +spi = SPI(1, 30_000_000, sck=Pin(10), mosi=Pin(11), miso=Pin(8)) ssd = SSD(spi, dc=pdc, cs=pcs, rst=prst) #, disp_mode=PORTRAIT | USD) diff --git a/drivers/st7789/st7789_4bit.py b/drivers/st7789/st7789_4bit.py index 49c1eb5..d341393 100644 --- a/drivers/st7789/st7789_4bit.py +++ b/drivers/st7789/st7789_4bit.py @@ -13,27 +13,17 @@ # SPI bus: default mode. Driver performs no read cycles. # Datasheet table 6 p44 scl write cycle 16ns == 62.5MHz -from time import sleep_ms, ticks_us, ticks_diff +from time import sleep_ms #, ticks_us, ticks_diff import framebuf import gc import micropython +import uasyncio as asyncio PORTRAIT = 0x20 REFLECT = 0x40 USD = 0x80 -#_INIT_SEQUENCE = ( - #b"\x01\x80\x96" # _SWRESET and Delay 150ms - #b"\x11\x80\xFF" # _SLPOUT and Delay 500ms - #b"\x3A\x81\x55\x0A" # _COLMOD and Delay 10ms - #b"\x36\x01\x08" # _MADCTL - #b"\x21\x80\x0A" # _INVON Hack and Delay 10ms - #b"\x13\x80\x0A" # _NORON and Delay 10ms - #b"\x36\x01\xC0" # _MADCTL - #b"\x29\x80\xFF" # _DISPON and Delay 500ms -#) - @micropython.viper def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): n = 0 @@ -62,7 +52,6 @@ class ST7789(framebuf.FrameBuffer): return ((b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8)) ^ 0xffff # rst and cs are active low, SPI is mode 0 - # TEST height=140, width=200, disp_mode=PORTRAIT|REFLECT def __init__(self, spi, cs, dc, rst, height=240, width=240, disp_mode=0, init_spi=False): self._spi = spi # Clock cycle time for write 16ns 62.5MHz max (read is 150ns) self._rst = rst # Pins @@ -167,21 +156,47 @@ class ST7789(framebuf.FrameBuffer): # Row address set self._wcd(b'\x2b', int.to_bytes(ys, 2, 'big') + int.to_bytes(ye, 2, 'big')) + #@micropython.native # Made virtually no difference to timing. def show(self): # Blocks for 83ms @60MHz SPI -# ts = ticks_us() + #ts = ticks_us() clut = ST7789.lut wd = self.width // 2 end = self.height * wd lb = self._linebuf buf = self._mvb - self._dc(0) - self._cs(0) if self._spi_init: # A callback was passed self._spi_init(self._spi) # Bus may be shared + self._dc(0) + self._cs(0) self._spi.write(b'\x2c') # RAMWR self._dc(1) for start in range(0, end, wd): _lcopy(lb, buf[start :], clut, wd) # Copy and map colors self._spi.write(lb) self._cs(1) -# print(ticks_diff(ticks_us(), ts)) + #print(ticks_diff(ticks_us(), ts)) + + # Asynchronous refresh with support for reducing blocking time. + 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 = ST7789.lut + wd = self.width // 2 + lb = self._linebuf + buf = self._mvb + while True: + line = 0 + for n in range(split): + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._dc(0) + self._cs(0) + self._spi.write(b'\x3c' if n else b'\x2c') # RAMWR/Write memory continue + self._dc(1) + for start in range(wd * line, wd * (line + lines), wd): + _lcopy(lb, buf[start :], clut, wd) # Copy and map colors + self._spi.write(lb) + line += lines + self._cs(1) + await asyncio.sleep(0) diff --git a/gui/demos/scale_ili.py b/gui/demos/scale_async.py similarity index 94% rename from gui/demos/scale_ili.py rename to gui/demos/scale_async.py index a58541b..4b4cd21 100644 --- a/gui/demos/scale_ili.py +++ b/gui/demos/scale_async.py @@ -1,4 +1,5 @@ -# scale_ili.py Test/demo of scale widget for nano-gui +# scale_async.py Test/demo of scale widget for nano-gui using asynchronous code +# Requires a supporting display (ili9341 or ST7789) # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2020 Peter Hinch