diff --git a/DRIVERS.md b/DRIVERS.md index ed8be91..205164b 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -55,15 +55,21 @@ access via the `Writer` and `CWriter` classes is documented 5. [ePaper displays](./DRIVERS.md#5-epaper-displays) 5.1 [Adafruit monochrome eInk Displays](./DRIVERS.md#51-adafruit-monochrome-eink-displays)      5.1.1 [EPD constructor args](./DRIVERS.md#511-epd-constructor-args) -      5.1.2 [EPD public methods](./DRIVERS.md#512-epd-public-methods) -      5.1.3 [EPD public bound variables](./DRIVERS.md#513-epd-public-bound-variables) -      5.1.4 [FeatherWing Wiring](./DRIVERS.md#514-featherwing-wiring) -      5.1.5 [Micropower use](./DRIVERS.md#515-micropower-use) +      5.1.2 [Public methods](./DRIVERS.md#512-public-methods) +      5.1.3 [Events](./DRIVERS.md#513-events) +      5.1.4 [Public bound variables](./DRIVERS.md#514-public-bound-variables) +      5.1.5 [FeatherWing Wiring](./DRIVERS.md#515-featherwing-wiring) +      5.1.6 [Micropower use](./DRIVERS.md#516-micropower-use) 5.2 [Waveshare eInk Display HAT](./DRIVERS.md#52-waveshare-eink-display-hat) Pi HAT repurposed for MP hosts.      5.2.1 [EPD constructor args](./DRIVERS.md#521-epd-constructor-args) -      5.2.2 [EPD public methods](./DRIVERS.md#522-epd-public-methods) -      5.2.3 [EPD public bound variables](./DRIVERS.md#523-epd-public-bound-variables) +      5.2.2 [Public methods](./DRIVERS.md#522-public-methods) +      5.2.3 [Events](./DRIVERS.md#523-events) +      5.2.4 [public bound variables](./DRIVERS.md#524-public-bound-variables) 5.3 [Waveshare 400x300 Pi Pico display](./DRIVERS.md#53-waveshare-400x300-pi-pico-display) Excellent display can also be used with other hosts. +      5.3.1 [Constructor args](./DRIVERS.md#531-constructor-args) +      5.3.2 [Public methods](./DRIVERS.md#532-public-methods) +      5.3.3 [Events](./DRIVERS.md#533-events) +      5.3.4 [Public bound variables](./DRIVERS.md#534-public-bound-variables) 6. [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support) 7. [Writing device drivers](./DRIVERS.md#7-writing-device-drivers) 8. [Links](./DRIVERS.md#8-links) @@ -1036,9 +1042,10 @@ see below. * `asyn=False` Setting this `True` invokes an asynchronous mode. See [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). -### 5.1.2 EPD public methods +### 5.1.2 Public methods + +All methods are synchronous. -##### Synchronous methods * `init` No args. Issues a hardware reset and initialises the hardware. This is called by the constructor. It needs to explicitly be called to exit from a deep sleep. @@ -1049,12 +1056,18 @@ see below. a period: `ready` status should be checked before issuing `refresh`. * `wait_until_ready` No args. Pause until the device is ready. -##### Asynchronous methods - * `updated` Asynchronous. No args. Pause until the framebuffer has been copied - to the display. - * `wait` Asynchronous. No args. Pause until the display refresh is complete. +### 5.1.3 Events -### 5.1.3 EPD public bound variables +These provide synchronisation in asynchronous applications where `asyn=True`. +They are only needed in more advanced asynchronous applications and their use +is discussed in [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). + * `updated` Set when framebuf has been copied to device. It is now safe to + modify widgets without risk of display corruption. + * `complete` Set when display update is complete. It is now safe to call + `ssd.refresh()`. + EPD. + +### 5.1.4 Public bound variables * `height` Integer. Height in pixels. Treat as read-only. * `width` Integer. Width in pixels. Treat as read-only. @@ -1063,7 +1076,11 @@ see below. seconds to enable viewing. This enables generic nanogui demos to be run on an EPD. -### 5.1.4 FeatherWing wiring +Note that in synchronous applications with `demo_mode=False`, `refresh` returns +while the display is updating. Applications should issue `wait_until_ready` +before issuing another refresh. + +### 5.1.5 FeatherWing wiring The [pinout is listed here](https://learn.adafruit.com/adafruit-eink-display-breakouts/pinouts-2). The `busy` line is brought out to a labelled pad on the PCB. It can be linked @@ -1096,7 +1113,7 @@ The FeatherWing has a reset button which shorts the RST line to Gnd. To avoid risk of damage to the microcontroller pin if the button is pressed, the pin should be configured as open drain. -### 5.1.5 Micropower use +### 5.1.6 Micropower use Developers of micropower applications will need to familiarise themselves with the power saving features of their board. Information may be found in @@ -1210,12 +1227,14 @@ Pins 26-40 unused and omitted. * `rst` An initialised output pin. Initial value should be 1. * `busy` An initialised input pin. * `landscape=False` By default the long axis is vertical. - * `asyn=False` + * `asyn=False` Setting this `True` invokes an asynchronous mode. See + [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). ### 5.2.2 EPD public methods -##### Synchronous methods - * `init` No args. Issues a hardware reset and initialises the hardware. This +All methods are synchronous. + +* `init` No args. Issues a hardware reset and initialises the hardware. This is called by the constructor. It needs to explicitly be called to exit from a deep sleep. * `sleep` No args. Puts the display into deep sleep. If called while a refresh @@ -1225,12 +1244,17 @@ Pins 26-40 unused and omitted. a period: `ready` status should be checked before issuing `refresh`. * `wait_until_ready` No args. Pause until the device is ready. -##### Asynchronous methods - * `updated` Asynchronous. No args. Pause until the framebuffer has been copied - to the display. - * `wait` Asynchronous. No args. Pause until the display refresh is complete. +### 5.2.3 Events -### 5.2.3 EPD public bound variables +These provide synchronisation in asynchronous applications where `asyn=True`. +They are only needed in more advanced asynchronous applications and their use +is discussed in [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). + * `updated` Set when framebuf has been copied to device. It is now safe to + modify widgets without risk of display corruption. + * `complete` Set when display update is complete. It is now safe to call + `ssd.refresh()`. + +### 5.2.4 Public bound variables * `height` Integer. Height in pixels. Treat as read-only. * `width` Integer. Width in pixels. Treat as read-only. @@ -1239,6 +1263,10 @@ Pins 26-40 unused and omitted. seconds to enable viewing. This enables generic nanogui demos to be run on an EPD. +Note that in synchronous applications with `demo_mode=False`, `refresh` returns +while the display is updating. Applications should issue `wait_until_ready` +before issuing another refresh. + ## 5.3 Waveshare 400x300 Pi Pico display The driver for this display now supports partial updates. @@ -1255,6 +1283,8 @@ gc.collect() # Precaution before instantiating framebuf. ssd = SSD() # Create a display instance. For normal applications. # ssd = SSD(asyn=True) # Alternative for asynchronous applications. ``` +### 5.3.1 Constructor args + For other hosts the pins need to be specified in `color_setup.py` via the following constructor args: @@ -1266,7 +1296,9 @@ following constructor args: * `asyn=False` Set `True` for asynchronous applications. Leave `False` for microgui where the arg has no effect. -##### Synchronous methods +### 5.3.2 Public methods + +All methods are synchronous. * `init` No args. Issues a hardware reset and initialises the hardware. This is called by the constructor. It needs to explicitly be called to exit from a @@ -1280,22 +1312,37 @@ following constructor args: * `set_partial()` Enable partial updates. * `set_full()` Restore normal update operation. - After issuing `set_partial()`, subsequent updates will be partial. Normal +After issuing `set_partial()`, subsequent updates will be partial. Normal updates are restored by issuing `set_full()`. These methods should not be issued while an update is in progress. Partial updates are fast and visually unobtrusive but they are prone to ghosting. -##### Asynchronous methods +### 5.3.3 Events - * `wait` No args. If an update is in progress, pause until the display refresh - is complete, otherwise return is immediate. - * `updated` No args. Pause until the framebuffer has been copied to the - display. It is now safe to modify the framebuf, but display update may still - be in progress. +These provide synchronisation in asynchronous applications where `asyn=True`. +They are only needed in more advanced asynchronous applications and their use +is discussed in [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). + * `updated` Set when framebuf has been copied to device. It is now safe to + modify widgets without risk of display corruption. + * `complete` Set when display update is complete. It is now safe to call + `ssd.refresh()`. -###### [Contents](./DRIVERS.md#contents) +### 5.3.4 Public bound variables + + * `height` Integer. Height in pixels. Treat as read-only. + * `width` Integer. Width in pixels. Treat as read-only. + * `demo_mode=False` Boolean. If set `True` after instantiating, `refresh()` + will block until display update is complete, and then for a further two + seconds to enable viewing. This enables generic nanogui demos to be run on an + EPD. + +Note that in synchronous applications with `demo_mode=False`, `refresh` returns +while the display is updating. Applications should issue `wait_until_ready` +before issuing another refresh. + + ###### [Contents](./DRIVERS.md#contents) # 6. EPD Asynchronous support @@ -1303,9 +1350,9 @@ The following applies to nano-gui. Under micro-gui the update mechanism is a background task. Use with micro-gui is covered [here](https://github.com/peterhinch/micropython-micro-gui/blob/main/README.md#10-epaper-displays). Further, the comments address the case where the driver is instantiated with -`asyn=True`. In the default case an EPD can be used like any other display. +`asyn=True`. -When GUI code issues +When synchronous code issues ```python refresh(ssd) # Several seconds on an EPD ``` @@ -1313,45 +1360,48 @@ the GUI updates the frame buffer contents and calls the device driver's `show` method. This causes the contents to be copied to the display hardware and a redraw to be inititated. This typically takes several seconds unless partial updates are enabled. The method (and hence `refresh`) blocks until the physical -refresh is complete. The device drivers block for an additional 2 seconds: this -enables demos written for normal displays to work (the 2 second pause allowing -the result of each refresh to be seen). +refresh is complete. If `demo_mode` is set, device drivers block for an +additional 2 seconds to enable demos written for normal displays to work (the +2 second pause allows the result of each refresh to be seen). This long blocking period is not ideal in asynchronous code, and the process is modified if, in `color_setup.py`, an `EPD` is instantiated with `asyn=True`. In this case `refresh` calls the `show` method as before, but `show` creates a task `._as_show` and returns immediately. The task yields to the scheduler as -necessary to ensure that blocking is limited to around 30ms. With `asyn=True` -synchronous applications will not work: it is necessary to take control of the -sequencing of refresh. +necessary to ensure that blocking is limited to around 30ms. If screen updates +take place at a low rate the only precaution necessary is to ensure that +sufficient time elapses between calls to `ssd.refresh()` for the update to +complete. For example the following code fragment illustrates an application +which performs a full EPD refresh once per minute: -In this case user code should ensure that changes to the framebuffer are -postponed until the buffer contents have been copied to the display. Further, a -subsequent refresh should be postponed until the physical refresh is complete. -To achieve this the `ssd` instance has the following methods: - * `.updated()` (async) Pauses until the buffer is copied to the device. - * `.wait()` (async) Pauses until physical refresh is complete. - * `.ready()` (synchronous) Immediate return: `True` if physical refresh is - complete. - -If `.refresh()` is issued before the physical display refresh is complete a -`RuntimeError` will occur. - -The following illustrates the kind of approach which may be used with a display -instantiated with `asyn=True`: ```python +async def run(): while True: - # Before refresh, ensure that a previous refresh is complete - # Not strictly necessary if .updated() used after refresh. - await ssd.wait() - refresh(ssd) # Immediate return. Creates a task to copy content to EPD. - # Wait until the framebuf content has been passed to EPD. - await ssd.updated() - # Trigger an event which allows other tasks to update the - # framebuffer in background - evt.set() # Waiting task must clear the Event - await asyncio.sleep(180) # + # get data + # Update screen widgets + ssd.refresh() # Launches background refresh + await asyncio.sleep(60) ``` +With `asyn=True` other running tasks experience latency measured in tens of ms. + +Finer control is available using the two public bound `Event` instances. This +fragment assumes an application with a single task performing refreshes. The +application has two `Event` instances, one requesting refresh and the other +requesting widget updates: +```python +async def refresh_task(): + while True: + await refresh_request.wait() # Another task has requested refresh + refresh_request.clear() + ssd.refresh() # Launch background refresh + await ssd.updated.wait() # Wait until framebuf copied to device + data_request.set() # Ask other tasks to update widgets + await ssd.complete.wait() + # Now safe to respond to refresh_request and issue ssd.refresh() +``` +The `updated` and `complete` events are cleared when `ssd.refresh` is called +and are set as the background refresh proceeds. + Some displays support partial updates. This is currently restricted to the [Pico Epaper 4.2"](https://www.waveshare.com/pico-epaper-4.2.htm). Partial updates are much faster and are visually non-intrusive at a cost of "ghosting" @@ -1363,7 +1413,7 @@ synchronous methods are provided: These must not be issued while an update is in progress. See the demo `eclock_async.py` for an example of managing partial updates: once -per hour a full update is performed. +per hour (on the half-hour) a full update is performed. ###### [Contents](./DRIVERS.md#contents) diff --git a/drivers/epaper/epaper2in7_fb.py b/drivers/epaper/epaper2in7_fb.py index 69b7484..8da044d 100644 --- a/drivers/epaper/epaper2in7_fb.py +++ b/drivers/epaper/epaper2in7_fb.py @@ -32,7 +32,8 @@ class EPD(framebuf.FrameBuffer): self._lsc = landscape self._asyn = asyn self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1). - self._updated = asyncio.Event() + self.updated = asyncio.Event() + self.complete = asyncio.Event() # Dimensions in pixels. Waveshare code is portrait mode. # Public bound variables required by nanogui. self.width = 264 if landscape else 176 @@ -130,15 +131,6 @@ class EPD(framebuf.FrameBuffer): dt = ticks_diff(ticks_ms(), t) print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000)) - async def wait(self): - await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready - while not self.ready(): - await asyncio.sleep_ms(100) - - # Pause until framebuf has been copied to device. - async def updated(self): - await self._updated.wait() - # For polling in asynchronous code. Just checks pin state. # 0 == busy. Comment in official code is wrong. Code is correct. def ready(self): @@ -196,13 +188,13 @@ class EPD(framebuf.FrameBuffer): await asyncio.sleep_ms(0) t = ticks_ms() - self._updated.set() # framebuf has now been copied to the device - self._updated.clear() + self.updated.set() # framebuf has now been copied to the device cmd(b'\x12') # DISPLAY_REFRESH await asyncio.sleep(1) while self._busy() == 0: await asyncio.sleep_ms(200) # Don't release lock until update is complete self._as_busy = False + self.complete.set() # draw the current frame memory. Blocking time ~180ms def show(self, buf1=bytearray(1)): @@ -210,6 +202,8 @@ class EPD(framebuf.FrameBuffer): if self._as_busy: raise RuntimeError('Cannot refresh: display is busy.') self._as_busy = True + self.updated.clear() + self.complete.clear() asyncio.create_task(self._as_show()) return t = ticks_us() diff --git a/drivers/epaper/epd29.py b/drivers/epaper/epd29.py index 8241fa8..9f416ee 100644 --- a/drivers/epaper/epd29.py +++ b/drivers/epaper/epd29.py @@ -42,7 +42,8 @@ class EPD(framebuf.FrameBuffer): # ._as_busy is set immediately on start of task. Cleared # when busy pin is logically false (physically 1). self._as_busy = False - self._updated = asyncio.Event() + self.updated = asyncio.Event() + self.complete = asyncio.Event() # Public bound variables required by nanogui. # Dimensions in pixels as seen by nanogui (landscape mode). self.width = 296 if landscape else 128 @@ -114,16 +115,6 @@ class EPD(framebuf.FrameBuffer): while not self.ready(): sleep_ms(100) - # Asynchronous wait on ready state. Pause (4.9s) for physical refresh. - async def wait(self): - await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready - while not self.ready(): - await asyncio.sleep_ms(100) - - # Pause until framebuf has been copied to device. - async def updated(self): - await self._updated.wait() - # Return immediate status. Pin state: 0 == busy. def ready(self): return not(self._as_busy or (self._busy() == 0)) @@ -162,8 +153,7 @@ class EPD(framebuf.FrameBuffer): t = ticks_ms() cmd(b'\x11') # Data stop - self._updated.set() - self._updated.clear() + self.updated.set() sleep_us(20) # Allow for data coming back: currently ignore this cmd(b'\x12') # DISPLAY_REFRESH # busy goes low now, for ~4.9 seconds. @@ -171,6 +161,7 @@ class EPD(framebuf.FrameBuffer): while self._busy() == 0: await asyncio.sleep_ms(200) self._as_busy = False + self.complete.set() # draw the current frame memory. def show(self, buf1=bytearray(1)): @@ -178,6 +169,8 @@ class EPD(framebuf.FrameBuffer): if self._as_busy: raise RuntimeError('Cannot refresh: display is busy.') self._as_busy = True # Immediate busy flag. Pin goes low much later. + self.updated.clear() + self.complete.clear() asyncio.create_task(self._as_show()) return diff --git a/drivers/epaper/pico_epaper_42.py b/drivers/epaper/pico_epaper_42.py index 19b2e08..dc67895 100644 --- a/drivers/epaper/pico_epaper_42.py +++ b/drivers/epaper/pico_epaper_42.py @@ -120,10 +120,17 @@ class EPD(framebuf.FrameBuffer): self.spi.init(baudrate = 4_000_000) self._asyn = asyn self._busy = False # Set immediately on .show(). Cleared when busy pin is logically false (physically 1). - self._updated = asyncio.Event() + self.updated = asyncio.Event() + self.complete = asyncio.Event() + # Public bound variables required by nanogui. + # Dimensions in pixels as seen by nanogui self.width = _EPD_WIDTH self.height = _EPD_HEIGHT + # Other public bound variable. + # Special mode enables demos written for generic displays to run. + self.demo_mode = False + self.buf = bytearray(_EPD_HEIGHT * _BWIDTH) self.mvb = memoryview(self.buf) self.ibuf = bytearray(1000) # Buffer for inverted pixels @@ -241,16 +248,6 @@ class EPD(framebuf.FrameBuffer): while not self.ready(): time.sleep_ms(100) - async def wait(self): - while not self.ready(): - await asyncio.sleep_ms(100) - - # Pause until framebuf has been copied to device. - async def updated(self): - self._updated.clear() - await self._updated.wait() - self._updated.clear() - # For polling in asynchronous code. Just checks pin state. # 0 == busy. Comment in official code is wrong. Code is correct. def ready(self): @@ -280,11 +277,12 @@ class EPD(framebuf.FrameBuffer): 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() while not self.busy_pin(): # Wait on display hardware await asyncio.sleep_ms(0) self._busy = False + self.complete.set() async def do_refresh(self, split): # For micro-gui assert (not self._busy), "Refresh while busy" @@ -295,6 +293,8 @@ class EPD(framebuf.FrameBuffer): raise RuntimeError('Cannot refresh: display is busy.') self._busy = True # Immediate busy flag. Pin goes low much later. if self._asyn: + self.updated.clear() + self.complete.clear() asyncio.create_task(self._as_show()) return self.send_command(b"\x13") @@ -308,6 +308,10 @@ class EPD(framebuf.FrameBuffer): nbytes = min(nbytes, nleft) self._busy = False self.display_on() + if not self.demo_mode: + # Immediate return to avoid blocking the whole application. + # User should wait for ready before calling refresh() + return self.wait_until_ready() time.sleep_ms(2000) # Give time for user to see result diff --git a/extras/demos/eclock_async.py b/extras/demos/eclock_async.py index 6fb9eb5..7bb0fc1 100644 --- a/extras/demos/eclock_async.py +++ b/extras/demos/eclock_async.py @@ -31,12 +31,12 @@ async def test(): wri.set_clip(True, True, False) # Clip to screen, no wrap refresh(ssd, True) if epaper: - await ssd.wait() + await ssd.complete.wait() ec = EClock(wri, 10, 10, 200, fgcolor=WHITE, bgcolor=BLACK) ec.value(t := time.localtime()) # Initial drawing refresh(ssd) if epaper: - await ssd.wait() + await ssd.complete.wait() mins = t[4] while True: @@ -51,7 +51,7 @@ async def test(): ec.value(t) refresh(ssd) if epaper: - await ssd.wait() + await ssd.complete.wait() await asyncio.sleep(10) try: