fix(eink): drive the whole panel on every update, not just the changes

The screen faded as the device was used: text went grey a few updates
after it was drawn, while whatever had just changed stayed crisp. Only a
full refresh brought it back, and the next few updates ate it again.

A partial update is differential -- the controller drives only the pixels
that differ between its "current" (0x24) and "previous" (0x26) RAM and
leaves the rest to hold their own charge, which this panel doesn't do
well. Priming the previous-image RAM with the inverse of the incoming
frame makes every pixel a difference, so all of them get driven to their
target, in both directions.

The inverse matters: priming with flat white makes only white->black a
difference, so ink is re-driven but never erased and every screen ever
shown accumulates as a ghost. Confirmed on hardware, both ways round.

Costs one extra full-screen RAM write (a few ms of SPI). The refresh
itself is unchanged -- the waveform clocks the whole panel regardless of
how many pixels it drives -- so nothing got slower. Clearing ghosts is
still the periodic full refresh's job (Settings > Full rfsh), which can
now stay off.

The helper needs GxEPD2_BW's private _buffer/_page_height, so it goes in
the patched copy of the header, which every e-ink build now includes
unconditionally rather than only screenshot-enabled ones. All e-ink
variants pin GxEPD2 1.6.2, the version that copy tracks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-28 15:02:28 +02:00
parent b36cc7730b
commit de16dbe32a
4 changed files with 34 additions and 8 deletions

View File

@@ -235,6 +235,15 @@ class GxEPD2_BW : public GxEPD2_GFX_BASE_CLASS
const uint8_t* getBuffer() { return _buffer; }
uint16_t getBufferSize() { return sizeof(_buffer); }
#endif
// Prime the controller's "previous image" RAM with the inverse of the frame
// display(true) is about to send, so that update drives every pixel instead
// of only the changed ones. Rationale in GxEPDDisplay::endFrame(); it lives
// here because it needs _buffer and _page_height. Same patched-copy note as
// above.
void writeInverseForRedrive()
{
epd2.writeImageAgain(_buffer, 0, 0, GxEPD2_Type::WIDTH, _page_height, true);
}
#if ENABLE_GxEPD2_GFX
GxEPD2_BW(GxEPD2_Type epd2_instance) : GxEPD2_GFX_BASE_CLASS(epd2, GxEPD2_Type::WIDTH_VISIBLE, GxEPD2_Type::HEIGHT), epd2(epd2_instance)
#else