Skip to content

Instantly share code, notes, and snippets.

@tai
Created September 3, 2021 16:01
Show Gist options
  • Select an option

  • Save tai/3e6f91ebd856b51f175a565d3a0bf2aa to your computer and use it in GitHub Desktop.

Select an option

Save tai/3e6f91ebd856b51f175a565d3a0bf2aa to your computer and use it in GitHub Desktop.
#
# MicroPython driver for WaveShare 4.2inch e-Paper Display (EPD)
#
# Current target of this code is ESP8266.
#
# ESP8266 requires following GPIO state on boot, and this
# conflicts with SPI pinout.
#
# - GPIO15=0 # SPI CS
# - GPIO2=1 # LED-2
# - GPIO0=1 # FLASH button
#
# GPIO15 is tricky as SPI CS is (normally high, active low).
# Add 10KOhm pull-down to GPIO15 to make it work.
#
from machine import Pin, SPI
# helper to map "D[0-8]" silk on ESP8266 DevKit to GPIO number
DIO = [16, 5, 4, 0, 2, 14, 12, 13, 15]
######################################################################
# SPI commands of UltraChip UC8176 driver chip
######################################################################
CMD_PSR = 0x00
CMD_PWR = 0x01
CMD_POF = 0x02
CMD_PFS = 0x03
CMD_PON = 0x04
CMD_PMES = 0x05
CMD_BTST = 0x06
CMD_DSLP = 0x07
CMD_DTM1 = 0x10
CMD_DSP = 0x11
CMD_DRF = 0x12
CMD_DTM2 = 0x13
# undocumented
CMD_LUT_VCOM0 = 0x20
CMD_LUT_WW = 0x21
CMD_LUT_BW = 0x22
CMD_LUT_WB = 0x23
CMD_LUT_BB = 0x24
CMD_PLL = 0x30
CMD_TSC = 0x40
CMD_TSE = 0x41
CMD_TSW = 0x42
CMD_TSR = 0x43
CMD_CDI = 0x50
CMD_LPD = 0x51
CMD_TCON = 0x60
CMD_TRES = 0x61
CMD_GSST = 0x65
CMD_REV = 0x70
CMD_FLG = 0x71
CMD_VCOM = 0x80
CMD_VV = 0x81
CMD_VDCS = 0x82
CMD_PTL = 0x90
CMD_PTIN = 0x91
CMD_PTOUT = 0x92
CMD_PGM = 0xA0
CMD_APG = 0xA1
CMD_ROTP = 0xA2
CMD_CCSET = 0xE0
CMD_PWS = 0xE3
CMD_TSSET = 0xE5
######################################################################
#
# LUT parameters used to optimzie voltage control on each electrodes
#
# These parameters are from either WaveShare source code or from
# following blog/project on WaveShare 4.2inch e-Paper display (EPD) and
# its structure (especially on DTM[12]/SRAM and LUT). Very informative.
#
# Blog: Fast partial refresh on 4.2" E-paper display from Waveshare / Good Display
# - https://benkrasnow.blogspot.com/2017/10/fast-partial-refresh-on-42-e-paper.html
# YouTube: E-paper hacking: fastest possible refresh rate
# - https://www.youtube.com/watch?v=MsbiO8EAsGw
# Arduino project with enhanced "epd4in2" library:
# - https://drive.google.com/open?id=0B4YXWiqYWB99UmRYQi1qdXJIVFk
# Datasheet of (very) similar chip:
# - https://www.smart-prototyping.com/image/data/9_Modules/EinkDisplay/GDEW0154T8/IL0373.pdf
# Datasheet from WaveShare (detail removed):
# - https://www.waveshare.com/w/upload/6/6a/4.2inch-e-paper-specification.pdf
#
######################################################################
LUT_VCOM0 = [
0x00, 0x17, 0x00, 0x00, 0x00, 0x02,
0x00, 0x17, 0x17, 0x00, 0x00, 0x02,
0x00, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_VCOM0_QUICK = [
0x00, 0x0E, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_WW = [
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_WW_QUICK = [
0xA0, 0x0E, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_BW = [
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_BW_QUICK = [
0xA0, 0x0E, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_BB = [
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_BB_QUICK = [
0x50, 0x0E, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_WB = [
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
LUT_WB_QUICK = [
0x50, 0x0E, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
# Helper to create a generator of packed pixel buffers
#
# Example:
# On 400x300 screen, (400 * 300 / 8) = 15000 bytes are needed.
# To avoid allocating whole bytes at once, this generator is
# used to create a data stream in NxM (ex. 1000x15) format.
#
def pixgen(v, n=1000, m=15):
buf = v.to_bytes(1, None) * n
for i in range(m):
yield buf
class EPD(object):
#
# Example:
# from epd import EPD, DIO
# spi = SPI(1, baudrate=2000000)
# dev = EPD(spi,
# busy=Pin(DIO[6], Pin.IN), reset=Pin(DIO[3], Pin.OUT),
# dc=Pin(DIO[4], Pin.OUT), cs=Pin(DIO[8], Pin.OUT))
# dev.test()
#
def __init__(self, spi, busy, reset, dc, cs):
self.spi = spi
self.reset_n = reset
self.dc = dc
self.busy_n = busy
self.cs_n = cs
self.init()
# Send command/data over SPI
def send(self, cmd, buf=None):
self.cs_n.off()
if cmd is not None:
self.dc.off()
self.spi.write(cmd.to_bytes(1, None))
if buf is not None:
self.dc.on()
self.spi.write(bytes(buf) if type(buf) is list else buf)
self.cs_n.on()
# Initialize EPD parameters
def init(self):
# not parameterized as many part of the code implicitly
# expects 400x300 size
w, h = 400, 300
self.reset()
self.send(CMD_PWR, [0x03, 0x00, 0x2b, 0x2b, 0xff])
self.send(CMD_BTST, [0x17, 0x17, 0x17])
self.send(CMD_PON)
self.send(CMD_PSR, [0xbf, 0x0b]) # 400x300, BW panel
self.send(CMD_PLL, [0x3c])
self.send(CMD_TRES, [w >> 8, w & 0xFF, h >> 8, h & 0xFF])
self.send(CMD_VDCS, [0x12])
self.send(CMD_CDI, [0x97])
#
# Prefill framebuffer #1 with the "most blank" value.
# This will cause EPD to do a "fullest update" as electrodes
# are updated based on difference between #1 and #2.
#
# When drawing for the first time, that image should be saved to
# framebuffer #1 as a "current framebuffer", so further updates
# will be more optimal.
#
self.set_dtm(1, pixgen(0xFF))
self.set_lut(full=None)
# Reset EPD. All registers will be reset to default values.
def reset(self):
self.cs_n.on()
self.dc.on()
self.reset_n.off()
self.reset_n.on()
# Select LUT data for full update or partial update.
#
# LUT value is supposed to control how voltage/signal is applied
# to electrode on update.
def set_lut(self, full=True):
# reset LUT for full refresh
if full is None:
full = True
self.lut_full = False
# return if LUT already has expected data
if self.lut_full == full:
return
self.lut_full = full
if full:
self.send(CMD_LUT_VCOM0, LUT_VCOM0)
self.send(CMD_LUT_WW, LUT_WW)
self.send(CMD_LUT_BW, LUT_BW)
self.send(CMD_LUT_WB, LUT_WB)
self.send(CMD_LUT_BB, LUT_BB)
else:
self.send(CMD_LUT_VCOM0, LUT_VCOM0_QUICK)
self.send(CMD_LUT_WW, LUT_WW_QUICK)
self.send(CMD_LUT_BW, LUT_BW_QUICK)
self.send(CMD_LUT_WB, LUT_WB_QUICK)
self.send(CMD_LUT_BB, LUT_BB_QUICK)
#
# Load data into framebuffer/SRAM (DTM1 or DTM2)
# DTM1 and DTM2 are compared to optimize a screen refresh process.
#
# DTM1 should hold pixels currently on screen.
# DTM2 should hold pixels to be drawn in next refresh.
#
# On reset, there is no "current screen".
# In that case, you can fill DTM1 with 0xFF to let EPD
# do the "fullest" refresh.
#
def set_dtm(self, dtm, bufs):
self.send(CMD_DTM1 if dtm == 1 else CMD_DTM2)
for buf in bufs:
self.send(None, buf)
# Trigger screen refresh
def refresh(self):
self.send(CMD_DRF)
#
# Do a full screen update (400x300 expected for now)
#
# Pixel data is given as iterator of buffers, instead of raw byte buffer.
# This interface allows you to save runtime usage of memory. It must
# provide total of (400 * 300 / 8) bytes (= packed pixels).
#
def draw_full(self, bufs):
# save as "new" framebuffer and refresh
self.set_dtm(2, bufs)
self.set_lut(full=True)
self.refresh()
# save as "current" framebuffer to make next update efficient
self.set_dtm(1, bufs)
#
# Do a partial screen update of (x, y, w, h) region.
#
# Pixel data is given as iterator of buffers, instead of raw byte buffer.
# This interface allows you to save runtime usage of memory. It must
# provide total of (w * h / 8) bytes (= packed pixels).
#
def draw_part(self, x, y, w, h, bufs):
self.send(CMD_PTIN)
self.send(CMD_PTL, [
x >> 8,
x & 0xF8,
(((x & 0xF8) + w - 1) >> 8),
(((x & 0xF8) + w - 1) & 0xF8) | 7,
y >> 8,
y & 0xFF,
(y + h - 1) >> 8,
(y + h - 1) & 0xFF,
0x01
])
# save as "new" framebuffer and refresh
self.set_dtm(2, bufs)
self.set_lut(full=False)
self.refresh()
# save as "current" framebuffer to make next update efficient
self.set_dtm(1, bufs)
self.send(CMD_PTOUT)
# test full update
def test(self, newval=0x55):
self.draw_full(pixgen(newval))
self.x = 16
self.y = 16
# test partial update
def test2(self, val=0x00):
self.draw_part(self.x, self.y, 16, 16, pixgen(val, 16, 16))
self.x = (self.x + 32) % 256
self.y = (self.y + 32) % 256
# Usage:
# import epd
# dev = epd.run()
def run():
# SPI clock can be fast as 20MHz, but I have problem debugging a signal that fast
spi = SPI(1, baudrate=2000000)
dev = EPD(spi,
busy=Pin(DIO[6], Pin.IN), reset=Pin(DIO[3], Pin.OUT),
dc=Pin(DIO[4], Pin.OUT), cs=Pin(DIO[8], Pin.OUT))
dev.test()
return dev
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment