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[] = {
175187typedef 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
182193static 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