Skip to content

Commit 86448e0

Browse files
committed
Add partialWindowUpdate function to the 13spectra driver
1 parent 98c2c9b commit 86448e0

3 files changed

Lines changed: 324 additions & 2 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Inkplate13SPECTRA_Partial_Update example for Soldered Inkplate 13SPECTRA
3+
Select "Soldered Inkplate 13SPECTRA" from Tools -> Board menu.
4+
5+
This example demonstrates partial screen updates on the Inkplate 13SPECTRA.
6+
It draws a grid of 100x100 pixel coloured squares covering the entire screen,
7+
then continuously picks a random square and updates only that square with a
8+
new colour using displayPartial(), leaving the rest of the screen untouched.
9+
10+
displayPartial(x, y, w, h) accepts coordinates in the same user space as all
11+
drawing functions (rotation=1: 1600 px wide, 1200 px tall).
12+
13+
Want to learn more about Inkplate? Visit www.inkplate.io
14+
Looking to get support? Write on our forums: https://forum.soldered.com/
15+
24 March 2026 by Soldered
16+
*/
17+
18+
#ifndef ARDUINO_INKPLATE13SPECTRA
19+
#error "Wrong board selection for this example, please select Soldered Inkplate 13SPECTRA in the boards menu."
20+
#endif
21+
22+
#include "Inkplate.h"
23+
24+
Inkplate display;
25+
26+
// Display dimensions at rotation=1
27+
#define DISPLAY_W 1600
28+
#define DISPLAY_H 1200
29+
30+
// Square size and grid dimensions
31+
#define SQUARE_SIZE 200
32+
#define GRID_COLS (DISPLAY_W / SQUARE_SIZE) // 8
33+
#define GRID_ROWS (DISPLAY_H / SQUARE_SIZE) // 6
34+
35+
// Available colours (indices 0-5 map to: black, white, yellow, red, blue, green)
36+
static const uint8_t COLORS[] = {0, 1, 2, 3, 4, 5};
37+
#define COLOR_COUNT 6
38+
39+
// Tracks the current colour of every square so we can pick a different one
40+
uint8_t squareColor[GRID_COLS][GRID_ROWS];
41+
42+
void setup()
43+
{
44+
Serial.begin(115200);
45+
46+
display.begin();
47+
display.clearDisplay();
48+
49+
// Fill each square with an initial colour, cycling through the palette
50+
for (int col = 0; col < GRID_COLS; col++)
51+
{
52+
for (int row = 0; row < GRID_ROWS; row++)
53+
{
54+
uint8_t color = COLORS[(col + row) % COLOR_COUNT];
55+
squareColor[col][row] = color;
56+
display.fillRect(col * SQUARE_SIZE, row * SQUARE_SIZE,
57+
SQUARE_SIZE, SQUARE_SIZE, color);
58+
}
59+
}
60+
61+
display.display();
62+
63+
Serial.println("Initial grid drawn. Waiting 5 seconds before partial updates begin.");
64+
delay(5000);
65+
}
66+
67+
void loop()
68+
{
69+
// Pick a random square
70+
int col = random(GRID_COLS);
71+
int row = random(GRID_ROWS);
72+
73+
// Pick a different colour than the current one
74+
uint8_t current = squareColor[col][row];
75+
uint8_t newColor;
76+
do
77+
{
78+
newColor = COLORS[random(COLOR_COUNT)];
79+
} while (newColor == current);
80+
81+
squareColor[col][row] = newColor;
82+
83+
// Update the framebuffer for just this square
84+
display.fillRect(col * SQUARE_SIZE, row * SQUARE_SIZE,
85+
SQUARE_SIZE, SQUARE_SIZE, newColor);
86+
87+
// Refresh only the changed square; pass true to keep panel on for next call.
88+
display.displayPartial(col * SQUARE_SIZE, row * SQUARE_SIZE,
89+
SQUARE_SIZE, SQUARE_SIZE, true);
90+
91+
Serial.print("Updated square (");
92+
Serial.print(col);
93+
Serial.print(", ");
94+
Serial.print(row);
95+
Serial.print(") to colour ");
96+
Serial.println(newColor);
97+
98+
// 3 second delay between refreshes
99+
delay(3000);
100+
}

src/boards/Inkplate13/Inkplate13Driver.cpp

Lines changed: 223 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
*/
337554
void 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+
*/
488707
void 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+
*/
500721
void EPDDriver::setPanelPinsToLow()
501722
{
502723
pinMode(SPECTRA133_DC_PIN, OUTPUT);

src/boards/Inkplate13/Inkplate13Driver.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class EPDDriver
3737
int initDriver(Inkplate *_inkplatePtr);
3838

3939
void display(bool _leaveOn = 0);
40+
void displayPartial(int16_t x, int16_t y, int16_t w, int16_t h, bool _leaveOn = 0);
4041
void selectDisplayMode(uint8_t displayMode);
4142
void clearDisplay();
4243

0 commit comments

Comments
 (0)