Skip to content

Commit d3eb6f2

Browse files
authored
Merge pull request #46 from mikeysklar/ssd1683-4.2-6381
Add SSD1683 4-gray displayio driver and example for 4.2" bare E-Ink Display (#6381)
2 parents 98a38de + d09acce commit d3eb6f2

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

adafruit_ssd1680.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,64 @@
108108
)
109109

110110

111+
_SSD1683_START_SEQUENCE = (
112+
b"\x12\x80\x00\xc8" # SW_RESET + 200ms delay (cold-boot charge-pump settle)
113+
b"\x0c\x00\x04\x8b\x9c\xa4\x0f" # BOOST_SOFTSTART
114+
b"\x11\x00\x01\x03" # RAM data entry mode
115+
b"\x3c\x00\x01\x03" # border
116+
b"\x21\x00\x02\x00\x00" # DISP_CTRL1 (no source-line shift)
117+
b"\x3f\x00\x01\x07" # END_OPTION
118+
b"\x03\x00\x01\x17" # gate voltage
119+
b"\x04\x00\x03\x41\xa8\x32" # source voltage
120+
b"\x2c\x00\x01\x30" # VCOM (default 0x30, offset 41)
121+
b"\x4e\x00\x01\x00" # RAM X count
122+
b"\x4f\x00\x02\x00\x00" # RAM Y count
123+
b"\x01\x00\x03\x00\x00\x00" # driver output control (gate lo/hi at offsets 54/55)
124+
)
125+
126+
_SSD1683_DISPLAY_UPDATE_MODE = b"\x22\x00\x01\xcf" # 4-gray LUT-based update
127+
128+
# 4-gray waveform LUT for GDEY042T81 / FPC-190 — native GxEPD2 order (unswapped).
129+
# Correct level order relies on the BW/Color RAM banks being swapped in __init__
130+
# (write_black_ram_command=0x26, write_color_ram_command=0x24).
131+
SSD1683_GRAY4_LUT = bytes.fromhex(
132+
# 5 waveform groups x 6 phase-rows (7 B each); GxEPD2 GDEY042T81 lut_4G[0:210]
133+
"010A1B0F030101" # group 0
134+
"050A010A010101"
135+
"05080302040101"
136+
"01040402000101"
137+
"01000000000101"
138+
"01000000000101"
139+
"010A1B0F030101" # group 1
140+
"054A018A010101"
141+
"05480382840101"
142+
"01848482000101"
143+
"01000000000101"
144+
"01000000000101"
145+
"010A1B8F030101" # group 2
146+
"054A018A010101"
147+
"05488382040101"
148+
"01040402000101"
149+
"01000000000101"
150+
"01000000000101"
151+
"018A1B8F030101" # group 3
152+
"054A018A010101"
153+
"05488302040101"
154+
"01040402000101"
155+
"01000000000101"
156+
"01000000000101"
157+
"018A9B8F030101" # group 4
158+
"054A018A010101"
159+
"05480342040101"
160+
"01040442000101"
161+
"01000000000101"
162+
"01000000000101"
163+
"00000000000000" # 2 reserved phase-rows
164+
"00000000000000"
165+
"020000" # frame-rate / repeat tail
166+
) # 227 bytes — GDEY042T81 / FPC-190 (matches Adafruit_EPD ssd1683.py)
167+
168+
111169
def detect_ssd1680_panel(data_pin, clk_pin, cs_pin, dc_pin, rst_pin):
112170
"""Read SSD1680 User ID (register 0x2E) via half-duplex bitbang SPI.
113171
@@ -233,3 +291,80 @@ def __init__(
233291
address_little_endian=True,
234292
two_byte_sequence_length=True,
235293
)
294+
295+
296+
# pylint: disable=too-few-public-methods
297+
class SSD1683(EPaperDisplay):
298+
r"""SSD1683 driver for 4-gray grayscale e-paper displays.
299+
300+
Designed for the 4.2" 400×300 GDEY042T81 panel (#6381, FPC-190 ribbon).
301+
Gate lines run along the height dimension (300), so DRIVER_CONTROL is set
302+
from height rather than width.
303+
304+
:param bus: The data bus the display is on
305+
:param \**kwargs:
306+
See below
307+
308+
:Keyword Arguments:
309+
* *width* (``int``) --
310+
Display width (400 for the 4.2" panel)
311+
* *height* (``int``) --
312+
Display height (300 for the 4.2" panel)
313+
* *rotation* (``int``) --
314+
Display rotation
315+
* *vcom* (``int``) --
316+
VCOM voltage register value (default 0x30)
317+
* *custom_lut* (``bytes``) --
318+
Custom waveform LUT; pass ``SSD1683_GRAY4_LUT`` for 4-gray mode
319+
"""
320+
321+
def __init__(self, bus: FourWire, vcom: int = 0x30, custom_lut: bytes = b"", **kwargs) -> None:
322+
kwargs["grayscale"] = True # SSD1683 always runs in 4-gray mode
323+
if "colstart" not in kwargs:
324+
kwargs["colstart"] = 0
325+
stop_sequence = bytearray(_STOP_SEQUENCE)
326+
try:
327+
bus.reset()
328+
# Cold-boot settle: after a true power-on, the controller's regulators
329+
# need time before the start sequence loads the LUT (mirrors the
330+
# framebuffer driver's sleep(0.1)+busy_wait). A short delay here plus the
331+
# longer SW_RESET delay below prevents a corrupt-LUT speckle on cold boot.
332+
time.sleep(0.2)
333+
except RuntimeError:
334+
stop_sequence = b""
335+
load_lut = b""
336+
display_update_mode = bytearray(_SSD1683_DISPLAY_UPDATE_MODE)
337+
if custom_lut:
338+
load_lut = b"\x32" + len(custom_lut).to_bytes(2) + custom_lut
339+
340+
start_sequence = bytearray(_SSD1683_START_SEQUENCE + load_lut + display_update_mode)
341+
start_sequence[41] = vcom
342+
343+
width = kwargs["width"]
344+
height = kwargs["height"]
345+
# Save gate count before any rotation swap — always the physical height (300)
346+
gate_height = height
347+
if "rotation" in kwargs and kwargs["rotation"] % 180 != 90:
348+
width, height = height, width
349+
start_sequence[54] = (gate_height - 1) & 0xFF
350+
start_sequence[55] = ((gate_height - 1) >> 8) & 0xFF
351+
352+
super().__init__(
353+
bus,
354+
start_sequence,
355+
stop_sequence,
356+
**kwargs,
357+
ram_width=255, # <256 forces 1-byte X (0x44) addressing; SSD168x X is byte-addressed
358+
ram_height=300,
359+
busy_state=True,
360+
write_black_ram_command=0x26, # swapped vs SSD1680: displayio packs luma
361+
write_color_ram_command=0x24, # bit7->black,bit6->color; SSD1683 wants the opposite
362+
set_column_window_command=0x44,
363+
set_row_window_command=0x45,
364+
set_current_column_command=0x4E,
365+
set_current_row_command=0x4F,
366+
refresh_display_command=0x20,
367+
always_toggle_chip_select=False,
368+
address_little_endian=True,
369+
two_byte_sequence_length=True,
370+
)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# SPDX-FileCopyrightText: 2026 Mikey Sklar, written for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
5+
"""4-gray "ThinkInk" info card for the Adafruit 4.2" 400x300 E-Ink Display (#6381).
6+
7+
A grayscale take on the factory demo: product text plus a 4-level gray ramp
8+
(white / light / dark / black) that shows all four shades at once.
9+
"""
10+
11+
import time
12+
13+
import board
14+
import busio
15+
import displayio
16+
import terminalio
17+
from adafruit_display_text import label
18+
from fourwire import FourWire
19+
20+
import adafruit_ssd1680
21+
22+
displayio.release_displays()
23+
24+
spi = busio.SPI(board.EPD_SCK, board.EPD_MOSI)
25+
display_bus = FourWire(
26+
spi,
27+
command=board.EPD_DC,
28+
chip_select=board.EPD_CS,
29+
reset=board.EPD_RESET,
30+
baudrate=1000000,
31+
)
32+
time.sleep(1)
33+
34+
display = adafruit_ssd1680.SSD1683(
35+
display_bus,
36+
width=400,
37+
height=300,
38+
busy_pin=board.EPD_BUSY,
39+
rotation=0,
40+
colstart=0,
41+
vcom=0x30,
42+
custom_lut=adafruit_ssd1680.SSD1683_GRAY4_LUT,
43+
)
44+
45+
W, H = 400, 300
46+
47+
# 4-gray palette (white -> black)
48+
pal = displayio.Palette(4)
49+
pal[0] = 0xFFFFFF # white
50+
pal[1] = 0xAAAAAA # light gray
51+
pal[2] = 0x555555 # dark gray
52+
pal[3] = 0x000000 # black
53+
WHITE, LIGHT, DARK, BLACK = 0, 1, 2, 3
54+
55+
g = displayio.Group()
56+
57+
# White background
58+
bg = displayio.Bitmap(W, H, 4)
59+
bg.fill(WHITE)
60+
g.append(displayio.TileGrid(bg, pixel_shader=pal))
61+
62+
# Big text, sized to fill the 400px width
63+
texts = [
64+
("Adafruit ThinkInk", 6, 10, 3, 0x000000),
65+
('4.2" 400x300', 6, 50, 4, 0x000000),
66+
("4-Gray E-Ink", 6, 95, 4, 0x555555),
67+
("SSD1683 #6381", 6, 140, 3, 0x000000),
68+
]
69+
for text, x, y, scale, color in texts:
70+
lbl = label.Label(terminalio.FONT, text=text, color=color, scale=scale)
71+
lbl.anchor_point = (0.0, 0.0) # top-left, like framebuf text()
72+
lbl.anchored_position = (x, y)
73+
g.append(lbl)
74+
75+
# 4-level gray ramp filling the bottom: black | dark | light | white, with borders
76+
RAMP_TOP = 180
77+
RAMP_H = H - RAMP_TOP
78+
SEG = W // 4
79+
order = (BLACK, DARK, LIGHT, WHITE) # left -> right
80+
ramp = displayio.Bitmap(W, RAMP_H, 4)
81+
for x in range(W):
82+
col = order[min(x // SEG, 3)]
83+
for y in range(RAMP_H):
84+
ramp[x, y] = col
85+
# black outline + dividers
86+
for x in range(W):
87+
ramp[x, 0] = BLACK
88+
ramp[x, RAMP_H - 1] = BLACK
89+
for y in range(RAMP_H):
90+
ramp[0, y] = BLACK
91+
ramp[W - 1, y] = BLACK
92+
for i in range(1, 4):
93+
ramp[i * SEG, y] = BLACK
94+
g.append(displayio.TileGrid(ramp, pixel_shader=pal, x=0, y=RAMP_TOP))
95+
96+
display.root_group = g
97+
print("Refreshing 4-gray info card...")
98+
display.refresh()
99+
print("Done.")
100+
101+
time.sleep(display.time_to_refresh + 5)
102+
while True:
103+
time.sleep(10)

0 commit comments

Comments
 (0)