[Python] Circuitpython driver for 4.2" e-ink display

Discussão em 'Python' iniciado por Stack, Março 17, 2025.

  1. Stack

    Stack Membro Participativo

    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:


    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...

Compartilhe esta Página