kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
Improve uasyncio support on ili9341 and st7789.
rodzic
e1a61b67e8
commit
29b183cab1
44
ASYNC.md
44
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)
|
||||
|
|
25
DRIVERS.md
25
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)
|
||||
|
||||
|
|
15
README.md
15
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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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()
|
Ładowanie…
Reference in New Issue