ePaper: Optimise driver, improve docs.

encoder_driver
peterhinch 2023-04-14 13:38:32 +01:00
rodzic 1424c06bb6
commit cbbededc60
2 zmienionych plików z 64 dodań i 36 usunięć

Wyświetl plik

@ -149,7 +149,7 @@ development so check for updates.
7.4 [Class TSequence](./README.md#74-class-tsequence) Plotting realtime, time sequential data. 7.4 [Class TSequence](./README.md#74-class-tsequence) Plotting realtime, time sequential data.
8. [ESP32 touch pads](./README.md#8-esp32-touch-pads) Replacing buttons with touch pads. 8. [ESP32 touch pads](./README.md#8-esp32-touch-pads) Replacing buttons with touch pads.
9. [Realtime applications](./README.md#9-realtime-applications) Accommodating tasks requiring fast RT performance. 9. [Realtime applications](./README.md#9-realtime-applications) Accommodating tasks requiring fast RT performance.
10. [ePaper displays](./README.md#10-epaper-displays) Using these techniques to provide a full refresh. 10. [ePaper displays](./README.md#10-epaper-displays) Guidance on using ePaper displays.
[Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, encoder interface, use of graphics primitives [Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, encoder interface, use of graphics primitives
[Appendix 2 Freezing bytecode](./README.md#appendix-2-freezing-bytecode) Optional way to save RAM. [Appendix 2 Freezing bytecode](./README.md#appendix-2-freezing-bytecode) Optional way to save RAM.
@ -506,10 +506,8 @@ Supported displays are as per
[the nano-gui list](https://github.com/peterhinch/micropython-nano-gui/blob/master/README.md#12-description). [the nano-gui list](https://github.com/peterhinch/micropython-nano-gui/blob/master/README.md#12-description).
In general ePaper and Sharp displays are unlikely to yield good results because In general ePaper and Sharp displays are unlikely to yield good results because
of slow and visually intrusive refreshing. However there is an exception: the of slow and visually intrusive refreshing. However there is an exception: the
[Waveshare pico_epaper_42](https://www.waveshare.com/pico-epaper-4.2.htm). This [Waveshare pico_epaper_42](https://www.waveshare.com/pico-epaper-4.2.htm). See
supports partial updates which work remarkably well with minimal ghosting. Note [10. ePaper displays](./README.md#10-epaper-displays).
that it can be used with hosts other than the Pico via the supplied cable. See
[ePaper displays](./README.md#10-epaper-displays).
Display drivers are documented [here](https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md). Display drivers are documented [here](https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md).
@ -3044,14 +3042,29 @@ The demo `gui/demos/audio.py` provides example usage.
# 10 ePaper displays # 10 ePaper displays
In general ePaper displays do not work well with micro-gui because refresh is
slow (seconds) and visually intrusive. Some displays support partial refresh
which is faster (hundreds of ms) and non-intrusive. The penalty is "ghosting"
where pixels which change from black to white do so imperfectly, leaving a grey
trace behind. The degree of ghosting varies between display types.
The [Waveshare pico_epaper_42](https://www.waveshare.com/pico-epaper-4.2.htm) The [Waveshare pico_epaper_42](https://www.waveshare.com/pico-epaper-4.2.htm)
is currently the only fully supported ePaper display, with a hardware_setup.py has quite a low level of ghosting. A full refresh takes about 2.1s and partial
copied or adapted from `setup_examples/pico_epaper_42_pico.py`. After an about 740ms. In use there is a visible lag between operating a user control and
initial refresh the driver is put into partial mode to provide reasonably a visible response, but it is usable. Currently this is the only fully
quick and visually satisfactory response to button events. However ghosting may supported ePaper display.
accumulate after long periods of running, and an application may occasionally
need to perform a full refresh. This requires the "done" interlock described It has a socket for a Pico or Pico W, but also comes with a cable suitable for
in section 9. connecting to any host. The hardware_setup.py should be copied or adapted from
`setup_examples/pico_epaper_42_pico.py`. If using the socket, default args may
be used (see code comment).
After an initial refresh to clear the screen the driver is put into partial
mode. This provides a reasonably quick and visually satisfactory response to
user inputs such as button events. See the
[epaper demo](https://github.com/peterhinch/micropython-micro-gui/blob/main/gui/demos/epaper.py).
This provides for a full refresh via the `reset` button. Provision of full
refresh is application dependent. It should be done as follows:
```python ```python
async def full_refresh(): async def full_refresh():

Wyświetl plik

@ -94,11 +94,14 @@ EPD_partial_lut_bb1 = b"\x00\x19\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00" \x00\x00\x00\x00"
# Invert: EPD is black on white
# 483/241 us for 2000 bytes (125/250MHz)
# Can't extend to 32 bits becaue of long ints
@micropython.viper @micropython.viper
def _linv(dest:ptr8, source:ptr8, length:int): def _linv(dest:ptr16, source:ptr16, length:int):
for n in range(length): for n in range(length):
c = source[n] c: uint = source[n]
dest[n] = c ^ 0xFF dest[n] = c ^ 0xFFFF
class EPD(framebuf.FrameBuffer): class EPD(framebuf.FrameBuffer):
# A monochrome approach should be used for coding this. The rgb method ensures # A monochrome approach should be used for coding this. The rgb method ensures
@ -122,6 +125,7 @@ class EPD(framebuf.FrameBuffer):
self.height = _EPD_HEIGHT self.height = _EPD_HEIGHT
self.buf = bytearray(_EPD_HEIGHT * _BWIDTH) self.buf = bytearray(_EPD_HEIGHT * _BWIDTH)
self.mvb = memoryview(self.buf) self.mvb = memoryview(self.buf)
self.ibuf = bytearray(1000) # Buffer for inverted pixels
mode = framebuf.MONO_HLSB mode = framebuf.MONO_HLSB
self.palette = BoolPalette(mode) self.palette = BoolPalette(mode)
super().__init__(self.buf, _EPD_WIDTH, _EPD_HEIGHT, mode) super().__init__(self.buf, _EPD_WIDTH, _EPD_HEIGHT, mode)
@ -252,44 +256,55 @@ class EPD(framebuf.FrameBuffer):
return not (self._busy or (self.busy_pin() == 0)) # 0 == busy return not (self._busy or (self.busy_pin() == 0)) # 0 == busy
@micropython.native @micropython.native
def _line(self, start, buf=bytearray(_BWIDTH)): # Sending 50 bytes 40us at 10MHz, 12ms for 300 lines def _bsend(self, start, nbytes):
_linv(buf, self.mvb[start:], _BWIDTH) # Invert image data for EPD buf = self.ibuf
_linv(buf, self.mvb[start:], nbytes >> 1) # Invert image data for EPD
self.send_bytes(buf) self.send_bytes(buf)
# Timing @10MHz/250MHz: full refresh 2.1s, partial 740ms # Time to convert and transmit 1000 bytes ~ 1ms: most of that is tx @ 10MHz
# Blocking with split=5 740/5=150ms # Yield every 16 transfers means blocking is ~16ms
async def _as_show(self, split): # Total convert and transmit time for 15000 bytes is ~15ms.
# Timing @10MHz/250MHz: full refresh 2.1s, partial 740ms: the bulk of the time
# is spent spinning on the busy pin and is CPU frequency independent.
async def _as_show(self):
self.send_command(b"\x13") self.send_command(b"\x13")
lps = _EPD_HEIGHT // split fbidx = 0 # Index into framebuf
idx = 0 nbytes = len(self.ibuf) # Bytes to send
#ttt = time.ticks_ms() nleft = len(self.buf) # Size of framebuf
for _ in range(split): # For each segment npass = 0
for _ in range(lps): while nleft > 0:
self._line(idx) self._bsend(fbidx, nbytes) # Invert, buffer and send nbytes
idx += _BWIDTH fbidx += nbytes # Adjust for bytes already sent
await asyncio.sleep_ms(0) nleft -= nbytes
#print("Time", time.ticks_diff(time.ticks_ms(), ttt)) nbytes = min(nbytes, nleft)
if not ((npass := npass + 1) % 16):
await asyncio.sleep_ms(0) # Control blocking time
self._updated.set() self._updated.set()
self.send_command(b"\x12") # Nonblocking .display_on() self.send_command(b"\x12") # Nonblocking .display_on()
while not self.busy_pin(): while not self.busy_pin(): # Wait on display hardware
await asyncio.sleep_ms(0) # About 1.7s for full refresh await asyncio.sleep_ms(0)
self._busy = False self._busy = False
#print("Time", time.ticks_diff(time.ticks_ms(), ttt)) # ~630ms
async def do_refresh(self, split): # For micro-gui async def do_refresh(self, split): # For micro-gui
assert (not self._busy), "Refresh while busy" assert (not self._busy), "Refresh while busy"
await self._as_show(split) # split=5 await self._as_show() # split=5
def show(self): # nanogui def show(self): # nanogui
if self._busy: if self._busy:
raise RuntimeError('Cannot refresh: display is busy.') raise RuntimeError('Cannot refresh: display is busy.')
self._busy = True # Immediate busy flag. Pin goes low much later. self._busy = True # Immediate busy flag. Pin goes low much later.
if self._asyn: if self._asyn:
asyncio.create_task(self._as_show(5)) # split into 5 segments asyncio.create_task(self._as_show())
return return
self.send_command(b"\x13") self.send_command(b"\x13")
for j in range(_EPD_HEIGHT): fbidx = 0 # Index into framebuf
self._line(j) nbytes = len(self.ibuf) # Bytes to send
nleft = len(self.buf) # Size of framebuf
while nleft > 0:
self._bsend(fbidx, nbytes) # Invert, buffer and send nbytes
fbidx += nbytes # Adjust for bytes already sent
nleft -= nbytes
nbytes = min(nbytes, nleft)
self._busy = False self._busy = False
self.display_on() self.display_on()
self.wait_until_ready() self.wait_until_ready()