From f4c6631c916b90d6a6108de745d71770bf085adc Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 13 Jan 2021 17:21:32 +0000 Subject: [PATCH] epd29.py passed initial tests. --- color_setup/epd96_demo.py | 40 +++++++++++++ drivers/epaper/epd29.py | 117 +++++++++++++++++++++++++------------ gui/demos/epd29_test.py | 119 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 38 deletions(-) create mode 100644 color_setup/epd96_demo.py create mode 100644 gui/demos/epd29_test.py diff --git a/color_setup/epd96_demo.py b/color_setup/epd96_demo.py new file mode 100644 index 0000000..6975eaf --- /dev/null +++ b/color_setup/epd96_demo.py @@ -0,0 +1,40 @@ +# epd96_demo.py Allow standard demos to run on ePaper. Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# As written, supports Adafruit 2.9" monochrome EPD with interface board. +# Interface breakout: https://www.adafruit.com/product/4224 +# Display: https://www.adafruit.com/product/4262 + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +# WIRING. Adafruit schematic is incorrect in that it references a nonexistent +# SD card so that interface has an extra pin. +# Pyb Breakout +# Vin Vin (1) +# Gnd Gnd (3) +# Y8 MOSI (6) +# Y6 SCK (4) +# Y4 BUSY (11) (Low = Busy) +# Y3 RST (10) +# Y2 CS (7) +# Y1 DC (8) +import machine +import gc + +from drivers.epaper.epd29 import EPD as SSD + +pdc = machine.Pin('Y1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1) +pbusy = machine.Pin('Y4', machine.Pin.IN) + +# Baudrate from https://learn.adafruit.com/adafruit-eink-display-breakouts/circuitpython-code-2 +# Datasheet P35 indicates up to 10MHz. +spi = machine.SPI(2, baudrate=1_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, pbusy) # Create a display instance +ssd.demo_mode = True diff --git a/drivers/epaper/epd29.py b/drivers/epaper/epd29.py index a78f049..7e329c2 100644 --- a/drivers/epaper/epd29.py +++ b/drivers/epaper/epd29.py @@ -19,8 +19,11 @@ import framebuf import uasyncio as asyncio +from micropython import const from time import sleep_ms, sleep_us, ticks_ms, ticks_us, ticks_diff +_MAX_BLOCK = const(20) # Maximum blocking time (ms) for asynchronous show. + class EPD(framebuf.FrameBuffer): # A monochrome approach should be used for coding this. The rgb method ensures # nothing breaks if users specify colors. @@ -34,24 +37,22 @@ class EPD(framebuf.FrameBuffer): self._dc = dc self._rst = rst # Active low. self._busy = busy # Active low on IL0373 - self._lsc = True # TODO this is here for test purposes. There is only one mode. - if asyn: - self._lock = asyncio.Lock() self._asyn = asyn # ._as_busy is set immediately on start of task. Cleared # when busy pin is logically false (physically 1). self._as_busy = False - # Public bound variables. - # Ones required by nanogui. + self._updated = asyncio.Event() + # Public bound variables required by nanogui. # Dimensions in pixels as seen by nanogui (landscape mode). self.width = 296 self.height = 128 + # Other public bound variable. # Special mode enables demos written for generic displays to run. self.demo_mode = False self._buffer = bytearray(self.height * self.width // 8) self._mvb = memoryview(self._buffer) - mode = framebuf.MONO_VLSB if self._lsc else framebuf.MONO_HLSB # TODO check this and set mode permanently + mode = framebuf.MONO_VLSB super().__init__(self._buffer, self.width, self.height, mode) self.init() @@ -102,7 +103,7 @@ class EPD(framebuf.FrameBuffer): cmd(b'\x61', b'\x80\x01\x28') # Note hex(296) == 0x128 # Set VCM_DC. 0 is datasheet default. I think Adafruit send 0x50 (-2.6V) rather than 0x12 (-1.0V) # https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/17 - cmd(b'\x82', b'\x12') # Set Vcom to -1.0V + cmd(b'\x82', b'\x12') # Set Vcom to -1.0V is my guess at Adafruit's intention. sleep_ms(50) print('Init Done.') @@ -116,53 +117,93 @@ class EPD(framebuf.FrameBuffer): dt = ticks_diff(ticks_ms(), t) print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000)) - # Asynchronous wait on ready state. + # Asynchronous wait on ready state. Pause (4.9s) for physical refresh. 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): + await self._updated.wait() + # Return immediate status. Pin state: 0 == busy. def ready(self): return not(self._as_busy or (self._busy() == 0)) - # draw the current frame memory. - def show(self, buf1=bytearray(1)): - #if self._asyn: - #self._as_busy = True - #asyncio.create_task(self._as_show()) - #return - t = ticks_us() + async def _as_show(self, buf1=bytearray(1)): mvb = self._mvb cmd = self._command + dat = self._data + cmd(b'\x13') + t = ticks_ms() + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for i in range(len(mvb)): + buf1[0] = mvb[idx] ^ 0xff + dat(buf1) + idx -= wid + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + if not(i & 0x0f) and (ticks_diff(ticks_ms(), t) > _MAX_BLOCK): + await asyncio.sleep_ms(0) + t = ticks_ms() + cmd(b'\x11') # Data stop + self._updated.set() + self._updated.clear() + sleep_us(20) # Allow for data coming back: currently ignore this + cmd(b'\x12') # DISPLAY_REFRESH + # busy goes low now, for ~4.9 seconds. + await asyncio.sleep(1) + while self._busy() == 0: + await asyncio.sleep_ms(200) + self._as_busy = False + + # draw the current frame memory. + def show(self, buf1=bytearray(1)): + if self._asyn: + if self._as_busy: + raise RuntimeError('Cannot refresh: display is busy.') + self._as_busy = True # Immediate busy flag. Pin goes low much later. + asyncio.create_task(self._as_show()) + return + + # t = ticks_us() + mvb = self._mvb + cmd = self._command + dat = self._data # DATA_START_TRANSMISSION_2 Datasheet P31 indicates this sets # busy pin low (True) and that it stays logically True until - # refresh is complete. Probably don't need _as_busy TODO + # refresh is complete. In my testing this doesn't happen. cmd(b'\x13') - - if self._lsc: # Landscape mode - wid = self.width - tbc = self.height // 8 # Vertical bytes per column - iidx = wid * (tbc - 1) # Initial index - idx = iidx # Index into framebuf - vbc = 0 # Current vertical byte count - hpc = 0 # Horizontal pixel count - for _ in range(len(mvb)): - buf1[0] = mvb[idx] ^ 0xff - self._data(buf1) - idx -= self.width - vbc += 1 - vbc %= tbc - if not vbc: - hpc += 1 - idx = iidx + hpc - else: - self._data(self._buffer) # TODO if this works don't need ._mvb + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for _ in range(len(mvb)): + buf1[0] = mvb[idx] ^ 0xff + dat(buf1) + idx -= wid + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc cmd(b'\x11') # Data stop sleep_us(20) # Allow for data coming back: currently ignore this - # Datasheet P14 is ambiguous over whether a refresh command is necessary here TODO cmd(b'\x12') # DISPLAY_REFRESH - te = ticks_us() - print('show time', ticks_diff(te, t)//1000, 'ms') + # 258ms to get here on Pyboard D + # Checking with scope, busy goes low now. For 4.9s. + # te = ticks_us() + # print('show time', ticks_diff(te, t)//1000, 'ms') if not self.demo_mode: # Immediate return to avoid blocking the whole application. # User should wait for ready before calling refresh() diff --git a/gui/demos/epd29_test.py b/gui/demos/epd29_test.py new file mode 100644 index 0000000..24066a9 --- /dev/null +++ b/gui/demos/epd29_test.py @@ -0,0 +1,119 @@ +# epd29_test.py Demo program for nano_gui on an Adafruit 2.9" flexible ePaper screen + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# color_setup must set landcsape False, asyn True and must not set demo_mode +import uasyncio as asyncio +from color_setup import ssd +from gui.core.writer import Writer +from gui.core.nanogui import refresh +from gui.widgets.meter import Meter +from gui.widgets.label import Label + +# Fonts +import gui.fonts.arial10 as arial10 +#import gui.fonts.courier20 as fixed +import gui.fonts.font6 as small + +# Some ports don't support uos.urandom. +# See https://github.com/peterhinch/micropython-samples/tree/master/random +def xorshift64star(modulo, seed = 0xf9ac6ba4): + x = seed + def func(): + nonlocal x + x ^= x >> 12 + x ^= ((x << 25) & 0xffffffffffffffff) # modulo 2**64 + x ^= x >> 27 + return (x * 0x2545F4914F6CDD1D) % modulo + return func + +async def fields(evt): + wri = Writer(ssd, small, verbose=False) + wri.set_clip(False, False, False) + textfield = Label(wri, 0, 2, wri.stringlen('longer')) + numfield = Label(wri, 25, 2, wri.stringlen('99.990'), bdcolor=None) + countfield = Label(wri, 0, 60, wri.stringlen('1')) + n = 1 + random = xorshift64star(65535) + while True: + for s in ('short', 'longer', '1', ''): + textfield.value(s) + numfield.value('{:5.2f}'.format(random() /1000)) + countfield.value('{:1d}'.format(n)) + n += 1 + await evt.wait() + +async def multi_fields(evt): + wri = Writer(ssd, small, verbose=False) + wri.set_clip(False, False, False) + + nfields = [] + dy = small.height() + 10 + row = 2 + col = 100 + width = wri.stringlen('99.990') + for txt in ('X:', 'Y:', 'Z:'): + Label(wri, row, col, txt) + nfields.append(Label(wri, row, col, width, bdcolor=None)) # Draw border + row += dy + + random = xorshift64star(2**24 - 1) + while True: + for _ in range(10): + for field in nfields: + value = random() / 167772 + field.value('{:5.2f}'.format(value)) + await evt.wait() + +async def meter(evt): + wri = Writer(ssd, arial10, verbose=False) + row = 10 + col = 150 + m0 = Meter(wri, row, col, height = 80, width = 15, divisions = 4, legends=('0.0', '0.5', '1.0')) + m1 = Meter(wri, row, col + 50, height = 80, width = 15, divisions = 4, legends=('-1', '0', '+1')) + m2 = Meter(wri, row, col + 100, height = 80, width = 15, divisions = 4, legends=('-1', '0', '+1')) + random = xorshift64star(2**24 - 1) + while True: + steps = 10 + for n in range(steps + 1): + m0.value(random() / 16777216) + m1.value(n/steps) + m2.value(1 - n/steps) + await evt.wait() + +async def main(): + refresh(ssd, True) # Clear display + await ssd.wait() + print('Ready') + evt = asyncio.Event() + asyncio.create_task(meter(evt)) + asyncio.create_task(multi_fields(evt)) + asyncio.create_task(fields(evt)) + while True: + # Normal procedure before refresh, but 10s sleep should mean it always returns immediately + await ssd.wait() + refresh(ssd) # Launches ._as_show() + await ssd.updated() + # Content has now been shifted out so coros can update + # framebuffer in background + evt.set() + evt.clear() + await asyncio.sleep(20) # Allow for slow refresh + + +tstr = '''Test of asynchronous code updating the EPD. This should +not be run for long periods as the EPD should not be updated more +frequently than every 180s. +''' + +print(tstr) + +try: + asyncio.run(main()) +except KeyboardInterrupt: + # Defensive code: avoid leaving EPD hardware in an undefined state. + print('Waiting for display to become idle') + ssd.wait_until_ready() # Synchronous code +finally: + _ = asyncio.new_event_loop()