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.
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.
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 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).
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
[Waveshare pico_epaper_42](https://www.waveshare.com/pico-epaper-4.2.htm). This
supports partial updates which work remarkably well with minimal ghosting. Note
that it can be used with hosts other than the Pico via the supplied cable. See
[ePaper displays](./README.md#10-epaper-displays).
[Waveshare pico_epaper_42](https://www.waveshare.com/pico-epaper-4.2.htm). See
[10. ePaper displays](./README.md#10-epaper-displays).
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
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)
is currently the only fully supported ePaper display, with a hardware_setup.py
copied or adapted from `setup_examples/pico_epaper_42_pico.py`. After an
initial refresh the driver is put into partial mode to provide reasonably
quick and visually satisfactory response to button events. However ghosting may
accumulate after long periods of running, and an application may occasionally
need to perform a full refresh. This requires the "done" interlock described
in section 9.
has quite a low level of ghosting. A full refresh takes about 2.1s and partial
about 740ms. In use there is a visible lag between operating a user control and
a visible response, but it is usable. Currently this is the only fully
supported ePaper display.
It has a socket for a Pico or Pico W, but also comes with a cable suitable for
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
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"
# 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
def _linv(dest:ptr8, source:ptr8, length:int):
def _linv(dest:ptr16, source:ptr16, length:int):
for n in range(length):
c = source[n]
dest[n] = c ^ 0xFF
c: uint = source[n]
dest[n] = c ^ 0xFFFF
class EPD(framebuf.FrameBuffer):
# 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.buf = bytearray(_EPD_HEIGHT * _BWIDTH)
self.mvb = memoryview(self.buf)
self.ibuf = bytearray(1000) # Buffer for inverted pixels
mode = framebuf.MONO_HLSB
self.palette = BoolPalette(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
@micropython.native
def _line(self, start, buf=bytearray(_BWIDTH)): # Sending 50 bytes 40us at 10MHz, 12ms for 300 lines
_linv(buf, self.mvb[start:], _BWIDTH) # Invert image data for EPD
def _bsend(self, start, nbytes):
buf = self.ibuf
_linv(buf, self.mvb[start:], nbytes >> 1) # Invert image data for EPD
self.send_bytes(buf)
# Timing @10MHz/250MHz: full refresh 2.1s, partial 740ms
# Blocking with split=5 740/5=150ms
async def _as_show(self, split):
# Time to convert and transmit 1000 bytes ~ 1ms: most of that is tx @ 10MHz
# Yield every 16 transfers means blocking is ~16ms
# 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")
lps = _EPD_HEIGHT // split
idx = 0
#ttt = time.ticks_ms()
for _ in range(split): # For each segment
for _ in range(lps):
self._line(idx)
idx += _BWIDTH
await asyncio.sleep_ms(0)
#print("Time", time.ticks_diff(time.ticks_ms(), ttt))
fbidx = 0 # Index into framebuf
nbytes = len(self.ibuf) # Bytes to send
nleft = len(self.buf) # Size of framebuf
npass = 0
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)
if not ((npass := npass + 1) % 16):
await asyncio.sleep_ms(0) # Control blocking time
self._updated.set()
self.send_command(b"\x12") # Nonblocking .display_on()
while not self.busy_pin():
await asyncio.sleep_ms(0) # About 1.7s for full refresh
while not self.busy_pin(): # Wait on display hardware
await asyncio.sleep_ms(0)
self._busy = False
#print("Time", time.ticks_diff(time.ticks_ms(), ttt)) # ~630ms
async def do_refresh(self, split): # For micro-gui
assert (not self._busy), "Refresh while busy"
await self._as_show(split) # split=5
await self._as_show() # split=5
def show(self): # nanogui
if self._busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._busy = True # Immediate busy flag. Pin goes low much later.
if self._asyn:
asyncio.create_task(self._as_show(5)) # split into 5 segments
asyncio.create_task(self._as_show())
return
self.send_command(b"\x13")
for j in range(_EPD_HEIGHT):
self._line(j)
fbidx = 0 # Index into framebuf
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.display_on()
self.wait_until_ready()