Skip to content

Commit 79b2f8c

Browse files
authored
Merge pull request #11003 from mikeysklar/magtag-ssd1680-lut-reset-main
MagTag SSD1680: reset fix, GxEPD2_4G LUT, unify 0x44+0xca panels
2 parents f3bd71d + 243bf0b commit 79b2f8c

1 file changed

Lines changed: 56 additions & 42 deletions

File tree

  • ports/espressif/boards/adafruit_magtag_2.9_grayscale

ports/espressif/boards/adafruit_magtag_2.9_grayscale/board.c

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "shared-bindings/busio/SPI.h"
1212
#include "shared-bindings/fourwire/FourWire.h"
1313
#include "shared-bindings/microcontroller/Pin.h"
14+
#include "shared-bindings/microcontroller/__init__.h"
1415
#include "shared-module/displayio/__init__.h"
1516
#include "supervisor/shared/board.h"
1617

@@ -128,38 +129,49 @@ const uint8_t ssd1680_display_start_sequence[] = {
128129
0x22, 0x00, 0x01, 0xc7 // display update mode
129130
};
130131

131-
// FPC-7519rev.b (User ID byte 0xca) requires lower VCOM for correct contrast.
132-
// VCOM=0x14 (-1.0V) confirmed by reading the panel's OTP register (cmd 0x2D, byte 1 = 0x14).
133-
// The 0x44 panel works correctly with the default VCOM=0x28, so keep them separate.
134-
const uint8_t ssd1680_vcom14_display_start_sequence[] = {
132+
// FPC-7519rev.b panels (User ID byte 0x44 or 0xca) need colstart=8 and tuned VCOM + LUT.
133+
// LUT: Good Display reference (GxEPD2_4G / GDEM029T94) with VS rows reversed (L0↔L3, L1↔L2).
134+
// Reason: CircuitPython maps luma 0→L0 and luma 255→L3. On this panel VSH1(0x40) drives WHITE and
135+
// VSL(0x20) drives BLACK, so L0 must carry the black-driving waveform and L3 the white-driving
136+
// waveform — opposite from GxEPD2_4G's Arduino convention (where index 0 = white constant).
137+
// VS=0x48 = VSH1/GND/VSL/GND alternating for DC balance.
138+
// VCOM=0x24 empirically tuned for FPC-7519rev.b contrast. Both 0x44 and 0xca share this sequence.
139+
const uint8_t ssd1680_fpc7519_display_start_sequence[] = {
135140
0x12, DELAY, 0x00, 0x14, // soft reset and wait 20ms
136141
0x11, 0x00, 0x01, 0x03, // Ram data entry mode
137142
0x3c, 0x00, 0x01, 0x03, // border color
138-
0x2c, 0x00, 0x01, 0x14, // Set vcom voltage (0x14 = -1.0V, tuned for FPC-7519rev.b)
143+
0x2c, 0x00, 0x01, 0x24, // Set vcom voltage (0x24 tuned for FPC-7519rev.b contrast)
139144
0x03, 0x00, 0x01, 0x17, // Set gate voltage
140145
0x04, 0x00, 0x03, 0x41, 0xae, 0x32, // Set source voltage
141146
0x4e, 0x00, 0x01, 0x01, // ram x count
142147
0x4f, 0x00, 0x02, 0x00, 0x00, // ram y count
143148
0x01, 0x00, 0x03, 0x27, 0x01, 0x00, // set display size
144-
0x32, 0x00, 0x99, // Update waveforms
145-
0x2a, 0x60, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L0
146-
0x20, 0x60, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L1
147-
0x28, 0x60, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L2
148-
0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L3
149-
0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L4
150-
0x00, 0x02, 0x00, 0x05, 0x14, 0x00, 0x00, // TP, SR, RP of Group0
151-
0x1E, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x01, // TP, SR, RP of Group1
152-
0x00, 0x02, 0x00, 0x05, 0x14, 0x00, 0x00, // TP, SR, RP of Group2
153-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group3
154-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group4
155-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group5
156-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group6
157-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group7
158-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group8
159-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group9
160-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group10
161-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group11
162-
0x24, 0x22, 0x22, 0x22, 0x23, 0x32, 0x00, 0x00, 0x00, // FR, XON
149+
0x32, 0x00, 0x99, // Update waveforms (153 bytes follow)
150+
// VS rows: only L0↔L3 are swapped relative to GxEPD2_4G; L1 and L2 stay in place.
151+
// Reason: CircuitPython luma maps 0→L0, 64-127→L2, 128-191→L1, 255→L3.
152+
// GxEPD2 uses L0=white-driver, L3=black-driver (opposite of CircuitPython for extremes).
153+
// GxEPD2 L1=lighter-gray, L2=darker-gray; CircuitPython L1=mid-bright, L2=mid-dark — same ordering.
154+
// So: swap only L0↔L3 (polarity); L1 and L2 keep their GxEPD2 positions (gradient preserved).
155+
// 0x48 = VSH1/GND/VSL/GND alternating — better DC balance than 0x60 (VSH1/VSL/GND/GND)
156+
0x20, 0x48, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L0 (darkest/black) ← GxEPD2 L3
157+
0x08, 0x48, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L1 (mid-light) ← GxEPD2 L1 (luma 128-191 → lighter)
158+
0x02, 0x48, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L2 (mid-dark) ← GxEPD2 L2 (luma 64-127 → darker)
159+
0x40, 0x48, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L3 (lightest/white) ← GxEPD2 L0
160+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L4 VCOM
161+
// Timing groups — Good Display reference timing
162+
0x0A, 0x19, 0x00, 0x03, 0x08, 0x00, 0x00, // Group0: 10+25 / 3+8 frames activation
163+
0x14, 0x01, 0x00, 0x14, 0x01, 0x00, 0x03, // Group1: 20+1 / 20+1 frames, RP=3 repeats
164+
0x0A, 0x03, 0x00, 0x08, 0x19, 0x00, 0x00, // Group2: 10+3 / 8+25 frames (mirror of G0)
165+
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // Group3: 1 frame settle
166+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group4
167+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group5
168+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group6
169+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group7
170+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group8
171+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group9
172+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group10
173+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Group11
174+
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, // XON
163175
0x22, 0x00, 0x01, 0xc7 // display update mode
164176
};
165177

@@ -175,8 +187,7 @@ const uint8_t ssd1680_display_refresh_sequence[] = {
175187
typedef enum {
176188
DISPLAY_IL0373,
177189
DISPLAY_SSD1680_COLSTART_0,
178-
DISPLAY_SSD1680_COLSTART_8,
179-
DISPLAY_SSD1680_COLSTART_8_VCOM14, // FPC-7519rev.b (User ID 0xca)
190+
DISPLAY_SSD1680_COLSTART_8, // FPC-7519rev.b (User ID 0x44 or 0xca)
180191
} display_type_t;
181192

182193
static display_type_t detect_display_type(void) {
@@ -185,13 +196,14 @@ static display_type_t detect_display_type(void) {
185196
// NOTE: the SSD1680 drives its response back on the MOSI/DATA line (GPIO35) in half-duplex
186197
// mode, NOT on the separate MISO line (GPIO37). Read with GPIO35 switched to input.
187198
// On the IL0373 it will return 0xff because it's not a valid register.
188-
// With SSD1680, we have seen three types:
199+
// With SSD1680, we have seen two types:
189200
// 1. The first batch of displays, labeled "FPC-A005 20.06.15 TRX", which needs colstart=0.
190201
// These have 10 bytes of zeros in the User ID.
191-
// 2. Second batch, labeled "FPC-7619rev.b", which needs colstart=8.
192-
// The USER ID for these boards is [0x44, 0x0, 0x4, 0x0, 0x25, 0x0, 0x1, 0x78, 0x2b, 0xe]
193-
// 3. Third batch, labeled "FPC-7519rev.b", which needs colstart=8.
194-
// The USER ID for these boards is [0xca, 0xfe, 0x0, 0x16, 0x80, 0x0, 0x75, 0x1, 0x0, 0x98]
202+
// 2. Later panels, labeled "FPC-7519rev.b", which need colstart=8 and a tuned LUT/VCOM.
203+
// Two controller variants exist within this panel generation:
204+
// User ID [0x44, 0x0, 0x4, 0x0, 0x25, 0x0, 0x1, 0x78, 0x2b, 0xe]
205+
// User ID [0xca, 0xfe, 0x0, 0x16, 0x80, 0x0, 0x75, 0x1, 0x0, 0x98]
206+
// Both carry the same ribbon label and show the same display characteristics.
195207
// So let's distinguish just by the first byte.
196208
digitalio_digitalinout_obj_t data;
197209
digitalio_digitalinout_obj_t clock;
@@ -214,9 +226,15 @@ static display_type_t detect_display_type(void) {
214226
common_hal_digitalio_digitalinout_switch_to_output(&chip_select, false, DRIVE_MODE_PUSH_PULL);
215227
common_hal_digitalio_digitalinout_switch_to_output(&data_command, false, DRIVE_MODE_PUSH_PULL);
216228
common_hal_digitalio_digitalinout_switch_to_output(&data, false, DRIVE_MODE_PUSH_PULL);
217-
common_hal_digitalio_digitalinout_switch_to_output(&reset, true, DRIVE_MODE_PUSH_PULL);
218229
common_hal_digitalio_digitalinout_switch_to_output(&clock, false, DRIVE_MODE_PUSH_PULL);
219230

231+
// Pulse RESET low to wake SSD1680 from deep sleep (entered via stop_sequence on prior run).
232+
// SSD1680 ignores all SPI commands while in deep sleep; only a hardware reset exits it.
233+
common_hal_digitalio_digitalinout_switch_to_output(&reset, false, DRIVE_MODE_PUSH_PULL);
234+
common_hal_mcu_delay_us(200);
235+
common_hal_digitalio_digitalinout_set_value(&reset, true);
236+
common_hal_mcu_delay_us(10000); // 10ms for controller to come out of reset
237+
220238
uint8_t status_read = 0x2e; // SSD1680 User ID register. Not a valid register on IL0373.
221239
for (int i = 0; i < 8; i++) {
222240
common_hal_digitalio_digitalinout_set_value(&data, (status_read & (1 << (7 - i))) != 0);
@@ -250,13 +268,12 @@ static display_type_t detect_display_type(void) {
250268
switch (status) {
251269
case 0xff:
252270
return DISPLAY_IL0373;
253-
default: // who knows? Just guess.
254271
case 0x00:
255272
return DISPLAY_SSD1680_COLSTART_0;
273+
default: // unknown SSD1680 variant — assume newer panel needs colstart=8
256274
case 0x44:
257-
return DISPLAY_SSD1680_COLSTART_8;
258275
case 0xca:
259-
return DISPLAY_SSD1680_COLSTART_8_VCOM14;
276+
return DISPLAY_SSD1680_COLSTART_8;
260277
}
261278
}
262279

@@ -304,14 +321,11 @@ void board_init(void) {
304321
common_hal_epaperdisplay_epaperdisplay_construct(display, &args);
305322
} else {
306323
epaperdisplay_construct_args_t args = EPAPERDISPLAY_CONSTRUCT_ARGS_DEFAULTS;
307-
// Default colstart is 0.
308-
if (display_type == DISPLAY_SSD1680_COLSTART_8 || display_type == DISPLAY_SSD1680_COLSTART_8_VCOM14) {
309-
args.colstart = 8;
310-
}
311324
args.bus = bus;
312-
if (display_type == DISPLAY_SSD1680_COLSTART_8_VCOM14) {
313-
args.start_sequence = ssd1680_vcom14_display_start_sequence;
314-
args.start_sequence_len = sizeof(ssd1680_vcom14_display_start_sequence);
325+
if (display_type == DISPLAY_SSD1680_COLSTART_8) {
326+
args.colstart = 8;
327+
args.start_sequence = ssd1680_fpc7519_display_start_sequence;
328+
args.start_sequence_len = sizeof(ssd1680_fpc7519_display_start_sequence);
315329
} else {
316330
args.start_sequence = ssd1680_display_start_sequence;
317331
args.start_sequence_len = sizeof(ssd1680_display_start_sequence);

0 commit comments

Comments
 (0)