@@ -182,6 +182,220 @@ void EPDDriver::display(bool _leaveOn)
182182 setPanelState (false );
183183}
184184
185+ /* *
186+ * @brief displayPartial refreshes only the specified rectangular region of the screen
187+ * using the PTLW (Partial Load Window) register of the GDEP133C02 controller.
188+ * Only the pixels inside the window are transferred to the panel; the rest of the
189+ * display is unaffected.
190+ *
191+ * @param int16_t x Left edge of the update window (user space)
192+ * @param int16_t y Top edge of the update window (user space)
193+ * @param int16_t w Width of the update window in pixels
194+ * @param int16_t h Height of the update window in pixels
195+ * @param bool _leaveOn If true, panel power is left on after the update
196+ */
197+ void EPDDriver::displayPartial (int16_t x, int16_t y, int16_t w, int16_t h, bool _leaveOn)
198+ {
199+ // Clip to the screen bounds for the current rotation.
200+ if (x < 0 ) { w += x; x = 0 ; }
201+ if (y < 0 ) { h += y; y = 0 ; }
202+ if (x + w > _inkplate->width ()) w = _inkplate->width () - x;
203+ if (y + h > _inkplate->height ()) h = _inkplate->height () - y;
204+ if (w <= 0 || h <= 0 ) return ;
205+
206+ // Map user rectangle to panel-native rectangle.
207+ // Panel native: col = 0..E_INK_WIDTH-1 (1199), row = 0..E_INK_HEIGHT-1 (1599).
208+ // Each case mirrors the per-pixel transform in writePixelInternal.
209+ int16_t colStart, colEnd, rowStart, rowEnd;
210+ switch (_inkplate->getRotation ())
211+ {
212+ case 0 :
213+ // User space: E_INK_WIDTH × E_INK_HEIGHT.
214+ // panel_col = (E_INK_WIDTH-1) - x, panel_row = (E_INK_HEIGHT-1) - y.
215+ colStart = (int16_t )E_INK_WIDTH - x - w;
216+ colEnd = (int16_t )E_INK_WIDTH - 1 - x;
217+ rowStart = (int16_t )E_INK_HEIGHT - y - h;
218+ rowEnd = (int16_t )E_INK_HEIGHT - 1 - y;
219+ break ;
220+ case 2 :
221+ // User space: E_INK_WIDTH × E_INK_HEIGHT.
222+ // panel_col = x, panel_row = y (identity — no transform applied in writePixelInternal).
223+ colStart = x;
224+ colEnd = x + w - 1 ;
225+ rowStart = y;
226+ rowEnd = y + h - 1 ;
227+ break ;
228+ case 3 :
229+ // User space: E_INK_HEIGHT × E_INK_WIDTH.
230+ // panel_col = (E_INK_WIDTH-1) - y, panel_row = x.
231+ colStart = (int16_t )E_INK_WIDTH - y - h;
232+ colEnd = (int16_t )E_INK_WIDTH - 1 - y;
233+ rowStart = x;
234+ rowEnd = x + w - 1 ;
235+ break ;
236+ default :
237+ case 1 :
238+ // User space: E_INK_HEIGHT × E_INK_WIDTH.
239+ // panel_col = y, panel_row = (E_INK_HEIGHT-1) - x.
240+ colStart = y;
241+ colEnd = y + h - 1 ;
242+ rowStart = (int16_t )E_INK_HEIGHT - x - w;
243+ rowEnd = (int16_t )E_INK_HEIGHT - 1 - x;
244+ break ;
245+ }
246+
247+ // PTLW alignment requirements (GDEP133C02):
248+ // H: colStart and (colEnd+1) must both be multiples of 4.
249+ // V: rowStart must be even; (rowEnd+1) must be even.
250+ colStart = (colStart / 4 ) * 4 ;
251+ colEnd = (((colEnd + 4 ) / 4 ) * 4 ) - 1 ;
252+ if (colEnd >= (int16_t )E_INK_WIDTH) colEnd = (int16_t )E_INK_WIDTH - 1 ;
253+ if (rowStart % 2 != 0 ) rowStart--;
254+ if (rowStart < 0 ) rowStart = 0 ;
255+ if ((rowEnd + 1 ) % 2 != 0 ) rowEnd++;
256+ if (rowEnd >= (int16_t )E_INK_HEIGHT) rowEnd = (int16_t )E_INK_HEIGHT - 1 ;
257+
258+ setPanelState (true );
259+
260+ const int16_t HALF_WIDTH = (int16_t )(E_INK_WIDTH / 2 ); // 600 pixels per chip
261+ const int16_t HALF_BYTES = HALF_WIDTH / 2 ; // 300 bytes per row per chip
262+
263+ bool masterNeeded = (colStart < HALF_WIDTH);
264+ bool slaveNeeded = (colEnd >= HALF_WIDTH);
265+
266+ // Both chips must receive a full PTLW+DTM cycle before DRF, otherwise the
267+ // uninvolved chip falls back to a full-panel refresh when DRF fires.
268+ // For the uninvolved chip, a minimal 4×4 null window is used: it reads the
269+ // existing framebuffer data (same as what is already on screen) so the
270+ // refresh produces no visible change on that side.
271+ static const uint8_t ptlwNull[9 ] = {
272+ 0x00 , 0x00 , // HRST = 0
273+ 0x00 , 0x07 , // HRED = 7
274+ 0x00 , 0x00 , // VRST = 0
275+ 0x00 , 0x01 , // VRED = 1
276+ 0x01 // PT = 1 (enable)
277+ };
278+
279+ // Master chip
280+ {
281+ uint8_t ptlwData[9 ];
282+ int16_t bytesPerRow, memColOff, rStart, rEnd;
283+
284+ if (masterNeeded)
285+ {
286+ int16_t lcs = colStart;
287+ int16_t lce = (colEnd < HALF_WIDTH) ? colEnd : (HALF_WIDTH - 1 );
288+ uint16_t HRST = (uint16_t )lcs * 2 ;
289+ uint16_t HRED = (uint16_t )(lce + 1 ) * 2 - 1 ;
290+ uint16_t VRST = (uint16_t )rowStart / 2 ;
291+ uint16_t VRED = (uint16_t )(rowEnd + 1 ) / 2 - 1 ;
292+ ptlwData[0 ] = HRST >> 8 ; ptlwData[1 ] = HRST & 0xFF ;
293+ ptlwData[2 ] = HRED >> 8 ; ptlwData[3 ] = HRED & 0xFF ;
294+ ptlwData[4 ] = VRST >> 8 ; ptlwData[5 ] = VRST & 0xFF ;
295+ ptlwData[6 ] = VRED >> 8 ; ptlwData[7 ] = VRED & 0xFF ;
296+ ptlwData[8 ] = 0x01 ;
297+ bytesPerRow = (lce - lcs + 1 ) / 2 ;
298+ memColOff = lcs / 2 ;
299+ rStart = rowStart;
300+ rEnd = rowEnd;
301+ }
302+ else
303+ {
304+ memcpy (ptlwData, ptlwNull, 9 );
305+ bytesPerRow = 2 ; // 4 px / 2 px-per-byte
306+ memColOff = 0 ; // top-left corner of master's region
307+ rStart = 0 ;
308+ rEnd = 3 ;
309+ }
310+
311+ SPI.beginTransaction (epdSpiSettings);
312+ digitalWrite (SPECTRA133_CS_M_PIN, LOW);
313+ SPI.write (SPECTRA133_REGISTER_CMD66);
314+ SPI.writeBytes (SPECTRA133_REGISTER_CMD66_V, sizeof (SPECTRA133_REGISTER_CMD66_V));
315+ digitalWrite (SPECTRA133_CS_M_PIN, HIGH);
316+ SPI.endTransaction ();
317+
318+ SPI.beginTransaction (epdSpiSettings);
319+ digitalWrite (SPECTRA133_CS_M_PIN, LOW);
320+ SPI.write (SPECTRA133_REGISTER_PTLW);
321+ SPI.writeBytes (ptlwData, 9 );
322+ digitalWrite (SPECTRA133_CS_M_PIN, HIGH);
323+ SPI.endTransaction ();
324+
325+ SPI.beginTransaction (epdSpiSettings);
326+ digitalWrite (SPECTRA133_CS_M_PIN, LOW);
327+ SPI.write (SPECTRA133_REGISTER_DTM);
328+ for (int16_t row = rStart; row <= rEnd; row++)
329+ SPI.writeBytes (DMemory4Bit + row * (E_INK_WIDTH / 2 ) + memColOff, bytesPerRow);
330+ digitalWrite (SPECTRA133_CS_M_PIN, HIGH);
331+ SPI.endTransaction ();
332+ }
333+
334+ // Slave chip
335+ waitForBusy ();
336+ {
337+ uint8_t ptlwData[9 ];
338+ int16_t bytesPerRow, memColOff, rStart, rEnd;
339+
340+ if (slaveNeeded)
341+ {
342+ int16_t lcs = (colStart >= HALF_WIDTH) ? (colStart - HALF_WIDTH) : 0 ;
343+ int16_t lce = colEnd - HALF_WIDTH;
344+ uint16_t HRST = (uint16_t )lcs * 2 ;
345+ uint16_t HRED = (uint16_t )(lce + 1 ) * 2 - 1 ;
346+ uint16_t VRST = (uint16_t )rowStart / 2 ;
347+ uint16_t VRED = (uint16_t )(rowEnd + 1 ) / 2 - 1 ;
348+ ptlwData[0 ] = HRST >> 8 ; ptlwData[1 ] = HRST & 0xFF ;
349+ ptlwData[2 ] = HRED >> 8 ; ptlwData[3 ] = HRED & 0xFF ;
350+ ptlwData[4 ] = VRST >> 8 ; ptlwData[5 ] = VRST & 0xFF ;
351+ ptlwData[6 ] = VRED >> 8 ; ptlwData[7 ] = VRED & 0xFF ;
352+ ptlwData[8 ] = 0x01 ;
353+ bytesPerRow = (lce - lcs + 1 ) / 2 ;
354+ memColOff = HALF_BYTES + lcs / 2 ;
355+ rStart = rowStart;
356+ rEnd = rowEnd;
357+ }
358+ else
359+ {
360+ memcpy (ptlwData, ptlwNull, 9 );
361+ bytesPerRow = 2 ; // 4 px / 2 px-per-byte
362+ memColOff = HALF_BYTES; // top-left corner of slave's region
363+ rStart = 0 ;
364+ rEnd = 3 ;
365+ }
366+
367+ SPI.beginTransaction (epdSpiSettings);
368+ digitalWrite (SPECTRA133_CS_S_PIN, LOW);
369+ SPI.write (SPECTRA133_REGISTER_CMD66);
370+ SPI.writeBytes (SPECTRA133_REGISTER_CMD66_V, sizeof (SPECTRA133_REGISTER_CMD66_V));
371+ digitalWrite (SPECTRA133_CS_S_PIN, HIGH);
372+ SPI.endTransaction ();
373+
374+ SPI.beginTransaction (epdSpiSettings);
375+ digitalWrite (SPECTRA133_CS_S_PIN, LOW);
376+ SPI.write (SPECTRA133_REGISTER_PTLW);
377+ SPI.writeBytes (ptlwData, 9 );
378+ digitalWrite (SPECTRA133_CS_S_PIN, HIGH);
379+ SPI.endTransaction ();
380+
381+ SPI.beginTransaction (epdSpiSettings);
382+ digitalWrite (SPECTRA133_CS_S_PIN, LOW);
383+ SPI.write (SPECTRA133_REGISTER_DTM);
384+ for (int16_t row = rStart; row <= rEnd; row++)
385+ SPI.writeBytes (DMemory4Bit + row * (E_INK_WIDTH / 2 ) + memColOff, bytesPerRow);
386+ digitalWrite (SPECTRA133_CS_S_PIN, HIGH);
387+ SPI.endTransaction ();
388+ }
389+
390+ // Both chips have received PTLW+DTM; trigger a coordinated refresh.
391+ waitForBusy ();
392+ sendCommand (SPECTRA133_REGISTER_DRF, SPECTRA133_REGISTER_DRF_V, sizeof (SPECTRA133_REGISTER_DRF_V), eChipIdBoth);
393+ waitForBusy ();
394+
395+ if (!_leaveOn)
396+ setPanelState (false );
397+ }
398+
185399/* *
186400 * @brief returns the current panel state, 0 for off, 1 for on
187401 *
@@ -334,6 +548,9 @@ void EPDDriver::sendCommand(uint8_t _cmd, const uint8_t *_parameters, uint32_t _
334548}
335549
336550
551+ /* *
552+ * @brief screenInit sends init commands to the panel.
553+ */
337554void EPDDriver::screenInit ()
338555{
339556 // Send magic values to the registers. These values are provided from the manufacturer.
@@ -484,7 +701,9 @@ double EPDDriver::readBattery()
484701 return (double (adc) * 2.0 / 1000 );
485702}
486703
487- // Method waits until the screen is ready to accept new commands.
704+ /* *
705+ * @brief Method waits until the screen is ready to accept new commands.
706+ */
488707void EPDDriver::waitForBusy ()
489708{
490709 // Wait until the screen is ready to accept new commads.
@@ -496,7 +715,9 @@ void EPDDriver::waitForBusy()
496715 }
497716}
498717
499- // Function helps empty capacitors, without this sometimes the panel refuses to refresh...
718+ /* *
719+ * @brief Function helps empty capacitors, without this sometimes the panel refuses to refresh...
720+ */
500721void EPDDriver::setPanelPinsToLow ()
501722{
502723 pinMode (SPECTRA133_DC_PIN, OUTPUT);
0 commit comments