initial commit, display with commands working

This commit is contained in:
aeroreyna
2026-01-03 20:37:58 -05:00
commit b7e3dde91a
276 changed files with 100087 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
# boolpalette.py Implement BoolPalette class
# This is a 2-value color palette for rendering monochrome glyphs to color
# FrameBuffer instances. Supports destinations with up to 16 bit color.
# Copyright (c) Peter Hinch 2021
# Released under the MIT license see LICENSE
import framebuf
class BoolPalette(framebuf.FrameBuffer):
def __init__(self, mode):
buf = bytearray(4) # OK for <= 16 bit color
super().__init__(buf, 2, 1, mode)
def fg(self, color): # Set foreground color
self.pixel(1, 0, color)
def bg(self, color):
self.pixel(0, 0, color)

View File

@@ -0,0 +1,286 @@
# ePaper2in13V3.py nanogui driver for Pico-ePpaper-2.13
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in13V3.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-10-10
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
WS_20_30_2IN13_V3 = b'\
\x80\x4A\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x40\x4A\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x80\x4A\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x40\x4A\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x0F\x00\x00\x00\x00\x00\x00\
\x0F\x00\x00\x0F\x00\x00\x02\
\x0F\x00\x00\x00\x00\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\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\
\x22\x22\x22\x22\x22\x22\x00\x00\x00'
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Public bound variables required by nanogui.
self.width = 250 if landscape else 128
self.height = 128 if landscape else 250
self.demo_mode = False # Special mode enables demos to run
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)
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(20)
self._rst(0)
sleep_ms(5)
self._rst(1)
sleep_ms(20)
# Initialisation
self.wait_until_ready()
self._command(b'\x12') # SWRESET
self.wait_until_ready()
self._command(b'\x01') # Driver output control
self._data(b'\xF9')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x11') # data entry mode
self._data(b'\x03')
self._command(b'\x44')
self._data(b'\x00')
self._data(b'\x0F')
self._command(b'\x45')
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\xF9')
self._data(b'\x00')
self._command(b'\x4E')
self._data(b'\x00')
self._command(b'\x4F')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x3C') # BorderWaveform
self._data(b'\x05')
self._command(b'\x21') # Display update control
self._data(b'\x00')
self._data(b'\x80')
self._command(b'\x18') # Read built-in temperature sensor
self._data(b'\x80')
self._command(b'\x32')
self._data(WS_20_30_2IN13_V3)
self.wait_until_ready()
self._command(b'\x3F')
self._data(b'\x22')
self._command(b'\x03')
self._data(b'\x17')
self._command(b'\x04')
self._data(b'\x41')
self._data(b'\x00')
self._data(b'\x32')
self._command(b'\x2C')
self._data(b'\x36')
print('Init Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
print('async full refresh')
cmd(b'\x22\xC7')
cmd(b'\x20') # DISPLAY_REFRESH
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
print('sync full refresh')
cmd(b'\x22\xC7')
cmd(b'\x20') # DISPLAY_REFRESH
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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()
self._command(b'\x10')
self._data(b'\x01')
self._rst(0) # According to schematic this turns off the power

View File

@@ -0,0 +1,290 @@
# ePaper2in13V4.py nanogui driver for Pico-ePpaper-2.13
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in13V4.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2023-08-12
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False, full=True):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._full = full
self._updated = asyncio.Event()
# Public bound variables required by nanogui.
self.width = 250 if landscape else 128
self.height = 128 if landscape else 250
self.demo_mode = False # Special mode enables demos to run
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)
if self._full:
self.init()
else:
self.init_partial()
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)
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(20)
self._rst(0)
sleep_ms(5)
self._rst(1)
sleep_ms(20)
# Initialisation
self.wait_until_ready()
self._command(b'\x12') # SWRESET
self.wait_until_ready()
self._command(b'\x01') # Driver output control
self._data(b'\xF9')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x11') # data entry mode
self._data(b'\x03')
self._command(b'\x44')
self._data(b'\x00')
self._data(b'\x0F')
self._command(b'\x45')
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\xF9')
self._data(b'\x00')
self._command(b'\x4E')
self._data(b'\x00')
self._command(b'\x4F')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x3C') # BorderWaveform
self._data(b'\x05')
self._command(b'\x21') # Display update control
self._data(b'\x00')
self._data(b'\x80')
self._command(b'\x18') # Read built-in temperature sensor
self._data(b'\x80')
self.wait_until_ready()
print('Init Done.')
def displayPartial(self, image):
# Hardware reset
self._rst(1)
sleep_ms(200)
self._rst(0)
sleep_ms(20) # 5ms in Waveshare code
self._rst(1)
sleep_ms(200)
# Initialisation
cmd = self._command
cmd(b'\x3C', b'\x80') # BorderWavefrom
scmd(b'\01', b'\xF9\x00\x00') # Driver output control
cmd(b'\x01', b'\x03') # data entry mode
cmd(b'\x44', b'\x00\x0F')
cmd(b'\x45', b'\x00\x00\x0F\x00')
cmd(b'\x4E', b'\x00')
cmd(b'\x4F', b'\x00\x00')
self.wait_until_ready()
print('Init Partial Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7') # DISPLAY_REFRESH
cmd(b'\x20')
else:
print('sync partial refresh')
cmd(b'\x22', b'\xFF') # DISPLAY_REFRESH
cmd(b'\x20')
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7') # DISPLAY_REFRESH
cmd(b'\x20')
else:
print('sync partial refresh')
cmd(b'\x22', b'\xFF') # DISPLAY_REFRESH
cmd(b'\x20')
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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()
self._command(b'\x10')
self._data(b'\x01')
self._rst(0) # According to schematic this turns off the power

View File

@@ -0,0 +1,327 @@
# ePaper2in13bV4.py nanogui driver for Pico-ePpaper-2.13-B
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in13bV4.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-10-10
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Public bound variables required by nanogui.
self.width = 250 if landscape else 128
self.height = 128 if landscape else 250
self.demo_mode = False # Special mode enables demos to run
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)
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(20)
self._rst(0)
sleep_ms(5)
self._rst(1)
sleep_ms(20)
# Initialisation
self.wait_until_ready()
self._command(b'\x12') # SWRESET
self.wait_until_ready()
self._command(b'\x01') # Driver output control
self._data(b'\xF9')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x11') # data entry mode
self._data(b'\x03')
self._command(b'\x44')
self._data(b'\x00')
self._data(b'\x0F')
self._command(b'\x45')
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\xF9')
self._data(b'\x00')
self._command(b'\x4E')
self._data(b'\x00')
self._command(b'\x4F')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x3C') # BorderWaveform
self._data(b'\x05')
self._command(b'\x18') # Read built-in temperature sensor
self._data(b'\x80')
self._command(b'\x21') # Display update control
self._data(b'\x80')
self._data(b'\x80')
print('Init Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
print('async full refresh')
cmd(b'\x20') # DISPLAY_REFRESH
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
print('sync full refresh')
cmd(b'\x20') # DISPLAY_REFRESH
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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()
self._command(b'\x10')
self._data(b'\x01')
self._rst(0) # According to schematic this turns off the power
class EPDred(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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
# Public bound variables required by nanogui.
self.width = 250 if landscape else 128
self.height = 128 if landscape else 250
self.demo_mode = False # Special mode enables demos to run
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)
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)
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)
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x26')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)

View File

@@ -0,0 +1,268 @@
# ePaper2in7.py nanogui driver for Pico-ePpaper-2.7
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in7.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-06-08
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Dimensions in pixels. Waveshare code is portrait mode.
# Public bound variables required by nanogui.
self.width = 264 if landscape else 176
self.height = 176 if landscape else 264
self.demo_mode = False # Special mode enables demos to run
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)
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) # 5ms in Waveshare code
self._rst(1)
sleep_ms(200)
# Initialisation
cmd = self._command
cmd(b'\x01', b'\x03\x00\x2B\x2B\x09') # POWER_SETTING: VDS_EN VDG_EN, VCOM_HV VGHL_LV[1] VGHL_LV[0], VDH, VDL, VDHR
cmd(b'\x06', b'\x07\x07\x17') # BOOSTER_SOFT_START
cmd(b'\xf8', b'\x60\xA5') # POWER_OPTIMIZATION
cmd(b'\xf8', b'\x89\xA5')
cmd(b'\xf8', b'\x90\x00')
cmd(b'\xf8', b'\x93\x2A')
cmd(b'\xf8', b'\xA0\xA5')
cmd(b'\xf8', b'\xA1\x00')
cmd(b'\xf8', b'\x73\x41')
cmd(b'\x16', b'\x00') # PARTIAL_DISPLAY_REFRESH
cmd(b'\x04') # POWER_ON
self.wait_until_ready()
cmd(b'\x00', b'\xAF') # PANEL_SETTING: KW-BF, KWR-AF, BWROTP 0f
cmd(b'\x30', b'\x3A') # PLL_CONTROL: 3A 100HZ, 29 150Hz, 39 200HZ 31 171HZ
cmd(b'\x50', b'\x57') # Vcom and data interval setting (PGH)
cmd(b'\x82', b'\x12') # VCM_DC_SETTING_REGISTER
sleep_ms(2) # No delay in official code
# Set LUT. Local bytes objects reduce RAM usage.
# Values from official code:
lut_vcom_dc =\
b'\x00\x00\x00\x08\x00\x00\x00\x02\x60\x28\x28\x00\x00\x01\x00'\
b'\x14\x00\x00\x00\x01\x00\x12\x12\x00\x00\x01\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
lut_ww =\
b'\x40\x08\x00\x00\x00\x02\x90\x28\x28\x00\x00\x01\x40\x14\x00'\
b'\x00\x00\x01\xA0\x12\x12\x00\x00\x01\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
lut_bb =\
b'\x80\x08\x00\x00\x00\x02\x90\x28\x28\x00\x00\x01\x80\x14\x00'\
b'\x00\x00\x01\x50\x12\x12\x00\x00\x01\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# Both agree on this:
lut_bw = lut_ww # R22H r
lut_wb = lut_bb # R23H w
cmd(b'\x20', lut_vcom_dc) # LUT_FOR_VCOM vcom
cmd(b'\x21', lut_ww) # LUT_WHITE_TO_WHITE ww --
cmd(b'\x22', lut_bw) # LUT_BLACK_TO_WHITE bw r
cmd(b'\x23', lut_bb) # LUT_WHITE_TO_BLACK wb w
cmd(b'\x24', lut_wb) # LUT_BLACK_TO_BLACK bb b
print('Init Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 0 == busy. Comment in official code is wrong. Code is correct.
def ready(self):
return not(self._as_busy or (self._busy() == 0)) # 0 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x10') # DATA_START_TRANSMISSION_1
self._dc(1) # For some reason don't need to deassert CS here
buf1[0] = 0xff
t = ticks_ms()
for i in range(len(mvb)):
self._cs(0) # but do when copying the framebuf
send(buf1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._cs(1)
cmd(b'\x13') # DATA_START_TRANSMISSION_2 not in datasheet
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
cmd(b'\x12') # DISPLAY_REFRESH
await asyncio.sleep(1)
while self._busy() == 0:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x10') # DATA_START_TRANSMISSION_1
self._dc(1) # For some reason don't need to deassert CS here
buf1[0] = 0xff
for i in range(len(mvb)):
self._cs(0) # but do when copying the framebuf
send(buf1)
self._cs(1)
cmd(b'\x13') # DATA_START_TRANSMISSION_2 not in datasheet
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
cmd(b'\x12') # DISPLAY_REFRESH
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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
cmd(b'\x50', b'\xf7') # From Waveshare code
cmd(b'\x02') # POWER_OFF
cmd(b'\x07', b'\xA5') # DEEP_SLEEP (Waveshare and mcauser)
self._rst(0) # According to schematic this turns off the power

View File

@@ -0,0 +1,254 @@
# ePaper2in7V2.py nanogui driver for Pico-ePpaper-2.7_V2
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in7V2.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-03-15
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False, full=True):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._full = full
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Dimensions in pixels. Waveshare code is portrait mode.
# Public bound variables required by nanogui.
self.width = 264 if landscape else 176
self.height = 176 if landscape else 264
self.demo_mode = False # Special mode enables demos to run
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)
if self._full:
self.init()
else:
self.init_partial()
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)
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(20) # 5ms in Waveshare code
self._rst(1)
sleep_ms(200)
# Initialisation
cmd = self._command
cmd(b'\x12') #SWRESET
self.wait_until_ready()
cmd(b'\x45', b'\x00\x00\x07\x01') #set Ram-Y address start/end position
cmd(b'\x4F', b'\x00\x00') # set RAM y address count to 0
cmd(b'\x11', b'\x03') # data entry mode
self.wait_until_ready()
print('Init Done.')
def init_partial(self):
# Hardware reset
self._rst(1)
sleep_ms(200)
self._rst(0)
sleep_ms(20) # 5ms in Waveshare code
self._rst(1)
sleep_ms(200)
# Initialisation
cmd = self._command
cmd(b'\x3c', b'\x80')
cmd(b'\x45', b'\x00\x00\x07\x01') #set Ram-Y address start/end position
cmd(b'\x4F', b'\x00\x00') # set RAM y address count to 0
cmd(b'\x11', b'\x03') # data entry mode
self.wait_until_ready()
print('Init Partial Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy. Comment in official code is wrong. Code is correct.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24') # DATA_START_TRANSMISSION_2 not in datasheet
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7') # DISPLAY_REFRESH
cmd(b'\x20')
else:
print('sync partial refresh')
cmd(b'\x22', b'\xFF') # DISPLAY_REFRESH
cmd(b'\x20')
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24') # DATA_START_TRANSMISSION_2 not in datasheet
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7') # DISPLAY_REFRESH
cmd(b'\x20')
else:
print('sync partial refresh')
cmd(b'\x22', b'\xFF') # DISPLAY_REFRESH
cmd(b'\x20')
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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
cmd(b'\x10', b'\01') # DEEP_SLEEP (Waveshare and mcauser)
self._rst(0) # According to schematic this turns off the power

View File

@@ -0,0 +1,287 @@
# ePaper2in9.py nanogui driver for Pico-ePpaper-2.9
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in9.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-09-08
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False, full=True):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._full = full
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Dimensions in pixels. Waveshare code is portrait mode.
# Public bound variables required by nanogui.
self.width = 296 if landscape else 128
self.height = 128 if landscape else 296
self.demo_mode = False # Special mode enables demos to run
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)
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(20) # 5ms in Waveshare code
self._rst(1)
sleep_ms(200)
# Initialisation
cmd = self._command
self.wait_until_ready()
cmd(b'\x12')
self.wait_until_ready() #SWRESET
cmd(b'\x01', b'\x27\x01\x00') # Driver output control
cmd(b'\x11', b'\x03')
cmd(b'\x21', b'\x00\x80')
cmd(b'\x44', b'\x00\x0F')
cmd(b'\x45', b'\x00\x00\x27\x01')
cmd(b'\x4E', b'\x00')
cmd(b'\x4F', b'\x00\x00')
self.wait_until_ready()
print('Init Done.')
def init_partial(self):
# Hardware reset
self._rst(0)
sleep_ms(2)
self._rst(1)
sleep_ms(2)
# Initialisation
cmd = self._command
lut_wf_2in9 =\
b'\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x40\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x0A\x00\x00\x00\x00\x00\x01'\
b'\x01\x00\x00\x00\x00\x00\x00'\
b'\x01\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x00\x00\x00'\
b'\x22\x22\x22\x22\x22\x22\x00\x00\x00'\
b'\x22\x17\x41\xB0\x32\x36'
cmd(b'\x32', lut_wf_2in9)
cmd(b'\x37', b'\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00')
cmd(b'\x3c', b'\x80')
cmd(b'\x22', b'\xc0')
cmd(b'\x20')
self.wait_until_ready()
cmd(b'\x44', b'\x00\x0F')
cmd(b'\x45', b'\x00\x00\x27\x01')
cmd(b'\x4E', b'\x00')
cmd(b'\x4F', b'\x00\x00')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24') # DATA_START_TRANSMISSION_2 not in datasheet
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
if self._full:
print('async full refresh')
cmd(b'\x22', b'\xF7')
cmd(b'\x20') # DISPLAY_REFRESH
else:
print('async partial refresh')
cmd(b'\x22', b'\x0F')
cmd(b'\x20') # DISPLAY_REFRESH
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if not self._full:
self.init_partial()
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24') # DATA_START_TRANSMISSION_2 not in datasheet
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7')
cmd(b'\x20') # DISPLAY_REFRESH
else:
print('sync partial refresh')
cmd(b'\x22', b'\x0F')
cmd(b'\x20') # DISPLAY_REFRESH
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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
cmd(b'\x10', b'\x01') # DEEP_SLEEP
self._rst(0) # According to schematic this turns off the power

View File

@@ -0,0 +1,301 @@
# ePaper3in7.py nanogui driver for Pico-ePpaper-3.7
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper3in7.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-10-08
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
EPD_3IN7_lut_1Gray_GC =b"\
\x2A\x05\x00\x00\x00\x00\x00\x00\x00\x00\
\x05\x2A\x00\x00\x00\x00\x00\x00\x00\x00\
\x2A\x15\x00\x00\x00\x00\x00\x00\x00\x00\
\x05\x0A\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x02\x03\x0A\x00\x02\x06\x0A\x05\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\x00\x00\x00\
\x22\x22\x22\x22\x22"
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Public bound variables required by nanogui.
self.width = 480 if landscape else 280
self.height = 280 if landscape else 480
self.demo_mode = False # Special mode enables demos to run
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)
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(20)
self._rst(0)
sleep_ms(5)
self._rst(1)
sleep_ms(20)
# Initialisation
self._command(b'\x12')
sleep_ms(300)
self._command(b'\x46')
self._data(b'\xF7')
self.wait_until_ready()
self._command(b'\x47')
self._data(b'\xF7')
self.wait_until_ready()
self._command(b'\x01') # setting gaet number
self._data(b'\xDF')
self._data(b'\x01')
self._data(b'\x00')
self._command(b'\x03') # set gate voltage
self._data(b'\x00')
self._command(b'\x04') # set source voltage
self._data(b'\x41')
self._data(b'\xA8')
self._data(b'\x32')
self._command(b'\x11') # set data entry sequence
self._data(b'\x03')
self._command(b'\x3C') # set border
self._data(b'\x03')
self._command(b'\x0C') # set booster strength
self._data(b'\xAE')
self._data(b'\xC7')
self._data(b'\xC3')
self._data(b'\xC0')
self._data(b'\xC0')
self._command(b'\x18') # set internal sensor on
self._data(b'\x80')
self._command(b'\x2C') # set vcom value
self._data(b'\x44')
self._command(b'\x37') # set display option, these setting turn on previous function
self._data(b'\x00') # can switch 1 gray or 4 gray
self._data(b'\xFF')
self._data(b'\xFF')
self._data(b'\xFF')
self._data(b'\xFF')
self._data(b'\x4F')
self._data(b'\xFF')
self._data(b'\xFF')
self._data(b'\xFF')
self._data(b'\xFF')
self._command(b'\x44') # setting X direction start/end position of RAM
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\x17')
self._data(b'\x01')
self._command(b'\x45') # setting Y direction start/end position of RAM
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\xDF')
self._data(b'\x01')
self._command(b'\x22') # Display Update Control 2
self._data(b'\xCF')
self._command(b'\x32')
self._data(EPD_3IN7_lut_1Gray_GC)
print('Init Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
print('async full refresh')
cmd(b'\x20') # DISPLAY_REFRESH
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
print('sync full refresh')
cmd(b'\x20') # DISPLAY_REFRESH
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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()
self._command(b'\x10')
self._data(b'\x03')
self._rst(0) # According to schematic this turns off the power

View File

@@ -0,0 +1,234 @@
# pico_epaper_42.py A 1-bit monochrome display driver for the Waveshare Pico
# ePaper 4.2" display.
# Adapted from the Waveshare driver by Peter Hinch Sept 2022.
# *****************************************************************************
# * | File : ePaper4in2.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2022-10-09
# # | Info : python demo
# -----------------------------------------------------------------------------
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
from machine import Pin, SPI
import framebuf
import time
import uasyncio as asyncio
from drivers.boolpalette import BoolPalette
# Display resolution
_EPD_WIDTH = const(400)
_BWIDTH = _EPD_WIDTH // 8
_EPD_HEIGHT = const(300)
RST_PIN = 12
DC_PIN = 8
CS_PIN = 9
BUSY_PIN = 13
EPD_lut_vcom0 = 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"
EPD_lut_ww = 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"
EPD_lut_bw = 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"
EPD_lut_wb = 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"
EPD_lut_bb = 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"
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))
def __init__(self, spi=None, cs=None, dc=None, rst=None, busy=None, landscape=None, asyn=False):
self.reset_pin = Pin(RST_PIN, Pin.OUT) if rst is None else rst
self.busy_pin = Pin(BUSY_PIN, Pin.IN, Pin.PULL_UP) if busy is None else busy
self.cs_pin = Pin(CS_PIN, Pin.OUT) if cs is None else cs
self.dc_pin = Pin(DC_PIN, Pin.OUT) if dc is None else dc
self.spi = SPI(1) if spi is None else spi
self.spi.init(baudrate=4_000_000)
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
self.width = _EPD_WIDTH
self.height = _EPD_HEIGHT
self.buf = bytearray(_EPD_HEIGHT * _BWIDTH)
self.mvb = memoryview(self.buf)
mode = framebuf.MONO_HLSB
self.palette = BoolPalette(mode)
super().__init__(self.buf, _EPD_WIDTH, _EPD_HEIGHT, mode)
self.init()
time.sleep_ms(500)
# Hardware reset
def reset(self):
for v in (1, 0, 1):
self.reset_pin(v)
time.sleep_ms(20)
def send_command(self, command):
self.dc_pin(0)
self.cs_pin(0)
self.spi.write(command)
self.cs_pin(1)
def send_bytes(self, data):
self.dc_pin(1)
self.cs_pin(0)
self.spi.write(data)
self.cs_pin(1)
def display_on(self):
self.send_command(b"\x12")
time.sleep_ms(100)
self.wait_until_ready()
def init(self):
self.reset()
self.send_command(b"\x01") # POWER SETTING
self.send_bytes(b"\x03")
self.send_bytes(b"\x00")
self.send_bytes(b"\x2b")
self.send_bytes(b"\x2b")
self.send_command(b"\x06") # boost soft start
self.send_bytes(b"\x17") # A
self.send_bytes(b"\x17") # B
self.send_bytes(b"\x17") # C
self.send_command(b"\x04") # POWER_ON
self.wait_until_ready()
self.send_command(b"\x00") # panel setting
self.send_bytes(b"\xbf") # KW-BF KWR-AF BWROTP 0f BWOTP 1f
self.send_bytes(b"\x0d")
self.send_command(b"\x30") # PLL setting
self.send_bytes(b"\x3C") # 3A 100HZ 29 150Hz 39 200HZ 31 171HZ
self.send_command(b"\x61") # resolution setting
self.send_bytes(b"\x01")
self.send_bytes(b"\x90") # 128
self.send_bytes(b"\x01")
self.send_bytes(b"\x2c")
self.send_command(b"\x82") # vcom_DC setting
self.send_bytes(b"\x28")
self.send_command(b"\x50") # VCOM AND DATA INTERVAL SETTING
self.send_bytes(b"\x97") # 97white border 77black border VBDF 17|D7 VBDW 97 VBDB 57 VBDF F7 VBDW 77 VBDB 37 VBDR B7
self.send_command(b"\x20")
self.send_bytes(EPD_lut_vcom0)
self.send_command(b"\x21")
self.send_bytes(EPD_lut_ww)
self.send_command(b"\x22")
self.send_bytes(EPD_lut_bw)
self.send_command(b"\x23")
self.send_bytes(EPD_lut_wb)
self.send_command(b"\x24")
self.send_bytes(EPD_lut_bb)
# Clear display
self.send_command(b"\x10")
for j in range(_EPD_HEIGHT):
self.send_bytes(b"\xff" * _BWIDTH)
self.send_command(b"\x13")
for j in range(_EPD_HEIGHT):
self.send_bytes(b"\xff" * _BWIDTH)
self.send_command(b"\x12")
time.sleep_ms(10)
self.display_on()
def wait_until_ready(self):
while(not self.ready()):
self.send_command(b"\x71")
time.sleep_ms(100)
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
self.send_command(b"\x71")
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 0 == busy. Comment in official code is wrong. Code is correct.
def ready(self):
return not(self._as_busy or (self.busy_pin() == 0)) # 0 == busy
def _line(self, n, buf=bytearray(_BWIDTH)):
img = self.mvb
s = n * _BWIDTH
for x, b in enumerate(img[s : s + _BWIDTH]):
buf[x] = b ^ 0xFF
self.send_bytes(buf)
async def _as_show(self):
self.send_command(b"\x13")
for j in range(_EPD_HEIGHT): # Loop would take ~300ms
self._line(j)
await asyncio.sleep_ms(0)
self.send_command(b"\x12") # Async .display_on()
while not self.busy_pin():
await asyncio.sleep_ms(10) # About 1.7s
self._updated.set()
self._updated.clear()
self._as_busy = False
def show(self):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True # Immediate busy flag. Pin goes low much later.
asyncio.create_task(self._as_show())
return
self.send_command(b"\x13")
for j in range(_EPD_HEIGHT):
self._line(j)
self.display_on()
self.wait_until_ready()
def sleep(self):
# self.send_command(b"\x02") # power off
# self.wait_until_ready()
self.send_command(b"\x07") # deep sleep
self.send_bytes(b"\xA5")

View File

@@ -0,0 +1,207 @@
# pico_epaper_42.py A 1-bit monochrome display driver for the Waveshare Pico
# ePaper 4.2" display.
# Adapted from the Waveshare driver by Peter Hinch Sept 2022.
# *****************************************************************************
# * | File : ePaper4in2.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2022-10-09
# # | Info : python demo
# -----------------------------------------------------------------------------
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
from machine import Pin, SPI
import framebuf
import time
import uasyncio as asyncio
from drivers.boolpalette import BoolPalette
# Display resolution
_EPD_WIDTH = const(400)
_BWIDTH = _EPD_WIDTH // 8
_EPD_HEIGHT = const(300)
RST_PIN = 12
DC_PIN = 8
CS_PIN = 9
BUSY_PIN = 13
EPD_LUT_ALL=b"\x01\x0A\x1B\x0F\x03\x01\x01\x05\x0A\x01\x0A\x01\x01\x01\x05\x08\x03\x02\x04\x01\x01\x01\x04\x04\x02\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\
\x01\x00\x00\x00\x00\x01\x01\x01\x0A\x1B\x0F\x03\x01\x01\x05\x4A\x01\x8A\x01\x01\x01\x05\x48\x03\x82\x84\x01\x01\x01\x84\x84\x82\x00\x01\x01\
\x01\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x01\x0A\x1B\x8F\x03\x01\x01\x05\x4A\x01\x8A\x01\x01\x01\x05\x48\x83\x82\x04\x01\x01\
\x01\x04\x04\x02\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x01\x8A\x1B\x8F\x03\x01\x01\
\x05\x4A\x01\x8A\x01\x01\x01\x05\x48\x83\x02\x04\x01\x01\x01\x04\x04\x02\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\
\x01\x00\x00\x00\x00\x01\x01\x01\x8A\x9B\x8F\x03\x01\x01\x05\x4A\x01\x8A\x01\x01\x01\x05\x48\x03\x42\x04\x01\x01\
\x01\x04\x04\x42\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x07\x17\x41\xA8\x32\x30"
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))
def __init__(self, spi=None, cs=None, dc=None, rst=None, busy=None, landscape=None, asyn=False):
self.reset_pin = Pin(RST_PIN, Pin.OUT) if rst is None else rst
self.busy_pin = Pin(BUSY_PIN, Pin.IN, Pin.PULL_UP) if busy is None else busy
self.cs_pin = Pin(CS_PIN, Pin.OUT) if cs is None else cs
self.dc_pin = Pin(DC_PIN, Pin.OUT) if dc is None else dc
self.spi = SPI(1) if spi is None else spi
self.spi.init(baudrate=4_000_000)
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
self.width = _EPD_WIDTH
self.height = _EPD_HEIGHT
self.buf = bytearray(_EPD_HEIGHT * _BWIDTH)
self.mvb = memoryview(self.buf)
mode = framebuf.MONO_HLSB
self.palette = BoolPalette(mode)
super().__init__(self.buf, _EPD_WIDTH, _EPD_HEIGHT, mode)
self.init()
time.sleep_ms(500)
# Hardware reset
def reset(self):
for v in (1, 0, 1):
self.reset_pin(v)
time.sleep_ms(20)
def send_command(self, command):
self.dc_pin(0)
self.cs_pin(0)
self.spi.write(command)
self.cs_pin(1)
def send_bytes(self, data):
self.dc_pin(1)
self.cs_pin(0)
self.spi.write(data)
self.cs_pin(1)
def display_on(self):
self.send_command(b"\x22")
self.send_bytes(b"\xF7")
self.send_command(b"\x20")
time.sleep_ms(100)
self.wait_until_ready()
def init(self):
self.reset()
self.wait_until_ready()
self.send_command(b"\x12") #SWRESET
self.wait_until_ready()
self.send_command(b"\x21") # Display update control
self.send_bytes(b"\x40")
self.send_bytes(b"\x00")
self.send_command(b"\x3C") # BorderWavefrom
self.send_bytes(b"\x05")
self.send_command(b"\x11") # data entry mode
self.send_bytes(b"\x03") # X-mode
self.send_command(b"\x44")
self.send_bytes(b"\x00")
self.send_bytes(b"\x31")
self.send_command(b"\x45")
self.send_bytes(b"\x00")
self.send_bytes(b"\x00")
self.send_bytes(b"\x2B")
self.send_bytes(b"\x01")
self.send_command(b"\x4E")
self.send_bytes(b"\x00")
self.send_command(b"\x4F")
self.send_bytes(b"\x00")
self.send_bytes(b"\x00")
self.wait_until_ready()
# Clear display
self.send_command(b"\x24")
for j in range(_EPD_HEIGHT):
self.send_bytes(b"\xff" * _BWIDTH)
self.send_command(b"\x26")
for j in range(_EPD_HEIGHT):
self.send_bytes(b"\xff" * _BWIDTH)
self.display_on()
def wait_until_ready(self):
while(self.ready()):
time.sleep_ms(100)
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 0 == busy. Comment in official code is wrong. Code is correct.
def ready(self):
return not(self._as_busy or (self.busy_pin() == 0)) # 0 == busy
def _line(self, n, buf=bytearray(_BWIDTH)):
img = self.mvb
s = n * _BWIDTH
for x, b in enumerate(img[s : s + _BWIDTH]):
buf[x] = b ^ 0xFF
self.send_bytes(buf)
async def _as_show(self):
self.send_command(b"\x24")
for j in range(_EPD_HEIGHT): # Loop would take ~300ms
self._line(j)
await asyncio.sleep_ms(0)
self.wait_until_ready()
self._updated.set()
self._updated.clear()
self._as_busy = False
def show(self):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True # Immediate busy flag. Pin goes low much later.
asyncio.create_task(self._as_show())
return
self.send_command(b"\x24")
for j in range(_EPD_HEIGHT):
self._line(j)
self.display_on()
self.wait_until_ready()
def sleep(self):
self.send_command(b"\x10") # deep sleep
self.send_bytes(b"\x01")

View File

@@ -0,0 +1,326 @@
# ePaper7in5b.py nanogui driver for Pico-ePpaper-7.5-B
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper7in5b.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2022-10-10
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._updated = asyncio.Event()
# Public bound variables required by nanogui.
self.width = 480 if landscape else 800
self.height = 800 if landscape else 480
self.demo_mode = False # Special mode enables demos to run
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)
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(20)
self._rst(0)
sleep_ms(5)
self._rst(1)
sleep_ms(20)
# Initialisation
self._command(b'\x06') # btst
self._data(b'\x17')
self._data(b'\x17')
self._data(b'\x28') # If an exception is displayed, try using 0x38
self._data(b'\x17')
self._command(b'\x04') # POWER ON
sleep_ms(100)
self.wait_until_ready()
self._command(b'\x00') # PANNEL SETTING
self._data(b'\x0F') # KW-3f KWR-2F BWROTP 0f BWOTP 1f
self._command(b'\x61') # tres
self._data(b'\x03') # source 800
self._data(b'\x20')
self._data(b'\x01') # gate 480
self._data(b'\xE0')
self._command(b'\x15')
self._data(b'\x00')
self._command(b'\x50') # VCOM AND DATA INTERVAL SETTING
self._data(b'\x11')
self._data(b'\x07')
self._command(b'\x60') # TCON SETTING
self._data(b'\x22')
self._command(b'\x65') # RESOLUTION SETTING
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\x00')
print('Init Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 0 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 0)) # 0 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x10')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
print('async full refresh')
cmd(b'\x12') # DISPLAY_REFRESH
await asyncio.sleep(1)
while self._busy() == 0:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x10')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
print('sync full refresh')
cmd(b'\x12') # DISPLAY_REFRESH
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
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._command(b'\x02')
self.wait_until_ready()
self._command(b'\x07')
self._data(b'\xA5')
self._rst(0) # According to schematic this turns off the power
class EPDred(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))
def __init__(self, spi, cs, dc, rst, busy, landscape=False):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
# Public bound variables required by nanogui.
self.width = 480 if landscape else 800
self.height = 800 if landscape else 480
self.demo_mode = False # Special mode enables demos to run
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)
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)
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)
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x13')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
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)):
self._cs(0)
buf1[0] = mvb[idx]
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = b
send(buf1)
self._cs(1)