kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
235 wiersze
9.0 KiB
Python
235 wiersze
9.0 KiB
Python
# 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 uasyncio as asyncio
|
|
from micropython import const
|
|
from time import sleep_ms, sleep_us, ticks_ms, ticks_us, ticks_diff
|
|
|
|
_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
|
|
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')
|