diff --git a/README.md b/README.md index ea19e36..711f9df 100644 --- a/README.md +++ b/README.md @@ -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(): diff --git a/drivers/epaper/pico_epaper_42.py b/drivers/epaper/pico_epaper_42.py index e63cf14..8b0c0c7 100644 --- a/drivers/epaper/pico_epaper_42.py +++ b/drivers/epaper/pico_epaper_42.py @@ -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()