# epd29.py nanogui driver for Adafruit Flexible 2.9" Black and White ePaper display. # [Interface breakout](https://www.adafruit.com/product/4224) # [Display](https://www.adafruit.com/product/4262) # EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui. # Copyright (c) Peter Hinch 2020-2023 # Released under the MIT license see LICENSE # Based on the following sources: # [CircuitPython code](https://github.com/adafruit/Adafruit_CircuitPython_IL0373) Author: Scott Shawcroft # [Adafruit setup guide](https://learn.adafruit.com/adafruit-eink-display-breakouts/circuitpython-code-2) # [IL0373 datasheet](https://www.smart-prototyping.com/image/data/9_Modules/EinkDisplay/GDEW0154T8/IL0373.pdf) # [Adafruit demo](https://github.com/adafruit/Adafruit_CircuitPython_IL0373/blob/3f4f52eb3a65173165da1908f93a95383b45a726/examples/il0373_flexible_2.9_monochrome.py) # [eInk breakout schematic](https://learn.adafruit.com/assets/57645) # Physical pixels are 296w 128h. However the driver views the display as 128w * 296h with the # Adfruit code transposing the axes. import framebuf import asyncio from micropython import const from time import sleep_ms, sleep_us, ticks_ms, ticks_us, ticks_diff from drivers.boolpalette import BoolPalette def asyncio_running(): try: _ = asyncio.current_task() except: return False return True 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. @staticmethod def rgb(r, g, b): return int((r > 127) or (g > 127) or (b > 127)) # Discard asyn: autodetect def __init__(self, spi, cs, dc, rst, busy, landscape=True, asyn=False): self._spi = spi self._cs = cs # Pins self._dc = dc self._rst = rst # Active low. self._busy = busy # Active low on IL0373 self._lsc = landscape # ._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.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 self.height = 128 if landscape else 296 # 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 landscape else framebuf.MONO_HLSB self.palette = BoolPalette(mode) super().__init__(self._buffer, self.width, self.height, mode) self.init() def _command(self, command, data=None): self._dc(0) self._cs(0) self._spi.write(command) self._cs(1) if data is not None: self._data(data) # Datasheet P26 seems to mandate CS False after each byte. Ugh. def _data(self, data, buf1=bytearray(1)): self._dc(1) for b in data: self._cs(0) buf1[0] = b self._spi.write(buf1) self._cs(1) def init(self): # Hardware reset self._rst(1) sleep_ms(200) self._rst(0) sleep_ms(200) self._rst(1) sleep_ms(200) # Initialisation cmd = self._command # Power setting. Data from Adafruit. # Datasheet default \x03\x00\x26\x26\x03 - slightly different voltages. cmd(b"\x01", b"\x03\x00\x2b\x2b\x09") # Booster soft start. Matches datasheet. cmd(b"\x06", b"\x17\x17\x17") cmd(b"\x04") # Power on sleep_ms(200) # Iss https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/16 cmd(b"\x00", b"\x9f") # CDI: As used by Adafruit. Datasheet is confusing on this. # See https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/11 # With 0x37 got white border on flexible display, black on FeatherWing # 0xf7 still produced black border on FeatherWing cmd(b"\x50", b"\x37") # PLL: correct for 150Hz as specified in Adafruit code cmd(b"\x30", b"\x29") # Resolution 128w * 296h as required by IL0373 cmd(b"\x61", b"\x80\x01\x28") # Note hex(296) == 0x128 # Set VCM_DC. Now clarified with Adafruit. # https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/17 cmd(b"\x82", b"\x12") # Set Vcom to -1.0V sleep_ms(50) print("Init Done.") # For use in synchronous code: blocking wait on ready state. def wait_until_ready(self): sleep_ms(50) while not self.ready(): sleep_ms(100) # Return immediate status. Pin state: 0 == busy. def ready(self): return not (self._as_busy or (self._busy() == 0)) async def _as_show(self, buf1=bytearray(1)): mvb = self._mvb cmd = self._command dat = self._data cmd(b"\x13") t = ticks_ms() 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 i in range(len(mvb)): buf1[0] = ~mvb[idx] 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() else: for i, b in enumerate(mvb): buf1[0] = ~b dat(buf1) 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() 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 self.complete.set() # draw the current frame memory. def show(self, buf1=bytearray(1)): if asyncio_running(): 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 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. 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] dat(buf1) idx -= wid vbc += 1 vbc %= tbc if not vbc: hpc += 1 idx = iidx + hpc else: for b in mvb: buf1[0] = ~b dat(buf1) cmd(b"\x11") # Data stop sleep_us(20) # Allow for data coming back: currently ignore this cmd(b"\x12") # DISPLAY_REFRESH # 258ms to get here on Pyboard D # Checking with scope, busy goes low now. For 4.9s. 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() sleep_ms(2000) # Give time for user to see result # to wake call init() def sleep(self): self._as_busy = False self.wait_until_ready() cmd = self._command # CDI: not sure about value or why we set this here. Copying Adafruit. cmd(b"\x50", b"\x17") # Set VCM_DC. 0 is datasheet default. cmd(b"\x82", b"\x00") # POWER_OFF. User code should pull ENA low to power down the display. cmd(b"\x02")