I've bought a Waveshare 4.2" SPI e-ink display (manual), and the widespread version is a "V2" that apparently uses the UC8176 controller. That's a problem, because I want to control it using circuit python and Adafruit doesn't provide a library for the UC8176, only the UC8151D. I've done my fair share of C and C++ development, but this is my first project in CircuitPython. And I have been banging my head against the wall for a few nights already, with zero progress to show. Given that it's not a trivial driver, I wanted to be safe and find the closest thing I could find online, but in my hours of research (back and forth) the best I could find were: Adafruit's Circuitpython UC8151D driver Someone's Micropython UC8176 driver This is supposed to be a quick project that uses Circuitpython's mass storage feature, so Micropython is a last resort (I have no idea how to develop in Micropython). I tried all the combinations I could think of from the most convenient to use to the absolute simplest, even with the help of AIs (useless in this instance), and I think the closest I got was when I converted the micropython driver to this horrible code but without surprise the screen remains silent which is of no help at all. import board import digitalio import busio import time # Display resolution _EPD_WIDTH = 400 _BWIDTH = _EPD_WIDTH // 8 _EPD_HEIGHT = 300 # Pin definitions _RST_PIN = board.D11 _DC_PIN = board.D10 _CS_PIN = board.D9 _BUSY_PIN = board.D6 # LUT elements vcom, ww, bw, wb, bb # Full screen update LUT lut_full = ( b"\x00\x08\x08\x00\x00\x02\x00\x0F\x0F\x00\x00\x01\x00\x08\x08\x00\ \x00\x02\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", b"\x50\x08\x08\x00\x00\x02\x90\x0F\x0F\x00\x00\x01\xA0\x08\x08\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", b"\x50\x08\x08\x00\x00\x02\x90\x0F\x0F\x00\x00\x01\xA0\x08\x08\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", b"\xA0\x08\x08\x00\x00\x02\x90\x0F\x0F\x00\x00\x01\x50\x08\x08\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", b"\x20\x08\x08\x00\x00\x02\x90\x0F\x0F\x00\x00\x01\x10\x08\x08\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", ) # Partial screen update LUT lut_part = ( 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\x00\x00\x00\x00", 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\x00\x00", b"\x80\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\x00\x00", b"\x40\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\x00\x00", 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\x00\x00", ) # [index into LUT, register address] lut_map = ((0, b"\x20"), (1, b"\x21"), (2, b"\x22"), (3, b"\x23"), (4, b"\x24")) class EPD: def __init__(self): # Initialize pins self._rst = digitalio.DigitalInOut(_RST_PIN) self._rst.direction = digitalio.Direction.OUTPUT self._busy_pin = digitalio.DigitalInOut(_BUSY_PIN) self._busy_pin.direction = digitalio.Direction.INPUT self._busy_pin.pull = digitalio.Pull.UP self._cs = digitalio.DigitalInOut(_CS_PIN) self._cs.direction = digitalio.Direction.OUTPUT self._dc = digitalio.DigitalInOut(_DC_PIN) self._dc.direction = digitalio.Direction.OUTPUT # Initialize SPI self._spi = busio.SPI(board.SCK, MOSI=board.MOSI) while not self._spi.try_lock(): pass self._spi.configure(baudrate=10_000_000) # 10MHz max # Dimensions in pixels self.width = _EPD_WIDTH self.height = _EPD_HEIGHT # Buffer for display self._buf = bytearray(_EPD_HEIGHT * _BWIDTH) self._ibuf = bytearray(1000) # Buffer for inverted pixels # Busy flag self._busy = False # Initialize display self.init() time.sleep(0.5) # Hardware reset def reset(self): for v in (True, False, True): self._rst.value = v time.sleep(0.02) def _command(self, command, data=None): self._dc.value = False self._cs.value = False self._spi.write(command) self._cs.value = True if data is not None: self._data(data) def _data(self, data): self._dc.value = True buf1 = bytearray(1) for b in data: self._cs.value = False buf1[0] = b self._spi.write(buf1) self._cs.value = True def display_on(self): self._command(b"\x12") time.sleep(0.1) self.wait_until_ready() def init(self): self.reset() cmd = self._command cmd(b"\x01", b"\x03\x00\x2b\x2b") # Booster soft start cmd(b"\x06", b"\x17\x17\x17") cmd(b"\x04") # Power on self.wait_until_ready() cmd(b"\x00", b"\xbf") # panel setting cmd(b"\x30", b"\x3c") # PLL setting cmd(b"\x61", b"\x01\x90\x01\x2C") # resolution setting cmd(b"\x82", b"\x28") # vcom_DC setting cmd(b"\x50", b"\x97") # VCOM AND DATA INTERVAL SETTING self.set_full() def send_lut(self, lm, lut): for idx, reg in lm: self._command(reg, lut[idx]) def set_full(self): # Normal full updates self.send_lut(lut_map, lut_full) def set_partial(self): # Partial updates self.send_lut(lut_map, lut_part) def wait_until_ready(self): while not self.ready(): time.sleep(0.1) def ready(self): return not (self._busy or (self._busy_pin.value == False)) # False == busy def _invert_buffer(self, source, dest, length): for i in range(length): dest = source ^ 0xFF def _bsend(self, start, nbytes): # Invert and send nbytes from source buffer self._invert_buffer(self._buf[start:start+nbytes], self._ibuf[:nbytes], nbytes) self._dc.value = True self._cs.value = False self._spi.write(self._ibuf[:nbytes]) self._cs.value = True def show(self): if self._busy: raise RuntimeError("Cannot refresh: display is busy.") self._busy = True self._command(b"\x13") fbidx = 0 # Index into framebuf nbytes = len(self._ibuf) # Bytes to send nleft = len(self._buf) # Size of framebuf while nleft > 0: bytes_to_send = min(nbytes, nleft) self._bsend(fbidx, bytes_to_send) fbidx += bytes_to_send nleft -= bytes_to_send self._busy = False self.display_on() def sleep(self): self._command(b"\x07", b"\xA5") # deep sleep # Example usage epd = EPD() # Fill buffer with a pattern (all black) for i in range(len(epd._buf)): epd._buf = 0xFF # Show the pattern epd.show() # Wait until ready epd.wait_until_ready() # Put the display to sleep epd.sleep() I'm on the verge of giving up this project entirely because I can't afford to work day and night with little sleep for a personal project even if it will help track my time on projects. Help would be extremely appreciated! Edit: looking at the circuitpython driver for the 8151D, at first it seemed to me like the only things I should need to modify are: _START_SEQUENCE (I don't need grayscale or color) _STOP_SEQUENCE write_black_ram_command and write_color_ram_command set busy_state to a DigitalIOPin for busy? TBC refresh_display_command But then how is partial refresh handled? I'm comparing with the manufacturer's Arduino R4 example code and some things don't fit. Continue reading...