Created
September 3, 2021 16:01
-
-
Save tai/3e6f91ebd856b51f175a565d3a0bf2aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # 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