diff --git a/libs/display/EInkDisplay/include/EInkDisplay.h b/libs/display/EInkDisplay/include/EInkDisplay.h index 4af4de3..a00037b 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,48 @@ 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); + // 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; - // 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; @@ -142,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); @@ -149,13 +187,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 +197,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 aff884c..ee3636d 100644 --- a/libs/display/EInkDisplay/src/EInkDisplay.cpp +++ b/libs/display/EInkDisplay/src/EInkDisplay.cpp @@ -4,447 +4,52 @@ #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; -} +// 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() { - setDisplayDimensions(X3_DISPLAY_WIDTH, X3_DISPLAY_HEIGHT); - _x3Mode = true; -} +void EInkDisplay::setDisplayX3() { _panel = std::make_unique(); } -void EInkDisplay::requestResync(uint8_t settlePasses) { - _x3ForceFullSyncNext = _x3Mode; - _x3ForcedConditionPassesNext = _x3Mode ? settlePasses : 0; -} +// Refresh-wait idle hook — single slot, see setIdleHook header comment. +EInkDisplay::IdleHook EInkDisplay::_idleHook = nullptr; +void* EInkDisplay::_idleHookCtx = nullptr; -void EInkDisplay::skipInitialResync() { - if (!_x3Mode) return; - _x3InitialFullSyncsRemaining = 0; - _x3RedRamSynced = true; +void EInkDisplay::setIdleHook(IdleHook hook, void* ctx) { + _idleHook = hook; + _idleHookCtx = ctx; } -// 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), +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 +62,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 +88,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 +96,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 +104,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 + digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } -void EInkDisplay::sendData(const uint8_t *data, uint16_t length) { +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(_dc, HIGH); // Data mode + digitalWrite(_cs, LOW); // Select chip + SPI.writeBytes(data, length); // Transfer all bytes + 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::waitWhileBusy(const char* comment) { pollBusy(comment, "Wait complete"); } -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::initDisplayController() { _panel->init(*this); } -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); - SPI.beginTransaction(spiSettings); - digitalWrite(_dc, HIGH); - digitalWrite(_cs, LOW); - for (uint16_t y = 0; y < displayHeight; y++) { - SPI.writeBytes(rowBuf, displayWidthBytes); - } - digitalWrite(_cs, HIGH); - 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::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::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 +164,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 +202,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 +236,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,592 +302,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; - // Half mode only applies if we're not already being promoted to full. - 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; - - if (doFullSync && _x3InitialFullSyncsRemaining > 0) { - _x3InitialFullSyncsRemaining--; - } - _x3ForceFullSyncNext = false; - _x3ForcedConditionPassesNext = 0; - return; - } - - // 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 + // 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; } - -#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++) { @@ -1780,20 +371,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..ca1b412 --- /dev/null +++ b/libs/display/EInkDisplay/src/X3Panel.cpp @@ -0,0 +1,573 @@ +#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); + 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; + } + } + 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..4a77d68 --- /dev/null +++ b/libs/display/EInkDisplay/src/X4Panel.cpp @@ -0,0 +1,304 @@ +#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); + EInkDisplay::tickIdleHook(); + if (millis() - start > 30000) break; + } + if (comment && Serial) { + Serial.printf("[%lu] %s: %s (%lu ms)\n", millis(), completeWord, comment, millis() - start); + } +}