From 6e89dcc79cd849272709bce39f7be102a759d99b Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 22 May 2026 01:33:52 -0700 Subject: [PATCH 1/3] fix(x3): clear post-full ghosting on the first differential refresh On X3 (UC81xx) with anti-aliasing off, the first fast/half differential after a full refresh garbled, bleeding the prior content: opening the reader menu or the first page turn after entering a book, and the first turn after the periodic full-refresh cadence. The controller's post-full state corrupts the next differential; it is not a DTM1-content problem (promoting that op to a half did not help, a full did). After any X3 full, spend that corrupt slot with a no-op fast of the just-displayed frame. DTM1 and DTM2 both hold it, so nothing visibly changes, but the controller is left in the post-fast state and the caller's next differential renders clean. --- libs/display/EInkDisplay/src/EInkDisplay.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/libs/display/EInkDisplay/src/EInkDisplay.cpp b/libs/display/EInkDisplay/src/EInkDisplay.cpp index aff884c..c347467 100644 --- a/libs/display/EInkDisplay/src/EInkDisplay.cpp +++ b/libs/display/EInkDisplay/src/EInkDisplay.cpp @@ -1263,7 +1263,6 @@ void EInkDisplay::displayBuffer(RefreshMode mode, const bool turnOffScreen) { const bool forcedFullSync = _x3ForceFullSyncNext; const bool doFullSync = (!fastMode && !halfMode) || !_x3RedRamSynced || _x3InitialFullSyncsRemaining > 0 || forcedFullSync; - // Half mode only applies if we're not already being promoted to full. const bool doHalfSync = halfMode && !doFullSync; if (Serial) { @@ -1380,6 +1379,21 @@ void EInkDisplay::displayBuffer(RefreshMode mode, const bool turnOffScreen) { sendCommand(CMD_X3_DATA_STOP); // commit DTM1 — no refresh follows _x3RedRamSynced = true; + // The first differential after a full garbles on X3: the controller's post-full state corrupts + // the next fast/half diff (not a DTM1-content issue; promoting that op to a half didn't help, a + // full did). Spend that slot here with a no-op fast of the just-displayed frame. DTM1 and DTM2 + // both hold it, so nothing visibly changes, but it leaves the controller in the post-fast state + // so the caller's next diff (menu open, first page turn, first turn after the periodic full) is + // clean instead of the garbling first-after-full. + if (doFullSync) { + loadLutBankX3WithCdi(0x29, 0x07, lut_x3_vcom_fast, lut_x3_ww_fast, lut_x3_bw_fast, lut_x3_wb_fast, + lut_x3_bb_fast); + sendPlaneX3(CMD_X3_DTM2, frameBuffer, false); + triggerRefreshX3(turnOffScreen, "(post-full settle)"); + sendPlaneX3(CMD_X3_DTM1, frameBuffer, false); + sendCommand(CMD_X3_DATA_STOP); + } + if (doFullSync && _x3InitialFullSyncsRemaining > 0) { _x3InitialFullSyncsRemaining--; } From 5d532e82ce69b1834addee81df87fb88d48e0bec Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 25 May 2026 02:17:37 -0700 Subject: [PATCH 2/3] refactor: introduce Panel hierarchy; retire `_x3Mode` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EInkDisplay was a runtime-switched monolith — a `_x3Mode` bool plus a fistful of `if (_x3Mode) ...` branches scattered across init, refresh, display, grayscale, and busy-poll code paths. Adding a third panel would have meant editing every one of those branches. This refactor introduces an abstract `Panel` base class with concrete X3Panel and X4Panel subclasses; EInkDisplay owns a `unique_ptr` and dispatches device-specific work through 18 virtual methods. What's in this commit: - **Panel hierarchy**: new `Panel.h` with abstract base + X4Panel declaration; `X3Panel.h` for the X3-specific helper surface that's too large to inline. Concrete impls live in `X3Panel.cpp` / `X4Panel.cpp` alongside their controller-specific LUT banks (`X3Luts.cpp` / `X4Luts.cpp`) and command opcodes (`X3Constants.h` / `X4Constants.h`). - **Panel selection at construction**: new `EInkDisplay(unique_ptr, sclk, mosi, cs, dc, rst, busy)` ctor. Legacy pin-only ctor still works (defaults to X4Panel for backcompat); `setDisplayX3()` is preserved as a `[[deprecated]]` shim that swaps the panel in place so existing firmware can migrate at its own pace. - **Friend-based access**: each Panel subclass is a `friend class` of EInkDisplay so device-specific code can reach into SPI/pin state directly. The alternative (a DisplayContext interface) was evaluated and rejected — see the design note at the top of `Panel.h` for the four-point rationale (hot-path perf without whole-program LTO, bounded panel set, same-author maintenance, EInkDisplay and Panel co-evolve). - **Scoped-enum API**: `RefreshMode` and `GrayPlane` promoted from C-style `enum` to `enum class : uint8_t`. The underlying-type pin locks any cache structure holding these enums at one byte (vs the 4-byte `int` default). Use sites qualify as `RefreshMode::FULL_REFRESH` / `GrayPlane::GRAY_PLANE_LSB` etc. Value names kept (no rename to `Full` / `Half` / `Fast`) to minimize churn at call sites; cosmetic rename can be a separate later pass. - **X3 state machine on X3Panel**: the resync flags (`_x3RedRamSynced`, `_x3InitialFullSyncsRemaining`, `_x3ForceFullSyncNext`, `_x3ForcedConditionPassesNext`, `_x3GrayState`) move to X3Panel so X4 builds carry none of it. `requestResync()` / `skipInitialResync()` on EInkDisplay become thin forwarders to `_panel->...`. Net: EInkDisplay shrinks from 1813 lines to ~390. Adding a third panel from here is a new TU + a friend declaration; no edits to EInkDisplay's dispatch. Hardware-verified on both X3 (port 101) and X4 (port 2101). --- .../display/EInkDisplay/include/EInkDisplay.h | 126 +- libs/display/EInkDisplay/include/Panel.h | 211 ++ libs/display/EInkDisplay/include/X3Luts.h | 44 + libs/display/EInkDisplay/include/X3Panel.h | 86 + libs/display/EInkDisplay/include/X4Luts.h | 10 + libs/display/EInkDisplay/src/EInkDisplay.cpp | 1691 ++--------------- libs/display/EInkDisplay/src/X3Constants.h | 45 + libs/display/EInkDisplay/src/X3Luts.cpp | 175 ++ libs/display/EInkDisplay/src/X3Panel.cpp | 571 ++++++ libs/display/EInkDisplay/src/X4Constants.h | 46 + libs/display/EInkDisplay/src/X4Luts.cpp | 141 ++ libs/display/EInkDisplay/src/X4Panel.cpp | 303 +++ 12 files changed, 1807 insertions(+), 1642 deletions(-) create mode 100644 libs/display/EInkDisplay/include/Panel.h create mode 100644 libs/display/EInkDisplay/include/X3Luts.h create mode 100644 libs/display/EInkDisplay/include/X3Panel.h create mode 100644 libs/display/EInkDisplay/include/X4Luts.h create mode 100644 libs/display/EInkDisplay/src/X3Constants.h create mode 100644 libs/display/EInkDisplay/src/X3Luts.cpp create mode 100644 libs/display/EInkDisplay/src/X3Panel.cpp create mode 100644 libs/display/EInkDisplay/src/X4Constants.h create mode 100644 libs/display/EInkDisplay/src/X4Luts.cpp create mode 100644 libs/display/EInkDisplay/src/X4Panel.cpp diff --git a/libs/display/EInkDisplay/include/EInkDisplay.h b/libs/display/EInkDisplay/include/EInkDisplay.h index 4af4de3..2ca751b 100644 --- a/libs/display/EInkDisplay/include/EInkDisplay.h +++ b/libs/display/EInkDisplay/include/EInkDisplay.h @@ -2,22 +2,39 @@ #include #include +#include + +#include "Panel.h" +#include "X3Panel.h" + class EInkDisplay { + friend class X3Panel; // grants X3-side helpers access to SPI/pin/state privates + friend class X4Panel; // ditto for X4-side panel logic + public: - // Constructor with pin configuration + // Preferred constructor: inject the panel at construction time. + // Example: + // EInkDisplay d(std::make_unique(), sclk, mosi, cs, dc, rst, busy); + // EInkDisplay d(std::make_unique(), sclk, mosi, cs, dc, rst, busy); + // `panel` must be non-null; ownership transfers to the display. + EInkDisplay(std::unique_ptr panel, int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy); + + // Legacy constructor. Defaults the panel to X4Panel so code that + // expects to call setDisplayX3() after construction still works. + // New code should use the panel-injecting constructor above; + // setDisplayX3() is deprecated. EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy); // Destructor ~EInkDisplay() = default; - // Refresh modes (guarded to avoid redefinition in test builds) - enum RefreshMode { - FULL_REFRESH, // Full refresh with complete waveform - HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed - FAST_REFRESH // Fast refresh using custom LUT - }; + // RefreshMode lives in Panel.h; reach via the qualified name. - // Set X3 panel geometry and mode (must be called before begin()) + // Set X3 panel geometry and mode (must be called before begin()). + // Deprecated: pass the panel to the constructor instead. Kept as a + // shim so existing callers can migrate at their own pace. + [[deprecated( + "Inject the panel at construction: EInkDisplay(std::make_unique(), sclk, mosi, cs, dc, rst, busy)")]] void setDisplayX3(); // Initialize the display hardware and driver @@ -34,16 +51,18 @@ class EInkDisplay { static constexpr uint32_t X3_BUFFER_SIZE = X3_DISPLAY_WIDTH_BYTES * X3_DISPLAY_HEIGHT; static constexpr uint32_t MAX_BUFFER_SIZE = 52272; // max(800x480, 792x528) / 8 - // Runtime dimensions - uint16_t getDisplayWidth() const { return displayWidth; } - uint16_t getDisplayHeight() const { return displayHeight; } - uint16_t getDisplayWidthBytes() const { return displayWidthBytes; } - uint32_t getBufferSize() const { return bufferSize; } + // Runtime dimensions (delegate to active panel) + uint16_t getDisplayWidth() const { return _panel->displayWidth(); } + uint16_t getDisplayHeight() const { return _panel->displayHeight(); } + uint16_t getDisplayWidthBytes() const { return _panel->displayWidthBytes(); } + uint32_t getBufferSize() const { return _panel->bufferSize(); } // Frame buffer operations void clearScreen(uint8_t color = 0xFF) const; - void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false) const; - void drawImageTransparent(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false) const; + void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + bool fromProgmem = false) const; + void drawImageTransparent(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + bool fromProgmem = false) const; #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE void swapBuffers(); #endif @@ -60,7 +79,7 @@ class EInkDisplay { // `yStart`. X4 writes each band as an independent windowed RAM write via // setRamArea; X3 (UC81xx) windows each band via PTL. Either way bands may be // streamed in any order. - enum GrayPlane { GRAY_PLANE_LSB, GRAY_PLANE_MSB }; + // GrayPlane lives in Panel.h; reach via the qualified name. void writeGrayscalePlaneStrip(GrayPlane plane, const uint8_t* rows, uint16_t yStart, uint16_t numRows); // True when the tiled/strip grayscale path is supported. X4 (SSD1677) windows @@ -70,12 +89,12 @@ class EInkDisplay { void cleanupGrayscaleBuffers(const uint8_t* bwBuffer); #endif - void displayBuffer(RefreshMode mode = FAST_REFRESH, bool turnOffScreen = false); + void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); // EXPERIMENTAL: Windowed update - display only a rectangular region void displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen = false); void displayGrayBuffer(bool turnOffScreen = false, const unsigned char* lut = nullptr, bool factoryMode = false); - void refreshDisplay(RefreshMode mode = FAST_REFRESH, bool turnOffScreen = false); + void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); // Hint the X3 policy to run a one-shot full resync on next update. void requestResync(uint8_t settlePasses = 0); @@ -96,35 +115,20 @@ class EInkDisplay { void deepSleep(); // Access to frame buffer - uint8_t* getFrameBuffer() const { - return frameBuffer; - } + uint8_t* getFrameBuffer() const { return frameBuffer; } // Save the current framebuffer to a PBM file (desktop/test builds only) void saveFrameBufferAsPBM(const char* filename); private: - // Internal geometry setter used by setDisplayX3(). - void setDisplayDimensions(uint16_t width, uint16_t height); - // Pin configuration int8_t _sclk, _mosi, _cs, _dc, _rst, _busy; - // Runtime display geometry - uint16_t displayWidth = DISPLAY_WIDTH; - uint16_t displayHeight = DISPLAY_HEIGHT; - uint16_t displayWidthBytes = DISPLAY_WIDTH_BYTES; - uint32_t bufferSize = BUFFER_SIZE; - bool _x3Mode = false; - bool _x3RedRamSynced = false; - struct X3GrayState { - bool lastBaseWasPartial = false; - bool lsbValid = false; - }; - X3GrayState _x3GrayState; - uint8_t _x3InitialFullSyncsRemaining = 0; - bool _x3ForceFullSyncNext = false; - uint8_t _x3ForcedConditionPassesNext = 0; + // Active panel: source of truth for all per-device behavior. + // Set by each constructor (X4Panel default for the legacy form, + // caller-supplied for the panel-injecting form). Never null after + // construction; the deprecated setDisplayX3() shim swaps it in place. + std::unique_ptr _panel; // Frame buffer (statically allocated) uint8_t frameBuffer0[MAX_BUFFER_SIZE]; uint8_t* frameBuffer; @@ -149,13 +153,9 @@ class EInkDisplay { void sendData(const uint8_t* data, uint16_t length); void waitForRefresh(const char* comment = nullptr); void waitWhileBusy(const char* comment = nullptr); - // Shared body for the two waits above. X4 (SSD1677) and X3 (UC81xx-class) - // use opposite BUSY-line polarities: - // X4: active HIGH. BUSY HIGH while working, drops LOW when done. - // X3: active LOW. BUSY HIGH when idle, drops LOW while working, returns - // HIGH when done. - // The per-panel polling logic therefore stays gated; consolidation here - // is the function body only. + // Delegates to Panel::pollBusy. Wait policy (X4 single-phase active-HIGH + // vs X3 two-phase active-LOW with sawLow early-return) lives on each + // Panel subclass. void pollBusy(const char* comment, const char* completeWord); void initDisplayController(); @@ -163,42 +163,10 @@ class EInkDisplay { void setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h); void writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size); - // X3 (UC81xx) primitives. Promoted from inline lambdas that used to be - // redefined in displayBuffer / displayGrayBuffer / grayscaleRevert. These - // fuse a command byte and a short data payload into one CS-low SPI - // transaction — used for LUT register / mode-select / partial-window - // writes where the payload is small. Bulk plane writes go through - // sendPlaneX3/fillPlaneX3 (separated sendCommand+sendData). Not an - // atomicity requirement, just convenience. - void sendCommandDataX3(uint8_t cmd, const uint8_t* data, uint16_t len); - void sendCommandDataByteX3(uint8_t cmd, uint8_t d0); - void sendCommandDataByteX3(uint8_t cmd, uint8_t d0, uint8_t d1); - // Bulk-write a pixel plane to one of the DTM RAM commands. Y-flips rows - // in-place (X3 controller scans gates upward), optionally inverts bits - // before sending, then restores the buffer. - void sendPlaneX3(uint8_t ramCmd, uint8_t* buf, bool invert); - // Fill an entire RAM plane with a single byte (e.g., 0xFF for white). - // Streams a small row buffer repeatedly so the framebuffer isn't touched. - void fillPlaneX3(uint8_t ramCmd, uint8_t fillByte); - // Load all 5 LUT registers (VCOM/WW/BW/WB/BB) in one call. Each pointer - // must reference a 42-byte LUT bank in PROGMEM/DRAM. - void loadLutBankX3(const uint8_t* vcom, const uint8_t* ww, - const uint8_t* bw, const uint8_t* wb, - const uint8_t* bb); - void loadLutBankX3WithCdi(uint8_t cdi0, const uint8_t* vcom, - const uint8_t* ww, const uint8_t* bw, - const uint8_t* wb, const uint8_t* bb); - void loadLutBankX3WithCdi(uint8_t cdi0, uint8_t cdi1, - const uint8_t* vcom, const uint8_t* ww, - const uint8_t* bw, const uint8_t* wb, - const uint8_t* bb); - // Power-on if needed, trigger refresh, optionally power-off. The `tag` - // string is included verbatim in busy-wait log lines. - void triggerRefreshX3(bool turnOffScreen, const char* tag); }; // Factory LUTs extracted from firmware V3.1.9_CH_X4_0117.bin. // Uses absolute 2-bit pixel encoding for single-pass grayscale refresh. // See EInkDisplay.cpp for encoding details. -extern const unsigned char lut_factory_fast[]; // 110 bytes, 60 frames, FR=0x44 +extern const unsigned char lut_factory_fast[]; // 110 bytes, 60 frames, FR=0x44 extern const unsigned char lut_factory_quality[]; // 110 bytes, 50 frames, FR=0x22 diff --git a/libs/display/EInkDisplay/include/Panel.h b/libs/display/EInkDisplay/include/Panel.h new file mode 100644 index 0000000..b248476 --- /dev/null +++ b/libs/display/EInkDisplay/include/Panel.h @@ -0,0 +1,211 @@ +#pragma once + +#include + +class EInkDisplay; // forward decl — pollBusy takes EInkDisplay& by friendship + +// Which grayscale plane a writeGrayscalePlaneStrip call targets. +// Underlying uint8_t keeps any struct field holding this enum to one +// byte; the implicit int default would inflate cache structures. +enum class GrayPlane : uint8_t { GRAY_PLANE_LSB, GRAY_PLANE_MSB }; + +// Refresh policy hint passed to Panel::displayBuffer. +// FULL_REFRESH: complete waveform; strongest drive, slowest. +// HALF_REFRESH: state-collapsed waveform; cleans ghosting cadence. +// FAST_REFRESH: differential against the previous frame; cheapest. +// Lives at top level so Panel.h can use it without circular include +// of EInkDisplay.h. +enum class RefreshMode : uint8_t { + FULL_REFRESH, + HALF_REFRESH, + FAST_REFRESH, +}; + +// Panel owns the per-device differences EInkDisplay needs to know +// about: geometry, controller init sequence, refresh dispatch, LUT +// data, busy-wait policy, grayscale flow, deep-sleep sequence. Adding +// a new panel is additive (a new subclass + a friend declaration); +// no edits to EInkDisplay's dispatch code. +// +// === Design: why friend, not an interface === +// +// Panel methods reach into EInkDisplay's private SPI / pin / state via +// a `friend class XPanel;` declaration on EInkDisplay (see the body of +// EInkDisplay.h). The alternative — a DisplayContext interface that +// enumerates the hardware ops Panel may call — was considered and +// rejected for this SDK for these reasons: +// +// 1. Hot-path performance. Bulk plane writes (sendPlaneX3, +// fillPlaneX3) and busy polling iterate inside tight loops. +// Routing every `sendCommand`/`sendData`/`digitalRead` through a +// virtual method on a DisplayContext interface adds a vtable +// lookup per call. The optimizer cannot devirtualize through +// `Panel*` without whole-program LTO that this project does not +// uniformly enable. Direct friend access compiles to the same +// machine code as if the body still lived on EInkDisplay. +// +// 2. The panel set is bounded. There are 2 panels today and at most +// a small handful ever expected — not 10, not 100. The "edit +// EInkDisplay.h to friend YPanel" cost is one line per panel, +// paid by the panel author at the moment they're already adding +// their subclass. +// +// 3. Same-author maintenance. Panel subclasses live in this SDK +// alongside EInkDisplay; both are written by the same hands. The +// encapsulation benefit of "panels can only see what +// DisplayContext exposes" defends against authors outside the +// threat model. +// +// 4. EInkDisplay and Panel co-evolve. Treating EInkDisplay's +// private layout as a stable contract that Panel must abide by +// would impose synchronization cost without buying isolation — +// every legitimate Panel change crosses that boundary anyway. +// +// Where this calculus would flip: external consumers writing their +// own Panel implementations against a stable EInkDisplay ABI, or +// first-class mocking of EInkDisplay for testing. Neither is in scope +// for this SDK today. +// +// To add a new panel: +// 1. Subclass Panel, implement the virtuals. +// 2. If your panel needs access to EInkDisplay privates (it almost +// certainly does — SPI, pins, framebuffer), add `friend class +// YPanel;` to EInkDisplay's class body. +// 3. Each device-specific method takes `EInkDisplay& d` as its +// first parameter; reach `d.sendCommand`, `d._cs`, etc. via the +// friend privilege. +// 4. Wire up panel selection at construction: +// EInkDisplay d(std::make_unique(), sclk, mosi, cs, dc, rst, busy); +class Panel { + public: + virtual ~Panel() = default; + + virtual uint16_t displayWidth() const = 0; + virtual uint16_t displayHeight() const = 0; + + uint16_t displayWidthBytes() const { return displayWidth() / 8; } + uint32_t bufferSize() const { return static_cast(displayWidthBytes()) * displayHeight(); } + + // X3-only state-machine hooks. Default no-op so X4 (and any future + // panel without a resync state) needs no explicit override. The + // public-API entry points on EInkDisplay just delegate here. + virtual void requestResync(uint8_t /*settlePasses*/) {} + virtual void skipInitialResync() {} + + // Wait for the panel's BUSY line to indicate "operation complete". + // X4 (SSD1677) is active HIGH: BUSY high while working, drops LOW + // when done. X3 (UC81xx) is active LOW with a two-phase HIGH→LOW→ + // HIGH transition plus an early-return when no LOW edge is seen. + // Each panel owns its own polling policy. + virtual void pollBusy(EInkDisplay& d, const char* comment, const char* completeWord) const = 0; + + // Run the controller-specific init sequence (panel-setting / power / + // booster / resolution / LUT bank). Called once from + // EInkDisplay::begin() after the GPIO reset. + virtual void init(EInkDisplay& d) const = 0; + + // Render the current framebuffer to the panel using `mode`. Owns + // mode-specific LUT loading, plane writing, refresh trigger, and + // (for X3) the resync state machine. EInkDisplay::displayBuffer + // handles only the panel-agnostic wake/grayscale-revert prologue + // and then delegates here. + virtual void displayBuffer(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) = 0; + + // Trigger one refresh of the current RAM contents with `mode`. On + // X4 this drives the SSD1677 power/clock/master-activation sequence + // directly; on X3 it just routes through displayBuffer (which owns + // the X3 LUT + plane plumbing). + virtual void refreshDisplay(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) = 0; + + // Render a 4-level grayscale buffer that was previously deposited via + // copyGrayscaleBuffers / writeGrayscalePlaneStrip. `lut` is the + // waveform LUT (nullable — panels pick a default). `factoryMode` + // selects absolute-mode rendering for image content vs differential + // mode for text-overlay AA. + virtual void displayGrayBuffer(EInkDisplay& d, bool turnOffScreen, const unsigned char* lut, bool factoryMode) = 0; + + // Repaint after a differential grayscale leaves the gray bank loaded + // in the LUT registers. Called by EInkDisplay::displayBuffer's + // prologue when inGrayscaleMode is set. + virtual void grayscaleRevert(EInkDisplay& d) = 0; + + // Window-only update: refresh a rectangular region without touching + // the rest of the screen. X3 currently routes through displayBuffer + // (full-screen refresh) since the partial-window plumbing wasn't + // worth a second X3-specific impl; X4 does a real windowed write. + virtual void displayWindow(EInkDisplay& d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen) = 0; + + // Two-plane grayscale RAM load. Panels know which controller RAM + // commands correspond to LSB vs MSB and whether a Y-flip is needed. + virtual void copyGrayscaleLsbBuffers(EInkDisplay& d, const uint8_t* lsbBuffer) = 0; + virtual void copyGrayscaleMsbBuffers(EInkDisplay& d, const uint8_t* msbBuffer) = 0; + virtual void copyGrayscaleBuffers(EInkDisplay& d, const uint8_t* lsbBuffer, const uint8_t* msbBuffer) = 0; + + // Streaming variant: load one horizontal band of a plane straight to + // controller RAM without a full plane buffer in MCU heap. Bands may + // be submitted in any order. + virtual void writeGrayscalePlaneStrip(EInkDisplay& d, GrayPlane plane, const uint8_t* rows, uint16_t yStart, + uint16_t numRows) = 0; + + // SINGLE_BUFFER_MODE: re-sync the RAM planes from a restored BW + // buffer so the next fast diff has a valid baseline after grayscale. + // Default no-op for panels that don't need it (or that operate in + // dual-buffer mode where the host preserves the previous frame). + virtual void cleanupGrayscaleBuffers(EInkDisplay& /*d*/, const uint8_t* /*bwBuffer*/) {} + + // Custom LUT injection for the X4 SSD1677 waveform registers. X3 + // doesn't expose registers in this shape (its LUTs go through the + // banked loadLutBankX3 path); X3Panel inherits the no-op default. + virtual void setCustomLUT(EInkDisplay& /*d*/, bool /*enabled*/, const unsigned char* /*lutData*/ = nullptr) {} + + // Park the controller in its lowest-power state. Each panel knows + // its own deep-sleep opcode sequence. + virtual void deepSleep(EInkDisplay& d) = 0; + + // SPI clock rate the panel can sustain. SSD1677 tops out at 40 MHz + // on this hardware; UC81xx caps at 16 MHz. + virtual uint32_t spiClockHz() const = 0; + + // How many forced FULL refreshes the panel needs at the top of + // begin() to settle into a clean state. Defaults to 0 (no warmup); + // X3 overrides to 2 because its UC81xx controller can latch garbled + // content from the prior session into the first user-visible diff. + virtual uint8_t initialFullSyncsAfterBegin() const { return 0; } + + // Extra settle time after the GPIO reset pulse before init() runs. + // Default 0 (rely on the reset pulse itself); X3 overrides with 50ms. + virtual void postResetDelay() const {} + + // Reset per-panel state at begin() time. Called after framebuffer + // memset but before init(). X3 uses this to zero its resync state + // machine + arm initialFullSyncsAfterBegin(); other panels can leave + // the default empty. + virtual void onBegin() {} +}; + +// SSD1677, 800x480 mono. The original X4 hardware. +class X4Panel : public Panel { + public: + uint16_t displayWidth() const override { return 800; } + uint16_t displayHeight() const override { return 480; } + uint32_t spiClockHz() const override { return 40000000; } + + void pollBusy(EInkDisplay& d, const char* comment, const char* completeWord) const override; + void init(EInkDisplay& d) const override; + void displayBuffer(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) override; + void refreshDisplay(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) override; + void displayGrayBuffer(EInkDisplay& d, bool turnOffScreen, const unsigned char* lut, bool factoryMode) override; + void grayscaleRevert(EInkDisplay& d) override; + void displayWindow(EInkDisplay& d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen) override; + void copyGrayscaleLsbBuffers(EInkDisplay& d, const uint8_t* lsbBuffer) override; + void copyGrayscaleMsbBuffers(EInkDisplay& d, const uint8_t* msbBuffer) override; + void copyGrayscaleBuffers(EInkDisplay& d, const uint8_t* lsbBuffer, const uint8_t* msbBuffer) override; + void writeGrayscalePlaneStrip(EInkDisplay& d, GrayPlane plane, const uint8_t* rows, uint16_t yStart, + uint16_t numRows) override; + void cleanupGrayscaleBuffers(EInkDisplay& d, const uint8_t* bwBuffer) override; + void setCustomLUT(EInkDisplay& d, bool enabled, const unsigned char* lutData = nullptr) override; + void deepSleep(EInkDisplay& d) override; +}; + +// X3Panel (UC81xx-class, 792x528) lives in X3Panel.h since it carries +// the X3-specific SPI helper surface. diff --git a/libs/display/EInkDisplay/include/X3Luts.h b/libs/display/EInkDisplay/include/X3Luts.h new file mode 100644 index 0000000..dca6a1a --- /dev/null +++ b/libs/display/EInkDisplay/include/X3Luts.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +// Extern declarations for the X3 LUT bank arrays. Definitions live in +// X3Luts.cpp; the sole in-tree consumer is X3Panel.cpp. +// +// Four banks for BW page-turn paths (full / half / fast / normal) and +// two banks for grayscale (gc for OEM 4-level grayscale, grayscale for +// the differential AA path). Each bank has the five LUT registers +// VCOM / WW / BW / WB / BB. + +extern const uint8_t lut_x3_vcom_normal[]; +extern const uint8_t lut_x3_ww_normal[]; +extern const uint8_t lut_x3_bw_normal[]; +extern const uint8_t lut_x3_wb_normal[]; +extern const uint8_t lut_x3_bb_normal[]; +extern const uint8_t lut_x3_vcom_half[]; +extern const uint8_t lut_x3_ww_half[]; +extern const uint8_t lut_x3_bw_half[]; +extern const uint8_t lut_x3_wb_half[]; +extern const uint8_t lut_x3_bb_half[]; +extern const uint8_t lut_x3_vcom_fast[]; +extern const uint8_t lut_x3_ww_fast[]; +extern const uint8_t lut_x3_bw_fast[]; +extern const uint8_t lut_x3_wb_fast[]; +extern const uint8_t lut_x3_bb_fast[]; +extern const uint8_t lut_x3_vcom_full[]; +extern const uint8_t lut_x3_ww_full[]; +extern const uint8_t lut_x3_bw_full[]; +extern const uint8_t lut_x3_wb_full[]; +extern const uint8_t lut_x3_bb_full[]; + +// grayscale + gc banks +extern const uint8_t lut_x3_vcom_grayscale[]; +extern const uint8_t lut_x3_ww_grayscale[]; +extern const uint8_t lut_x3_bw_grayscale[]; +extern const uint8_t lut_x3_wb_grayscale[]; +extern const uint8_t lut_x3_bb_grayscale[]; +extern const uint8_t lut_x3_vcom_gc[]; +extern const uint8_t lut_x3_ww_gc[]; +extern const uint8_t lut_x3_bw_gc[]; +extern const uint8_t lut_x3_wb_gc[]; +extern const uint8_t lut_x3_bb_gc[]; diff --git a/libs/display/EInkDisplay/include/X3Panel.h b/libs/display/EInkDisplay/include/X3Panel.h new file mode 100644 index 0000000..bd4c9d8 --- /dev/null +++ b/libs/display/EInkDisplay/include/X3Panel.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include "Panel.h" + +class EInkDisplay; // forward decl — X3Panel methods take EInkDisplay& by friendship + +// UC81xx-class, 792x528. Implements the full Panel virtual surface for +// the X3 controller, plus a set of X3-specific SPI helpers +// (sendCommandDataX3, sendPlaneX3, fillPlaneX3, loadLutBankX3, +// triggerRefreshX3) used by the X3 implementations to fuse short +// command-data writes, bulk-write planes with Y-flip, load LUT banks, +// and drive the refresh trigger with the X3 power-cycle sequence. +// +// X3-specific runtime state (the resync state machine + grayscale +// validity flag) also lives here so X4 builds carry none of it. +class X3Panel : public Panel { + public: + uint16_t displayWidth() const override { return 792; } + uint16_t displayHeight() const override { return 528; } + uint32_t spiClockHz() const override { return 16000000; } + uint8_t initialFullSyncsAfterBegin() const override { return 2; } + void postResetDelay() const override; + void onBegin() override; + + void requestResync(uint8_t settlePasses) override; + void skipInitialResync() override; + void pollBusy(EInkDisplay& d, const char* comment, const char* completeWord) const override; + void init(EInkDisplay& d) const override; + void displayBuffer(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) override; + void refreshDisplay(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) override; + void displayGrayBuffer(EInkDisplay& d, bool turnOffScreen, const unsigned char* lut, bool factoryMode) override; + void grayscaleRevert(EInkDisplay& d) override; + void displayWindow(EInkDisplay& d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen) override; + void copyGrayscaleLsbBuffers(EInkDisplay& d, const uint8_t* lsbBuffer) override; + void copyGrayscaleMsbBuffers(EInkDisplay& d, const uint8_t* msbBuffer) override; + void copyGrayscaleBuffers(EInkDisplay& d, const uint8_t* lsbBuffer, const uint8_t* msbBuffer) override; + void writeGrayscalePlaneStrip(EInkDisplay& d, GrayPlane plane, const uint8_t* rows, uint16_t yStart, + uint16_t numRows) override; + void cleanupGrayscaleBuffers(EInkDisplay& d, const uint8_t* bwBuffer) override; + void deepSleep(EInkDisplay& d) override; + + // X3-specific runtime state. Owned here (not on EInkDisplay) so + // X4 builds carry none of it and the resync state machine lives + // with the code that mutates it. + bool _x3RedRamSynced = false; + struct X3GrayState { + bool lastBaseWasPartial = false; + bool lsbValid = false; + }; + X3GrayState _x3GrayState; + uint8_t _x3InitialFullSyncsRemaining = 0; + bool _x3ForceFullSyncNext = false; + uint8_t _x3ForcedConditionPassesNext = 0; + + // SPI command + optional data, fused into one CS-low transaction. + // Used for LUT register / mode-select / partial-window writes where + // the payload is small. Bulk plane writes use sendPlaneX3 instead. + void sendCommandDataX3(EInkDisplay& d, uint8_t cmd, const uint8_t* data, uint16_t len) const; + void sendCommandDataByteX3(EInkDisplay& d, uint8_t cmd, uint8_t d0) const; + void sendCommandDataByteX3(EInkDisplay& d, uint8_t cmd, uint8_t d0, uint8_t d1) const; + + // Bulk-write a pixel plane to one of the DTM RAM commands. Y-flips + // rows in-place (X3 controller scans gates upward), optionally + // inverts bits before sending, then restores the buffer. + void sendPlaneX3(EInkDisplay& d, uint8_t ramCmd, uint8_t* buf, bool invert) const; + + // Fill an entire RAM plane with a single byte (e.g. 0xFF for white). + // Streams a small stack row buffer repeatedly so the framebuffer + // isn't touched. + void fillPlaneX3(EInkDisplay& d, uint8_t ramCmd, uint8_t fillByte) const; + + // Load all 5 LUT registers (VCOM/WW/BW/WB/BB) in one call. Each + // pointer must reference a 42-byte LUT bank in PROGMEM/DRAM. + void loadLutBankX3(EInkDisplay& d, const uint8_t* vcom, const uint8_t* ww, const uint8_t* bw, const uint8_t* wb, + const uint8_t* bb) const; + void loadLutBankX3WithCdi(EInkDisplay& d, uint8_t cdi0, const uint8_t* vcom, const uint8_t* ww, const uint8_t* bw, + const uint8_t* wb, const uint8_t* bb) const; + void loadLutBankX3WithCdi(EInkDisplay& d, uint8_t cdi0, uint8_t cdi1, const uint8_t* vcom, const uint8_t* ww, + const uint8_t* bw, const uint8_t* wb, const uint8_t* bb) const; + + // Power-on if needed, trigger refresh, optionally power-off. The + // `tag` string appears verbatim in busy-wait log lines. + void triggerRefreshX3(EInkDisplay& d, bool turnOffScreen, const char* tag) const; +}; diff --git a/libs/display/EInkDisplay/include/X4Luts.h b/libs/display/EInkDisplay/include/X4Luts.h new file mode 100644 index 0000000..0791f5a --- /dev/null +++ b/libs/display/EInkDisplay/include/X4Luts.h @@ -0,0 +1,10 @@ +#pragma once + +// Extern declarations for the X4 LUT bank data. Definitions live in +// X4Luts.cpp; consumers are X4Panel.cpp and any external code that +// reaches in by name (HAL / firmware grayscale chooser). + +extern const unsigned char lut_grayscale[]; +extern const unsigned char lut_grayscale_revert[]; +extern const unsigned char lut_factory_fast[]; +extern const unsigned char lut_factory_quality[]; diff --git a/libs/display/EInkDisplay/src/EInkDisplay.cpp b/libs/display/EInkDisplay/src/EInkDisplay.cpp index c347467..8101784 100644 --- a/libs/display/EInkDisplay/src/EInkDisplay.cpp +++ b/libs/display/EInkDisplay/src/EInkDisplay.cpp @@ -4,447 +4,43 @@ #include #include -// SSD1677 command definitions -// Initialization and reset -#define CMD_SOFT_RESET 0x12 // Soft reset -#define CMD_BOOSTER_SOFT_START 0x0C // Booster soft-start control -#define CMD_DRIVER_OUTPUT_CONTROL 0x01 // Driver output control -#define CMD_BORDER_WAVEFORM 0x3C // Border waveform control -#define CMD_TEMP_SENSOR_CONTROL 0x18 // Temperature sensor control - -// RAM and buffer management -#define CMD_DATA_ENTRY_MODE 0x11 // Data entry mode -#define CMD_SET_RAM_X_RANGE 0x44 // Set RAM X address range -#define CMD_SET_RAM_Y_RANGE 0x45 // Set RAM Y address range -#define CMD_SET_RAM_X_COUNTER 0x4E // Set RAM X address counter -#define CMD_SET_RAM_Y_COUNTER 0x4F // Set RAM Y address counter -#define CMD_WRITE_RAM_BW 0x24 // Write to BW RAM (current frame) -#define CMD_WRITE_RAM_RED 0x26 // Write to RED RAM (used for fast refresh) -#define CMD_AUTO_WRITE_BW_RAM 0x46 // Auto write BW RAM -#define CMD_AUTO_WRITE_RED_RAM 0x47 // Auto write RED RAM - -// Display update and refresh -#define CMD_DISPLAY_UPDATE_CTRL1 0x21 // Display update control 1 -#define CMD_DISPLAY_UPDATE_CTRL2 0x22 // Display update control 2 -#define CMD_MASTER_ACTIVATION 0x20 // Master activation -#define CTRL1_NORMAL 0x00 // Normal mode - compare RED vs BW for partial -#define CTRL1_BYPASS_RED 0x40 // Bypass RED RAM (treat as 0) - for full refresh - -// LUT and voltage settings -#define CMD_WRITE_LUT 0x32 // Write LUT -#define CMD_GATE_VOLTAGE 0x03 // Gate voltage -#define CMD_SOURCE_VOLTAGE 0x04 // Source voltage -#define CMD_WRITE_VCOM 0x2C // Write VCOM -#define CMD_WRITE_TEMP 0x1A // Write temperature - -// Power management -#define CMD_DEEP_SLEEP 0x10 // Deep sleep - -// UC81xx-class command definitions (X3 controller) -// Opcodes overlap with SSD1677 but have different meanings; keep the -// CMD_X3_ prefix when referencing from X3-only code paths. -// -// Initialization -#define CMD_X3_PANEL_SETTING 0x00 // PSR -#define CMD_X3_POWER_SETTING 0x01 // PWR -#define CMD_X3_POWER_OFF 0x02 // POF -#define CMD_X3_POWER_OFF_SEQ 0x03 // PFS -#define CMD_X3_POWER_ON 0x04 // PON -#define CMD_X3_BOOSTER_SOFT_START 0x06 // BTST -// RAM data transfer -#define CMD_X3_DTM1 0x10 // Display Start Transmission 1 ("old" RAM plane) -#define CMD_X3_DATA_STOP 0x11 // DSP — commit the preceding DTMx data stream -#define CMD_X3_DTM2 0x13 // Display Start Transmission 2 ("new" RAM plane) -// Refresh control -#define CMD_X3_DISPLAY_REFRESH 0x12 // DRF — trigger refresh, implicitly closes DTM2 -// LUT register bank -#define CMD_X3_LUT_VCOM 0x20 // LUTC -#define CMD_X3_LUT_WW 0x21 // LUTWW -#define CMD_X3_LUT_BW 0x22 // LUTBW -#define CMD_X3_LUT_WB 0x23 // LUTWB -#define CMD_X3_LUT_BB 0x24 // LUTBB -// Configuration -#define CMD_X3_PLL_CONTROL 0x30 // PLL -#define CMD_X3_VCOM_DATA_INTERVAL 0x50 // CDI — VCOM and data interval setting (mode select) -#define CMD_X3_RESOLUTION 0x61 // TRES -#define CMD_X3_GATE_SOURCE_START 0x65 // GSST -#define CMD_X3_VCOM_DC 0x82 // VDCS -#define CMD_X3_LV_SELECTION 0xE1 // Source LV / FT_GS selection -// Partial update window -#define CMD_X3_PARTIAL_WINDOW 0x90 // PTL — set partial window coords -#define CMD_X3_PARTIAL_IN 0x91 // PTIN — enter partial mode -#define CMD_X3_PARTIAL_OUT 0x92 // PTOUT — exit partial mode - -// Custom LUT for fast refresh (differential 3-pass mode, 12 frames) -const unsigned char lut_grayscale[] PROGMEM = { - // 00 black/white - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 01 light gray - 0x54, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 10 gray - 0xAA, 0xA0, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 11 dark gray - 0xA2, 0x22, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // L4 (VCOM) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - - // TP/RP groups (global timing) - 0x01, 0x01, 0x01, 0x01, 0x00, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames) - 0x01, 0x01, 0x01, 0x01, 0x00, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames) - 0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames) - 0x00, 0x00, 0x00, 0x00, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0 - - // Frame rate - 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, - - // Voltages (VGH, VSH1, VSH2, VSL, VCOM) - 0x17, 0x41, 0xA8, 0x32, 0x30, - - // Reserved - 0x00, 0x00}; - -const unsigned char lut_grayscale_revert[] PROGMEM = { - // 00 black/white - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 10 gray - 0x54, 0x54, 0x54, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 01 light gray - 0xA8, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 11 dark gray - 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // L4 (VCOM) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - - // TP/RP groups (global timing) - 0x01, 0x01, 0x01, 0x01, 0x01, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames) - 0x01, 0x01, 0x01, 0x01, 0x01, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames) - 0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames) - 0x01, 0x01, 0x01, 0x01, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0 - 0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0 - - // Frame rate - 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, - - // Voltages (VGH, VSH1, VSH2, VSL, VCOM) - 0x17, 0x41, 0xA8, 0x32, 0x30, - - // Reserved - 0x00, 0x00}; - -// X3 differential BW page-turn LUTs — community-authored. -// Required because loading the OEM img bank for full-sync/grayscale leaves -// absolute-mode waveforms in the controller's LUT registers. Subsequent -// fast-diff triggers reuse those registers, producing grey overlay artifacts. -// Loading this bank before fast-diff overwrites the absolute waveforms with -// differential B→W / W→B transitions, restoring clean page turns. -// Values mirror the OEM V5.6.21 X3 firmware LUT bank at flash offset -// 0x402ad0 (mode-2 entry per command 0x20..0x24). Timing parameters are -// slightly tighter than our prior values (byte 2: 02→01, byte 7: 05→04, -// byte 9: 00→01) and bb's transition pattern differs structurally -// (header 0x10→0x00 + byte 6 0x00→0x04). -const uint8_t lut_x3_vcom_normal[] PROGMEM = { - 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_ww_normal[] PROGMEM = { - 0x20, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bw_normal[] PROGMEM = { - 0xAA, 0x06, 0x01, 0x06, 0x06, 0x01, 0xA0, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_wb_normal[] PROGMEM = { - 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bb_normal[] PROGMEM = { - 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x04, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -// X3 scrub LUTs — extracted from OEM V5.6.21 firmware at flash offset -// 0x402ad0 (mode 1 of the bank). Distinguishing feature vs `_full`: the -// WW/BW pair (cmds 0x21/0x22) and the WB/BB pair (cmds 0x23/0x24) are -// byte-identical, which collapses the controller's per-state LUT selection -// to "drive every pixel that DTM2 says should be white using one strong -// waveform; drive every pixel DTM2 says should be black using another." -// DTM1's contents become irrelevant. Used to scrub the panel back to a -// clean state after differential grayscale (AA), where DTM1 holds the AA -// LSB plane and is no longer a valid "previous BW frame" for diffing. -const uint8_t lut_x3_vcom_half[] PROGMEM = { - 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_ww_half[] PROGMEM = { - 0xAA, 0x06, 0x01, 0x06, 0x06, 0x01, 0xA0, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bw_half[] PROGMEM = { - 0xAA, 0x06, 0x01, 0x06, 0x06, 0x01, 0xA0, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_wb_half[] PROGMEM = { - 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bb_half[] PROGMEM = { - 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -// X3 turbo LUTs from papyrix-reader: same voltage patterns as full, -// shortened timing for fast differential updates. -const uint8_t lut_x3_vcom_fast[] PROGMEM = { - 0x00, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_ww_fast[] PROGMEM = { - 0x20, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bw_fast[] PROGMEM = { - 0xAA, 0x04, 0x02, 0x04, 0x04, 0x01, 0x80, 0x04, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_wb_fast[] PROGMEM = { - 0x55, 0x04, 0x02, 0x04, 0x04, 0x01, 0x40, 0x04, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bb_fast[] PROGMEM = { - 0x10, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -// X3 differential grayscale LUTs — mechanical port of the X4 lut_grayscale -// VS patterns into the X3's 5-cell bank format. Used for text-only AA pages -// where the BW content is already on screen and grey levels overlay it. -// GRAYSCALE encoding cell mapping: BB=no change, WW=dark gray, BW=medium gray. -// WB is never selected by GRAYSCALE encoding but populated with state 01 -// (light gray) for completeness. -const uint8_t lut_x3_vcom_grayscale[] PROGMEM = { - 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_ww_grayscale[] PROGMEM = { - // State 11 (dark gray): single phase, weak drive matching original X3 - // behavior - 0x20, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bw_grayscale[] PROGMEM = { - // State 10 (medium gray): single phase, moderate drive matching original X3 - // behavior - 0x80, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_wb_grayscale[] PROGMEM = { - // State 01 (light gray): single phase, X4 VS[0] = 0x54 — never selected - 0x54, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bb_grayscale[] PROGMEM = { - // State 00 (no change): VS = 0x00 — pixels stay at their existing BW state - 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -// X3 stock full/quality image-write LUTs — extracted from OEM firmware -// V5.6.21-X3-EN-PROD-0519_180550.bin at flash offset 0x402b28. -// OEM loaders set CDI 0x29,0x07 before loading this bank. -const uint8_t lut_x3_vcom_full[] PROGMEM = { - 0x00, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_ww_full[] PROGMEM = { - 0x4A, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bw_full[] PROGMEM = { - 0x0A, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_wb_full[] PROGMEM = { - 0x04, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x40, 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bb_full[] PROGMEM = { - 0x84, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x40, 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -// X3 OEM GC (grayscale/anti-aliased text) LUTs from V5.6.21 at flash -// offset 0x402f74. OEM sets CDI 0x97 before loading this bank, triggers a -// refresh, then leaves CDI at 0xD7 afterward. -const uint8_t lut_x3_vcom_gc[] PROGMEM = { - 0x01, 0x1A, 0x1A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_ww_gc[] PROGMEM = { - 0x01, 0x5A, 0x9A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bw_gc[] PROGMEM = { - 0x01, 0x1A, 0x9A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_wb_gc[] PROGMEM = { - 0x01, 0x1A, 0x5A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const uint8_t lut_x3_bb_gc[] PROGMEM = { - 0x01, 0x9A, 0x5A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -void EInkDisplay::setDisplayDimensions(uint16_t width, uint16_t height) { - displayWidth = width; - displayHeight = height; - displayWidthBytes = width / 8; - bufferSize = displayWidthBytes * height; - _x3Mode = false; -} - -void EInkDisplay::setDisplayX3() { - setDisplayDimensions(X3_DISPLAY_WIDTH, X3_DISPLAY_HEIGHT); - _x3Mode = true; -} - -void EInkDisplay::requestResync(uint8_t settlePasses) { - _x3ForceFullSyncNext = _x3Mode; - _x3ForcedConditionPassesNext = _x3Mode ? settlePasses : 0; -} - -void EInkDisplay::skipInitialResync() { - if (!_x3Mode) return; - _x3InitialFullSyncsRemaining = 0; - _x3RedRamSynced = true; -} - -// Factory LUT extracted from firmware V3.1.9_CH_X4_0117.bin by CrazyCoder. -// Uses absolute 2-bit pixel encoding: BW RAM = bit0 (LSB), RED RAM = bit1 -// (MSB). Pixel states: {RED=0,BW=0}=black, {RED=0,BW=1}=dark gray, -// {RED=1,BW=0}=light gray, {RED=1,BW=1}=white. - -// Fast mode (LUT1): 60 waveform frames, FR=0x44, VCOM=-2.0V. -// Used for XTH reading in container mode. ~40% faster than quality mode. -const unsigned char lut_factory_fast[] PROGMEM = { - // VS patterns (LUT0-LUT3 + VCOM), 10 bytes each - 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT0: state 00 (black) - 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT1: state 01 (dark gray) - 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT2: state 10 (light gray) - 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT3: state 11 (white) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM - // TP/RP timing groups (G0-G9), 5 bytes each - 0x09, 0x0C, 0x03, 0x03, 0x00, // G0: 27 frames - 0x0F, 0x03, 0x07, 0x03, 0x00, // G1: 28 frames - 0x03, 0x00, 0x02, 0x00, 0x00, // G2: 5 frames - 0x00, 0x00, 0x00, 0x00, 0x00, // G3 - 0x00, 0x00, 0x00, 0x00, 0x00, // G4 - 0x00, 0x00, 0x00, 0x00, 0x00, // G5 - 0x00, 0x00, 0x00, 0x00, 0x00, // G6 - 0x00, 0x00, 0x00, 0x00, 0x00, // G7 - 0x00, 0x00, 0x00, 0x00, 0x00, // G8 - 0x00, 0x00, 0x00, 0x00, 0x00, // G9 - // Frame rate (higher = faster clock): 0x44 = 68 - 0x44, 0x44, 0x44, 0x44, 0x44, - // Voltages: VGH, VSH1, VSH2, VSL, VCOM - 0x17, 0x41, 0xA8, 0x32, 0x50}; - -// Quality mode (LUT2): 50 waveform frames, FR=0x22, VCOM=-1.2V. -// Used for standalone XTH wallpapers/covers. Less ghosting, ~67% slower than -// fast mode. -const unsigned char lut_factory_quality[] PROGMEM = { - // VS patterns (LUT0-LUT3 + VCOM), 10 bytes each - 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT0: state 00 (black) - 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT1: state 01 (dark gray) - 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT2: state 10 (light gray) - 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, // LUT3: state 11 (white) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM - // TP/RP timing groups (G0-G9), 5 bytes each - 0x08, 0x0B, 0x02, 0x03, 0x00, // G0: 24 frames - 0x0C, 0x02, 0x07, 0x02, 0x00, // G1: 23 frames - 0x01, 0x00, 0x02, 0x00, 0x00, // G2: 3 frames - 0x00, 0x00, 0x00, 0x00, 0x00, // G3 - 0x00, 0x00, 0x00, 0x00, 0x00, // G4 - 0x00, 0x00, 0x00, 0x00, 0x00, // G5 - 0x00, 0x00, 0x00, 0x00, 0x00, // G6 - 0x00, 0x00, 0x00, 0x00, 0x00, // G7 - 0x00, 0x00, 0x00, 0x00, 0x00, // G8 - 0x00, 0x00, 0x00, 0x00, - 0x01, // G9 (RP[9]=1, no practical effect: all-zero timing) - // Frame rate (lower = slower clock): 0x22 = 34 - 0x22, 0x22, 0x22, 0x22, 0x22, - // Voltages: VGH, VSH1, VSH2, VSL, VCOM - 0x17, 0x41, 0xA8, 0x32, 0x30}; - -EInkDisplay::EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, - int8_t rst, int8_t busy) - : _sclk(sclk), _mosi(mosi), _cs(cs), _dc(dc), _rst(rst), _busy(busy), +// setRamArea / writeRamBuffer here still use SSD1677 opcodes (those +// methods are X4-specific helpers reached via friend from X4Panel). +// X3 opcodes and LUT data are X3-only; EInkDisplay touches neither. +#include "X4Constants.h" + +void EInkDisplay::setDisplayX3() { _panel = std::make_unique(); } + +void EInkDisplay::requestResync(uint8_t settlePasses) { _panel->requestResync(settlePasses); } + +void EInkDisplay::skipInitialResync() { _panel->skipInitialResync(); } + +EInkDisplay::EInkDisplay(std::unique_ptr panel, int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, + int8_t busy) + : _sclk(sclk), + _mosi(mosi), + _cs(cs), + _dc(dc), + _rst(rst), + _busy(busy), + _panel(std::move(panel)), frameBuffer(nullptr), #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE frameBufferActive(nullptr), #endif customLutActive(false) { + if (Serial) Serial.printf("[%lu] EInkDisplay: Constructor called (panel-injected)\n", millis()); if (Serial) - Serial.printf("[%lu] EInkDisplay: Constructor called\n", millis()); - if (Serial) - Serial.printf("[%lu] SCLK=%d, MOSI=%d, CS=%d, DC=%d, RST=%d, BUSY=%d\n", - millis(), sclk, mosi, cs, dc, rst, busy); + Serial.printf("[%lu] SCLK=%d, MOSI=%d, CS=%d, DC=%d, RST=%d, BUSY=%d\n", millis(), sclk, mosi, cs, dc, rst, busy); } +// Legacy form — defaults the panel to X4Panel so the deprecated +// setDisplayX3() shim can still swap it before begin(). +EInkDisplay::EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) + : EInkDisplay(std::make_unique(), sclk, mosi, cs, dc, rst, busy) {} + void EInkDisplay::begin() { - if (Serial) - Serial.printf("[%lu] EInkDisplay: begin() called\n", millis()); + if (Serial) Serial.printf("[%lu] EInkDisplay: begin() called\n", millis()); isScreenOn = false; customLutActive = false; @@ -457,33 +53,22 @@ void EInkDisplay::begin() { #endif // Initialize to white - memset(frameBuffer0, 0xFF, bufferSize); - _x3RedRamSynced = false; - _x3InitialFullSyncsRemaining = _x3Mode ? 2 : 0; - _x3ForceFullSyncNext = false; - _x3ForcedConditionPassesNext = 0; - _x3GrayState = {}; + memset(frameBuffer0, 0xFF, _panel->bufferSize()); + _panel->onBegin(); #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE - if (Serial) - Serial.printf("[%lu] Static frame buffer (%lu bytes)\n", millis(), - bufferSize); + if (Serial) Serial.printf("[%lu] Static frame buffer (%lu bytes)\n", millis(), _panel->bufferSize()); #else - memset(frameBuffer1, 0xFF, bufferSize); - if (Serial) - Serial.printf("[%lu] Static frame buffers (2 x %lu bytes)\n", millis(), - bufferSize); + memset(frameBuffer1, 0xFF, _panel->bufferSize()); + if (Serial) Serial.printf("[%lu] Static frame buffers (2 x %lu bytes)\n", millis(), _panel->bufferSize()); #endif - if (Serial) - Serial.printf("[%lu] Initializing e-ink display driver...\n", millis()); + if (Serial) Serial.printf("[%lu] Initializing e-ink display driver...\n", millis()); // Initialize SPI with custom pins SPI.begin(_sclk, -1, _mosi, _cs); - const uint32_t spiHz = _x3Mode ? 16000000 : 40000000; + const uint32_t spiHz = _panel->spiClockHz(); spiSettings = SPISettings(spiHz, MSBFIRST, SPI_MODE0); - if (Serial) - Serial.printf("[%lu] SPI initialized at %lu Hz, Mode 0\n", millis(), - spiHz); + if (Serial) Serial.printf("[%lu] SPI initialized at %lu Hz, Mode 0\n", millis(), spiHz); // Setup GPIO pins pinMode(_cs, OUTPUT); @@ -494,8 +79,7 @@ void EInkDisplay::begin() { digitalWrite(_cs, HIGH); digitalWrite(_dc, HIGH); - if (Serial) - Serial.printf("[%lu] GPIO pins configured\n", millis()); + if (Serial) Serial.printf("[%lu] GPIO pins configured\n", millis()); // Reset display resetDisplay(); @@ -503,8 +87,7 @@ void EInkDisplay::begin() { // Initialize display controller initDisplayController(); - if (Serial) - Serial.printf("[%lu] E-ink display driver initialized\n", millis()); + if (Serial) Serial.printf("[%lu] E-ink display driver initialized\n", millis()); } // ============================================================================ @@ -512,346 +95,59 @@ void EInkDisplay::begin() { // ============================================================================ void EInkDisplay::resetDisplay() { - if (Serial) - Serial.printf("[%lu] Resetting display...\n", millis()); + if (Serial) Serial.printf("[%lu] Resetting display...\n", millis()); digitalWrite(_rst, HIGH); delay(20); digitalWrite(_rst, LOW); delay(2); digitalWrite(_rst, HIGH); delay(20); - if (Serial) - Serial.printf("[%lu] Display reset complete\n", millis()); - if (_x3Mode) { - delay(50); - return; - } + if (Serial) Serial.printf("[%lu] Display reset complete\n", millis()); + _panel->postResetDelay(); } -void EInkDisplay::waitForRefresh(const char *comment) { - pollBusy(comment, "Refresh done"); -} +void EInkDisplay::waitForRefresh(const char* comment) { pollBusy(comment, "Refresh done"); } -void EInkDisplay::pollBusy(const char *comment, const char *completeWord) { - unsigned long start = millis(); - if (!_x3Mode) { - // X4: BUSY held HIGH while busy, drops LOW when done. - while (digitalRead(_busy) == HIGH) { - delay(1); - if (millis() - start > 30000) - break; - } - } else { - // X3 (UC81xx-class): BUSY is active LOW. Idle = HIGH, working = LOW. - // After a command that does work, BUSY transitions HIGH -> LOW (work - // starts) -> HIGH (work done). We poll up to 1s for the HIGH -> LOW - // edge (race protection: the controller may not assert BUSY until - // shortly after the trigger returns), then up to 30s for the - // LOW -> HIGH edge. If we never observe the LOW phase the operation - // either completed faster than we could see or was a no-op, and we - // skip the completion log line. - bool sawLow = false; - while (digitalRead(_busy) == HIGH) { - delay(1); - if (millis() - start > 1000) - break; - } - if (digitalRead(_busy) == LOW) { - sawLow = true; - while (digitalRead(_busy) == LOW) { - delay(1); - if (millis() - start > 30000) - break; - } - } - if (!sawLow) - return; - } - if (comment && Serial) - Serial.printf("[%lu] %s: %s (%lu ms)\n", millis(), completeWord, comment, - millis() - start); +void EInkDisplay::pollBusy(const char* comment, const char* completeWord) { + _panel->pollBusy(*this, comment, completeWord); } void EInkDisplay::sendCommand(uint8_t command) { SPI.beginTransaction(spiSettings); - digitalWrite(_dc, LOW); // Command mode - digitalWrite(_cs, LOW); // Select chip + digitalWrite(_dc, LOW); // Command mode + digitalWrite(_cs, LOW); // Select chip SPI.transfer(command); - digitalWrite(_cs, HIGH); // Deselect chip + digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } void EInkDisplay::sendData(uint8_t data) { SPI.beginTransaction(spiSettings); - digitalWrite(_dc, HIGH); // Data mode - digitalWrite(_cs, LOW); // Select chip + digitalWrite(_dc, HIGH); // Data mode + digitalWrite(_cs, LOW); // Select chip SPI.transfer(data); - digitalWrite(_cs, HIGH); // Deselect chip - SPI.endTransaction(); -} - -void EInkDisplay::sendData(const uint8_t *data, uint16_t length) { - SPI.beginTransaction(spiSettings); - digitalWrite(_dc, HIGH); // Data mode - digitalWrite(_cs, LOW); // Select chip - SPI.writeBytes(data, length); // Transfer all bytes - digitalWrite(_cs, HIGH); // Deselect chip + digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } -// ---- X3 (UC81xx) primitives ---------------------------------------------- -// `sendCommandDataX3` / `sendCommandDataByteX3` bundle a command byte and a -// short data payload into a single CS-low SPI transaction. Used for LUT -// register writes (cmd 0x20-0x24 + 42 bytes), mode select (cmd 0x50 + 2 -// bytes), and partial-window descriptors (cmd 0x90 + 9 bytes). Saves one -// CS toggle vs the separated form. -// -// The bulk plane-write helpers (`sendPlaneX3`, `fillPlaneX3`) and the init -// RAM-clear use the separated `sendCommand()` + `sendData()` form instead. -// UC81xx accepts both for DTM1/DTM2 streams; the separation makes the -// in-place Y-flip and row-streaming patterns simpler to express. This is -// not a hard atomicity requirement of the controller. - -void EInkDisplay::sendCommandDataX3(uint8_t cmd, const uint8_t *data, - uint16_t len) { - SPI.beginTransaction(spiSettings); - digitalWrite(_cs, LOW); - digitalWrite(_dc, LOW); - SPI.transfer(cmd); - if (len > 0 && data != nullptr) { - digitalWrite(_dc, HIGH); - SPI.writeBytes(data, len); - } - digitalWrite(_cs, HIGH); - SPI.endTransaction(); -} - -void EInkDisplay::sendCommandDataByteX3(uint8_t cmd, uint8_t d0) { - const uint8_t d[1] = {d0}; - sendCommandDataX3(cmd, d, 1); -} - -void EInkDisplay::sendCommandDataByteX3(uint8_t cmd, uint8_t d0, uint8_t d1) { - const uint8_t d[2] = {d0, d1}; - sendCommandDataX3(cmd, d, 2); -} - -void EInkDisplay::sendPlaneX3(uint8_t ramCmd, uint8_t *buf, bool invert) { - // The X3 controller scans gates upward (UD=1), so the first byte sent - // maps to the bottom-left pixel. Our framebuffer stores row 0 at offset - // 0 (top), so we Y-flip rows before sending and restore after. Avoids - // allocating a transposed copy. - auto flipRowsInPlace = [&](uint8_t *p) { - uint8_t rowTmp[128]; - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = p + static_cast(top) * displayWidthBytes; - uint8_t *rowB = p + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - }; - auto invertBuffer = [&](uint8_t *p) { - auto *w = reinterpret_cast(p); - for (uint32_t i = 0; i < bufferSize / 4; i++) - w[i] = ~w[i]; - }; - if (invert) invertBuffer(buf); - flipRowsInPlace(buf); - sendCommand(ramCmd); - sendData(buf, static_cast(bufferSize)); - flipRowsInPlace(buf); - if (invert) invertBuffer(buf); -} - -void EInkDisplay::fillPlaneX3(uint8_t ramCmd, uint8_t fillByte) { - // Fill an entire RAM plane with a constant byte. Streams a small stack - // row buffer repeatedly inside a single SPI transaction so the - // framebuffer (~50 KB) doesn't need to be touched or memset. - uint8_t rowBuf[128]; - memset(rowBuf, fillByte, displayWidthBytes); - sendCommand(ramCmd); +void EInkDisplay::sendData(const uint8_t* data, uint16_t length) { SPI.beginTransaction(spiSettings); - digitalWrite(_dc, HIGH); - digitalWrite(_cs, LOW); - for (uint16_t y = 0; y < displayHeight; y++) { - SPI.writeBytes(rowBuf, displayWidthBytes); - } - digitalWrite(_cs, HIGH); + digitalWrite(_dc, HIGH); // Data mode + digitalWrite(_cs, LOW); // Select chip + SPI.writeBytes(data, length); // Transfer all bytes + digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } -void EInkDisplay::loadLutBankX3(const uint8_t *vcom, const uint8_t *ww, - const uint8_t *bw, const uint8_t *wb, - const uint8_t *bb) { - sendCommandDataX3(CMD_X3_LUT_VCOM, vcom, 42); - sendCommandDataX3(CMD_X3_LUT_WW, ww, 42); - sendCommandDataX3(CMD_X3_LUT_BW, bw, 42); - sendCommandDataX3(CMD_X3_LUT_WB, wb, 42); - sendCommandDataX3(CMD_X3_LUT_BB, bb, 42); -} - -void EInkDisplay::loadLutBankX3WithCdi(uint8_t cdi0, const uint8_t *vcom, - const uint8_t *ww, const uint8_t *bw, - const uint8_t *wb, const uint8_t *bb) { - sendCommandDataByteX3(CMD_X3_VCOM_DATA_INTERVAL, cdi0); - loadLutBankX3(vcom, ww, bw, wb, bb); -} +void EInkDisplay::waitWhileBusy(const char* comment) { pollBusy(comment, "Wait complete"); } -void EInkDisplay::loadLutBankX3WithCdi(uint8_t cdi0, uint8_t cdi1, - const uint8_t *vcom, - const uint8_t *ww, const uint8_t *bw, - const uint8_t *wb, const uint8_t *bb) { - sendCommandDataByteX3(CMD_X3_VCOM_DATA_INTERVAL, cdi0, cdi1); - loadLutBankX3(vcom, ww, bw, wb, bb); -} +void EInkDisplay::initDisplayController() { _panel->init(*this); } -void EInkDisplay::triggerRefreshX3(bool turnOffScreen, const char *tag) { - if (!isScreenOn) { - sendCommand(CMD_X3_POWER_ON); - char buf[32]; - snprintf(buf, sizeof(buf), " X3_PON%s", tag); - waitForRefresh(buf); - isScreenOn = true; - } - if (Serial) - Serial.printf("[%lu] X3_OEM_TRIGGER=DRF%s\n", millis(), tag); - sendCommand(CMD_X3_DISPLAY_REFRESH); - { - char buf[32]; - snprintf(buf, sizeof(buf), " X3_DRF%s", tag); - waitForRefresh(buf); - } - if (turnOffScreen) { - sendCommand(CMD_X3_POWER_OFF); - char buf[32]; - snprintf(buf, sizeof(buf), " X3_POF%s", tag); - waitForRefresh(buf); - isScreenOn = false; - } -} - -void EInkDisplay::waitWhileBusy(const char *comment) { - pollBusy(comment, "Wait complete"); -} - -void EInkDisplay::initDisplayController() { -#ifndef X3_USE_X4_INIT - if (_x3Mode) { - sendCommand(CMD_X3_PANEL_SETTING); - sendData(0x3F); // OEM value - sendData(0x0A); // OEM value (was 0x08) - sendCommand(CMD_X3_RESOLUTION); - sendData(0x03); - sendData(0x18); - sendData(0x02); - sendData(0x58); - sendCommand(CMD_X3_GATE_SOURCE_START); - sendData(0x00); - sendData(0x00); - sendData(0x00); - sendData(0x00); - sendCommand(CMD_X3_POWER_OFF_SEQ); - sendData(0x20); // OEM value (was 0x1D) - sendCommand(CMD_X3_POWER_SETTING); - sendData(0x07); - sendData(0x17); - sendData(0x3F); - sendData(0x3F); - sendData(0x17); - sendCommand(CMD_X3_VCOM_DC); - sendData(0x24); // OEM value (was 0x1D) - sendCommand(CMD_X3_BOOSTER_SOFT_START); - sendData(0x25); - sendData(0x25); - sendData(0x3C); - sendData(0x37); - sendCommand(CMD_X3_PLL_CONTROL); - sendData(0x09); - sendCommand(CMD_X3_LV_SELECTION); - sendData(0x02); - - // Match the X4 init's RAM-clear step. The X3 panel runs a UC81xx-class - // controller, not the SSD1677 we drive on X4, so the convenient - // AUTO_WRITE_BW_RAM (0x47) / AUTO_WRITE_RED_RAM (0x48) built-ins X4 uses - // to fill both planes with white don't exist here — those opcodes aren't - // defined in UC81xx. We do the bulk SPI write manually using the - // existing 0x10 (old) / 0x13 (new) RAM plane write commands. Without - // this, RAM retains whatever the panel was showing before reset and - // the first differential refresh diffs against that stale content, - // letting the prior screen bleed through the first user-rendered frame. - if (frameBuffer) { - memset(frameBuffer, 0xFF, bufferSize); - sendCommand(CMD_X3_DTM1); - sendData(frameBuffer, static_cast(bufferSize)); - sendCommand(CMD_X3_DATA_STOP); // commit DTM1 — required because no - // refresh follows this RAM-clear - sendCommand(CMD_X3_DTM2); - sendData(frameBuffer, static_cast(bufferSize)); - sendCommand(CMD_X3_DATA_STOP); // commit DTM2 — same reason - // Leave frameBuffer at 0xFF (white) so it matches the RAM state we - // just wrote and matches begin()'s earlier memset(frameBuffer0, 0xFF). - } - - isScreenOn = false; - return; - } -#endif - - if (Serial) - Serial.printf("[%lu] Initializing SSD1677 controller...\n", millis()); - - const uint8_t TEMP_SENSOR_INTERNAL = 0x80; - - // Soft reset - sendCommand(CMD_SOFT_RESET); - waitWhileBusy(" CMD_SOFT_RESET"); - - // Temperature sensor control (internal) - sendCommand(CMD_TEMP_SENSOR_CONTROL); - sendData(TEMP_SENSOR_INTERNAL); - - // Booster soft-start control (GDEQ0426T82 specific values) - sendCommand(CMD_BOOSTER_SOFT_START); - sendData(0xAE); - sendData(0xC7); - sendData(0xC3); - sendData(0xC0); - sendData(0x40); - - // Driver output control: set display height and scan direction - sendCommand(CMD_DRIVER_OUTPUT_CONTROL); - sendData((displayHeight - 1) % 256); - sendData((displayHeight - 1) / 256); - sendData(0x02); // SM=1 (interlaced), TB=0 - - // Border waveform control - sendCommand(CMD_BORDER_WAVEFORM); - sendData(0x01); - - // Set up full screen RAM area - setRamArea(0, 0, displayWidth, displayHeight); - - if (Serial) - Serial.printf("[%lu] Clearing RAM buffers...\n", millis()); - sendCommand(CMD_AUTO_WRITE_BW_RAM); // Auto write BW RAM - sendData(0xF7); - waitWhileBusy(" CMD_AUTO_WRITE_BW_RAM"); - - sendCommand(CMD_AUTO_WRITE_RED_RAM); // Auto write RED RAM - sendData(0xF7); // Fill with white pattern - waitWhileBusy(" CMD_AUTO_WRITE_RED_RAM"); - - if (Serial) - Serial.printf("[%lu] SSD1677 controller initialized\n", millis()); -} - -void EInkDisplay::setRamArea(const uint16_t x, uint16_t y, uint16_t w, - uint16_t h) { +void EInkDisplay::setRamArea(const uint16_t x, uint16_t y, uint16_t w, uint16_t h) { constexpr uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01; // Reverse Y coordinate (gates are reversed on this display) - y = displayHeight - y - h; + y = _panel->displayHeight() - y - h; // Set data entry mode (X increment, Y decrement for reversed gates) sendCommand(CMD_DATA_ENTRY_MODE); @@ -859,39 +155,35 @@ void EInkDisplay::setRamArea(const uint16_t x, uint16_t y, uint16_t w, // Set RAM X address range (start, end) - X is in PIXELS sendCommand(CMD_SET_RAM_X_RANGE); - sendData(x % 256); // start low byte - sendData(x / 256); // start high byte - sendData((x + w - 1) % 256); // end low byte - sendData((x + w - 1) / 256); // end high byte + sendData(x % 256); // start low byte + sendData(x / 256); // start high byte + sendData((x + w - 1) % 256); // end low byte + sendData((x + w - 1) / 256); // end high byte // Set RAM Y address range (start, end) - Y is in PIXELS sendCommand(CMD_SET_RAM_Y_RANGE); - sendData((y + h - 1) % 256); // start low byte - sendData((y + h - 1) / 256); // start high byte - sendData(y % 256); // end low byte - sendData(y / 256); // end high byte + sendData((y + h - 1) % 256); // start low byte + sendData((y + h - 1) / 256); // start high byte + sendData(y % 256); // end low byte + sendData(y / 256); // end high byte // Set RAM X address counter - X is in PIXELS sendCommand(CMD_SET_RAM_X_COUNTER); - sendData(x % 256); // low byte - sendData(x / 256); // high byte + sendData(x % 256); // low byte + sendData(x / 256); // high byte // Set RAM Y address counter - Y is in PIXELS sendCommand(CMD_SET_RAM_Y_COUNTER); - sendData((y + h - 1) % 256); // low byte - sendData((y + h - 1) / 256); // high byte + sendData((y + h - 1) % 256); // low byte + sendData((y + h - 1) / 256); // high byte } -void EInkDisplay::clearScreen(const uint8_t color) const { - memset(frameBuffer, color, bufferSize); -} +void EInkDisplay::clearScreen(const uint8_t color) const { memset(frameBuffer, color, _panel->bufferSize()); } -void EInkDisplay::drawImage(const uint8_t *imageData, const uint16_t x, - const uint16_t y, const uint16_t w, +void EInkDisplay::drawImage(const uint8_t* imageData, const uint16_t x, const uint16_t y, const uint16_t w, const uint16_t h, const bool fromProgmem) const { if (!frameBuffer) { - if (Serial) - Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); + if (Serial) Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); return; } @@ -901,35 +193,29 @@ void EInkDisplay::drawImage(const uint8_t *imageData, const uint16_t x, // Copy image data to frame buffer for (uint16_t row = 0; row < h; row++) { const uint16_t destY = y + row; - if (destY >= displayHeight) - break; + if (destY >= _panel->displayHeight()) break; - const uint16_t destOffset = destY * displayWidthBytes + (x / 8); + const uint16_t destOffset = destY * _panel->displayWidthBytes() + (x / 8); const uint16_t srcOffset = row * imageWidthBytes; for (uint16_t col = 0; col < imageWidthBytes; col++) { - if ((x / 8 + col) >= displayWidthBytes) - break; + if ((x / 8 + col) >= _panel->displayWidthBytes()) break; if (fromProgmem) { - frameBuffer[destOffset + col] = - pgm_read_byte(&imageData[srcOffset + col]); + frameBuffer[destOffset + col] = pgm_read_byte(&imageData[srcOffset + col]); } else { frameBuffer[destOffset + col] = imageData[srcOffset + col]; } } } - if (Serial) - Serial.printf("[%lu] Image drawn to frame buffer\n", millis()); + if (Serial) Serial.printf("[%lu] Image drawn to frame buffer\n", millis()); } // Draws only black pixels from the image, leaves white pixels clear (unchanged // in framebuffer) -void EInkDisplay::drawImageTransparent(const uint8_t *imageData, - const uint16_t x, const uint16_t y, - const uint16_t w, const uint16_t h, - const bool fromProgmem) const { +void EInkDisplay::drawImageTransparent(const uint8_t* imageData, const uint16_t x, const uint16_t y, const uint16_t w, + const uint16_t h, const bool fromProgmem) const { if (!frameBuffer) { Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); return; @@ -941,239 +227,64 @@ void EInkDisplay::drawImageTransparent(const uint8_t *imageData, // Copy only black pixels to frame buffer for (uint16_t row = 0; row < h; row++) { const uint16_t destY = y + row; - if (destY >= displayHeight) - break; + if (destY >= _panel->displayHeight()) break; - const uint16_t destOffset = destY * displayWidthBytes + (x / 8); + const uint16_t destOffset = destY * _panel->displayWidthBytes() + (x / 8); const uint16_t srcOffset = row * imageWidthBytes; for (uint16_t col = 0; col < imageWidthBytes; col++) { - if ((x / 8 + col) >= displayWidthBytes) - break; + if ((x / 8 + col) >= _panel->displayWidthBytes()) break; - uint8_t srcByte = fromProgmem ? pgm_read_byte(&imageData[srcOffset + col]) - : imageData[srcOffset + col]; + uint8_t srcByte = fromProgmem ? pgm_read_byte(&imageData[srcOffset + col]) : imageData[srcOffset + col]; frameBuffer[destOffset + col] &= srcByte; } } - if (Serial) - Serial.printf("[%lu] Transparent image drawn to frame buffer\n", - millis()); + if (Serial) Serial.printf("[%lu] Transparent image drawn to frame buffer\n", millis()); } -void EInkDisplay::writeRamBuffer(uint8_t ramBuffer, const uint8_t *data, - uint32_t size) { - const char *bufferName = (ramBuffer == CMD_WRITE_RAM_BW) ? "BW" : "RED"; +void EInkDisplay::writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size) { + const char* bufferName = (ramBuffer == CMD_WRITE_RAM_BW) ? "BW" : "RED"; const unsigned long startTime = millis(); - if (Serial) - Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", - startTime, bufferName, size); + if (Serial) Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size); sendCommand(ramBuffer); sendData(data, size); const unsigned long duration = millis() - startTime; - if (Serial) - Serial.printf("[%lu] %s RAM write complete (%lu ms)\n", millis(), - bufferName, duration); + if (Serial) Serial.printf("[%lu] %s RAM write complete (%lu ms)\n", millis(), bufferName, duration); } -void EInkDisplay::setFramebuffer(const uint8_t *bwBuffer) const { - memcpy(frameBuffer, bwBuffer, bufferSize); -} +void EInkDisplay::setFramebuffer(const uint8_t* bwBuffer) const { memcpy(frameBuffer, bwBuffer, _panel->bufferSize()); } #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE void EInkDisplay::swapBuffers() { - uint8_t *temp = frameBuffer; + uint8_t* temp = frameBuffer; frameBuffer = frameBufferActive; frameBufferActive = temp; } #endif void EInkDisplay::grayscaleRevert() { - if (!inGrayscaleMode) { - return; - } - + if (!inGrayscaleMode) return; inGrayscaleMode = false; - - if (_x3Mode) { - // X3: scrub the panel back to clean white using the OEM scrub LUT bank. - // After differential grayscale (AA), DTM1 holds the AA LSB plane and - // DTM2 holds the AA MSB plane — neither is a valid "previous BW frame" - // for a normal differential refresh against. Reusing `_full` here drove - // some pixels with the wrong waveform (BB state had no drive) and let - // ghost text accumulate page-to-page. - // - // Instead we write all-white to both RAM planes, then apply the scrub - // bank. Scrub's WW/BW pair are byte-identical (and so are its WB/BB - // pair), meaning the controller picks the drive waveform from DTM2 - // alone and ignores DTM1 state — the same trick X4's - // lut_grayscale_revert uses with state-coded patterns. With both - // planes white, every pixel gets the "drive to white" waveform and the - // panel ends in a clean known state. _x3RedRamSynced is set true - // because DTM1 now matches DTM2 (both all-white) so the next BW page - // turn can fast-diff cleanly. - fillPlaneX3(CMD_X3_DTM1, 0xFF); - sendCommand(CMD_X3_DATA_STOP); - fillPlaneX3(CMD_X3_DTM2, 0xFF); - sendCommand(CMD_X3_DATA_STOP); - // CDI 0xA9 (absolute mode) — _half bank was extracted from OEM's - // scrub/half loader (FUN_420a0e7c) which sets CDI 0xA9 before loading - // these exact bytes. Using 0x29 (differential) here caused the controller - // to misinterpret pixel state codes and drove unbalanced charge per pixel. - loadLutBankX3WithCdi(0xA9, 0x07, lut_x3_vcom_half, lut_x3_ww_half, - lut_x3_bw_half, lut_x3_wb_half, lut_x3_bb_half); - triggerRefreshX3(/*turnOffScreen=*/false, "(revert)"); - _x3RedRamSynced = true; - return; - } - - // X4: load the revert LUT and fast refresh - setCustomLUT(true, lut_grayscale_revert); - refreshDisplay(FAST_REFRESH); - setCustomLUT(false); + _panel->grayscaleRevert(*this); } -void EInkDisplay::copyGrayscaleLsbBuffers(const uint8_t *lsbBuffer) { - if (!lsbBuffer) { - _x3GrayState.lsbValid = false; - return; - } - - if (_x3Mode) { - // X3 grayscale: write LSB plane raw to "old" RAM (DTM1). - // Y-flip in-place, bulk send, Y-flip back. The const_cast is safe because - // the buffer is fully restored before returning. - auto *buf = const_cast(lsbBuffer); - uint8_t rowTmp[128]; - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = buf + static_cast(top) * displayWidthBytes; - uint8_t *rowB = buf + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - sendCommand(CMD_X3_DTM1); - sendData(buf, static_cast(bufferSize)); - sendCommand(CMD_X3_DATA_STOP); // no refresh follows; commit DTM1 - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = buf + static_cast(top) * displayWidthBytes; - uint8_t *rowB = buf + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - _x3GrayState.lsbValid = true; - return; - } - setRamArea(0, 0, displayWidth, displayHeight); - writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, bufferSize); +void EInkDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { + _panel->copyGrayscaleLsbBuffers(*this, lsbBuffer); } -void EInkDisplay::copyGrayscaleMsbBuffers(const uint8_t *msbBuffer) { - if (!msbBuffer) { - return; - } - - if (_x3Mode) { - if (!_x3GrayState.lsbValid) { - return; - } - - // X3 grayscale: write MSB plane raw to "new" RAM (DTM2). - auto *buf = const_cast(msbBuffer); - uint8_t rowTmp[128]; - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = buf + static_cast(top) * displayWidthBytes; - uint8_t *rowB = buf + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - sendCommand(CMD_X3_DTM2); - sendData(buf, static_cast(bufferSize)); - sendCommand(CMD_X3_DATA_STOP); // no refresh follows; commit DTM2 - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = buf + static_cast(top) * displayWidthBytes; - uint8_t *rowB = buf + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - return; - } - setRamArea(0, 0, displayWidth, displayHeight); - writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, bufferSize); +void EInkDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { + _panel->copyGrayscaleMsbBuffers(*this, msbBuffer); } -void EInkDisplay::copyGrayscaleBuffers(const uint8_t *lsbBuffer, - const uint8_t *msbBuffer) { - if (_x3Mode) { - copyGrayscaleLsbBuffers(lsbBuffer); - copyGrayscaleMsbBuffers(msbBuffer); - return; - } - setRamArea(0, 0, displayWidth, displayHeight); - writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, bufferSize); - writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, bufferSize); +void EInkDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + _panel->copyGrayscaleBuffers(*this, lsbBuffer, msbBuffer); } -void EInkDisplay::writeGrayscalePlaneStrip(GrayPlane plane, const uint8_t *rows, - uint16_t yStart, uint16_t numRows) { - if (!rows || numRows == 0) - return; - - if (_x3Mode) { - // X3 (UC81xx) has no SSD1677-style RAM windowing, but PTL partial-window is - // the equivalent: window to this band, then write its rows to the DTM plane - // (LSB -> 0x10, MSB -> 0x13). Rows are emitted bottom-first within the band - // to reproduce the whole-plane Y-flip the non-streaming path applies (X3 - // scans gates upward). Window Y is logical, matching the post-condition - // pass's full-screen PTL usage; band placement is verified on-device. - // Each band's data is its own CS-low burst so the SPI bus is free for - // SD-card font reads between bands. - const uint8_t ramCmd = (plane == GRAY_PLANE_LSB) ? CMD_X3_DTM1 : CMD_X3_DTM2; - const uint16_t xEnd = displayWidth - 1; - const uint16_t yEnd = yStart + numRows - 1; - const uint8_t win[9] = {0, - 0, - static_cast(xEnd >> 8), - static_cast(xEnd & 0xFF), - static_cast(yStart >> 8), - static_cast(yStart & 0xFF), - static_cast(yEnd >> 8), - static_cast(yEnd & 0xFF), - 0x01}; - sendCommand(CMD_X3_PARTIAL_IN); - sendCommandDataX3(CMD_X3_PARTIAL_WINDOW, win, 9); - sendCommand(ramCmd); - SPI.beginTransaction(spiSettings); - digitalWrite(_dc, HIGH); - digitalWrite(_cs, LOW); - for (int r = static_cast(numRows) - 1; r >= 0; r--) - SPI.writeBytes(rows + static_cast(r) * displayWidthBytes, displayWidthBytes); - digitalWrite(_cs, HIGH); - SPI.endTransaction(); - sendCommand(CMD_X3_PARTIAL_OUT); - // X3 displayGrayBuffer gates on lsbValid; the tiled path bypasses - // copyGrayscaleLsbBuffers, so mark it when the LSB plane lands. - if (plane == GRAY_PLANE_LSB) - _x3GrayState.lsbValid = true; - return; - } - - // X4 (SSD1677): window the RAM to just this band and write it. setRamArea - // already maps logical y to the panel's reversed gates, and a band written - // here lands at the same RAM rows the full-frame write would use for those - // rows, so bands compose in any order with no reordering. - const uint8_t ramCmd = - (plane == GRAY_PLANE_LSB) ? CMD_WRITE_RAM_BW : CMD_WRITE_RAM_RED; - setRamArea(0, yStart, displayWidth, numRows); - sendCommand(ramCmd); - sendData(rows, static_cast(static_cast(numRows) * - displayWidthBytes)); +void EInkDisplay::writeGrayscalePlaneStrip(GrayPlane plane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) { + _panel->writeGrayscalePlaneStrip(*this, plane, rows, yStart, numRows); } #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE @@ -1182,606 +293,63 @@ void EInkDisplay::writeGrayscalePlaneStrip(GrayPlane plane, const uint8_t *rows, * buffer to reconstruct the RED buffer for proper differential fast refreshes * following a grayscale display. */ -void EInkDisplay::cleanupGrayscaleBuffers(const uint8_t *bwBuffer) { - if (_x3Mode) { - if (!bwBuffer) { - return; - } - - // Rebase both X3 planes from restored BW buffer. Y-flip once, send to - // both RAMs (same data), flip back. - auto *buf = const_cast(bwBuffer); - uint8_t rowTmp[128]; - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = buf + static_cast(top) * displayWidthBytes; - uint8_t *rowB = buf + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - sendCommand(CMD_X3_DTM2); - sendData(buf, static_cast(bufferSize)); - sendCommand(CMD_X3_DATA_STOP); // commit DTM2 — no refresh follows - sendCommand(CMD_X3_DTM1); - sendData(buf, static_cast(bufferSize)); - sendCommand(CMD_X3_DATA_STOP); // commit DTM1 — no refresh follows - for (uint16_t top = 0, bot = displayHeight - 1; top < bot; top++, bot--) { - uint8_t *rowA = buf + static_cast(top) * displayWidthBytes; - uint8_t *rowB = buf + static_cast(bot) * displayWidthBytes; - memcpy(rowTmp, rowA, displayWidthBytes); - memcpy(rowA, rowB, displayWidthBytes); - memcpy(rowB, rowTmp, displayWidthBytes); - } - - _x3RedRamSynced = true; - _x3ForceFullSyncNext = false; - _x3ForcedConditionPassesNext = 0; - inGrayscaleMode = false; - return; - } - - setRamArea(0, 0, displayWidth, displayHeight); - writeRamBuffer(CMD_WRITE_RAM_RED, bwBuffer, bufferSize); - inGrayscaleMode = false; -} +void EInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { _panel->cleanupGrayscaleBuffers(*this, bwBuffer); } #endif void EInkDisplay::displayBuffer(RefreshMode mode, const bool turnOffScreen) { if (!isScreenOn && !turnOffScreen) { - // Waking the panel from off: force HALF refresh so the wake transition - // gets a stronger waveform than a fast differential, matching the X4 - // policy. Applies to both X4 and X3. - mode = HALF_REFRESH; - } - - // If currently in grayscale mode, revert first to black/white - if (inGrayscaleMode) { - grayscaleRevert(); - } - - if (_x3Mode) { - // X3 update policy mirrors X4's three-tier refresh hierarchy: - // - // FAST_REFRESH -> `_fast` differential (~4-frame phase). Cheap, used - // for most page turns. DTM1 holds the prior frame; turbo LUTs apply - // transition waveforms based on (DTM2, DTM1) state pairs. - // HALF_REFRESH -> `_half` differential (~6-frame phase, state- - // collapsed). Stronger than turbo because WW=BW and WB=BB make the - // drive depend only on the target frame (DTM2), ignoring any stale - // residue in DTM1. Used for the reader's periodic ghosting-cleanup - // cadence (`displayWithRefreshCycle`, every N pages). Faster than - // a full sync because it's still single-phase 1-bit-per-pixel. - // FULL_REFRESH -> `_full` OEM quality bank from a white baseline. - // Strongest drive, used at boot, after wake, and when the caller - // explicitly requests it. Also forced when DTM1 is unsynced (after AA). - // - // DTM1 ("old" RAM) on the controller stores the previous frame for - // differential updates. POWER_ON (0x04) re-powers the charge pump when - // needed. - const bool fastMode = (mode == FAST_REFRESH); - const bool halfMode = (mode == HALF_REFRESH); - const bool forcedFullSync = _x3ForceFullSyncNext; - const bool doFullSync = (!fastMode && !halfMode) || !_x3RedRamSynced || - _x3InitialFullSyncsRemaining > 0 || forcedFullSync; - const bool doHalfSync = halfMode && !doFullSync; - - if (Serial) { - const char *tag = doFullSync ? "FULL" : doHalfSync ? "HALF" : "FAST"; - Serial.printf("[%lu] X3_OEM_%s\n", millis(), tag); - } - _x3GrayState.lastBaseWasPartial = !doFullSync; - - if (doFullSync) { - loadLutBankX3WithCdi(0x29, 0x07, lut_x3_vcom_full, lut_x3_ww_full, - lut_x3_bw_full, lut_x3_wb_full, lut_x3_bb_full); - // Plane semantics for `_full` in differential mode: DTM1 holds the - // "old" frame, DTM2 holds the "new" frame, and the controller diffs - // them per pixel to pick the transition LUT (WW/BW/WB/BB). - // - // OEM writes old → DTM1 and new → DTM2 from a stored previous frame. - // We don't keep a software previous-frame buffer (would cost ~60 KB - // on a memory-constrained C3), so we use an all-white baseline in - // DTM1 instead. Differential interpretation becomes "drive every - // pixel from white to its current target" — black-target pixels get - // the strong WB transition drive (cleans ghost residue), white-target - // pixels get a light WW drive (no work needed). That's the classic - // ghost-buster full refresh. - // - // The post-refresh DTM1 sync at the end of this function updates - // DTM1 to the current frame so subsequent fast diffs work normally. - fillPlaneX3(CMD_X3_DTM1, 0xFF); - sendCommand(CMD_X3_DATA_STOP); - sendPlaneX3(CMD_X3_DTM2, frameBuffer, false); - } else if (doHalfSync) { - // Half: _half (scrub) LUTs in absolute mode. WW=BW and WB=BB in this - // bank, so the controller picks waveform per-pixel from the target - // state code in DTM2/DTM1 — drive every pixel to its target - // regardless of accumulated residue. OEM uses CDI 0xA9 with this - // bank (FUN_420a0e7c); using 0x29 here caused unbalanced drive that - // accumulated DC bias per pixel under repeated use. - loadLutBankX3WithCdi(0xA9, 0x07, lut_x3_vcom_half, lut_x3_ww_half, - lut_x3_bw_half, lut_x3_wb_half, lut_x3_bb_half); - sendPlaneX3(CMD_X3_DTM2, frameBuffer, false); - } else { - // Fast differential: turbo LUTs, DTM1 retains previous frame. - loadLutBankX3WithCdi(0x29, 0x07, lut_x3_vcom_fast, lut_x3_ww_fast, - lut_x3_bw_fast, lut_x3_wb_fast, lut_x3_bb_fast); - sendPlaneX3(CMD_X3_DTM2, frameBuffer, false); - } - - // Note: this branch re-issues POWER_ON when doFullSync is true even if - // the screen is already on (re-powers the charge pump for the - // higher-current full refresh). The triggerRefreshX3 helper only - // power-ons when !isScreenOn, so we inline the sequence here rather - // than use the helper. - if (!isScreenOn || doFullSync) { - sendCommand(CMD_X3_POWER_ON); - waitForRefresh(" X3_PON"); - isScreenOn = true; - } - if (Serial) - Serial.printf("[%lu] X3_OEM_TRIGGER=DRF\n", millis()); - sendCommand(CMD_X3_DISPLAY_REFRESH); - waitForRefresh(" X3_DRF"); - if (turnOffScreen) { - sendCommand(CMD_X3_POWER_OFF); - waitForRefresh(" X3_POF"); - isScreenOn = false; - } - - if (!fastMode) - delay(200); - - uint8_t postConditionPasses = 0; - if (doFullSync) { - if (forcedFullSync) - postConditionPasses = _x3ForcedConditionPassesNext; - else if (_x3InitialFullSyncsRemaining == 1) - postConditionPasses = 1; - } - - if (postConditionPasses > 0) { - const uint16_t xStart = 0; - const uint16_t xEnd = static_cast(displayWidth - 1); - const uint16_t yStart = 0; - const uint16_t yEnd = static_cast(displayHeight - 1); - const uint8_t w[9] = {static_cast(xStart >> 8), - static_cast(xStart & 0xFF), - static_cast(xEnd >> 8), - static_cast(xEnd & 0xFF), - static_cast(yStart >> 8), - static_cast(yStart & 0xFF), - static_cast(yEnd >> 8), - static_cast(yEnd & 0xFF), - 0x01}; - - // CDI 0xA9 (absolute) — _normal bank was extracted from OEM's - // normal loader (FUN_420a12a0) which sets CDI 0xA9 before loading. - loadLutBankX3WithCdi(0xA9, 0x07, lut_x3_vcom_normal, - lut_x3_ww_normal, lut_x3_bw_normal, - lut_x3_wb_normal, lut_x3_bb_normal); - - for (uint8_t i = 0; i < postConditionPasses; i++) { - if (Serial) - Serial.printf("[%lu] X3_OEM_COND %u/%u\n", millis(), - static_cast(i + 1), - static_cast(postConditionPasses)); - sendCommand(CMD_X3_PARTIAL_IN); - sendCommandDataX3(CMD_X3_PARTIAL_WINDOW, w, 9); - sendPlaneX3(CMD_X3_DTM2, frameBuffer, false); - sendCommand(CMD_X3_PARTIAL_OUT); - triggerRefreshX3(/*turnOffScreen=*/false, "(cond)"); - } - } - - // Sync DTM1 ("old" RAM) with non-inverted current frame for next fast diff. - sendPlaneX3(CMD_X3_DTM1, frameBuffer, false); - sendCommand(CMD_X3_DATA_STOP); // commit DTM1 — no refresh follows - _x3RedRamSynced = true; - - // The first differential after a full garbles on X3: the controller's post-full state corrupts - // the next fast/half diff (not a DTM1-content issue; promoting that op to a half didn't help, a - // full did). Spend that slot here with a no-op fast of the just-displayed frame. DTM1 and DTM2 - // both hold it, so nothing visibly changes, but it leaves the controller in the post-fast state - // so the caller's next diff (menu open, first page turn, first turn after the periodic full) is - // clean instead of the garbling first-after-full. - if (doFullSync) { - loadLutBankX3WithCdi(0x29, 0x07, lut_x3_vcom_fast, lut_x3_ww_fast, lut_x3_bw_fast, lut_x3_wb_fast, - lut_x3_bb_fast); - sendPlaneX3(CMD_X3_DTM2, frameBuffer, false); - triggerRefreshX3(turnOffScreen, "(post-full settle)"); - sendPlaneX3(CMD_X3_DTM1, frameBuffer, false); - sendCommand(CMD_X3_DATA_STOP); - } - - if (doFullSync && _x3InitialFullSyncsRemaining > 0) { - _x3InitialFullSyncsRemaining--; - } - _x3ForceFullSyncNext = false; - _x3ForcedConditionPassesNext = 0; - return; + // Wake from off: force HALF so the wake transition gets a stronger + // waveform than a fast differential. Applies to both X4 and X3. + mode = RefreshMode::HALF_REFRESH; } - - // Set up full screen RAM area - setRamArea(0, 0, displayWidth, displayHeight); - - if (mode != FAST_REFRESH) { - // For full refresh, write to both buffers before refresh - writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, bufferSize); - writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, bufferSize); - } else { - // For fast refresh, write to BW buffer only - writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, bufferSize); - // In single buffer mode, the RED RAM should already contain the previous - // frame In dual buffer mode, we write back frameBufferActive which is the - // last frame -#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE - writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, bufferSize); -#endif - } - -#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE - swapBuffers(); -#endif - - // Refresh the display - refreshDisplay(mode, turnOffScreen); - -#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE - // In single buffer mode always sync RED RAM after refresh to prepare for next - // fast refresh This ensures RED contains the currently displayed frame for - // differential comparison - setRamArea(0, 0, displayWidth, displayHeight); - writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, bufferSize); -#endif + if (inGrayscaleMode) grayscaleRevert(); + _panel->displayBuffer(*this, mode, turnOffScreen); } // EXPERIMENTAL: Windowed update support // Displays only a rectangular region of the frame buffer, preserving the rest // of the screen. Requirements: x and w must be byte-aligned (multiples of 8 // pixels) -void EInkDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, - const bool turnOffScreen) { - if (Serial) - Serial.printf("[%lu] Displaying window at (%d,%d) size (%dx%d)\n", - millis(), x, y, w, h); - - // Validate bounds - if (x + w > displayWidth || y + h > displayHeight) { - if (Serial) - Serial.printf("[%lu] ERROR: Window bounds exceed display dimensions!\n", - millis()); - return; - } - - // Validate byte alignment - if (x % 8 != 0 || w % 8 != 0) { - if (Serial) - Serial.printf("[%lu] ERROR: Window x and width must be byte-aligned " - "(multiples of 8)!\n", - millis()); - return; - } - - if (!frameBuffer) { - if (Serial) - Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); - return; - } - - if (_x3Mode) { - // X3 uses a different command set for windowed RAM addressing (0x91/0x90/ - // 0x92) than X4 (setRamArea + CMD_WRITE_RAM_*). Rather than maintain a - // second X3-specific partial-update implementation, route X3 through the - // shared displayBuffer pipeline. Visual result is equivalent; only - // difference is the unchanged region of the screen also refreshes. - // displayBuffer already handles inGrayscaleMode revert and the wake-from- - // off HALF refresh policy. - displayBuffer(FAST_REFRESH, turnOffScreen); - return; - } - - // displayWindow is not supported while the rest of the screen has grayscale - // content, revert it - if (inGrayscaleMode) { - grayscaleRevert(); - } - - // Calculate window buffer size - const uint16_t windowWidthBytes = w / 8; - const uint32_t windowBufferSize = windowWidthBytes * h; - - if (Serial) - Serial.printf("[%lu] Window buffer size: %lu bytes (%d x %d pixels)\n", - millis(), windowBufferSize, w, h); - - // Allocate temporary buffer on stack - std::vector windowBuffer(windowBufferSize); - - // Extract window region from frame buffer - for (uint16_t row = 0; row < h; row++) { - const uint16_t srcY = y + row; - const uint16_t srcOffset = srcY * displayWidthBytes + (x / 8); - const uint16_t dstOffset = row * windowWidthBytes; - memcpy(&windowBuffer[dstOffset], &frameBuffer[srcOffset], windowWidthBytes); - } - - // Configure RAM area for window - setRamArea(x, y, w, h); - - // Write to BW RAM (current frame) - writeRamBuffer(CMD_WRITE_RAM_BW, windowBuffer.data(), windowBufferSize); - -#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE - // Dual buffer: Extract window from frameBufferActive (previous frame) - std::vector previousWindowBuffer(windowBufferSize); - for (uint16_t row = 0; row < h; row++) { - const uint16_t srcY = y + row; - const uint16_t srcOffset = srcY * displayWidthBytes + (x / 8); - const uint16_t dstOffset = row * windowWidthBytes; - memcpy(&previousWindowBuffer[dstOffset], &frameBufferActive[srcOffset], - windowWidthBytes); - } - writeRamBuffer(CMD_WRITE_RAM_RED, previousWindowBuffer.data(), - windowBufferSize); -#endif - - // Perform fast refresh - refreshDisplay(FAST_REFRESH, turnOffScreen); - -#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE - // Post-refresh: Sync RED RAM with current window (for next fast refresh) - setRamArea(x, y, w, h); - writeRamBuffer(CMD_WRITE_RAM_RED, windowBuffer.data(), windowBufferSize); -#endif - - if (Serial) - Serial.printf("[%lu] Window display complete\n", millis()); +void EInkDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const bool turnOffScreen) { + _panel->displayWindow(*this, x, y, w, h, turnOffScreen); } -void EInkDisplay::displayGrayBuffer(const bool turnOffScreen, - const unsigned char *lut, - const bool factoryMode) { - if (_x3Mode) { - // X3 uses a different command set from X4 — command bytes 0x20-0x22 are - // LUT registers on X3 but CTRL/activation commands on X4. The X4 path - // (setCustomLUT + refreshDisplay) cannot be used on X3. - drawGrayscale = false; - if (!_x3GrayState.lsbValid) { - return; - } - - // Match X4 semantics: differential grayscale leaves the gray bank loaded - // in the LUT registers, so a subsequent BW page turn must run - // grayscaleRevert first to drive pixels back to clean BW. Factory - // absolute mode handles its own cleanup, so no revert is needed there. - inGrayscaleMode = !factoryMode; - - if (factoryMode) { - // Factory absolute mode - use image/factory LUTs. - // Note: X3 has no separate fast factory LUTs. Fast mode falls back to - // quality (lut_x3_*_full) with a warning. - if (Serial) { - const char *modeTag = (lut == lut_factory_fast) - ? "factory_fast (fallback to quality)" - : "factory_quality"; - Serial.printf("[%lu] X3_GRAY_MODE=%s\n", millis(), modeTag); - } - // CDI 0x29 (differential) — _full bank's OEM CDI is 0x29 per - // FUN_420a1218 / FUN_420a14a0. Factory-mode grayscale loaders in the - // OEM firmware use this same bank with the same CDI. - loadLutBankX3WithCdi(0x29, 0x07, lut_x3_vcom_full, lut_x3_ww_full, - lut_x3_bw_full, lut_x3_wb_full, lut_x3_bb_full); - } else { - // Differential grayscale mode - if (Serial) - Serial.printf("[%lu] X3_GRAY_MODE=oem_gc\n", millis()); - loadLutBankX3WithCdi(0x97, lut_x3_vcom_gc, lut_x3_ww_gc, - lut_x3_bw_gc, lut_x3_wb_gc, lut_x3_bb_gc); - } - - triggerRefreshX3(turnOffScreen, "(gray)"); - if (!factoryMode) { - // OEM's GC path leaves CDI at 0xD7 after the grayscale refresh. - sendCommandDataByteX3(CMD_X3_VCOM_DATA_INTERVAL, 0xD7); - } - - _x3RedRamSynced = false; - _x3ForceFullSyncNext = false; - _x3ForcedConditionPassesNext = 0; - - _x3GrayState.lsbValid = false; - return; - } - drawGrayscale = false; - // Differential mode keeps this fallback set for callers that do not re-sync - // controller RAM with cleanupGrayscaleBuffers(). Reader AA does perform that - // cleanup after restoring its BW frame, which clears this flag before the - // next BW page turn. - inGrayscaleMode = !factoryMode; - - const unsigned char *selectedLut = lut; - if (selectedLut == nullptr) { - selectedLut = factoryMode ? lut_factory_quality : lut_grayscale; - } - setCustomLUT(true, selectedLut); - - if (factoryMode) { - // Factory absolute mode: explicit full power cycle sequence. - // CRITICAL: reset CTRL1 to normal — a prior HALF_REFRESH leaves CTRL1=0x40 - // (BYPASS_RED) which would ignore RED RAM and break 4-level grayscale. - sendCommand(CMD_DISPLAY_UPDATE_CTRL1); - sendData(CTRL1_NORMAL); // 0x00 - // 0xC7 = CLOCK_ON(0x80) + ANALOG_ON(0x40) + DISPLAY_START(0x04) + - // ANALOG_OFF(0x02) + CLOCK_OFF(0x01) — full self-contained power - // cycle. - sendCommand(CMD_DISPLAY_UPDATE_CTRL2); - sendData(0xC7); - sendCommand(CMD_MASTER_ACTIVATION); - waitWhileBusy("factory_gray"); - isScreenOn = false; // 0xC7 always powers down after update - } else { - refreshDisplay(FAST_REFRESH, turnOffScreen); - } - - setCustomLUT(false); +void EInkDisplay::displayGrayBuffer(const bool turnOffScreen, const unsigned char* lut, const bool factoryMode) { + _panel->displayGrayBuffer(*this, turnOffScreen, lut, factoryMode); } -void EInkDisplay::refreshDisplay(const RefreshMode mode, - const bool turnOffScreen) { - if (_x3Mode) { - displayBuffer(mode, turnOffScreen); - return; - } - - // Configure Display Update Control 1 - sendCommand(CMD_DISPLAY_UPDATE_CTRL1); - sendData((mode == FAST_REFRESH) - ? CTRL1_NORMAL - : CTRL1_BYPASS_RED); // Configure buffer comparison mode - - // best guess at display mode bits: - // bit | hex | name | effect - // ----+-----+--------------------------+------------------------------------------- - // 7 | 80 | CLOCK_ON | Start internal oscillator - // 6 | 40 | ANALOG_ON | Enable analog power rails (VGH/VGL - // drivers) 5 | 20 | TEMP_LOAD | Load temperature (internal - // or I2C) 4 | 10 | LUT_LOAD | Load waveform LUT 3 | 08 | - // MODE_SELECT | Mode 1/2 2 | 04 | DISPLAY_START | - // Run display 1 | 02 | ANALOG_OFF_PHASE | Shutdown step 1 - // (undocumented) 0 | 01 | CLOCK_OFF | Disable internal - // oscillator - - // Select appropriate display mode based on refresh type - uint8_t displayMode = 0x00; - - // Enable counter and analog if not already on - if (!isScreenOn) { - isScreenOn = true; - displayMode |= 0xC0; // Set CLOCK_ON and ANALOG_ON bits - } - - // Turn off screen if requested - if (turnOffScreen) { - isScreenOn = false; - displayMode |= 0x03; // Set ANALOG_OFF_PHASE and CLOCK_OFF bits - } - - if (mode == FULL_REFRESH) { - displayMode |= 0x34; - } else if (mode == HALF_REFRESH) { - // Write high temp to the register for a faster refresh - sendCommand(CMD_WRITE_TEMP); - sendData(0x5A); - displayMode |= 0xD4; - } else { // FAST_REFRESH - displayMode |= customLutActive ? 0x0C : 0x1C; - } - - // Power on and refresh display - const char *refreshType = (mode == FULL_REFRESH) ? "full" - : (mode == HALF_REFRESH) ? "half" - : "fast"; - if (Serial) - Serial.printf("[%lu] Powering on display 0x%02X (%s refresh)...\n", - millis(), displayMode, refreshType); - sendCommand(CMD_DISPLAY_UPDATE_CTRL2); - sendData(displayMode); - - sendCommand(CMD_MASTER_ACTIVATION); - - // Wait for display to finish updating - if (Serial) - Serial.printf("[%lu] Waiting for display refresh...\n", millis()); - waitWhileBusy(refreshType); +void EInkDisplay::refreshDisplay(const RefreshMode mode, const bool turnOffScreen) { + _panel->refreshDisplay(*this, mode, turnOffScreen); } -void EInkDisplay::setCustomLUT(const bool enabled, - const unsigned char *lutData) { - if (enabled) { - if (Serial) - Serial.printf("[%lu] Loading custom LUT...\n", millis()); - - // Load custom LUT (first 105 bytes: VS + TP/RP + frame rate) - sendCommand(CMD_WRITE_LUT); - for (uint16_t i = 0; i < 105; i++) { - sendData(pgm_read_byte(&lutData[i])); - } - - // Set voltage values from bytes 105-109 - sendCommand(CMD_GATE_VOLTAGE); // VGH - sendData(pgm_read_byte(&lutData[105])); - - sendCommand(CMD_SOURCE_VOLTAGE); // VSH1, VSH2, VSL - sendData(pgm_read_byte(&lutData[106])); // VSH1 - sendData(pgm_read_byte(&lutData[107])); // VSH2 - sendData(pgm_read_byte(&lutData[108])); // VSL - - sendCommand(CMD_WRITE_VCOM); // VCOM - sendData(pgm_read_byte(&lutData[109])); - - customLutActive = true; - if (Serial) - Serial.printf("[%lu] Custom LUT loaded\n", millis()); - } else { - customLutActive = false; - if (Serial) - Serial.printf("[%lu] Custom LUT disabled\n", millis()); - } +void EInkDisplay::setCustomLUT(const bool enabled, const unsigned char* lutData) { + _panel->setCustomLUT(*this, enabled, lutData); } -void EInkDisplay::deepSleep() { - if (Serial) - Serial.printf("[%lu] Preparing display for deep sleep...\n", millis()); - - // First, power down the display properly - // This shuts down the analog power rails and clock - if (isScreenOn) { - sendCommand(CMD_DISPLAY_UPDATE_CTRL1); - sendData(CTRL1_BYPASS_RED); // Normal mode - - sendCommand(CMD_DISPLAY_UPDATE_CTRL2); - sendData(0x03); // Set ANALOG_OFF_PHASE (bit 1) and CLOCK_OFF (bit 0) - - sendCommand(CMD_MASTER_ACTIVATION); - - // Wait for the power-down sequence to complete - waitWhileBusy(" display power-down"); +void EInkDisplay::deepSleep() { _panel->deepSleep(*this); } - isScreenOn = false; - } - - // Now enter deep sleep mode - if (Serial) - Serial.printf("[%lu] Entering deep sleep mode...\n", millis()); - sendCommand(CMD_DEEP_SLEEP); - sendData(0x01); // Enter deep sleep -} - -void EInkDisplay::saveFrameBufferAsPBM(const char *filename) { +void EInkDisplay::saveFrameBufferAsPBM(const char* filename) { #ifndef ARDUINO - const uint8_t *buffer = getFrameBuffer(); + const uint8_t* buffer = getFrameBuffer(); std::ofstream file(filename, std::ios::binary); if (!file) { - if (Serial) - Serial.printf("Failed to open %s for writing\n", filename); + if (Serial) Serial.printf("Failed to open %s for writing\n", filename); return; } // Rotate the image 90 degrees counterclockwise when saving // Original buffer: 800x480 (landscape) // Output image: 480x800 (portrait) - const int DISPLAY_WIDTH_LOCAL = DISPLAY_WIDTH; // 800 - const int DISPLAY_HEIGHT_LOCAL = DISPLAY_HEIGHT; // 480 + const int DISPLAY_WIDTH_LOCAL = DISPLAY_WIDTH; // 800 + const int DISPLAY_HEIGHT_LOCAL = DISPLAY_HEIGHT; // 480 const int DISPLAY_WIDTH_BYTES_LOCAL = DISPLAY_WIDTH_LOCAL / 8; - file << "P4\n"; // Binary PBM + file << "P4\n"; // Binary PBM file << DISPLAY_HEIGHT_LOCAL << " " << DISPLAY_WIDTH_LOCAL << "\n"; // Create rotated buffer - std::vector rotatedBuffer( - (DISPLAY_HEIGHT_LOCAL / 8) * DISPLAY_WIDTH_LOCAL, 0); + std::vector rotatedBuffer((DISPLAY_HEIGHT_LOCAL / 8) * DISPLAY_WIDTH_LOCAL, 0); for (int outY = 0; outY < DISPLAY_WIDTH_LOCAL; outY++) { for (int outX = 0; outX < DISPLAY_HEIGHT_LOCAL; outX++) { @@ -1794,20 +362,17 @@ void EInkDisplay::saveFrameBufferAsPBM(const char *filename) { int outByteIndex = outY * (DISPLAY_HEIGHT_LOCAL / 8) + (outX / 8); int outBitPosition = 7 - (outX % 8); - if (!isWhite) { // Invert: e-ink white=1 -> PBM black=1 + if (!isWhite) { // Invert: e-ink white=1 -> PBM black=1 rotatedBuffer[outByteIndex] |= (1 << outBitPosition); } } } - file.write(reinterpret_cast(rotatedBuffer.data()), - rotatedBuffer.size()); + file.write(reinterpret_cast(rotatedBuffer.data()), rotatedBuffer.size()); file.close(); - if (Serial) - Serial.printf("Saved framebuffer to %s\n", filename); + if (Serial) Serial.printf("Saved framebuffer to %s\n", filename); #else (void)filename; - if (Serial) - Serial.println("saveFrameBufferAsPBM is not supported on Arduino builds."); + if (Serial) Serial.println("saveFrameBufferAsPBM is not supported on Arduino builds."); #endif } diff --git a/libs/display/EInkDisplay/src/X3Constants.h b/libs/display/EInkDisplay/src/X3Constants.h new file mode 100644 index 0000000..37769c3 --- /dev/null +++ b/libs/display/EInkDisplay/src/X3Constants.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +// UC81xx-class command opcodes (X3 controller). Opcodes overlap with +// SSD1677 (X4) but mean different things; the CMD_X3_ prefix marks +// the X3-only code paths that use them. +// +// Internal SDK header. Consumed by X3Panel.cpp; not exported. + +// Initialization +constexpr uint8_t CMD_X3_PANEL_SETTING = 0x00; // PSR +constexpr uint8_t CMD_X3_POWER_SETTING = 0x01; // PWR +constexpr uint8_t CMD_X3_POWER_OFF = 0x02; // POF +constexpr uint8_t CMD_X3_POWER_OFF_SEQ = 0x03; // PFS +constexpr uint8_t CMD_X3_POWER_ON = 0x04; // PON +constexpr uint8_t CMD_X3_BOOSTER_SOFT_START = 0x06; // BTST + +// RAM data transfer +constexpr uint8_t CMD_X3_DTM1 = 0x10; // Display Start Transmission 1 ("old" RAM plane) +constexpr uint8_t CMD_X3_DATA_STOP = 0x11; // DSP — commit the preceding DTMx data stream +constexpr uint8_t CMD_X3_DTM2 = 0x13; // Display Start Transmission 2 ("new" RAM plane) + +// Refresh control +constexpr uint8_t CMD_X3_DISPLAY_REFRESH = 0x12; // DRF — trigger refresh, implicitly closes DTM2 + +// LUT register bank +constexpr uint8_t CMD_X3_LUT_VCOM = 0x20; // LUTC +constexpr uint8_t CMD_X3_LUT_WW = 0x21; // LUTWW +constexpr uint8_t CMD_X3_LUT_BW = 0x22; // LUTBW +constexpr uint8_t CMD_X3_LUT_WB = 0x23; // LUTWB +constexpr uint8_t CMD_X3_LUT_BB = 0x24; // LUTBB + +// Configuration +constexpr uint8_t CMD_X3_PLL_CONTROL = 0x30; // PLL +constexpr uint8_t CMD_X3_VCOM_DATA_INTERVAL = 0x50; // CDI — VCOM and data interval setting (mode select) +constexpr uint8_t CMD_X3_RESOLUTION = 0x61; // TRES +constexpr uint8_t CMD_X3_GATE_SOURCE_START = 0x65; // GSST +constexpr uint8_t CMD_X3_VCOM_DC = 0x82; // VDCS +constexpr uint8_t CMD_X3_LV_SELECTION = 0xE1; // Source LV / FT_GS selection + +// Partial update window +constexpr uint8_t CMD_X3_PARTIAL_WINDOW = 0x90; // PTL — set partial window coords +constexpr uint8_t CMD_X3_PARTIAL_IN = 0x91; // PTIN — enter partial mode +constexpr uint8_t CMD_X3_PARTIAL_OUT = 0x92; // PTOUT — exit partial mode diff --git a/libs/display/EInkDisplay/src/X3Luts.cpp b/libs/display/EInkDisplay/src/X3Luts.cpp new file mode 100644 index 0000000..12cab67 --- /dev/null +++ b/libs/display/EInkDisplay/src/X3Luts.cpp @@ -0,0 +1,175 @@ +#include "X3Luts.h" + +#include // PROGMEM + +// X3 LUT bank data — all 30 arrays consumed by X3Panel.cpp: +// BW page-turn (vcom/ww/bw/wb/bb x full/half/fast/normal) plus the +// grayscale + gc banks. Definitions carry `extern` to match the +// header declarations — without it the `const`-at-namespace-scope +// rule gives them internal linkage and X3Panel.cpp cannot see them. + +// X3 differential BW page-turn LUTs — community-authored. +// Required because loading the OEM img bank for full-sync/grayscale leaves +// absolute-mode waveforms in the controller's LUT registers. Subsequent +// fast-diff triggers reuse those registers, producing grey overlay artifacts. +// Loading this bank before fast-diff overwrites the absolute waveforms with +// differential B→W / W→B transitions, restoring clean page turns. +// Values mirror the OEM V5.6.21 X3 firmware LUT bank at flash offset +// 0x402ad0 (mode-2 entry per command 0x20..0x24). Timing parameters are +// slightly tighter than our prior values (byte 2: 02→01, byte 7: 05→04, +// byte 9: 00→01) and bb's transition pattern differs structurally +// (header 0x10→0x00 + byte 6 0x00→0x04). +extern const uint8_t lut_x3_vcom_normal[] PROGMEM = {0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_ww_normal[] PROGMEM = {0x20, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bw_normal[] PROGMEM = {0xAA, 0x06, 0x01, 0x06, 0x06, 0x01, 0xA0, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_wb_normal[] PROGMEM = {0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bb_normal[] PROGMEM = {0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x04, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// X3 scrub LUTs — extracted from OEM V5.6.21 firmware at flash offset +// 0x402ad0 (mode 1 of the bank). Distinguishing feature vs `_full`: the +// WW/BW pair (cmds 0x21/0x22) and the WB/BB pair (cmds 0x23/0x24) are +// byte-identical, which collapses the controller's per-state LUT selection +// to "drive every pixel that DTM2 says should be white using one strong +// waveform; drive every pixel DTM2 says should be black using another." +// DTM1's contents become irrelevant. Used to scrub the panel back to a +// clean state after differential grayscale (AA), where DTM1 holds the AA +// LSB plane and is no longer a valid "previous BW frame" for diffing. +extern const uint8_t lut_x3_vcom_half[] PROGMEM = {0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_ww_half[] PROGMEM = {0xAA, 0x06, 0x01, 0x06, 0x06, 0x01, 0xA0, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bw_half[] PROGMEM = {0xAA, 0x06, 0x01, 0x06, 0x06, 0x01, 0xA0, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_wb_half[] PROGMEM = {0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bb_half[] PROGMEM = {0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// X3 turbo LUTs from papyrix-reader: same voltage patterns as full, +// shortened timing for fast differential updates. +extern const uint8_t lut_x3_vcom_fast[] PROGMEM = {0x00, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_ww_fast[] PROGMEM = {0x20, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bw_fast[] PROGMEM = {0xAA, 0x04, 0x02, 0x04, 0x04, 0x01, 0x80, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_wb_fast[] PROGMEM = {0x55, 0x04, 0x02, 0x04, 0x04, 0x01, 0x40, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bb_fast[] PROGMEM = {0x10, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// X3 stock full/quality image-write LUTs — extracted from OEM firmware +// V5.6.21-X3-EN-PROD-0519_180550.bin at flash offset 0x402b28. +// OEM loaders set CDI 0x29,0x07 before loading this bank. +extern const uint8_t lut_x3_vcom_full[] PROGMEM = {0x00, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_ww_full[] PROGMEM = {0x4A, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bw_full[] PROGMEM = {0x0A, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_wb_full[] PROGMEM = {0x04, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x40, 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bb_full[] PROGMEM = {0x84, 0x18, 0x04, 0x0E, 0x0A, 0x01, 0x40, 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// X3 differential grayscale LUTs — mechanical port of the X4 lut_grayscale +// VS patterns into the X3's 5-cell bank format. Used for text-only AA pages +// where the BW content is already on screen and grey levels overlay it. +// GRAYSCALE encoding cell mapping: BB=no change, WW=dark gray, BW=medium gray. +// WB is never selected by GRAYSCALE encoding but populated with state 01 +// (light gray) for completeness. +extern const uint8_t lut_x3_vcom_grayscale[] PROGMEM = { + 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_ww_grayscale[] PROGMEM = { + // State 11 (dark gray): single phase, weak drive matching original X3 + // behavior + 0x20, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bw_grayscale[] PROGMEM = { + // State 10 (medium gray): single phase, moderate drive matching original X3 + // behavior + 0x80, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_wb_grayscale[] PROGMEM = { + // State 01 (light gray): single phase, X4 VS[0] = 0x54 — never selected + 0x54, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bb_grayscale[] PROGMEM = { + // State 00 (no change): VS = 0x00 — pixels stay at their existing BW state + 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// X3 OEM GC (grayscale/anti-aliased text) LUTs from V5.6.21 at flash +// offset 0x402f74. OEM sets CDI 0x97 before loading this bank, triggers a +// refresh, then leaves CDI at 0xD7 afterward. +extern const uint8_t lut_x3_vcom_gc[] PROGMEM = {0x01, 0x1A, 0x1A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_ww_gc[] PROGMEM = {0x01, 0x5A, 0x9A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bw_gc[] PROGMEM = {0x01, 0x1A, 0x9A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_wb_gc[] PROGMEM = {0x01, 0x1A, 0x5A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +extern const uint8_t lut_x3_bb_gc[] PROGMEM = {0x01, 0x9A, 0x5A, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; diff --git a/libs/display/EInkDisplay/src/X3Panel.cpp b/libs/display/EInkDisplay/src/X3Panel.cpp new file mode 100644 index 0000000..bf0d814 --- /dev/null +++ b/libs/display/EInkDisplay/src/X3Panel.cpp @@ -0,0 +1,571 @@ +#include "X3Panel.h" + +#include +#include + +#include +#include + +#include "EInkDisplay.h" +#include "X3Constants.h" +#include "X3Luts.h" + +// X3-only state-machine hooks, dispatched here via the Panel virtual +// override (EInkDisplay's public-API entry points just forward). +// requestResync arms the next refresh to be a forced full + N settle +// passes; the dispatch paths consume these flags. skipInitialResync +// zeroes the warm-restart counter so the first two paints aren't +// promoted to FULL. +void X3Panel::requestResync(uint8_t settlePasses) { + _x3ForceFullSyncNext = true; + _x3ForcedConditionPassesNext = settlePasses; +} + +void X3Panel::skipInitialResync() { + _x3InitialFullSyncsRemaining = 0; + _x3RedRamSynced = true; +} + +void X3Panel::postResetDelay() const { delay(50); } + +void X3Panel::onBegin() { + _x3RedRamSynced = false; + _x3InitialFullSyncsRemaining = initialFullSyncsAfterBegin(); + _x3ForceFullSyncNext = false; + _x3ForcedConditionPassesNext = 0; + _x3GrayState = {}; +} + +// X3 refreshDisplay just routes through displayBuffer — the X3 path +// owns power-on/LUT-load/plane-write/trigger/post-conditioning, so +// re-issuing it is the natural "refresh from current state". +void X3Panel::refreshDisplay(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) { + displayBuffer(d, mode, turnOffScreen); +} + +// X3 grayscaleRevert: scrub the panel to clean white using the OEM +// half (scrub) bank. After differential grayscale, DTM1 holds the +// AA LSB plane and DTM2 holds the AA MSB plane — neither is a valid +// "previous BW frame" for a normal diff. Write all-white to both +// planes, then load the scrub bank (WW=BW, WB=BB collapse) so the +// controller drives every pixel toward its DTM2-target regardless +// of DTM1. With both planes white, every pixel gets "drive to white". +void X3Panel::grayscaleRevert(EInkDisplay& d) { + fillPlaneX3(d, CMD_X3_DTM1, 0xFF); + d.sendCommand(CMD_X3_DATA_STOP); + fillPlaneX3(d, CMD_X3_DTM2, 0xFF); + d.sendCommand(CMD_X3_DATA_STOP); + // CDI 0xA9 (absolute mode) per OEM scrub loader (FUN_420a0e7c). + loadLutBankX3WithCdi(d, 0xA9, 0x07, lut_x3_vcom_half, lut_x3_ww_half, lut_x3_bw_half, lut_x3_wb_half, lut_x3_bb_half); + triggerRefreshX3(d, /*turnOffScreen=*/false, "(revert)"); + _x3RedRamSynced = true; +} + +// X3 displayWindow: route through displayBuffer (full-screen refresh) +// rather than maintain a second X3-specific partial-update path. Visual +// difference: unchanged regions also refresh. displayBuffer already +// handles inGrayscaleMode revert and the wake-from-off HALF policy. +void X3Panel::displayWindow(EInkDisplay& d, uint16_t /*x*/, uint16_t /*y*/, uint16_t /*w*/, uint16_t /*h*/, + bool turnOffScreen) { + displayBuffer(d, RefreshMode::FAST_REFRESH, turnOffScreen); +} + +// X3 grayscale RAM load — LSB plane to DTM1, MSB plane to DTM2. Each +// plane is Y-flipped in place (X3 scans gates upward), bulk-sent, then +// flipped back. const_cast is safe because the buffer is fully +// restored before returning. +static void x3FlipRowsInPlace(uint8_t* p, uint16_t height, uint16_t widthBytes) { + uint8_t rowTmp[128]; + for (uint16_t top = 0, bot = height - 1; top < bot; top++, bot--) { + uint8_t* rowA = p + static_cast(top) * widthBytes; + uint8_t* rowB = p + static_cast(bot) * widthBytes; + memcpy(rowTmp, rowA, widthBytes); + memcpy(rowA, rowB, widthBytes); + memcpy(rowB, rowTmp, widthBytes); + } +} + +void X3Panel::copyGrayscaleLsbBuffers(EInkDisplay& d, const uint8_t* lsbBuffer) { + if (!lsbBuffer) { + _x3GrayState.lsbValid = false; + return; + } + auto* buf = const_cast(lsbBuffer); + x3FlipRowsInPlace(buf, displayHeight(), displayWidthBytes()); + d.sendCommand(CMD_X3_DTM1); + d.sendData(buf, static_cast(bufferSize())); + d.sendCommand(CMD_X3_DATA_STOP); // no refresh follows; commit DTM1 + x3FlipRowsInPlace(buf, displayHeight(), displayWidthBytes()); + _x3GrayState.lsbValid = true; +} + +void X3Panel::copyGrayscaleMsbBuffers(EInkDisplay& d, const uint8_t* msbBuffer) { + if (!msbBuffer) return; + if (!_x3GrayState.lsbValid) return; + auto* buf = const_cast(msbBuffer); + x3FlipRowsInPlace(buf, displayHeight(), displayWidthBytes()); + d.sendCommand(CMD_X3_DTM2); + d.sendData(buf, static_cast(bufferSize())); + d.sendCommand(CMD_X3_DATA_STOP); // no refresh follows; commit DTM2 + x3FlipRowsInPlace(buf, displayHeight(), displayWidthBytes()); +} + +void X3Panel::copyGrayscaleBuffers(EInkDisplay& d, const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + copyGrayscaleLsbBuffers(d, lsbBuffer); + copyGrayscaleMsbBuffers(d, msbBuffer); +} + +// X3 streaming: PTL partial-window to the band, then bulk-write the +// rows (bottom-first within the band to reproduce the whole-plane +// Y-flip; X3 scans gates upward). Each band is its own CS-low burst +// so SD-card font reads can interleave between bands. +void X3Panel::writeGrayscalePlaneStrip(EInkDisplay& d, GrayPlane plane, const uint8_t* rows, uint16_t yStart, + uint16_t numRows) { + if (!rows || numRows == 0) return; + const uint8_t ramCmd = (plane == GrayPlane::GRAY_PLANE_LSB) ? CMD_X3_DTM1 : CMD_X3_DTM2; + const uint16_t xEnd = displayWidth() - 1; + const uint16_t yEnd = yStart + numRows - 1; + const uint8_t win[9] = {0, + 0, + static_cast(xEnd >> 8), + static_cast(xEnd & 0xFF), + static_cast(yStart >> 8), + static_cast(yStart & 0xFF), + static_cast(yEnd >> 8), + static_cast(yEnd & 0xFF), + 0x01}; + d.sendCommand(CMD_X3_PARTIAL_IN); + sendCommandDataX3(d, CMD_X3_PARTIAL_WINDOW, win, 9); + d.sendCommand(ramCmd); + SPI.beginTransaction(d.spiSettings); + digitalWrite(d._dc, HIGH); + digitalWrite(d._cs, LOW); + for (int r = static_cast(numRows) - 1; r >= 0; r--) { + SPI.writeBytes(rows + static_cast(r) * displayWidthBytes(), displayWidthBytes()); + } + digitalWrite(d._cs, HIGH); + SPI.endTransaction(); + d.sendCommand(CMD_X3_PARTIAL_OUT); + // displayGrayBuffer gates on lsbValid; the tiled path bypasses + // copyGrayscaleLsbBuffers, so mark it when the LSB plane lands. + if (plane == GrayPlane::GRAY_PLANE_LSB) _x3GrayState.lsbValid = true; +} + +// X3 cleanupGrayscaleBuffers: rebase BOTH planes from a restored BW +// buffer (Y-flip once, send to both RAMs, flip back). After this both +// planes hold the BW frame, so the next FAST diff has matching DTM1 +// and a target DTM2 = caller's next frame. +void X3Panel::cleanupGrayscaleBuffers(EInkDisplay& d, const uint8_t* bwBuffer) { +#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE + if (!bwBuffer) return; + auto* buf = const_cast(bwBuffer); + x3FlipRowsInPlace(buf, displayHeight(), displayWidthBytes()); + d.sendCommand(CMD_X3_DTM2); + d.sendData(buf, static_cast(bufferSize())); + d.sendCommand(CMD_X3_DATA_STOP); + d.sendCommand(CMD_X3_DTM1); + d.sendData(buf, static_cast(bufferSize())); + d.sendCommand(CMD_X3_DATA_STOP); + x3FlipRowsInPlace(buf, displayHeight(), displayWidthBytes()); + _x3RedRamSynced = true; + _x3ForceFullSyncNext = false; + _x3ForcedConditionPassesNext = 0; + d.inGrayscaleMode = false; +#else + (void)d; + (void)bwBuffer; +#endif +} + +// X3 displayGrayBuffer: X3 has no SSD1677-style setCustomLUT path; +// LUTs go through the banked loadLutBankX3WithCdi helpers. +// factoryMode picks the OEM _full bank (no separate fast factory LUTs +// on X3 — fast falls back to quality with a log line). Differential +// mode uses the OEM gc (grayscale/AA) bank with CDI 0x97 → 0xD7. +void X3Panel::displayGrayBuffer(EInkDisplay& d, bool turnOffScreen, const unsigned char* lut, bool factoryMode) { + d.drawGrayscale = false; + if (!_x3GrayState.lsbValid) return; + + // Differential mode leaves the gray bank loaded in the LUT registers, + // so a subsequent BW page turn must run grayscaleRevert first. + d.inGrayscaleMode = !factoryMode; + + if (factoryMode) { + if (Serial) { + // lut_factory_fast/quality are X4 externs; equality-compare to + // tag the log line. X3 still uses _full bank either way. + extern const unsigned char lut_factory_fast[]; + const char* modeTag = (lut == lut_factory_fast) ? "factory_fast (fallback to quality)" : "factory_quality"; + Serial.printf("[%lu] X3_GRAY_MODE=%s\n", millis(), modeTag); + } + // CDI 0x29 (differential) — _full bank's OEM CDI per FUN_420a1218. + loadLutBankX3WithCdi(d, 0x29, 0x07, lut_x3_vcom_full, lut_x3_ww_full, lut_x3_bw_full, lut_x3_wb_full, + lut_x3_bb_full); + } else { + if (Serial) Serial.printf("[%lu] X3_GRAY_MODE=oem_gc\n", millis()); + loadLutBankX3WithCdi(d, 0x97, lut_x3_vcom_gc, lut_x3_ww_gc, lut_x3_bw_gc, lut_x3_wb_gc, lut_x3_bb_gc); + } + + triggerRefreshX3(d, turnOffScreen, "(gray)"); + if (!factoryMode) { + // OEM's GC path leaves CDI at 0xD7 after the grayscale refresh. + sendCommandDataByteX3(d, CMD_X3_VCOM_DATA_INTERVAL, 0xD7); + } + + _x3RedRamSynced = false; + _x3ForceFullSyncNext = false; + _x3ForcedConditionPassesNext = 0; + _x3GrayState.lsbValid = false; +} + +// X3 deepSleep: UC81xx Deep Sleep (DSLP = 0x07) with check-code 0xA5. +// Power down via POWER_OFF first if the panel is on. +void X3Panel::deepSleep(EInkDisplay& d) { + if (Serial) Serial.printf("[%lu] Preparing X3 for deep sleep...\n", millis()); + if (d.isScreenOn) { + d.sendCommand(CMD_X3_POWER_OFF); + d.waitForRefresh(" X3_POF(sleep)"); + d.isScreenOn = false; + } + sendCommandDataByteX3(d, /*DSLP*/ 0x07, /*check-code*/ 0xA5); + if (Serial) Serial.printf("[%lu] X3 in deep sleep\n", millis()); +} + +// X3 displayBuffer: three-tier refresh hierarchy with a state machine +// that decides FAST / HALF / FULL per call. Honors the resync hooks +// (initial-full-syncs counter, forced-full flag) and posts the cond +// passes + the post-full settle pass (the PR #14 ghost fix). Per-mode +// LUT bank is loaded via the X3 SPI helpers; DTM2 always gets the new +// frame; DTM1 gets a white baseline for FULL (we don't keep a software +// previous-frame copy on the C3) and is synced to the just-displayed +// frame at the end of every refresh so the next FAST diff has the +// right "previous" plane. +void X3Panel::displayBuffer(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) { + const bool fastMode = (mode == RefreshMode::FAST_REFRESH); + const bool halfMode = (mode == RefreshMode::HALF_REFRESH); + const bool forcedFullSync = _x3ForceFullSyncNext; + const bool doFullSync = + (!fastMode && !halfMode) || !_x3RedRamSynced || _x3InitialFullSyncsRemaining > 0 || forcedFullSync; + const bool doHalfSync = halfMode && !doFullSync; + + if (Serial) { + const char* tag = doFullSync ? "FULL" : doHalfSync ? "HALF" : "FAST"; + Serial.printf("[%lu] X3_OEM_%s\n", millis(), tag); + } + _x3GrayState.lastBaseWasPartial = !doFullSync; + + if (doFullSync) { + loadLutBankX3WithCdi(d, 0x29, 0x07, lut_x3_vcom_full, lut_x3_ww_full, lut_x3_bw_full, lut_x3_wb_full, + lut_x3_bb_full); + // FULL plane semantics: DTM1 = all-white baseline (no software + // previous-frame copy on the C3), DTM2 = new frame. Controller + // diffs per pixel: black-target pixels get strong WB drive (cleans + // ghost residue), white-target pixels get a light WW drive. DTM1 + // is re-synced to the current frame after the refresh below. + fillPlaneX3(d, CMD_X3_DTM1, 0xFF); + d.sendCommand(CMD_X3_DATA_STOP); + sendPlaneX3(d, CMD_X3_DTM2, d.frameBuffer, false); + } else if (doHalfSync) { + // HALF: _half (scrub) LUTs in absolute mode. WW=BW and WB=BB + // collapse drive to depend on the target frame (DTM2) only; DTM1's + // contents become irrelevant. OEM uses CDI 0xA9 with this bank + // (FUN_420a0e7c); 0x29 caused DC bias accumulation under repeat. + loadLutBankX3WithCdi(d, 0xA9, 0x07, lut_x3_vcom_half, lut_x3_ww_half, lut_x3_bw_half, lut_x3_wb_half, + lut_x3_bb_half); + sendPlaneX3(d, CMD_X3_DTM2, d.frameBuffer, false); + } else { + // FAST differential: turbo LUTs, DTM1 retains previous frame. + loadLutBankX3WithCdi(d, 0x29, 0x07, lut_x3_vcom_fast, lut_x3_ww_fast, lut_x3_bw_fast, lut_x3_wb_fast, + lut_x3_bb_fast); + sendPlaneX3(d, CMD_X3_DTM2, d.frameBuffer, false); + } + + // Re-issue POWER_ON for FULL even when the screen is already on (the + // charge pump needs the bump for FULL's higher-current drive). + // triggerRefreshX3 only power-ons when !isScreenOn, so inline here. + if (!d.isScreenOn || doFullSync) { + d.sendCommand(CMD_X3_POWER_ON); + d.waitForRefresh(" X3_PON"); + d.isScreenOn = true; + } + if (Serial) Serial.printf("[%lu] X3_OEM_TRIGGER=DRF\n", millis()); + d.sendCommand(CMD_X3_DISPLAY_REFRESH); + d.waitForRefresh(" X3_DRF"); + if (turnOffScreen) { + d.sendCommand(CMD_X3_POWER_OFF); + d.waitForRefresh(" X3_POF"); + d.isScreenOn = false; + } + + if (!fastMode) delay(200); + + uint8_t postConditionPasses = 0; + if (doFullSync) { + if (forcedFullSync) + postConditionPasses = _x3ForcedConditionPassesNext; + else if (_x3InitialFullSyncsRemaining == 1) + postConditionPasses = 1; + } + + if (postConditionPasses > 0) { + const uint16_t xStart = 0; + const uint16_t xEnd = static_cast(displayWidth() - 1); + const uint16_t yStart = 0; + const uint16_t yEnd = static_cast(displayHeight() - 1); + const uint8_t w[9] = { + static_cast(xStart >> 8), static_cast(xStart & 0xFF), static_cast(xEnd >> 8), + static_cast(xEnd & 0xFF), static_cast(yStart >> 8), static_cast(yStart & 0xFF), + static_cast(yEnd >> 8), static_cast(yEnd & 0xFF), 0x01}; + + // CDI 0xA9 (absolute) — _normal bank from OEM's normal loader + // (FUN_420a12a0) which sets CDI 0xA9 before loading. + loadLutBankX3WithCdi(d, 0xA9, 0x07, lut_x3_vcom_normal, lut_x3_ww_normal, lut_x3_bw_normal, lut_x3_wb_normal, + lut_x3_bb_normal); + + for (uint8_t i = 0; i < postConditionPasses; i++) { + if (Serial) + Serial.printf("[%lu] X3_OEM_COND %u/%u\n", millis(), static_cast(i + 1), + static_cast(postConditionPasses)); + d.sendCommand(CMD_X3_PARTIAL_IN); + sendCommandDataX3(d, CMD_X3_PARTIAL_WINDOW, w, 9); + sendPlaneX3(d, CMD_X3_DTM2, d.frameBuffer, false); + d.sendCommand(CMD_X3_PARTIAL_OUT); + triggerRefreshX3(d, /*turnOffScreen=*/false, "(cond)"); + } + } + + // Sync DTM1 ("old" RAM) with the just-displayed frame so the next + // FAST diff has the right baseline. + sendPlaneX3(d, CMD_X3_DTM1, d.frameBuffer, false); + d.sendCommand(CMD_X3_DATA_STOP); // commit DTM1 — no refresh follows + _x3RedRamSynced = true; + + // Post-full settle (PR #14 ghost fix): the first differential after a + // FULL garbles on X3 because the controller's post-full state corrupts + // the next fast/half diff. Spend that slot here with a no-op FAST of + // the just-displayed frame. DTM1 and DTM2 both hold it, so nothing + // visibly changes, but the controller is left in the post-fast state + // so the caller's next diff (menu open, first page turn) is clean. + if (doFullSync) { + loadLutBankX3WithCdi(d, 0x29, 0x07, lut_x3_vcom_fast, lut_x3_ww_fast, lut_x3_bw_fast, lut_x3_wb_fast, + lut_x3_bb_fast); + sendPlaneX3(d, CMD_X3_DTM2, d.frameBuffer, false); + triggerRefreshX3(d, turnOffScreen, "(post-full settle)"); + sendPlaneX3(d, CMD_X3_DTM1, d.frameBuffer, false); + d.sendCommand(CMD_X3_DATA_STOP); + } + + if (doFullSync && _x3InitialFullSyncsRemaining > 0) { + _x3InitialFullSyncsRemaining--; + } + _x3ForceFullSyncNext = false; + _x3ForcedConditionPassesNext = 0; +} + +// UC81xx init: panel-setting / resolution / gate-source start / +// power-off-seq / power-setting / VCOM DC / booster soft-start / PLL / +// LV-selection. Then a manual RAM-clear via DTM1/DTM2 + DATA_STOP +// commits (UC81xx has no AUTO_WRITE_*_RAM convenience opcodes like +// SSD1677 does); without this the first differential refresh diffs +// against stale RAM and the prior screen bleeds through. Leaves +// frameBuffer at 0xFF so it matches the RAM state just written and +// matches begin()'s earlier memset(frameBuffer0, 0xFF). +void X3Panel::init(EInkDisplay& d) const { + d.sendCommand(CMD_X3_PANEL_SETTING); + d.sendData(0x3F); // OEM value + d.sendData(0x0A); // OEM value (was 0x08) + d.sendCommand(CMD_X3_RESOLUTION); + d.sendData(0x03); + d.sendData(0x18); + d.sendData(0x02); + d.sendData(0x58); + d.sendCommand(CMD_X3_GATE_SOURCE_START); + d.sendData(0x00); + d.sendData(0x00); + d.sendData(0x00); + d.sendData(0x00); + d.sendCommand(CMD_X3_POWER_OFF_SEQ); + d.sendData(0x20); // OEM value (was 0x1D) + d.sendCommand(CMD_X3_POWER_SETTING); + d.sendData(0x07); + d.sendData(0x17); + d.sendData(0x3F); + d.sendData(0x3F); + d.sendData(0x17); + d.sendCommand(CMD_X3_VCOM_DC); + d.sendData(0x24); // OEM value (was 0x1D) + d.sendCommand(CMD_X3_BOOSTER_SOFT_START); + d.sendData(0x25); + d.sendData(0x25); + d.sendData(0x3C); + d.sendData(0x37); + d.sendCommand(CMD_X3_PLL_CONTROL); + d.sendData(0x09); + d.sendCommand(CMD_X3_LV_SELECTION); + d.sendData(0x02); + + if (d.frameBuffer) { + memset(d.frameBuffer, 0xFF, bufferSize()); + d.sendCommand(CMD_X3_DTM1); + d.sendData(d.frameBuffer, static_cast(bufferSize())); + d.sendCommand(CMD_X3_DATA_STOP); + d.sendCommand(CMD_X3_DTM2); + d.sendData(d.frameBuffer, static_cast(bufferSize())); + d.sendCommand(CMD_X3_DATA_STOP); + } + + d.isScreenOn = false; +} + +// X3 (UC81xx-class) BUSY polarity: active LOW. Idle = HIGH, working +// = LOW. After a command that does work, BUSY transitions HIGH -> LOW +// (work starts) -> HIGH (work done). We poll up to 1s for the +// HIGH -> LOW edge (race protection: the controller may not assert +// BUSY until shortly after the trigger returns), then up to 30s for +// the LOW -> HIGH edge. If we never observe the LOW phase the +// operation either completed faster than we could see or was a no-op, +// and we skip the completion log line. +void X3Panel::pollBusy(EInkDisplay& d, const char* comment, const char* completeWord) const { + unsigned long start = millis(); + bool sawLow = false; + while (digitalRead(d._busy) == HIGH) { + delay(1); + if (millis() - start > 1000) break; + } + if (digitalRead(d._busy) == LOW) { + sawLow = true; + while (digitalRead(d._busy) == LOW) { + delay(1); + if (millis() - start > 30000) break; + } + } + if (!sawLow) return; + if (comment && Serial) { + Serial.printf("[%lu] %s: %s (%lu ms)\n", millis(), completeWord, comment, millis() - start); + } +} + +// `sendCommandDataX3` / `sendCommandDataByteX3` bundle a command byte and a +// short data payload into a single CS-low SPI transaction. Used for LUT +// register writes (cmd 0x20-0x24 + 42 bytes), mode select (cmd 0x50 + 2 +// bytes), and partial-window descriptors (cmd 0x90 + 9 bytes). Saves one +// CS toggle vs the separated form. +// +// The bulk plane-write helpers (`sendPlaneX3`, `fillPlaneX3`) and the init +// RAM-clear use the separated `sendCommand()` + `sendData()` form instead. +// UC81xx accepts both for DTM1/DTM2 streams; the separation makes the +// in-place Y-flip and row-streaming patterns simpler to express. This is +// not a hard atomicity requirement of the controller. +void X3Panel::sendCommandDataX3(EInkDisplay& d, uint8_t cmd, const uint8_t* data, uint16_t len) const { + SPI.beginTransaction(d.spiSettings); + digitalWrite(d._cs, LOW); + digitalWrite(d._dc, LOW); + SPI.transfer(cmd); + if (len > 0 && data != nullptr) { + digitalWrite(d._dc, HIGH); + SPI.writeBytes(data, len); + } + digitalWrite(d._cs, HIGH); + SPI.endTransaction(); +} + +void X3Panel::sendCommandDataByteX3(EInkDisplay& d, uint8_t cmd, uint8_t d0) const { + const uint8_t buf[1] = {d0}; + sendCommandDataX3(d, cmd, buf, 1); +} + +void X3Panel::sendCommandDataByteX3(EInkDisplay& d, uint8_t cmd, uint8_t d0, uint8_t d1) const { + const uint8_t buf[2] = {d0, d1}; + sendCommandDataX3(d, cmd, buf, 2); +} + +void X3Panel::sendPlaneX3(EInkDisplay& d, uint8_t ramCmd, uint8_t* buf, bool invert) const { + // The X3 controller scans gates upward (UD=1), so the first byte sent + // maps to the bottom-left pixel. Our framebuffer stores row 0 at offset + // 0 (top), so we Y-flip rows before sending and restore after. Avoids + // allocating a transposed copy. + auto flipRowsInPlace = [&](uint8_t* p) { + uint8_t rowTmp[128]; + for (uint16_t top = 0, bot = displayHeight() - 1; top < bot; top++, bot--) { + uint8_t* rowA = p + static_cast(top) * displayWidthBytes(); + uint8_t* rowB = p + static_cast(bot) * displayWidthBytes(); + memcpy(rowTmp, rowA, displayWidthBytes()); + memcpy(rowA, rowB, displayWidthBytes()); + memcpy(rowB, rowTmp, displayWidthBytes()); + } + }; + auto invertBuffer = [&](uint8_t* p) { + auto* w = reinterpret_cast(p); + for (uint32_t i = 0; i < bufferSize() / 4; i++) w[i] = ~w[i]; + }; + if (invert) invertBuffer(buf); + flipRowsInPlace(buf); + d.sendCommand(ramCmd); + d.sendData(buf, static_cast(bufferSize())); + flipRowsInPlace(buf); + if (invert) invertBuffer(buf); +} + +void X3Panel::fillPlaneX3(EInkDisplay& d, uint8_t ramCmd, uint8_t fillByte) const { + // Fill an entire RAM plane with a constant byte. Streams a small stack + // row buffer repeatedly inside a single SPI transaction so the + // framebuffer (~50 KB) doesn't need to be touched or memset. + uint8_t rowBuf[128]; + memset(rowBuf, fillByte, displayWidthBytes()); + d.sendCommand(ramCmd); + SPI.beginTransaction(d.spiSettings); + digitalWrite(d._dc, HIGH); + digitalWrite(d._cs, LOW); + for (uint16_t y = 0; y < displayHeight(); y++) { + SPI.writeBytes(rowBuf, displayWidthBytes()); + } + digitalWrite(d._cs, HIGH); + SPI.endTransaction(); +} + +void X3Panel::loadLutBankX3(EInkDisplay& d, const uint8_t* vcom, const uint8_t* ww, const uint8_t* bw, + const uint8_t* wb, const uint8_t* bb) const { + sendCommandDataX3(d, CMD_X3_LUT_VCOM, vcom, 42); + sendCommandDataX3(d, CMD_X3_LUT_WW, ww, 42); + sendCommandDataX3(d, CMD_X3_LUT_BW, bw, 42); + sendCommandDataX3(d, CMD_X3_LUT_WB, wb, 42); + sendCommandDataX3(d, CMD_X3_LUT_BB, bb, 42); +} + +void X3Panel::loadLutBankX3WithCdi(EInkDisplay& d, uint8_t cdi0, const uint8_t* vcom, const uint8_t* ww, + const uint8_t* bw, const uint8_t* wb, const uint8_t* bb) const { + sendCommandDataByteX3(d, CMD_X3_VCOM_DATA_INTERVAL, cdi0); + loadLutBankX3(d, vcom, ww, bw, wb, bb); +} + +void X3Panel::loadLutBankX3WithCdi(EInkDisplay& d, uint8_t cdi0, uint8_t cdi1, const uint8_t* vcom, const uint8_t* ww, + const uint8_t* bw, const uint8_t* wb, const uint8_t* bb) const { + sendCommandDataByteX3(d, CMD_X3_VCOM_DATA_INTERVAL, cdi0, cdi1); + loadLutBankX3(d, vcom, ww, bw, wb, bb); +} + +void X3Panel::triggerRefreshX3(EInkDisplay& d, bool turnOffScreen, const char* tag) const { + if (!d.isScreenOn) { + d.sendCommand(CMD_X3_POWER_ON); + char buf[32]; + snprintf(buf, sizeof(buf), " X3_PON%s", tag); + d.waitForRefresh(buf); + d.isScreenOn = true; + } + if (Serial) Serial.printf("[%lu] X3_OEM_TRIGGER=DRF%s\n", millis(), tag); + d.sendCommand(CMD_X3_DISPLAY_REFRESH); + { + char buf[32]; + snprintf(buf, sizeof(buf), " X3_DRF%s", tag); + d.waitForRefresh(buf); + } + if (turnOffScreen) { + d.sendCommand(CMD_X3_POWER_OFF); + char buf[32]; + snprintf(buf, sizeof(buf), " X3_POF%s", tag); + d.waitForRefresh(buf); + d.isScreenOn = false; + } +} + +// ===================================================================== diff --git a/libs/display/EInkDisplay/src/X4Constants.h b/libs/display/EInkDisplay/src/X4Constants.h new file mode 100644 index 0000000..546e87a --- /dev/null +++ b/libs/display/EInkDisplay/src/X4Constants.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +// SSD1677 command opcodes (X4 controller). Some opcodes overlap with +// the UC81xx (X3) controller but mean different things; the bare +// CMD_ prefix marks the X4-only code paths that use them. +// +// Internal SDK header. Consumed by X4Panel.cpp and by the X4-specific +// helpers still living on EInkDisplay (setRamArea, writeRamBuffer). +// Not exported. + +// Initialization and reset +constexpr uint8_t CMD_SOFT_RESET = 0x12; // Soft reset +constexpr uint8_t CMD_BOOSTER_SOFT_START = 0x0C; // Booster soft-start control +constexpr uint8_t CMD_DRIVER_OUTPUT_CONTROL = 0x01; // Driver output control +constexpr uint8_t CMD_BORDER_WAVEFORM = 0x3C; // Border waveform control +constexpr uint8_t CMD_TEMP_SENSOR_CONTROL = 0x18; // Temperature sensor control + +// RAM and buffer management +constexpr uint8_t CMD_DATA_ENTRY_MODE = 0x11; // Data entry mode +constexpr uint8_t CMD_SET_RAM_X_RANGE = 0x44; // Set RAM X address range +constexpr uint8_t CMD_SET_RAM_Y_RANGE = 0x45; // Set RAM Y address range +constexpr uint8_t CMD_SET_RAM_X_COUNTER = 0x4E; // Set RAM X address counter +constexpr uint8_t CMD_SET_RAM_Y_COUNTER = 0x4F; // Set RAM Y address counter +constexpr uint8_t CMD_WRITE_RAM_BW = 0x24; // Write to BW RAM (current frame) +constexpr uint8_t CMD_WRITE_RAM_RED = 0x26; // Write to RED RAM (used for fast refresh) +constexpr uint8_t CMD_AUTO_WRITE_BW_RAM = 0x46; // Auto write BW RAM +constexpr uint8_t CMD_AUTO_WRITE_RED_RAM = 0x47; // Auto write RED RAM + +// Display update and refresh +constexpr uint8_t CMD_DISPLAY_UPDATE_CTRL1 = 0x21; // Display update control 1 +constexpr uint8_t CMD_DISPLAY_UPDATE_CTRL2 = 0x22; // Display update control 2 +constexpr uint8_t CMD_MASTER_ACTIVATION = 0x20; // Master activation +constexpr uint8_t CTRL1_NORMAL = 0x00; // Normal mode - compare RED vs BW for partial +constexpr uint8_t CTRL1_BYPASS_RED = 0x40; // Bypass RED RAM (treat as 0) - for full refresh + +// LUT and voltage settings +constexpr uint8_t CMD_WRITE_LUT = 0x32; // Write LUT +constexpr uint8_t CMD_GATE_VOLTAGE = 0x03; // Gate voltage +constexpr uint8_t CMD_SOURCE_VOLTAGE = 0x04; // Source voltage +constexpr uint8_t CMD_WRITE_VCOM = 0x2C; // Write VCOM +constexpr uint8_t CMD_WRITE_TEMP = 0x1A; // Write temperature + +// Power management +constexpr uint8_t CMD_DEEP_SLEEP = 0x10; // Deep sleep diff --git a/libs/display/EInkDisplay/src/X4Luts.cpp b/libs/display/EInkDisplay/src/X4Luts.cpp new file mode 100644 index 0000000..3258813 --- /dev/null +++ b/libs/display/EInkDisplay/src/X4Luts.cpp @@ -0,0 +1,141 @@ +#include "X4Luts.h" + +#include // PROGMEM + +// X4 (SSD1677) LUT data: differential grayscale, grayscale-revert, and +// the two factory waveform banks extracted from V3.1.9_CH_X4_0117 +// firmware (by CrazyCoder). The factory banks use absolute 2-bit pixel +// encoding: BW RAM = bit0 (LSB), RED RAM = bit1 (MSB). Pixel states: +// {RED=0,BW=0}=black, {RED=0,BW=1}=dark gray, {RED=1,BW=0}=light gray, +// {RED=1,BW=1}=white. +// +// Consumed by X4Panel.cpp (displayGrayBuffer, grayscaleRevert) and +// exported via X4Luts.h for any external lookup. Definitions carry +// `extern` to defeat the const-internal-linkage rule. + +// Custom LUT for fast refresh (differential 3-pass mode, 12 frames) +extern const unsigned char lut_grayscale[] PROGMEM = { + // 00 black/white + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 01 light gray + 0x54, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 10 gray + 0xAA, 0xA0, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 11 dark gray + 0xA2, 0x22, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // L4 (VCOM) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // TP/RP groups (global timing) + 0x01, 0x01, 0x01, 0x01, 0x00, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames) + 0x01, 0x01, 0x01, 0x01, 0x00, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames) + 0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames) + 0x00, 0x00, 0x00, 0x00, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0 + + // Frame rate + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + + // Voltages (VGH, VSH1, VSH2, VSL, VCOM) + 0x17, 0x41, 0xA8, 0x32, 0x30, + + // Reserved + 0x00, 0x00}; + +extern const unsigned char lut_grayscale_revert[] PROGMEM = { + // 00 black/white + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 10 gray + 0x54, 0x54, 0x54, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 01 light gray + 0xA8, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 11 dark gray + 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // L4 (VCOM) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // TP/RP groups (global timing) + 0x01, 0x01, 0x01, 0x01, 0x01, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames) + 0x01, 0x01, 0x01, 0x01, 0x01, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames) + 0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames) + 0x01, 0x01, 0x01, 0x01, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0 + 0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0 + + // Frame rate + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + + // Voltages (VGH, VSH1, VSH2, VSL, VCOM) + 0x17, 0x41, 0xA8, 0x32, 0x30, + + // Reserved + 0x00, 0x00}; + +// Fast mode (LUT1): 60 waveform frames, FR=0x44, VCOM=-2.0V. +// Used for XTH reading in container mode. ~40% faster than quality mode. +extern const unsigned char lut_factory_fast[] PROGMEM = { + // VS patterns (LUT0-LUT3 + VCOM), 10 bytes each + 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT0: state 00 (black) + 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT1: state 01 (dark gray) + 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT2: state 10 (light gray) + 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT3: state 11 (white) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM + // TP/RP timing groups (G0-G9), 5 bytes each + 0x09, 0x0C, 0x03, 0x03, 0x00, // G0: 27 frames + 0x0F, 0x03, 0x07, 0x03, 0x00, // G1: 28 frames + 0x03, 0x00, 0x02, 0x00, 0x00, // G2: 5 frames + 0x00, 0x00, 0x00, 0x00, 0x00, // G3 + 0x00, 0x00, 0x00, 0x00, 0x00, // G4 + 0x00, 0x00, 0x00, 0x00, 0x00, // G5 + 0x00, 0x00, 0x00, 0x00, 0x00, // G6 + 0x00, 0x00, 0x00, 0x00, 0x00, // G7 + 0x00, 0x00, 0x00, 0x00, 0x00, // G8 + 0x00, 0x00, 0x00, 0x00, 0x00, // G9 + // Frame rate (higher = faster clock): 0x44 = 68 + 0x44, 0x44, 0x44, 0x44, 0x44, + // Voltages: VGH, VSH1, VSH2, VSL, VCOM + 0x17, 0x41, 0xA8, 0x32, 0x50}; + +// Quality mode (LUT2): 50 waveform frames, FR=0x22, VCOM=-1.2V. +// Used for standalone XTH wallpapers/covers. Less ghosting, ~67% slower than +// fast mode. +extern const unsigned char lut_factory_quality[] PROGMEM = { + // VS patterns (LUT0-LUT3 + VCOM), 10 bytes each + 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT0: state 00 (black) + 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT1: state 01 (dark gray) + 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT2: state 10 (light gray) + 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // LUT3: state 11 (white) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM + // TP/RP timing groups (G0-G9), 5 bytes each + 0x08, 0x0B, 0x02, 0x03, 0x00, // G0: 24 frames + 0x0C, 0x02, 0x07, 0x02, 0x00, // G1: 23 frames + 0x01, 0x00, 0x02, 0x00, 0x00, // G2: 3 frames + 0x00, 0x00, 0x00, 0x00, 0x00, // G3 + 0x00, 0x00, 0x00, 0x00, 0x00, // G4 + 0x00, 0x00, 0x00, 0x00, 0x00, // G5 + 0x00, 0x00, 0x00, 0x00, 0x00, // G6 + 0x00, 0x00, 0x00, 0x00, 0x00, // G7 + 0x00, 0x00, 0x00, 0x00, 0x00, // G8 + 0x00, 0x00, 0x00, 0x00, + 0x01, // G9 (RP[9]=1, no practical effect: all-zero timing) + // Frame rate (lower = slower clock): 0x22 = 34 + 0x22, 0x22, 0x22, 0x22, 0x22, + // Voltages: VGH, VSH1, VSH2, VSL, VCOM + 0x17, 0x41, 0xA8, 0x32, 0x30}; diff --git a/libs/display/EInkDisplay/src/X4Panel.cpp b/libs/display/EInkDisplay/src/X4Panel.cpp new file mode 100644 index 0000000..16af33c --- /dev/null +++ b/libs/display/EInkDisplay/src/X4Panel.cpp @@ -0,0 +1,303 @@ +#include + +#include "EInkDisplay.h" +#include "Panel.h" +#include "X4Constants.h" +#include "X4Luts.h" + +// X4 (SSD1677) BUSY polarity: active HIGH. BUSY held HIGH while the +// controller is working, drops LOW when the operation completes. +// Single-phase poll with a 30s safety timeout. +// SSD1677 init: soft reset, internal temp sensor, GDEQ0426T82-specific +// booster soft-start voltages, driver-output dimensions, border waveform, +// then RAM-area windowing and a one-shot AUTO_WRITE for each plane to +// clear stale content. Without the AUTO_WRITE clears the first +// differential refresh diffs against pre-reset RAM and the prior screen +// bleeds through. +void X4Panel::init(EInkDisplay& d) const { + if (Serial) Serial.printf("[%lu] Initializing SSD1677 controller...\n", millis()); + + constexpr uint8_t TEMP_SENSOR_INTERNAL = 0x80; + + d.sendCommand(CMD_SOFT_RESET); + d.waitWhileBusy(" CMD_SOFT_RESET"); + + d.sendCommand(CMD_TEMP_SENSOR_CONTROL); + d.sendData(TEMP_SENSOR_INTERNAL); + + // Booster soft-start (GDEQ0426T82-specific values). + d.sendCommand(CMD_BOOSTER_SOFT_START); + d.sendData(0xAE); + d.sendData(0xC7); + d.sendData(0xC3); + d.sendData(0xC0); + d.sendData(0x40); + + // Driver output: display height + scan direction (SM=1 interlaced, TB=0). + d.sendCommand(CMD_DRIVER_OUTPUT_CONTROL); + d.sendData((displayHeight() - 1) % 256); + d.sendData((displayHeight() - 1) / 256); + d.sendData(0x02); + + d.sendCommand(CMD_BORDER_WAVEFORM); + d.sendData(0x01); + + d.setRamArea(0, 0, displayWidth(), displayHeight()); + + if (Serial) Serial.printf("[%lu] Clearing RAM buffers...\n", millis()); + d.sendCommand(CMD_AUTO_WRITE_BW_RAM); + d.sendData(0xF7); + d.waitWhileBusy(" CMD_AUTO_WRITE_BW_RAM"); + + d.sendCommand(CMD_AUTO_WRITE_RED_RAM); + d.sendData(0xF7); + d.waitWhileBusy(" CMD_AUTO_WRITE_RED_RAM"); + + if (Serial) Serial.printf("[%lu] SSD1677 controller initialized\n", millis()); +} + +// X4 displayBuffer: set RAM area, write framebuffer to BW (and RED for +// non-FAST modes), then trigger the requested refresh via +// EInkDisplay::refreshDisplay. SINGLE_BUFFER_MODE syncs RED post-refresh +// to prepare for the next fast differential; dual-buffer-mode preserves +// the previous frame in frameBufferActive for the same purpose. +void X4Panel::displayBuffer(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) { + d.setRamArea(0, 0, displayWidth(), displayHeight()); + + if (mode != RefreshMode::FAST_REFRESH) { + // Full / half: write the new frame to both BW and RED. + d.writeRamBuffer(CMD_WRITE_RAM_BW, d.frameBuffer, bufferSize()); + d.writeRamBuffer(CMD_WRITE_RAM_RED, d.frameBuffer, bufferSize()); + } else { + // Fast: BW gets the new frame; RED retains the prior frame for + // differential comparison. Single-buffer mode: RED is already + // synced from the previous refresh's post-step below. Dual-buffer + // mode: write back from frameBufferActive (the previous frame). + d.writeRamBuffer(CMD_WRITE_RAM_BW, d.frameBuffer, bufferSize()); +#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE + d.writeRamBuffer(CMD_WRITE_RAM_RED, d.frameBufferActive, bufferSize()); +#endif + } + +#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE + d.swapBuffers(); +#endif + + d.refreshDisplay(mode, turnOffScreen); + +#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE + // Sync RED RAM with the just-displayed frame so the next fast + // differential has the right "previous frame" to diff against. + d.setRamArea(0, 0, displayWidth(), displayHeight()); + d.writeRamBuffer(CMD_WRITE_RAM_RED, d.frameBuffer, bufferSize()); +#endif +} + +// SSD1677 refreshDisplay: pick the mode bits (NORMAL vs BYPASS_RED for +// FAST vs other), set CTRL2 to drive the requested transition, then +// master-activation + waitWhileBusy on the named refresh type. +void X4Panel::refreshDisplay(EInkDisplay& d, RefreshMode mode, bool turnOffScreen) { + d.sendCommand(CMD_DISPLAY_UPDATE_CTRL1); + d.sendData((mode == RefreshMode::FAST_REFRESH) ? CTRL1_NORMAL : CTRL1_BYPASS_RED); + + uint8_t displayMode = 0x00; + if (!d.isScreenOn) { + d.isScreenOn = true; + displayMode |= 0xC0; // CLOCK_ON + ANALOG_ON + } + if (turnOffScreen) { + d.isScreenOn = false; + displayMode |= 0x03; // ANALOG_OFF_PHASE + CLOCK_OFF + } + if (mode == RefreshMode::FULL_REFRESH) { + displayMode |= 0x34; + } else if (mode == RefreshMode::HALF_REFRESH) { + d.sendCommand(CMD_WRITE_TEMP); + d.sendData(0x5A); + displayMode |= 0xD4; + } else { // FAST_REFRESH + displayMode |= d.customLutActive ? 0x0C : 0x1C; + } + + const char* refreshType = (mode == RefreshMode::FULL_REFRESH) ? "full" + : (mode == RefreshMode::HALF_REFRESH) ? "half" + : "fast"; + if (Serial) Serial.printf("[%lu] Powering on display 0x%02X (%s refresh)...\n", millis(), displayMode, refreshType); + d.sendCommand(CMD_DISPLAY_UPDATE_CTRL2); + d.sendData(displayMode); + d.sendCommand(CMD_MASTER_ACTIVATION); + if (Serial) Serial.printf("[%lu] Waiting for display refresh...\n", millis()); + d.waitWhileBusy(refreshType); +} + +void X4Panel::displayWindow(EInkDisplay& d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen) { + if (Serial) Serial.printf("[%lu] Displaying window at (%d,%d) size (%dx%d)\n", millis(), x, y, w, h); + + if (x + w > displayWidth() || y + h > displayHeight()) { + if (Serial) Serial.printf("[%lu] ERROR: Window bounds exceed display dimensions!\n", millis()); + return; + } + if (x % 8 != 0 || w % 8 != 0) { + if (Serial) Serial.printf("[%lu] ERROR: Window x and width must be byte-aligned (multiples of 8)!\n", millis()); + return; + } + if (!d.frameBuffer) { + if (Serial) Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); + return; + } + if (d.inGrayscaleMode) grayscaleRevert(d); + + const uint16_t windowWidthBytes = w / 8; + const uint32_t windowBufferSize = windowWidthBytes * h; + if (Serial) + Serial.printf("[%lu] Window buffer size: %lu bytes (%d x %d pixels)\n", millis(), windowBufferSize, w, h); + + std::vector windowBuffer(windowBufferSize); + for (uint16_t row = 0; row < h; row++) { + const uint16_t srcOffset = (y + row) * displayWidthBytes() + (x / 8); + memcpy(&windowBuffer[row * windowWidthBytes], &d.frameBuffer[srcOffset], windowWidthBytes); + } + d.setRamArea(x, y, w, h); + d.writeRamBuffer(CMD_WRITE_RAM_BW, windowBuffer.data(), windowBufferSize); + +#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE + std::vector previousWindowBuffer(windowBufferSize); + for (uint16_t row = 0; row < h; row++) { + const uint16_t srcOffset = (y + row) * displayWidthBytes() + (x / 8); + memcpy(&previousWindowBuffer[row * windowWidthBytes], &d.frameBufferActive[srcOffset], windowWidthBytes); + } + d.writeRamBuffer(CMD_WRITE_RAM_RED, previousWindowBuffer.data(), windowBufferSize); +#endif + + refreshDisplay(d, RefreshMode::FAST_REFRESH, turnOffScreen); + +#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE + d.setRamArea(x, y, w, h); + d.writeRamBuffer(CMD_WRITE_RAM_RED, windowBuffer.data(), windowBufferSize); +#endif + if (Serial) Serial.printf("[%lu] Window display complete\n", millis()); +} + +extern const unsigned char lut_grayscale[]; +extern const unsigned char lut_grayscale_revert[]; +extern const unsigned char lut_factory_quality[]; + +void X4Panel::displayGrayBuffer(EInkDisplay& d, bool turnOffScreen, const unsigned char* lut, bool factoryMode) { + d.drawGrayscale = false; + // Differential mode keeps inGrayscaleMode set; reader AA clears it + // via cleanupGrayscaleBuffers before the next BW page turn. + d.inGrayscaleMode = !factoryMode; + + const unsigned char* selectedLut = lut; + if (selectedLut == nullptr) selectedLut = factoryMode ? lut_factory_quality : lut_grayscale; + setCustomLUT(d, true, selectedLut); + + if (factoryMode) { + // Absolute mode: explicit full power cycle. CTRL1 normal because a + // prior HALF leaves it at BYPASS_RED which would break 4-level grayscale. + d.sendCommand(CMD_DISPLAY_UPDATE_CTRL1); + d.sendData(CTRL1_NORMAL); + d.sendCommand(CMD_DISPLAY_UPDATE_CTRL2); + d.sendData(0xC7); // CLOCK+ANALOG+DISPLAY+ANALOG_OFF+CLOCK_OFF self-contained cycle + d.sendCommand(CMD_MASTER_ACTIVATION); + d.waitWhileBusy("factory_gray"); + d.isScreenOn = false; + } else { + refreshDisplay(d, RefreshMode::FAST_REFRESH, turnOffScreen); + } + setCustomLUT(d, false); +} + +void X4Panel::copyGrayscaleLsbBuffers(EInkDisplay& d, const uint8_t* lsbBuffer) { + if (!lsbBuffer) return; + d.setRamArea(0, 0, displayWidth(), displayHeight()); + d.writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, bufferSize()); +} + +void X4Panel::copyGrayscaleMsbBuffers(EInkDisplay& d, const uint8_t* msbBuffer) { + if (!msbBuffer) return; + d.setRamArea(0, 0, displayWidth(), displayHeight()); + d.writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, bufferSize()); +} + +void X4Panel::copyGrayscaleBuffers(EInkDisplay& d, const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + d.setRamArea(0, 0, displayWidth(), displayHeight()); + d.writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, bufferSize()); + d.writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, bufferSize()); +} + +void X4Panel::writeGrayscalePlaneStrip(EInkDisplay& d, GrayPlane plane, const uint8_t* rows, uint16_t yStart, + uint16_t numRows) { + if (!rows || numRows == 0) return; + const uint8_t ramCmd = (plane == GrayPlane::GRAY_PLANE_LSB) ? CMD_WRITE_RAM_BW : CMD_WRITE_RAM_RED; + d.setRamArea(0, yStart, displayWidth(), numRows); + d.sendCommand(ramCmd); + d.sendData(rows, static_cast(static_cast(numRows) * displayWidthBytes())); +} + +void X4Panel::cleanupGrayscaleBuffers(EInkDisplay& d, const uint8_t* bwBuffer) { +#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE + if (!bwBuffer) return; + d.setRamArea(0, 0, displayWidth(), displayHeight()); + d.writeRamBuffer(CMD_WRITE_RAM_RED, bwBuffer, bufferSize()); + d.inGrayscaleMode = false; +#else + (void)d; + (void)bwBuffer; +#endif +} + +void X4Panel::setCustomLUT(EInkDisplay& d, bool enabled, const unsigned char* lutData) { + if (enabled) { + if (Serial) Serial.printf("[%lu] Loading custom LUT...\n", millis()); + d.sendCommand(CMD_WRITE_LUT); + for (uint16_t i = 0; i < 105; i++) d.sendData(pgm_read_byte(&lutData[i])); + d.sendCommand(CMD_GATE_VOLTAGE); + d.sendData(pgm_read_byte(&lutData[105])); + d.sendCommand(CMD_SOURCE_VOLTAGE); + d.sendData(pgm_read_byte(&lutData[106])); + d.sendData(pgm_read_byte(&lutData[107])); + d.sendData(pgm_read_byte(&lutData[108])); + d.sendCommand(CMD_WRITE_VCOM); + d.sendData(pgm_read_byte(&lutData[109])); + d.customLutActive = true; + if (Serial) Serial.printf("[%lu] Custom LUT loaded\n", millis()); + } else { + d.customLutActive = false; + if (Serial) Serial.printf("[%lu] Custom LUT disabled\n", millis()); + } +} + +void X4Panel::deepSleep(EInkDisplay& d) { + if (Serial) Serial.printf("[%lu] Preparing display for deep sleep...\n", millis()); + if (d.isScreenOn) { + d.sendCommand(CMD_DISPLAY_UPDATE_CTRL1); + d.sendData(0x00); + d.sendCommand(CMD_DISPLAY_UPDATE_CTRL2); + d.sendData(0x83); + d.sendCommand(CMD_MASTER_ACTIVATION); + d.waitWhileBusy("display_off"); + d.isScreenOn = false; + } + d.sendCommand(CMD_DEEP_SLEEP); + d.sendData(0x01); // DSM1 + if (Serial) Serial.printf("[%lu] Display in deep sleep\n", millis()); +} + +extern const unsigned char lut_grayscale_revert[]; +void X4Panel::grayscaleRevert(EInkDisplay& d) { + setCustomLUT(d, true, lut_grayscale_revert); + refreshDisplay(d, RefreshMode::FAST_REFRESH, false); + setCustomLUT(d, false); +} + +void X4Panel::pollBusy(EInkDisplay& d, const char* comment, const char* completeWord) const { + unsigned long start = millis(); + while (digitalRead(d._busy) == HIGH) { + delay(1); + if (millis() - start > 30000) break; + } + if (comment && Serial) { + Serial.printf("[%lu] %s: %s (%lu ms)\n", millis(), completeWord, comment, millis() - start); + } +} From 3c488a254c81f7944543b73d8c2f46c23a88243b Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Sun, 24 May 2026 20:55:38 -0700 Subject: [PATCH 3/3] feat(eink): add idle hook for refresh-wait (ported to Panel pollBusy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of the original feat/eink-idle-hook commit (9df4199) onto the Panel-virtualized SDK. Same public API and contract; the call sites moved because pollBusy is no longer a single method on EInkDisplay. waitForRefresh / waitWhileBusy poll the BUSY line in a 1 ms delay loop for 400-700 ms per typical page turn. That window is CPU-idle and a natural slot for amortizing background work the reader is otherwise forced to run after displayBuffer returns (chunked next-chapter indexing, for example). setIdleHook installs a C-style void(void*) callback + context; the polling loops invoke it after each 1 ms poll. The hook is a function pointer (not std::function) to keep binary cost flat across SDK consumers, and fires on whatever task drove the wait into the SDK (the render task in the existing reader architecture). Caller contract: the hook must return within ~50-100 ms (it delays BUSY detection by exactly that much), and must be safe to call from the calling task. Returning immediately when there is no work to do (one cheap pointer check) is the common case. Differences vs the original commit: * Storage stays on EInkDisplay (single static slot for the whole SDK — installing a second hook replaces the first; same as pre-refactor). * setIdleHook / tickIdleHook are public static methods on EInkDisplay. tickIdleHook is the per-loop tick point that each panel's pollBusy invokes; it's `static inline` so the null-hook check is one pointer compare with no call overhead. * X4Panel::pollBusy gets 1 tick call (single-phase HIGH-wait). X3Panel::pollBusy gets 2 tick calls (two-phase HIGH→LOW→HIGH wait). Total 3 call sites vs 3 in the pre-refactor monolithic pollBusy — identical poll cadence. Built on top of the Panel-abstraction branch (PR #15). Image +60 bytes for the 3 tick call sites, the static slot, and the setter body. Hardware verified both panels: * X4: half=1554ms, fast=418/419ms, EpubReader render cycle clean. Timings identical to pre-hook (the null hook check is a no-op). * X3: full=926ms, post-full settle=417ms, conditional resync 455ms, grayscale (gc bank) 2346ms, fast=378ms. Same. Zero ERR/assert/abort on either device. --- .../display/EInkDisplay/include/EInkDisplay.h | 34 +++++++++++++++++++ libs/display/EInkDisplay/src/EInkDisplay.cpp | 9 +++++ libs/display/EInkDisplay/src/X3Panel.cpp | 2 ++ libs/display/EInkDisplay/src/X4Panel.cpp | 1 + 4 files changed, 46 insertions(+) diff --git a/libs/display/EInkDisplay/include/EInkDisplay.h b/libs/display/EInkDisplay/include/EInkDisplay.h index 2ca751b..a00037b 100644 --- a/libs/display/EInkDisplay/include/EInkDisplay.h +++ b/libs/display/EInkDisplay/include/EInkDisplay.h @@ -120,6 +120,34 @@ class EInkDisplay { // Save the current framebuffer to a PBM file (desktop/test builds only) void saveFrameBufferAsPBM(const char* filename); + // Refresh-wait idle hook. + // + // The e-ink refresh wait polls the BUSY line in a 1 ms `delay(1)` loop + // for ~400-700 ms per page turn on typical reader content. That window + // is CPU-idle and a natural place to run other work — chunked + // background section indexing in the reader, for example. Callers + // install a void(void*) function pointer + context via setIdleHook; + // each panel's BUSY-poll loop invokes it after every 1 ms poll. The + // hook is intentionally NOT a std::function (binary-size discipline, + // see open-x4-sdk Resource Protocol). + // + // Threading: the hook fires on whatever task calls into the wait (in + // practice the render task). The hook implementation MUST be safe to + // call from that task and MUST NOT take more than ~50-100 ms or it + // delays detection of the BUSY transition. Returning quickly when + // there is no work to do (one cheap pointer check) is the common case. + // + // Storage is a single static slot for the whole SDK — installing a + // second hook replaces the first. + using IdleHook = void (*)(void* ctx); + static void setIdleHook(IdleHook hook, void* ctx); + + // Invoked by Panel::pollBusy implementations from their 1 ms poll + // loops. Inline for zero overhead on the common null-hook case. + static inline void tickIdleHook() { + if (_idleHook) _idleHook(_idleHookCtx); + } + private: // Pin configuration int8_t _sclk, _mosi, _cs, _dc, _rst, _busy; @@ -146,6 +174,12 @@ class EInkDisplay { bool inGrayscaleMode = false; bool drawGrayscale = false; + // Idle-hook storage (single slot for the whole SDK; see setIdleHook + // comment above). Static because the hook policy doesn't depend on + // which EInkDisplay instance the wait was driven from. + static IdleHook _idleHook; + static void* _idleHookCtx; + // Low-level display control void resetDisplay(); void sendCommand(uint8_t command); diff --git a/libs/display/EInkDisplay/src/EInkDisplay.cpp b/libs/display/EInkDisplay/src/EInkDisplay.cpp index 8101784..ee3636d 100644 --- a/libs/display/EInkDisplay/src/EInkDisplay.cpp +++ b/libs/display/EInkDisplay/src/EInkDisplay.cpp @@ -11,6 +11,15 @@ void EInkDisplay::setDisplayX3() { _panel = std::make_unique(); } +// Refresh-wait idle hook — single slot, see setIdleHook header comment. +EInkDisplay::IdleHook EInkDisplay::_idleHook = nullptr; +void* EInkDisplay::_idleHookCtx = nullptr; + +void EInkDisplay::setIdleHook(IdleHook hook, void* ctx) { + _idleHook = hook; + _idleHookCtx = ctx; +} + void EInkDisplay::requestResync(uint8_t settlePasses) { _panel->requestResync(settlePasses); } void EInkDisplay::skipInitialResync() { _panel->skipInitialResync(); } diff --git a/libs/display/EInkDisplay/src/X3Panel.cpp b/libs/display/EInkDisplay/src/X3Panel.cpp index bf0d814..ca1b412 100644 --- a/libs/display/EInkDisplay/src/X3Panel.cpp +++ b/libs/display/EInkDisplay/src/X3Panel.cpp @@ -430,12 +430,14 @@ void X3Panel::pollBusy(EInkDisplay& d, const char* comment, const char* complete bool sawLow = false; while (digitalRead(d._busy) == HIGH) { delay(1); + EInkDisplay::tickIdleHook(); if (millis() - start > 1000) break; } if (digitalRead(d._busy) == LOW) { sawLow = true; while (digitalRead(d._busy) == LOW) { delay(1); + EInkDisplay::tickIdleHook(); if (millis() - start > 30000) break; } } diff --git a/libs/display/EInkDisplay/src/X4Panel.cpp b/libs/display/EInkDisplay/src/X4Panel.cpp index 16af33c..4a77d68 100644 --- a/libs/display/EInkDisplay/src/X4Panel.cpp +++ b/libs/display/EInkDisplay/src/X4Panel.cpp @@ -295,6 +295,7 @@ void X4Panel::pollBusy(EInkDisplay& d, const char* comment, const char* complete unsigned long start = millis(); while (digitalRead(d._busy) == HIGH) { delay(1); + EInkDisplay::tickIdleHook(); if (millis() - start > 30000) break; } if (comment && Serial) {