diff --git a/ASYNC.md b/ASYNC.md index e9908f1..8c8b074 100644 --- a/ASYNC.md +++ b/ASYNC.md @@ -1,5 +1,9 @@ # nanogui: Use in asynchronous code +###### [Main README](../README.md) + +###### [Driver doc](../DRIVERS.md) + ## Blocking The suitability of `nanogui` for use with cooperative schedulers such as @@ -24,11 +28,42 @@ applications which might wait for user input from a switch this blocking is not apparent and the response appears immediate. It may have consequences in applications performing fast concurrent input over devices such as UARTs. +### Reducing latency + +Some display drivers have an asynchronous `do_refresh()` method which takes a +single optional arg `split=4`. This may be used in place of the synchronous +`refresh()` method. With the default value the method will yield to the +scheduler four times during a refresh, reducing the latency experienced by +other tasks by a factor of four. A `ValueError` will result if `split` is not +an integer divisor of the `height` passed to the constructor. + +Such applications should issue the synchronous +```python +refresh(ssd, True) +``` +at the start to initialise the display. This will block for the full refresh +period. + +The coroutine performing screen refresh might use the following for portability +between devices having a `do_refresh` method and those that do not: +```python + while True: + # Update widgets + if hasattr(ssd, 'do_refresh'): + # Option to reduce uasyncio latency + await ssd.do_refresh() + else: + # Normal synchronous call + refresh(ssd) + await asyncio.sleep_ms(250) # Determine update rate +``` + ## Demo scripts -These require uasyncio V3. This is incorporated in daily builds and will be -available in release builds starting with MicroPython V1.13. The demos assume -a Pyboard. +These require uasyncio V3. This is incorporated in daily builds and became +available in release builds starting with MicroPython V1.13. The `asnano` and +`asnano_sync` demos assume a Pyboard. `scale.py` is portable between hosts and +sufficiently large displays. * `asnano.py` Runs until the usr button is pressed. In this demo each meter updates independently and mutually asynchronously to test the response to @@ -37,5 +72,8 @@ a Pyboard. themselves as data becomes available but screen updates occur asynchronously at a low frequency. An asynchronous iterator is used to stop the demo when the pyboard usr button is pressed. + * `scale.py` Illustrates the use of `do_refresh()` where available. ###### [Main README](../README.md) + +###### [Driver doc](../DRIVERS.md) diff --git a/DRIVERS.md b/DRIVERS.md index 93d6138..50506d2 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -348,16 +348,9 @@ to use the `micropython.native` decorator. 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 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_async.py`. The initial synchronous `refresh` call -will block for the full refresh period. +If this is run the display will be refreshed, but will periodically yield to +the scheduler enabling other tasks to run. This is documented +[here](./ASYNC.md). Another option to reduce blocking is overclocking the SPI bus. @@ -457,16 +450,8 @@ no special precautions are required. This period may be unacceptable for some 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. +run the display will be refreshed, but will periodically yield to the scheduler +enabling other tasks to run. This is documented [here](./ASYNC.md). ###### [Contents](./DRIVERS.md#contents) diff --git a/README.md b/README.md index fe9e64e..249b6ef 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ my GUI's employ the American spelling of `color`. ## 1.1 Change log +26 Mar 2021 Add ST7789. Alter uasyncio support on ili9341. 14 Mar 2021 Tested on Pi Pico. 17 Jan 2021 Add ePaper drivers. Ensure monochrome and color setup requirements are @@ -150,6 +151,9 @@ Compatible and tested display drivers include: * Drivers for Adafruit ST7735R based TFT's: [1.8 inch](https://www.adafruit.com/product/358) and [1.44 inch](https://www.adafruit.com/product/2088) documented [here](./DRIVERS.md#4-drivers-for-st7735r). + * Drivers for Adafruit ST7789 TFT's: + [1.3 inch](https://www.adafruit.com/product/4313) and +[1.54 inch](https://www.adafruit.com/product/3787). * Drivers for ILI9341 such as [Adafruit 3.2 inch](https://www.adafruit.com/product/1743) documented [here](./DRIVERS.md#5-drivers-for-ili9341). * [Adafruit 2.9 inch ePaper display](https://www.adafruit.com/product/4262) @@ -198,7 +202,8 @@ code. This stuff is easier than you might think. # 2. Files and Dependencies -Firmware should be V1.13 or later. +Firmware should be V1.13 or later. At the time of writing the Pi Pico was new: +firmware should be from a daily build or >=V1.15 when it arrives. Installation comprises copying the `gui` and `drivers` directories, with their contents, plus a hardware configuration file, to the target. The directory @@ -258,11 +263,10 @@ Demos for larger displays. * `aclock.py` Analog clock demo. Cross platform. * `alevel.py` Spirit level using Pyboard accelerometer. * `fpt.py` Plot demo. Cross platform. - * `scale.py` A demo of the new `Scale` widget. Cross platform. + * `scale.py` A demo of the `Scale` widget. Cross platform. Uses `uasyncio`. * `asnano_sync.py` Two Pyboard specific demos using the GUI with `uasyncio`. * `asnano.py` Could readily be adapted for other targets. * `tbox.py` Demo `Textbox` class. Cross-platform. - * `scale_ili.py` A special demo of the asychronous mode of the ILI9341 driver. Demos for ePaper displays: * `waveshare_test.py` For the Waveshare eInk Display HAT 2.7" 176*274 display. @@ -279,7 +283,9 @@ Demos for Sharp displays: Usage with `uasyncio` is discussed [here](./ASYNC.md). In summary the GUI works well with `uasyncio` but the blocking which occurs during transfer of the -framebuffer to the display may affect more demanding applications. +framebuffer to the display may affect more demanding applications. Some display +drivers have an additional asynchronous refresh method. This may optionally be +used to mitigate the resultant latency. ###### [Contents](./README.md#contents) @@ -322,6 +328,7 @@ copied to the hardware root as `color_setup.py`. Example files: * `st7735r144_setup.py` For a Pyboard with an [Adafruit 1.44 inch TFT display](https://www.adafruit.com/product/2088). * `ili9341_setup.py` A 240*320 ILI9341 display on ESP32. + * `ssd7789.py` Example with SSD7789 driver and Pi Pico host. * `waveshare_setup.py` 176*274 ePaper display. * `epd29_sync.py` Adafruit 2.9 inch ePaper display for synchronous code. * `epd29_async.py` Adafruit 2.9 inch ePaper display: `uasyncio` applications. diff --git a/drivers/ili93xx/ili9341.py b/drivers/ili93xx/ili9341.py index f7f329a..5271d1b 100644 --- a/drivers/ili93xx/ili9341.py +++ b/drivers/ili93xx/ili9341.py @@ -66,6 +66,7 @@ class ILI9341(framebuf.FrameBuffer): 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) @@ -138,15 +139,15 @@ class ILI9341(framebuf.FrameBuffer): 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 + async with self._lock: + 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 # 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 diff --git a/drivers/st7789/st7789_4bit.py b/drivers/st7789/st7789_4bit.py index d341393..9a1558e 100644 --- a/drivers/st7789/st7789_4bit.py +++ b/drivers/st7789/st7789_4bit.py @@ -47,6 +47,7 @@ class ST7789(framebuf.FrameBuffer): # 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 + # For some reason color must be inverted on this controller. @staticmethod def rgb(r, g, b): return ((b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8)) ^ 0xffff @@ -59,7 +60,8 @@ class ST7789(framebuf.FrameBuffer): self._cs = cs self.height = height # Required by Writer class self.width = width - self._spi_init = init_spi + self._spi_init = init_spi # Possible user callback + self._lock = asyncio.Lock() mode = framebuf.GS4_HMSB # Use 4bit greyscale. gc.collect() buf = bytearray(height * width // 2) @@ -98,7 +100,8 @@ class ST7789(framebuf.FrameBuffer): self._cs(1) # Initialise the hardware. Blocks 163ms. Adafruit have various sleep delays - # where I can find no requirement in the datasheet. I have removed them. + # where I can find no requirement in the datasheet. I removed them with + # other redundant code. def _init(self, disp_mode): self._hwreset() # Hardware reset. Blocks 3ms if self._spi_init: # A callback was passed @@ -114,15 +117,15 @@ class ST7789(framebuf.FrameBuffer): cmd(b'\x13') # NORON Normal display mode # Adafruit skip setting CA and RA. We do it to enable rotation and - # reflection. Also hopefully to help portability. Set display window - # depending on mode, .height and .width. + # reflection. Also hopefully to localise any display portability issues? + # Set display window depending on mode, .height and .width. self.set_window(disp_mode) # d7..d5 of MADCTL determine rotation/orientation datasheet P124, P231 # d7 = MY page addr order # d6 = MX col addr order # d5 = MV row/col exchange wcd(b'\x36', int.to_bytes(disp_mode, 1, 'little')) - cmd(b'\x29') # DISPON + cmd(b'\x29') # DISPON. Adafruit then delay 500ms. # Define the mapping between RAM and the display # May need modifying for non-Adafruit hardware which may use a different @@ -178,14 +181,14 @@ class ST7789(framebuf.FrameBuffer): # 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: + async with self._lock: + 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 line = 0 for n in range(split): if self._spi_init: # A callback was passed diff --git a/gui/demos/scale.py b/gui/demos/scale.py index ee7615e..2f83df9 100644 --- a/gui/demos/scale.py +++ b/gui/demos/scale.py @@ -1,12 +1,15 @@ # scale.py Test/demo of scale widget for nano-gui # Released under the MIT License (MIT). See LICENSE. -# Copyright (c) 2020 Peter Hinch +# Copyright (c) 2020-2021 Peter Hinch # Usage: # import gui.demos.scale # Initialise hardware and framebuf before importing modules. +# Uses uasyncio and also the asynchronous do_refresh method if the driver +# supports it. + from color_setup import ssd # Create a display instance from gui.core.nanogui import refresh @@ -44,7 +47,12 @@ async def default(scale, lbl): cv += delta scale.value(cv) lbl.value('{:4.3f}'.format(cv)) - refresh(ssd) + if hasattr(ssd, 'do_refresh'): + # Option to reduce uasyncio latency + await ssd.do_refresh() + else: + # Normal synchronous call + refresh(ssd) await asyncio.sleep_ms(250) val, cv = v2, v1 @@ -68,7 +76,10 @@ def test(): lbl = Label(wri, ssd.height - wri.height - 2, 2, 50, bgcolor = DARKGREEN, bdcolor = RED, fgcolor=WHITE) - scale = Scale(wri, 45, 2, width = 124, tickcb = tickcb, + # do_refresh is called with arg 4. In landscape mode this splits screen + # into segments of 240/4=60 lines. Here we ensure a scale straddles + # this boundary + scale = Scale(wri, 55, 2, width = 124, tickcb = tickcb, pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN) asyncio.run(default(scale, lbl)) diff --git a/gui/demos/scale_async.py b/gui/demos/scale_async.py deleted file mode 100644 index 4b4cd21..0000000 --- a/gui/demos/scale_async.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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 - -# Usage: -# import gui.demos.scale - -# Initialise hardware and framebuf before importing modules. -from color_setup import ssd # Create a display instance - -from gui.core.nanogui import refresh -from gui.core.writer import CWriter - -import uasyncio as asyncio -from gui.core.colors import * -import gui.fonts.arial10 as arial10 -from gui.widgets.label import Label -from gui.widgets.scale import Scale - -# COROUTINES -async def radio(scale): - cv = 88.0 # Current value - val = 108.0 # Target value - while True: - v1, v2 = val, cv - steps = 200 - delta = (val - cv) / steps - for _ in range(steps): - cv += delta - # Map user variable to -1.0..+1.0 - scale.value(2 * (cv - 88)/(108 - 88) - 1) - await asyncio.sleep_ms(200) - val, cv = v2, v1 - -async def default(scale, lbl): - asyncio.create_task(ssd.do_refresh(4)) - cv = -1.0 # Current - val = 1.0 - while True: - v1, v2 = val, cv - steps = 400 - delta = (val - cv) / steps - for _ in range(steps): - cv += delta - scale.value(cv) - lbl.value('{:4.3f}'.format(cv)) - await asyncio.sleep_ms(250) - val, cv = v2, v1 - - -def test(): - def tickcb(f, c): - if f > 0.8: - return RED - if f < -0.8: - return BLUE - return c - def legendcb(f): - return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88)) - refresh(ssd, True) # Initialise and clear display. - CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it - wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) - wri.set_clip(True, True, False) - scale1 = Scale(wri, 2, 2, width = 124, legendcb = legendcb, - pointercolor=RED, fontcolor=YELLOW) - asyncio.create_task(radio(scale1)) - - lbl = Label(wri, ssd.height - wri.height - 2, 2, 50, - bgcolor = DARKGREEN, bdcolor = RED, fgcolor=WHITE) - # do_refresh is called with arg 4. In landscape mode this splits screen - # into segments of 240/4=60 lines. Here we ensure a scale straddles - # this boundary - scale = Scale(wri, 55, 2, width = 124, tickcb = tickcb, - pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN) - asyncio.run(default(scale, lbl)) - -test()