From b9506de37a1f656ff7966d91f7887008b002dc6f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 19 Jul 2024 13:56:20 +0100 Subject: [PATCH] Minor changes to driver pico_epaper_42_v2.py. --- DRIVERS.md | 63 ++++++++++-------- drivers/epaper/pico_epaper_42_v2.py | 99 ++++++++++++++++------------- 2 files changed, 92 insertions(+), 70 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index eabf1b7..823024d 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -1379,9 +1379,9 @@ before issuing another refresh. ## 5.3 Waveshare 400x300 Pi Pico display -This display has excellent support for partial updates which are fast, visually -unobtrusive updates. They have the drawback of "ghosting" where the remnants of -the previous image is visible. At any time a full update may be performed which +This display has excellent support for partial updates which are fast and +visually unobtrusive. They have the drawback of "ghosting" where remnants of the +previous image are visible. At any time a full update may be performed which removes all trace of ghosting. This model of display has low levels of ghosting and thus is supported by micro-gui. The model supports hosts other than the Pico via a supplied cable. @@ -1404,11 +1404,11 @@ All drivers have identical args and methods. The 4.2" displays support a Pi Pico or Pico W plugged into the rear of the unit. Alternatively it can be connected to any other host using the supplied -cable. With a Pico variant the `color_setup` file is very simple: +cable. With a Pico variant plugged in the `color_setup` file is very simple: ```python import machine import gc -from drivers.epaper.pico_epaper_42 import EPD as SSD +from drivers.epaper.pico_epaper_42_v2 import EPD as SSD # V2 driver gc.collect() # Precaution before instantiating framebuf. ssd = SSD() # Create a display instance based on a Pico in socket. @@ -1424,33 +1424,39 @@ following constructor args: * `rst=None` A `Pin` instance defined as `Pin.OUT`. * `busy=None` A `Pin` instance defined as `Pin.IN, Pin.PULL_UP`. -The `asyn` arg has been removed: the driver now detects asynchronous use. - ### 5.3.2 Public methods -All methods are synchronous. +All methods are synchronous. Common API (nanogui and microgui): -* `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. `sleep` should be called - before a power down to avoid leaving the display in an abnormal state. See note - on current consumption. - * `ready` No args. After issuing a `refresh` the device will become busy for - a period: `ready` status should be checked before issuing `refresh`. - * `wait_until_ready` No args. Pause until the device is ready. * `set_partial()` Enable partial updates (does nothing on greyscale driver). * `set_full()` Restore normal update operation (null on greyscale driver). -On the 1-bit driver, 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. + On the 1-bit driver, 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. In the case of synchronous + applications, issue `.wait_until_ready`. Asynchronous and microgui applications + should wait on the `rfsh_done` event. + +Nanogui API: + + * `sleep` No args. Applications should call this before power down to ensure + the display is put into the correct state. + * `ready` No args. After issuing a `refresh` the device will become busy for + a period: `ready` status should be checked before issuing `refresh`. + * `wait_until_ready` No args. Pause until the device is ready. This should be + run before issuing `refresh` or `sleep`. + * `init` No args. Issues a hardware reset and initialises the hardware. This + is called by the constructor. It may be used to recover from a `sleep` state + but this is not recommended for V2 displays (see note on current consumption). ### 5.3.3 Events These provide synchronisation in asynchronous applications. They are only needed in more advanced asynchronous applications and their use is discussed in -[EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). +[EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support). They are +necessary in microgui applications to synchronise changes between partial and +full refrresh modes. See +[this demo](https://github.com/peterhinch/micropython-micro-gui/blob/main/gui/demos/epaper.py). * `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 @@ -1465,9 +1471,13 @@ needed in more advanced asynchronous applications and their use is discussed in seconds to enable viewing. This enables generic nanogui demos to be run on an EPD. - Class variable: - * `MAXBLOCK = 25` Defines the maximum period (in ms) that an asynchronous + The following are intended for use in micro-gui applications: + + * `maxblock=25` Defines the maximum period (in ms) that the asynchronous refresh can block before yielding to the scheduler. + * `blank_on_exit=True` On application shutdown by default the display is + cleared. Setting this `False` overrides this, leaving the display contents in + place. Note that in synchronous applications with `demo_mode=False`, `refresh` returns while the display is updating. Applications should issue `wait_until_ready` @@ -1499,14 +1509,17 @@ Color values of 0 (white) to 3 (black) can explicitly be specified. ### 5.3.6 Current consumption -This was measured on a V2 display. +This was measured on a V2 display. The Waveshare driver has a `sleep` method +which claims to put the device into a deep sleep mode. Their docs indicate a +sleep current of 0.01μA. This was not borne out by measurement: * ~5mA while doing a full update. * ~1.2mA while running the micro-gui epaper.py demo. This performs continuous partial updates. * 92μA while inactive. * 92μA after running `.sleep`. Conclusion: there is no reason to call `.sleep` other than in preparation for a -shutdown. +shutdown, consequently the method is not provided. I believe the discrepancy is +caused by the supply current of the level translator. ###### [Contents](./DRIVERS.md#contents) diff --git a/drivers/epaper/pico_epaper_42_v2.py b/drivers/epaper/pico_epaper_42_v2.py index 862d7f5..70d32e5 100644 --- a/drivers/epaper/pico_epaper_42_v2.py +++ b/drivers/epaper/pico_epaper_42_v2.py @@ -1,3 +1,5 @@ +# pico_epaper_42_v2.py + # Materials used for discovery can be found here # https://www.waveshare.com/wiki/4.2inch_e-Paper_Module_Manual#Introduction # Note, at the time of writing this, none of the source materials have working @@ -28,7 +30,11 @@ # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -# + +# Waveshare URLs +# Main page: https://www.waveshare.com/pico-epaper-4.2.htm +# Wiki: https://www.waveshare.com/wiki/Pico-ePaper-4.2 +# Code: https://github.com/waveshareteam/Pico_ePaper_Code/blob/main/python/Pico-ePaper-4.2_V2.py from machine import Pin, SPI import framebuf @@ -69,7 +75,6 @@ def _linv(dest: ptr32, source: ptr32, length: int): class EPD(framebuf.FrameBuffer): - MAXBLOCK = 25 # Max async blocking time in ms # A monochrome approach should be used for coding this. The rgb method ensures # nothing breaks if users specify colors. @staticmethod @@ -81,15 +86,14 @@ class EPD(framebuf.FrameBuffer): self._busy_pin = Pin(_BUSY_PIN, Pin.IN, Pin.PULL_UP) if busy is None else busy self._cs = Pin(_CS_PIN, Pin.OUT) if cs is None else cs self._dc = Pin(_DC_PIN, Pin.OUT) if dc is None else dc - self._spi = ( - SPI(1, sck=Pin(10), mosi=Pin(11), miso=Pin(28)) if spi is None else spi - ) + self._spi = SPI(1, sck=Pin(10), mosi=Pin(11), miso=Pin(28)) if spi is None else spi self._spi.init(baudrate=4_000_000) # Busy flag: set immediately on .show(). Cleared when busy pin is logically false. self._busy = False # Async API self.updated = asyncio.Event() self.complete = asyncio.Event() + self.maxblock = 25 # partial refresh self._partial = False @@ -100,6 +104,7 @@ class EPD(framebuf.FrameBuffer): # Other public bound variable. # Special mode enables demos written for generic displays to run. self.demo_mode = False + self.blank_on_exit = True self._buf = bytearray(_EPD_HEIGHT * _BWIDTH) self._mvb = memoryview(self._buf) @@ -130,7 +135,7 @@ class EPD(framebuf.FrameBuffer): self._spi.write(data) self._cs(1) - def display_on(self): + def _display_on(self): if self._partial: self._command(b"\x22") self._data(b"\xFF") @@ -140,6 +145,7 @@ class EPD(framebuf.FrameBuffer): self._data(b"\xF7") self._command(b"\x20") + # Called by constructor. Application use is deprecated. def init(self): self.reset() # hardware reset @@ -147,8 +153,9 @@ class EPD(framebuf.FrameBuffer): self.wait_until_ready() self.set_full() - self.display_on() + self._display_on() + # Common API def set_full(self): self._partial = False @@ -194,10 +201,15 @@ class EPD(framebuf.FrameBuffer): self._spi.write(buf) self._cs(1) + # Send the frame buffer. If running asyncio, return whenever MAXBLOCK ms elapses + # so that caller can yield to the scheduler. + # Returns no. of bytes outstanding. def _send_bytes(self): fbidx = 0 # Index into framebuf nbytes = len(self._ibuf) # Bytes to send nleft = len(self._buf) # Size of framebuf + asyn = asyncio_running() + def inner(): nonlocal fbidx nonlocal nbytes @@ -208,20 +220,30 @@ class EPD(framebuf.FrameBuffer): fbidx += nbytes # Adjust for bytes already sent nleft -= nbytes nbytes = min(nbytes, nleft) - if time.ticks_diff(time.ticks_ms(), ts) > EPD.MAXBLOCK: - return nbytes # Probably not all done; quit and call again + if asyn and time.ticks_diff(time.ticks_ms(), ts) > self.maxblock: + return nbytes # Probably not all done; quit. Caller yields, calls again return 0 # All done + return inner - # Specific method for micro-gui. Unsuitable EPD's lack this method. Micro-gui - # does not test for asyncio as this is guaranteed to be up. - async def do_refresh(self, split): + # micro-gui API; asyncio is running. + async def do_refresh(self, split): # split = 5 assert not self._busy, "Refresh while busy" if self._partial: - await self._as_show_partial() # split=5 + await self._as_show_partial() else: - await self._as_show_full() # split=5 + await self._as_show_full() + def shutdown(self, clear=False): + time.sleep(1) # Empirically necessary (ugh) + self.fill(0) + self.set_full() + if clear or self.blank_on_exit: + self.show() + self.wait_until_ready() + self.sleep() + + # nanogui API def show(self): if self._busy: raise RuntimeError("Cannot refresh: display is busy.") @@ -229,6 +251,12 @@ class EPD(framebuf.FrameBuffer): self._show_partial() else: self._show_full() + 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) # Demo mode: give time for user to see result def _show_full(self): self._busy = True # Immediate busy flag. Pin goes low much later. @@ -238,39 +266,29 @@ class EPD(framebuf.FrameBuffer): asyncio.create_task(self._as_show_full()) return + # asyncio is not running, hence sb() will not time out. self._command(b"\x24") sb = self._send_bytes() # Instantiate closure - while sb(): - pass + sb() # Run to completion self._command(b"\x26") - sb = self._send_bytes() # Instantiate closure - while sb(): - pass + sb = self._send_bytes() # Create new instance + sb() 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) # Demo mode: give time for user to see result + self._display_on() async def _as_show_full(self): self._command(b"\x24") sb = self._send_bytes() # Instantiate closure while sb(): - await asyncio.sleep_ms(0) + await asyncio.sleep_ms(0) # Timed out. Yield and continue. self._command(b"\x26") - sb = self._send_bytes() # Instantiate closure + sb = self._send_bytes() # New closure instance while sb(): await asyncio.sleep_ms(0) self.updated.set() - self.display_on() + self._display_on() while self._busy_pin(): await asyncio.sleep_ms(0) self._busy = False @@ -286,19 +304,9 @@ class EPD(framebuf.FrameBuffer): self._command(b"\x24") sb = self._send_bytes() # Instantiate closure - while sb(): - pass + sb() 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) # Demo mode: give time for user to see result + self._display_on() async def _as_show_partial(self): self._command(b"\x24") @@ -307,12 +315,13 @@ class EPD(framebuf.FrameBuffer): await asyncio.sleep_ms(0) self.updated.set() - self.display_on() + self._display_on() while self._busy_pin(): await asyncio.sleep_ms(0) self._busy = False self.complete.set() + # nanogui API def wait_until_ready(self): while not self.ready(): time.sleep_ms(100)