Improve uasyncio support on ili9341 and st7789.

pull/8/head
Peter Hinch 2021-03-26 10:57:37 +00:00
rodzic e1a61b67e8
commit 29b183cab1
7 zmienionych plików z 97 dodań i 131 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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.

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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))

Wyświetl plik

@ -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()