Skip to content

Commit 5ed07ba

Browse files
authored
Merge pull request #11002 from mikeysklar/magtag-ssd1680-lut-reset-10.2.x
MagTag SSD1680: reset fix, GxEPD2_4G LUT, unify 0x44+0xca panels
2 parents 2321a96 + a6ad2d0 commit 5ed07ba

1 file changed

Lines changed: 60 additions & 41 deletions

File tree

  • ports/espressif/boards/adafruit_magtag_2.9_grayscale

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

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
#include "supervisor/board.h"
88

99
#include "mpconfigboard.h"
10+
#include "driver/gpio.h"
1011
#include "shared-bindings/busio/SPI.h"
1112
#include "shared-bindings/fourwire/FourWire.h"
1213
#include "shared-bindings/microcontroller/Pin.h"
14+
#include "shared-bindings/microcontroller/__init__.h"
1315
#include "shared-module/displayio/__init__.h"
1416
#include "supervisor/shared/board.h"
1517

@@ -127,38 +129,49 @@ const uint8_t ssd1680_display_start_sequence[] = {
127129
0x22, 0x00, 0x01, 0xc7 // display update mode
128130
};
129131

130-
// FPC-7519rev.b (User ID byte 0xca) requires lower VCOM for correct contrast.
131-
// VCOM=0x14 (-1.0V) confirmed by reading the panel's OTP register (cmd 0x2D, byte 1 = 0x14).
132-
// The 0x44 panel works correctly with the default VCOM=0x28, so keep them separate.
133-
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[] = {
134140
0x12, DELAY, 0x00, 0x14, // soft reset and wait 20ms
135141
0x11, 0x00, 0x01, 0x03, // Ram data entry mode
136142
0x3c, 0x00, 0x01, 0x03, // border color
137-
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)
138144
0x03, 0x00, 0x01, 0x17, // Set gate voltage
139145
0x04, 0x00, 0x03, 0x41, 0xae, 0x32, // Set source voltage
140146
0x4e, 0x00, 0x01, 0x01, // ram x count
141147
0x4f, 0x00, 0x02, 0x00, 0x00, // ram y count
142148
0x01, 0x00, 0x03, 0x27, 0x01, 0x00, // set display size
143-
0x32, 0x00, 0x99, // Update waveforms
144-
0x2a, 0x60, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L0
145-
0x20, 0x60, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L1
146-
0x28, 0x60, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L2
147-
0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L3
148-
0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VS L4
149-
0x00, 0x02, 0x00, 0x05, 0x14, 0x00, 0x00, // TP, SR, RP of Group0
150-
0x1E, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x01, // TP, SR, RP of Group1
151-
0x00, 0x02, 0x00, 0x05, 0x14, 0x00, 0x00, // TP, SR, RP of Group2
152-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group3
153-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group4
154-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group5
155-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group6
156-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group7
157-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group8
158-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group9
159-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group10
160-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP, SR, RP of Group11
161-
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
162175
0x22, 0x00, 0x01, 0xc7 // display update mode
163176
};
164177

@@ -174,19 +187,23 @@ const uint8_t ssd1680_display_refresh_sequence[] = {
174187
typedef enum {
175188
DISPLAY_IL0373,
176189
DISPLAY_SSD1680_COLSTART_0,
177-
DISPLAY_SSD1680_COLSTART_8,
178-
DISPLAY_SSD1680_COLSTART_8_VCOM14, // FPC-7519rev.b (User ID 0xca)
190+
DISPLAY_SSD1680_COLSTART_8, // FPC-7519rev.b (User ID 0x44 or 0xca)
179191
} display_type_t;
180192

181193
static display_type_t detect_display_type(void) {
182-
// Bitbang 4-wire SPI with a bidirectional data line to read the first word of register 0x2e,
194+
// Bitbang 4-wire SPI with a bidirectional data line to read the first byte of register 0x2e,
183195
// which is the 10-byte USER ID.
196+
// NOTE: the SSD1680 drives its response back on the MOSI/DATA line (GPIO35) in half-duplex
197+
// mode, NOT on the separate MISO line (GPIO37). Read with GPIO35 switched to input.
184198
// On the IL0373 it will return 0xff because it's not a valid register.
185199
// With SSD1680, we have seen two types:
186200
// 1. The first batch of displays, labeled "FPC-A005 20.06.15 TRX", which needs colstart=0.
187-
// These have 10 byes of zeros in the User ID
188-
// 2. Second batch, labeled "FPC-7619rev.b", which needs colstart=8.
189-
// The USER ID for these boards is [0x44, 0x0, 0x4, 0x0, 0x25, 0x0, 0x1, 0x78, 0x2b, 0xe]
201+
// These have 10 bytes of zeros in the User ID.
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.
190207
// So let's distinguish just by the first byte.
191208
digitalio_digitalinout_obj_t data;
192209
digitalio_digitalinout_obj_t clock;
@@ -209,9 +226,15 @@ static display_type_t detect_display_type(void) {
209226
common_hal_digitalio_digitalinout_switch_to_output(&chip_select, false, DRIVE_MODE_PUSH_PULL);
210227
common_hal_digitalio_digitalinout_switch_to_output(&data_command, false, DRIVE_MODE_PUSH_PULL);
211228
common_hal_digitalio_digitalinout_switch_to_output(&data, false, DRIVE_MODE_PUSH_PULL);
212-
common_hal_digitalio_digitalinout_switch_to_output(&reset, true, DRIVE_MODE_PUSH_PULL);
213229
common_hal_digitalio_digitalinout_switch_to_output(&clock, false, DRIVE_MODE_PUSH_PULL);
214230

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+
215238
uint8_t status_read = 0x2e; // SSD1680 User ID register. Not a valid register on IL0373.
216239
for (int i = 0; i < 8; i++) {
217240
common_hal_digitalio_digitalinout_set_value(&data, (status_read & (1 << (7 - i))) != 0);
@@ -245,13 +268,12 @@ static display_type_t detect_display_type(void) {
245268
switch (status) {
246269
case 0xff:
247270
return DISPLAY_IL0373;
248-
default: // who knows? Just guess.
249271
case 0x00:
250272
return DISPLAY_SSD1680_COLSTART_0;
273+
default: // unknown SSD1680 variant — assume newer panel needs colstart=8
251274
case 0x44:
252-
return DISPLAY_SSD1680_COLSTART_8;
253275
case 0xca:
254-
return DISPLAY_SSD1680_COLSTART_8_VCOM14;
276+
return DISPLAY_SSD1680_COLSTART_8;
255277
}
256278
}
257279

@@ -299,14 +321,11 @@ void board_init(void) {
299321
common_hal_epaperdisplay_epaperdisplay_construct(display, &args);
300322
} else {
301323
epaperdisplay_construct_args_t args = EPAPERDISPLAY_CONSTRUCT_ARGS_DEFAULTS;
302-
// Default colstart is 0.
303-
if (display_type == DISPLAY_SSD1680_COLSTART_8 || display_type == DISPLAY_SSD1680_COLSTART_8_VCOM14) {
304-
args.colstart = 8;
305-
}
306324
args.bus = bus;
307-
if (display_type == DISPLAY_SSD1680_COLSTART_8_VCOM14) {
308-
args.start_sequence = ssd1680_vcom14_display_start_sequence;
309-
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);
310329
} else {
311330
args.start_sequence = ssd1680_display_start_sequence;
312331
args.start_sequence_len = sizeof(ssd1680_display_start_sequence);

0 commit comments

Comments
 (0)