diff --git a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h index 26d5bbfe..56c47a2b 100644 --- a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h +++ b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h @@ -98,6 +98,8 @@ class FreeInkDisplay { enum GrayPlane { GRAY_PLANE_LSB, GRAY_PLANE_MSB }; void writeGrayscalePlaneStrip(GrayPlane plane, const uint8_t* rows, uint16_t yStart, uint16_t numRows); bool supportsStripGrayscale() const; + // False on B/W-only panels (Murphy M3): consumers skip building gray planes. + bool supportsGrayscale() const; // Restore controller RAM and frameBuffer to the given BW baseline after // grayscale. Available in both buffer modes (CrossPoint's dual-buffer HAL // wraps it directly). diff --git a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp index eb77f4a1..b7c181e2 100644 --- a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp +++ b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp @@ -829,6 +829,10 @@ bool FreeInkDisplay::supportsStripGrayscale() const { return !_inverted && _driver && _driver->supportsStripGrayscale(); } +bool FreeInkDisplay::supportsGrayscale() const { + return _driver && _driver->supportsGrayscale(); +} + void FreeInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { syncPendingAsync(); if (!_inverted) { diff --git a/libs/display/FreeInkDisplay/src/driver/PanelDriver.h b/libs/display/FreeInkDisplay/src/driver/PanelDriver.h index 94f23ccd..ad79fa0a 100644 --- a/libs/display/FreeInkDisplay/src/driver/PanelDriver.h +++ b/libs/display/FreeInkDisplay/src/driver/PanelDriver.h @@ -101,6 +101,11 @@ class PanelDriver { // --- grayscale (dual-plane LSB/MSB) --- virtual bool supportsStripGrayscale() const { return false; } + // Whether the panel can display grayscale AT ALL. Consumers use this to skip + // building gray planes entirely on B/W-only panels (e.g. Murphy M3), whose + // displayGray must be a no-op. Default true: every pre-existing driver + // implements a real grayscale path. + virtual bool supportsGrayscale() const { return true; } // Display `fb` as the base frame for a grayscale overlay that follows. // X3 runs the OEM pipeline (the "AA-pre-BW(mid)" bank as a differential // base update with calibrated drives); panels without a dedicated base diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index ede448ca..643a589d 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -4,6 +4,14 @@ #include "../lut/Uc8253MurphyLuts.h" +// OEM alternate/partial refresh package for RefreshMode::Fast: data-driven init +// variant + ALT LUT bank + 0x17/0xA5 trigger (see display() below). Default ON; +// build with -DFREEINK_MURPHY_OEM_PARTIAL=0 to restore the previous synthetic +// destination-drive FAST bank under the mode-0 init if the port misbehaves. +#ifndef FREEINK_MURPHY_OEM_PARTIAL +#define FREEINK_MURPHY_OEM_PARTIAL 1 +#endif + namespace freeink { namespace { // UC8253 command set (shared with the X3 panel; Murphy uses a different init, @@ -34,14 +42,67 @@ constexpr uint8_t CMD_VCOM_DC = 0x82; constexpr uint16_t CTRL_W = 240; constexpr uint16_t CTRL_H = 416; constexpr uint16_t CTRL_WB = CTRL_W / 8; // 30 + +#if FREEINK_MURPHY_OEM_PARTIAL +// OEM auto-refresh trigger used by the alternate/partial mode instead of 0x12. +// Ghidra FUN_42038f74/FUN_42038fa4 else-branch: FUN_420384e8(obj, 0x17, 0xA5) — +// command 0x17 with the single data byte 0xA5, then a BUSY wait. No window +// coordinate bytes exist anywhere in the decompiled trigger or init path; the +// refresh covers whatever the DTM planes hold (we always write the full +// 240x416 frame). On the UC8151/UC8253 family 0x17 is the AUTO register and +// 0xA5 runs the PON -> DRF -> POF sequence internally — consistent with the +// OEM alt init never issuing 0x04 (power on) itself. +constexpr uint8_t CMD_AUTO_SEQUENCE = 0x17; +constexpr uint8_t AUTO_PON_DRF_POF = 0xA5; + +// ALT bank register mapping, per the Ghidra LUT loader FUN_42038b60 else +// branch. The loader keys on a one-shot RTC flag (boot value 0, cleared again +// after any use), so the boot-default steady state loads: +// 0x20 <- MURPHY_LUT_20_ALT (56 B) 0x21 <- MURPHY_LUT_21_ALT (42 B) +// 0x22 <- MURPHY_LUT_23_ALT_B (56 B) 0x23 <- MURPHY_LUT_22_ALT_A (42 B) +// 0x24 <- MURPHY_LUT_24_ALT (42 B) +// i.e. the "22_A"/"23_B" array names from the findings doc are CROSSED over +// registers 0x22/0x23 here. That steady-state mapping is also the physically +// consistent one under the doc's voltage-code decode (0x8F = VSL/to-white bit, +// 0x4F = VSH/to-black bit): BW (0x22) ends on a to-white kick (0f 8f 0f) and +// WB (0x23) on a to-black kick (4f 8f 4f). +// FIXME(doc ambiguity): with the one-shot flag SET the OEM loads the swapped +// arrangement (0x22 <- 22_ALT_A @42, 0x23 <- 23_ALT_B @56) for exactly one +// refresh — the arrangement the findings-doc array names suggest. No writer of +// that flag appears in the reverse-engineering docs, so the swap's purpose is +// unknown; we ship the boot-default steady-state mapping only. +constexpr Uc8253MurphyLutBank ALT_BANK = { + MURPHY_LUT_20_ALT, // vcom 0x20 + MURPHY_LUT_21_ALT, // ww 0x21 + MURPHY_LUT_23_ALT_B, // bw 0x22 (56 B — see mapping note above) + MURPHY_LUT_22_ALT_A, // wb 0x23 (42 B) + MURPHY_LUT_24_ALT, // bb 0x24 +}; +constexpr Uc8253MurphyLutLens ALT_LENS = { + sizeof(MURPHY_LUT_20_ALT), // 56 (loader writes 0x38) + sizeof(MURPHY_LUT_21_ALT), // 42 (0x2a) + sizeof(MURPHY_LUT_23_ALT_B), // 56 (0x38) + sizeof(MURPHY_LUT_22_ALT_A), // 42 (0x2a) + sizeof(MURPHY_LUT_24_ALT), // 42 (0x2a) +}; +#endif // FREEINK_MURPHY_OEM_PARTIAL } // namespace const Uc8253MurphyConfig& uc8253MurphyDefaultConfig() { static const Uc8253MurphyConfig cfg = { {MURPHY_LUT_20_DEFAULT, MURPHY_LUT_21_DEFAULT, MURPHY_LUT_22_DEFAULT, MURPHY_LUT_23_DEFAULT, MURPHY_LUT_24_DEFAULT}, {MURPHY_LUT_20_FAST, MURPHY_LUT_21_FAST, MURPHY_LUT_22_FAST, MURPHY_LUT_23_FAST, MURPHY_LUT_24_FAST}, - {MURPHY_LUT_LEN_VCOM, MURPHY_LUT_LEN_WW, MURPHY_LUT_LEN_BW, MURPHY_LUT_LEN_WB, MURPHY_LUT_LEN_BB}, // 56/42/56/42/42 - 8, // promote FAST -> full every 8 refreshes + {MURPHY_LUT_LEN_VCOM, MURPHY_LUT_LEN_WW, MURPHY_LUT_LEN_BW, MURPHY_LUT_LEN_WB, MURPHY_LUT_LEN_BB}, // 42 each (OEM writes ten 42-byte payloads) + // Non-flashing FAST refreshes are allowed to run this many times in a row + // before one is promoted to the OEM three-phase GC bank (the inversion + // flash that clears accumulated residue). Applies to both FAST flavors: + // the OEM partial kicks (FREEINK_MURPHY_OEM_PARTIAL, single short kick + // per pixel) and the synthetic destination-drive bank (flag=0) are + // DC-unbalanced per refresh, so the cadence is the anti-ghosting story: + // lower = cleaner, higher = fewer flashes. 4 after hardware feedback + // (6 left a distracting residue while reading). + // (Interval semantics: N fast refreshes pass, the N+1th promotes.) + 4, }; return cfg; } @@ -60,12 +121,12 @@ PanelGeometry Uc8253MurphyDriver::geometry() const { return {_fbW, _fbH, _fbWb, static_cast(_fbWb) * _fbH}; } -void Uc8253MurphyDriver::loadLut(EpdBus& bus, const Uc8253MurphyLutBank& bank) { - bus.cmdData(CMD_LUT_VCOM, bank.vcom, _cfg.lens.vcom); - bus.cmdData(CMD_LUT_WW, bank.ww, _cfg.lens.ww); - bus.cmdData(CMD_LUT_BW, bank.bw, _cfg.lens.bw); - bus.cmdData(CMD_LUT_WB, bank.wb, _cfg.lens.wb); - bus.cmdData(CMD_LUT_BB, bank.bb, _cfg.lens.bb); +void Uc8253MurphyDriver::loadLut(EpdBus& bus, const Uc8253MurphyLutBank& bank, const Uc8253MurphyLutLens& lens) { + bus.cmdData(CMD_LUT_VCOM, bank.vcom, lens.vcom); + bus.cmdData(CMD_LUT_WW, bank.ww, lens.ww); + bus.cmdData(CMD_LUT_BW, bank.bw, lens.bw); + bus.cmdData(CMD_LUT_WB, bank.wb, lens.wb); + bus.cmdData(CMD_LUT_BB, bank.bb, lens.bb); } // Rotate the landscape framebuffer (416x240) into the controller's portrait RAM @@ -131,6 +192,51 @@ void Uc8253MurphyDriver::initController(EpdBus& bus) { } +#if FREEINK_MURPHY_OEM_PARTIAL +// OEM data-driven init variant — Ghidra FUN_420389ec else-branch, byte-exact, +// in OEM command order (0x00, 0x01, 0x06, 0x30, 0x82, 0x61, 0x50). The single +// data bytes 0x30=0x09, 0x82=0x07, 0x50=0xD7 are immediate in the decompile; +// the 0x00/0x01/0x06/0x61 payloads are pointer-fed (lengths 2/5/3/3). The +// findings doc spells out only the 0x01 payload ("03 10 3F 3F 03", extracted +// at 0x3c236ca3 — display_driver.md line 93-97); the other three were read +// from the doc's cited device dump (app0_seg0_3c190020.bin) by resolving the +// literal pool FUN_420389ec loads from (seg3+0x2274..0x2280): the pointers are +// contiguous — 0x00 @0x3c236ca1 = FF 01, 0x01 @0x3c236ca3 = 03 10 3F 3F 03 +// (matches the doc byte-for-byte, anchoring the extraction), 0x06 @0x3c236ca8 +// = 17 37 3D, 0x61 @0x3c236cab = F0 01 A0. The same 13-byte block appears +// verbatim in the upstream OEM touch v525 firmware at file offset 0xab5e1. +// Unlike the mode-0 init there is NO power-on (0x04) here: the 0x17/0xA5 auto +// trigger powers the panel itself (see triggerRefreshAlt). +void Uc8253MurphyDriver::initControllerAlt(EpdBus& bus) { + static constexpr uint8_t PSR_ALT[2] = {0xFF, 0x01}; + static constexpr uint8_t PWR_ALT[5] = {0x03, 0x10, 0x3F, 0x3F, 0x03}; + static constexpr uint8_t BTST_ALT[3] = {0x17, 0x37, 0x3D}; + static constexpr uint8_t RES_ALT[3] = {0xF0, 0x01, 0xA0}; // 240 x 416, same as mode-0 + bus.cmdData(CMD_PANEL_SETTING, PSR_ALT, sizeof(PSR_ALT)); + bus.cmdData(CMD_POWER_SETTING, PWR_ALT, sizeof(PWR_ALT)); + bus.cmdData(CMD_BOOSTER_SOFT_START, BTST_ALT, sizeof(BTST_ALT)); + bus.cmd(CMD_PLL_CONTROL); + bus.data(0x09); + bus.cmd(CMD_VCOM_DC); + bus.data(0x07); + bus.cmdData(CMD_RESOLUTION, RES_ALT, sizeof(RES_ALT)); + bus.cmd(CMD_VCOM_DATA_INTERVAL); + bus.data(0xD7); +} + +// OEM alternate refresh trigger: 0x17 with data 0xA5, then BUSY wait (Ghidra +// FUN_42038f74 else-branch). 0xA5 auto-sequences PON -> DRF -> POF inside the +// controller, so no explicit 0x04 before or 0x02 after — matching the OEM, +// whose power-off helper is a no-op in this mode (its powered flag is only set +// by an explicit 0x04). The panel is therefore always off after this returns. +void Uc8253MurphyDriver::triggerRefreshAlt(EpdBus& bus) { + bus.cmd(CMD_AUTO_SEQUENCE); + bus.data(AUTO_PON_DRF_POF); + bus.waitBusy(" M3_AUTO"); + _isScreenOn = false; +} +#endif // FREEINK_MURPHY_OEM_PARTIAL + void Uc8253MurphyDriver::begin(EpdBus& bus) { bus.reset(200); // Murphy panel wants a long post-reset settle _isScreenOn = false; @@ -145,10 +251,9 @@ void Uc8253MurphyDriver::display(EpdBus& bus, const uint8_t* fb, const uint8_t* // without settling). A fresh reset+init each time clears that residue. bus.reset(200); _isScreenOn = false; - initController(bus); - // FAST (DU) ghosts over time, so promote to a full (GC) refresh every - // ghostClearInterval refreshes. + // FAST ghosts over time (both the OEM partial kicks and the synthetic DU + // bank), so promote to a full (GC) refresh every ghostClearInterval refreshes. bool useFast = (mode == RefreshMode::Fast); if (useFast) { if (_cfg.ghostClearInterval != 0 && _fastRefreshCount >= _cfg.ghostClearInterval) { @@ -161,20 +266,42 @@ void Uc8253MurphyDriver::display(EpdBus& bus, const uint8_t* fb, const uint8_t* _fastRefreshCount = 0; } - loadLut(bus, useFast ? _cfg.fast : _cfg.def); - - // Full (GC) refresh writes the new frame to BOTH planes, so only WW/BB fire and - // every pixel is fully driven to its target — clean, no half-flipped pixels. - // FAST (DU) refresh is differential: old frame -> DTM1, new -> DTM2, so unchanged - // pixels take WW/BB and changed pixels take the quick BW/WB transition kicks. - // Without a previous frame (single-buffer builds) fall back to both-planes-new. - if (useFast && prev != nullptr) { - writePlane(bus, CMD_DTM1, prev); - writePlane(bus, CMD_DTM2, fb); - } else { - writePlane(bus, CMD_DTM1, fb); +#if FREEINK_MURPHY_OEM_PARTIAL + if (useFast) { + // OEM alternate/partial package: data-driven init + ALT bank + 0x17/0xA5. + // This path is differential — the ALT bank's BW/WB kicks exist to fire on + // DTM1 != DTM2 (display_driver.md: "LUTBW/LUTWB exist for the alternate + // 0x17/0xA5 partial-refresh path and only do a short kick"): old frame -> + // DTM1, new frame -> DTM2, OEM plane order (0x10 then 0x13). On + // single-buffer builds the facade passes prev == nullptr; fall back to the + // same buffer in both planes, which degrades gracefully to the gentle + // same-state WW/BB kicks only (changed pixels then rely on the next + // full/promoted refresh — nothing mis-drives). + initControllerAlt(bus); + loadLut(bus, ALT_BANK, ALT_LENS); + writePlane(bus, CMD_DTM1, prev != nullptr ? prev : fb); writePlane(bus, CMD_DTM2, fb); + // 0xA5 auto-sequences PON -> DRF -> POF, so the panel ends powered off no + // matter what turnOff asked for; the next display() re-inits from reset + // anyway, so an unused-on flag is never left dangling. + (void)turnOff; + triggerRefreshAlt(bus); + return; } +#endif + + initController(bus); + loadLut(bus, useFast ? _cfg.fast : _cfg.def, _cfg.lens); + + // OEM full-refresh scheme: the SAME buffer goes to both planes, always — only + // WW/BB fire and every pixel is driven straight to its target. A real + // differential (old->DTM1, new->DTM2) through the DEFAULT/FAST LUTs leaves + // pixels half-flipped (verified empirically, see the Murphy repo display + // findings), so it is deliberately not attempted even when a previous frame + // exists. + (void)prev; + writePlane(bus, CMD_DTM1, fb); + writePlane(bus, CMD_DTM2, fb); triggerRefresh(bus, turnOff); } diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h index 83162b6f..0206c49b 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h @@ -10,13 +10,22 @@ // // Refresh: the controller is hardware-reset and re-initialised before every // refresh (manufacturer guidance) so stale LUT/RAM state can't leave pixels -// half-latched. A full (GC) refresh then writes the new frame to BOTH planes so -// only WW/BB fire and every pixel is fully driven to target — clean. A FAST (DU) -// refresh is differential: previous frame -> DTM1 (old), new frame -> DTM2 (new), -// so unchanged pixels take WW/BB and changed pixels take the quick BW/WB transition -// kicks; the driver promotes a fast refresh to a full one every ghostClearInterval -// refreshes since DU ghosts over time. Without a previous frame (single-buffer -// builds) the fast path falls back to both-planes-new (WW/BB only). +// half-latched. A full (GC) refresh runs the OEM mode-0 init + DEFAULT bank, +// writes the new frame to BOTH planes so only WW/BB fire (every pixel fully +// driven to target), and triggers via 0x12 — clean. +// +// FAST is the OEM's alternate/partial package (FREEINK_MURPHY_OEM_PARTIAL, +// default 1): the data-driven init variant (0x82=0x07, 0x50=0xD7, pointer-fed +// 0x00/0x01/0x06/0x61 payloads), the ALT LUT bank (short single-kick +// waveforms), and the 0x17/0xA5 auto refresh trigger — Ghidra FUN_420389ec +// else-branch / FUN_42038b60 / FUN_42038f74. It is differential: previous +// frame -> DTM1 (old), new frame -> DTM2 (new), so unchanged pixels take the +// gentle WW/BB kicks and changed pixels the BW/WB transitions. Without a +// previous frame (facade passes prev=nullptr on single-buffer builds) it falls +// back to both-planes-new. The driver still promotes a fast refresh to a full +// one every ghostClearInterval refreshes since partial kicks ghost over time. +// Build with -DFREEINK_MURPHY_OEM_PARTIAL=0 to fall back to the old synthetic +// destination-drive FAST bank under the mode-0 init + 0x12 trigger. // // Selection: linked only when -DFREEINK_DRIVER_UC8253_MURPHY (Murphy board env). @@ -46,8 +55,8 @@ struct Uc8253MurphyLutLens { struct Uc8253MurphyConfig { Uc8253MurphyLutBank def; // GC (ghost-clearing) waveforms - Uc8253MurphyLutBank fast; // DU (quick) waveforms - Uc8253MurphyLutLens lens; // per-register LUT byte counts + Uc8253MurphyLutBank fast; // synthetic DU waveforms (used only when FREEINK_MURPHY_OEM_PARTIAL=0) + Uc8253MurphyLutLens lens; // per-register LUT byte counts for def/fast uint8_t ghostClearInterval; // promote FAST -> full every N refreshes }; @@ -65,11 +74,21 @@ class Uc8253MurphyDriver : public PanelDriver { void deepSleep(EpdBus& bus) override; void display(EpdBus& bus, const uint8_t* fb, const uint8_t* prev, RefreshMode mode, bool turnOff) override; + // B/W only: no gray LUT bank, no plane ingest (grayscale AA was tried on this + // panel and abandoned — asymmetric VSH/VSL rails, see the Murphy repo display + // findings). The PanelDriver default would refresh the renderer's gray PLANE + // as if it were a B/W frame — a near-solid-black page — so it must be a no-op + // here. supportsGrayscale() lets consumers skip building the planes at all. + void displayGray(EpdBus&, const uint8_t*, bool, const unsigned char*, bool) override {} + bool supportsGrayscale() const override { return false; } + private: void initController(EpdBus& bus); - void loadLut(EpdBus& bus, const Uc8253MurphyLutBank& bank); + void initControllerAlt(EpdBus& bus); // OEM data-driven init variant (partial path) + void loadLut(EpdBus& bus, const Uc8253MurphyLutBank& bank, const Uc8253MurphyLutLens& lens); void writePlane(EpdBus& bus, uint8_t command, const uint8_t* fb); // rotates 416x240 fb -> 240x416 RAM void triggerRefresh(EpdBus& bus, bool turnOff); + void triggerRefreshAlt(EpdBus& bus); // OEM 0x17/0xA5 auto sequence (partial path) const Uc8253MurphyConfig& _cfg; diff --git a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h index b3cd86c4..a4792dc0 100644 --- a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h +++ b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h @@ -2,75 +2,117 @@ #include -// UC8253 waveform LUTs for the Murphy M3 (CrowPanel 3.7", 240x416), transcribed -// verbatim from the manufacturer's reference ("M3 LUT.txt"). Five registers per -// bank: 0x20 VCOM, 0x21 WW, 0x22 BW, 0x23 WB, 0x24 BB. +// UC8253 waveform LUTs for the Murphy M3 (CrowPanel 3.7", 240x416). Five +// registers per bank: 0x20 VCOM, 0x21 WW, 0x22 BW, 0x23 WB, 0x24 BB — each 42 +// bytes (the OEM firmware writes ten 42-byte payloads; confirmed byte-identical +// in the touch v525 dump at 0x3c23b7e0). // -// Per-register lengths follow the manufacturer header: VCOM (0x20) and BW (0x22) -// are 56 bytes (8 groups of 7); WW/WB/BB are 42 bytes (6 groups). Each group is -// 7 bytes: a level-select byte, four sub-phase frame counts, then repeat bytes. +// DEFAULT is the OEM full-refresh set: three 7-byte phase groups per register — +// kick to the opposite polarity (the visible ghost-clearing inversion flash), +// a brief settle, then the destination drive. DC-balanced; this is what clears +// ghosting. // -// DEFAULT is the manufacturer "HALF_REFRESH" (GC) set: a multi-level, ghost- -// clearing drive. FAST is the "FAST_REFRESH" (DU) set: a quick single-direction -// drive. Both sets carry real BW/WB transition waveforms, so the driver runs a -// differential refresh (old->DTM1, new->DTM2): unchanged pixels take WW/BB, -// changed pixels take BW/WB. The driver still promotes a fast refresh to a full -// one every MURPHY_GHOST_CLEAR_INTERVAL refreshes to flush residual ghosting. +// FAST is the community-sdk non-flashing derivation (commit 684effb): the +// destination-drive phase alone — here run TWICE (two identical drive groups) +// for a stronger latch, which measurably reduces per-refresh ghosting on +// hardware at the cost of a slightly longer (still non-flashing) update. No inversion sweep, so residual charge +// accumulates — the driver promotes every MURPHY_GHOST_CLEAR_INTERVAL'th fast +// refresh to a DEFAULT one to flush it. +// +// ALT is the OEM's ALTERNATE bank (0f8f4f / 4f8f0f / ...), a package deal with +// the data-driven init (0x82=0x07, 0x50=0xD7) and the 0x17/0xA5 refresh +// trigger (Ghidra: FUN_42038b60 else-branch loads them to 0x20..0x24). Run +// under the mode-0 init they flash without properly latching and ghost badly +// (community-sdk 59bc0e5 hit the same failure) — the driver only ever loads +// them as part of the full alternate package (FREEINK_MURPHY_OEM_PARTIAL Fast +// path in Uc8253MurphyDriver.cpp), never under the mode-0 init. namespace freeink { -// Per-register LUT lengths (manufacturer header: 56/42/56/42/42 bytes). -constexpr uint8_t MURPHY_LUT_LEN_VCOM = 56; +// All five registers take 42-byte payloads on this panel. +constexpr uint8_t MURPHY_LUT_LEN_VCOM = 42; constexpr uint8_t MURPHY_LUT_LEN_WW = 42; -constexpr uint8_t MURPHY_LUT_LEN_BW = 56; +constexpr uint8_t MURPHY_LUT_LEN_BW = 42; constexpr uint8_t MURPHY_LUT_LEN_WB = 42; constexpr uint8_t MURPHY_LUT_LEN_BB = 42; -// --- DEFAULT / "HALF_REFRESH" (GC) ------------------------------------------- +// --- DEFAULT / full refresh (OEM set: kick -> settle -> drive) --------------- constexpr uint8_t MURPHY_LUT_20_DEFAULT[MURPHY_LUT_LEN_VCOM] = { // R20 VCOM - 0x01, 0x0f, 0x0f, 0x0f, 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, + 0x01, 0x08, 0x08, 0x08, 0x08, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, + 0x01, 0x08, 0x08, 0x08, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +constexpr uint8_t MURPHY_LUT_21_DEFAULT[MURPHY_LUT_LEN_WW] = { // R21 WW (-> white) + 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, + 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -constexpr uint8_t MURPHY_LUT_21_DEFAULT[MURPHY_LUT_LEN_WW] = { // R21 WW (white->white) - 0x01, 0x4f, 0x8f, 0x0f, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_22_DEFAULT[MURPHY_LUT_LEN_BW] = { // R22 BW (-> white) + 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, + 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +constexpr uint8_t MURPHY_LUT_23_DEFAULT[MURPHY_LUT_LEN_WB] = { // R23 WB (-> black) + 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +constexpr uint8_t MURPHY_LUT_24_DEFAULT[MURPHY_LUT_LEN_BB] = { // R24 BB (-> black) + 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// --- FAST / non-flashing (destination-drive phase only) ---------------------- +constexpr uint8_t MURPHY_LUT_20_FAST[MURPHY_LUT_LEN_VCOM] = { // R20 VCOM + 0x01, 0x08, 0x08, 0x08, 0x08, 0x01, 0x01, 0x01, 0x08, 0x08, 0x08, 0x08, 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}; -constexpr uint8_t MURPHY_LUT_22_DEFAULT[MURPHY_LUT_LEN_BW] = { // R22 BW/LUTR (black->white) - 0x01, 0x0f, 0x8f, 0x0f, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_21_FAST[MURPHY_LUT_LEN_WW] = { // R21 WW -> W + 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x01, 0x88, 0x88, 0x88, 0x88, 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}; +constexpr uint8_t MURPHY_LUT_22_FAST[MURPHY_LUT_LEN_BW] = { // R22 BW -> W + 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x01, 0x88, 0x88, 0x88, 0x88, 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}; -constexpr uint8_t MURPHY_LUT_23_DEFAULT[MURPHY_LUT_LEN_WB] = { // R23 WB/LUTW (white->black) - 0x01, 0x4f, 0x8f, 0x4f, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_23_FAST[MURPHY_LUT_LEN_WB] = { // R23 WB -> B + 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x01, 0x48, 0x48, 0x48, 0x48, 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}; -constexpr uint8_t MURPHY_LUT_24_DEFAULT[MURPHY_LUT_LEN_BB] = { // R24 BB/LUTB (black->black) - 0x01, 0x0f, 0x8f, 0x4f, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_24_FAST[MURPHY_LUT_LEN_BB] = { // R24 BB -> B + 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x01, 0x48, 0x48, 0x48, 0x48, 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}; -// --- FAST / "FAST_REFRESH" (DU) ---------------------------------------------- -constexpr uint8_t MURPHY_LUT_20_FAST[MURPHY_LUT_LEN_VCOM] = { // R20 VCOM - 0x01, 0x0f, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// --- ALT / OEM partial-refresh bank (0x17/0xA5 trigger) ---------------------- +// Byte tables verbatim from murphy/m3/findings/display_driver.md "Alternate Set +// (mixed 56/42 bytes, used with 0x17/0xA5 refresh)" lines 188-246, extracted +// from the device dump (app0_seg0_3c190020.bin) at the addresses noted per +// array; re-verified byte-for-byte against that dump and confirmed present in +// the upstream OEM touch v525 firmware (oem_touch_v525_grayscale_luts.md). +// Single short kick per register (no ghost-clearing inversion sweep): 0x4F has +// the VSH (to-black) bit, 0x8F the VSL (to-white) bit, 0x0F neither. +// +// Register assignment is NOT one-to-one with the array names: see the ALT bank +// mapping note in Uc8253MurphyDriver.cpp (FUN_42038b60 swaps which table feeds +// 0x22 vs 0x23 on a one-shot RTC flag; names below follow the findings doc). + +constexpr uint8_t MURPHY_LUT_20_ALT[56] = { // R20 VCOM, 56 B @ 0x3c23706c + 0x01, 0x0F, 0x0F, 0x0F, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -constexpr uint8_t MURPHY_LUT_21_FAST[MURPHY_LUT_LEN_WW] = { // R21 W2W - 0x01, 0x0f, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_21_ALT[42] = { // 42 B @ 0x3c237042 (loaded to R21) + 0x01, 0x4F, 0x8F, 0x0F, 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}; -constexpr uint8_t MURPHY_LUT_22_FAST[MURPHY_LUT_LEN_BW] = { // R22 BW/LUTR - 0x01, 0x8f, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_22_ALT_A[42] = { // 42 B @ 0x3c236fe0 + 0x01, 0x4F, 0x8F, 0x4F, 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}; -constexpr uint8_t MURPHY_LUT_23_FAST[MURPHY_LUT_LEN_WB] = { // R23 WB/LUTW - 0x01, 0x4f, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_23_ALT_B[56] = { // 56 B @ 0x3c23700a + 0x01, 0x0F, 0x8F, 0x0F, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -constexpr uint8_t MURPHY_LUT_24_FAST[MURPHY_LUT_LEN_BB] = { // R24 BB/LUTB - 0x01, 0x0f, 0x41, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +constexpr uint8_t MURPHY_LUT_24_ALT[42] = { // 42 B @ 0x3c236fb6 (loaded to R24) + 0x01, 0x0F, 0x8F, 0x4F, 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}; diff --git a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp index 7262e1a1..a0c18e63 100644 --- a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp +++ b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp @@ -342,6 +342,12 @@ BatteryMonitor::Status BatteryMonitor::readStatus() const { status.chargingKnown = true; status.charging = digitalRead(_chargeStatusPin) == LOW; } +#if FREEINK_USB_PRESENCE_AS_CHARGING + else { + status.chargingKnown = true; + status.charging = freeink::usbHostPresent(); + } +#endif } return status; } @@ -394,7 +400,12 @@ bool BatteryMonitor::isCharging() const { return readM5Pm1Status(status) && status.chargingKnown && status.charging; } if (_chargeStatusPin < 0) { +#if FREEINK_USB_PRESENCE_AS_CHARGING + // No status GPIO exists on this board — report USB presence instead. + return freeink::usbHostPresent(); +#else return false; +#endif } // MCP73832-style /STAT: LOW while charging. return digitalRead(_chargeStatusPin) == LOW; diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index 8ea171d9..596344a5 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -269,7 +269,46 @@ // must define USE_BLOCK_DEVICE_INTERFACE=1 for the SdFat FsVolume these mount on. // Override with -DFREEINK_SD_SDMMC=0/1. #ifndef FREEINK_SD_SDMMC -#define FREEINK_SD_SDMMC (FREEINK_DEVICE_DELINK || FREEINK_DEVICE_X4PRO) +#define FREEINK_SD_SDMMC (FREEINK_DEVICE_DELINK || FREEINK_DEVICE_X4PRO || FREEINK_DEVICE_MURPHY) +#endif + +// Boards with no charger-status GPIO at all (Murphy M3: the TP4054's CHRG pin +// drives only its LED) can report "USB host present" as the charging state — +// the native USB-Serial/JTAG peripheral's SOF frame counter advances only +// while a host is attached, so it needs no wiring. Only used when +// batteryChargeStatus is PIN_UNASSIGNED. Override with 0/1 per build. +#ifndef FREEINK_USB_PRESENCE_AS_CHARGING +#define FREEINK_USB_PRESENCE_AS_CHARGING (FREEINK_DEVICE_MURPHY) +#endif + +#if FREEINK_USB_PRESENCE_AS_CHARGING +#include "soc/usb_serial_jtag_reg.h" +namespace freeink { +// USB host presence via SOF activity: the USB-Serial/JTAG frame counter +// advances at 1 kHz only while a host is attached. First call records a +// baseline and reports absent; any later call that sees the counter move marks +// presence, decaying after 1s without movement (covers detach). Shared by +// BatteryMonitor::isCharging() and consumer firmware's USB-connected checks. +inline bool usbHostPresent() { + static uint32_t lastFram = 0; + static unsigned long lastChangeMs = 0; + static bool baselineSet = false; + static bool changeSeen = false; + const uint32_t fram = REG_READ(USB_SERIAL_JTAG_FRAM_NUM_REG); + const unsigned long now = millis(); + if (!baselineSet) { + baselineSet = true; + lastFram = fram; + return false; + } + if (fram != lastFram) { + lastFram = fram; + lastChangeMs = now; + changeSeen = true; + } + return changeSeen && (now - lastChangeMs) < 1000; +} +} // namespace freeink #endif // Serial log transport hint for consumer firmware. Boards can share the same MCU @@ -596,6 +635,12 @@ struct BoardProfile { // Power-rail latch pins (see PowerConfig). Defaulted so existing profiles // need no change; a board with a latch sets it. PowerConfig power = {}; + // Hold time (ms) on the shared confirm/power button before a press means + // POWER (sleep). Defaulted so existing profiles need no change; boards where + // that pin is also the primary select button (e.g. Murphy M3's middle key) + // set a longer hold so deliberate selects don't trip sleep. Kept ahead of + // displayControllerVariant so Murphy's positional trailing 1500 maps here. + unsigned long confirmPowerHoldMs = 400; // Panel-controller variant byte, filled in at boot by the display probe when it // matters (UC8279 800x480: VER byte2 LUT_VER, 0x02 vs 0x68 — selects which AA // waveform table the driver uploads). 0 = not probed / not applicable. @@ -834,22 +879,51 @@ constexpr BoardProfile MURPHY_M3 = { 240, {4, 3, 5, 6, 7, 8, PIN_UNASSIGNED}, 0, // displaySpiHz: 0 -> Murphy UC8253 driver default (4 MHz) - {39, 13, 40, 10, PIN_UNASSIGNED, true, 0}, + // SD is native 4-bit SDMMC (stock firmware uses SD_MMC.setPins; wiring + // confirmed by the reverse schematic: D1=14 D0=15 CLK=16 CMD=17 D3=18 + // D2=21). This SdPins entry carries only powerEnable=GPIO10, which gates + // the card rail through an AO3401 P-FET and is ACTIVE-LOW (drive LOW to + // power the card) — the sdmmc field below owns the bus. + {PIN_UNASSIGNED, PIN_UNASSIGNED, PIN_UNASSIGNED, PIN_UNASSIGNED, 10, false, 0, false}, {PIN_UNASSIGNED, 0, PIN_UNASSIGNED, PIN_UNASSIGNED, 1, 2, 0, false}, 9, // batteryAdc: stock firmware samples analogRead(9) for battery voltage - PIN_UNASSIGNED, // batteryChargeStatus: not identified - 3.030303f, // stock firmware scales ADC by 0.0016 / 0.33, implying a 1:0.33 divider + PIN_UNASSIGNED, // batteryChargeStatus: none — TP4054 CHRG drives only its LED (GPIO47 + // probe-verified static across plug/unplug); see FREEINK_USB_PRESENCE_AS_CHARGING + // VBAT divider is 680k/680k (reverse schematic) = x2.0. Hardware-verified: + // the pin reads ~2.07V with a charging pack (x2 = 4.15V, plausible; the old + // 3.030303 stock-firmware-derived guess yielded an impossible 6.3V). + 2.0f, PIN_UNASSIGNED, - {TouchController::Chsc6x, 13, 12, 44, 45, 0x2e, 24, 224, 24, 398, false, 0, true, false}, + // Touch: CHSC6x-class at 0x2e. GPIO45 is NOT a controller reset — it gates the + // touch rail through a CH442E analog switch + AO3401 P-FET, ACTIVE-LOW (probing + // confirmed: with GPIO45 HIGH both touch AND the ES8388 vanish from the I2C + // bus). Carried as powerEnable with powerEnableActiveHigh=false. + // Mount: the digitizer reports raw x over the SHORT axis (24..224) and raw y + // over the LONG axis (24..398), but the framebuffer is landscape 416x240 — + // swapXY=true so mapped x tracks the long axis; ranges are POST-swap + // (x: 24/398 long, y: 24/224 short). flipY from the corner-tap test. + {TouchController::Chsc6x, 13, 12, 44, PIN_UNASSIGNED, 0x2e, 24, 398, 24, 224, false, 0, true, + false, /*powerEnable=*/45, /*swapXY=*/true, false, /*flipY=*/true, false, + /*powerEnableActiveHigh=*/false}, {48, 25000, 10, true}, - // NOTE: the SPI SD pin guess above (39/13/40) predates the OEM firmware - // audio recovery and conflicts with the proven I2S pins (39/40/41/42) and - // shared I2C (13). Audio is the verified owner of those pins. MURPHY_AUDIO, NO_LEDS, NO_FLIP, - NO_SDMMC, - NO_GAUGE}; + {16, 17, 15, 14, 21, 18, 4}, // 4-bit SDMMC: CLK=16 CMD=17 D0=15 D1=14 D2=21 D3=18 + NO_GAUGE, + NO_MIC, + // RX8010SJ RTC (0x32) + AHT30 (0x38) share the I2C bus but have no SDK driver + // types yet (RtcType covers PCF8563/DS3231; temp path expects SHT40), so the + // sensors block stays empty until those drivers exist. + NO_SENSORS, + 0.6f, // uiScale: 240x416 logical canvas @ ~130 PPI — half the X4 canvas, ~0.6x its PPI. + // (Advisory: CrossPoint implements density via its compile-time kUiDensityScale.) + // GPIO43 gates the ENTIRE I2C bus rail (touch, ES8388, RX8010, AHT30) through a + // dedicated LDO — probing confirmed the bus scans empty with it low. The OEM + // firmware drives it HIGH at boot. Carried as power.latch0 so holdPowerRails() + // asserts it before any I2C user comes up. + {43, PIN_UNASSIGNED}, + 1500}; // confirmPowerHoldMs: GPIO0 middle key doubles as power — long hold to avoid stray sleeps // --- de-link (X4-class GDEQ0426T82 panel on ESP32-S3) — SSD1677 + frontlight --- // Reuses the SSD1677 driver (same controller/panel as X4); differs at the board diff --git a/libs/hardware/InputManager/include/InputManager.h b/libs/hardware/InputManager/include/InputManager.h index 49beb357..2f7dd601 100644 --- a/libs/hardware/InputManager/include/InputManager.h +++ b/libs/hardware/InputManager/include/InputManager.h @@ -221,6 +221,9 @@ class InputManager { bool readChsc6xPoint(TouchPoint &point); bool decodeChsc6xFrame(const uint8_t *data, size_t len, TouchPoint &point) const; + int touchAxisSpanAvg() const; // mean of the active digitizer's raw X/Y spans + int touchTapSlopPx() const; + int touchSwipeMinPx() const; uint16_t mapTouchAxis(uint16_t raw, uint16_t rawMin, uint16_t rawMax, uint16_t outMax) const; void beginGt911(); @@ -280,15 +283,23 @@ class InputManager { static constexpr int ADC_NO_BUTTON = 3900; static constexpr unsigned long DEBOUNCE_DELAY = 5; static constexpr unsigned long CONFIRM_BACK_HOLD_MS = 650; - static constexpr unsigned long CONFIRM_POWER_HOLD_MS = 400; + // Hold time before the shared confirm/power pin means POWER (sleep) is a + // per-board value: BoardProfile::confirmPowerHoldMs. Boards where that pin is + // also the primary select button want a longer hold so deliberate selects + // don't trip sleep. // Touch timing / protocol constants (ported from the Murphy M3 CHSC6x // driver). static constexpr unsigned long TOUCH_IRQ_PULSE_MS = 120; // release hold-over after last valid read static constexpr unsigned long TOUCH_SAMPLE_DELAY_MS = 8; // I2C poll cadence + // Baseline values in GT911 (800x480-class) mapped units; consumed via + // touchTapSlopPx()/touchSwipeMinPx(), which scale them DOWN for digitizers + // smaller than the reference span (never up — larger existing panels keep + // these tuned values), so small panels need the same physical finger travel. static constexpr int TOUCH_TAP_SLOP_PX = 28; static constexpr int TOUCH_SWIPE_MIN_PX = 60; + static constexpr int GT911_REFERENCE_SPAN = 640; // avg raw-axis span these were tuned at static constexpr unsigned long TOUCH_SWIPE_MAX_MS = 700; static constexpr uint8_t TOUCH_READ_COMMAND = 0x00; static constexpr uint8_t TOUCH_FRAME_SIZE = 16; diff --git a/libs/hardware/InputManager/src/InputManager.cpp b/libs/hardware/InputManager/src/InputManager.cpp index 90a23124..a16fcc00 100644 --- a/libs/hardware/InputManager/src/InputManager.cpp +++ b/libs/hardware/InputManager/src/InputManager.cpp @@ -353,7 +353,7 @@ void InputManager::updateConfirmPowerHold(const unsigned long currentTime) { if (pressed && s_sharedConfirmPowerShortPressEmitsPower) { nextState |= (1 << BTN_POWER); } else if (pressed && - currentTime - confirmPowerPressStart >= CONFIRM_POWER_HOLD_MS) { + currentTime - confirmPowerPressStart >= BoardConfig::ACTIVE.confirmPowerHoldMs) { confirmPowerLongPressActive = true; nextState |= (1 << BTN_POWER); } @@ -629,7 +629,8 @@ bool InputManager::wasSwipe(float &nxStart, float &nyStart, float &nxEnd, static_cast(touchUpPoint.y) - static_cast(touchDownPoint.y); const int adx = absInt(dx); const int ady = absInt(dy); - if (adx < TOUCH_SWIPE_MIN_PX && ady < TOUCH_SWIPE_MIN_PX) + const int swipeMin = touchSwipeMinPx(); + if (adx < swipeMin && ady < swipeMin) return false; const auto &t = BoardConfig::ACTIVE.touch; const uint16_t w = (t.rawMaxX > t.rawMinX) @@ -669,6 +670,18 @@ void InputManager::beginTouch() { if (t.controller == BoardConfig::TouchController::None) { return; } + // Power the touch rail first, for every controller type (boards that gate it: + // Sticky's active-high TOUCH_EN, X4 Pro's active-low GPIO2, Murphy M3's + // active-low GPIO45 CH442E switch). Active level + settle before any reset + // dance or I2C probe. No-op when unassigned. gpio_hold_dis first: the sleep + // path holds this pin at its OFF level and the hold survives the deep-sleep + // wake reset; the ON write is a no-op until it is released. + if (t.powerEnable >= 0) { + gpio_hold_dis(static_cast(t.powerEnable)); + pinMode(t.powerEnable, OUTPUT); + digitalWrite(t.powerEnable, t.powerEnableActiveHigh ? HIGH : LOW); + delay(50); + } if (t.controller == BoardConfig::TouchController::Gt911) { beginGt911(); return; @@ -678,7 +691,9 @@ void InputManager::beginTouch() { // instead (see decodeChsc6xFrame / updateTouchFromIrq). if (t.sda >= 0 && t.scl >= 0 && t.i2cAddress != 0) { Wire.begin(t.sda, t.scl, 100000); - Wire.setTimeOut(4); + // 10 ms: a 16-byte frame at 100 kHz is ~1.7 ms on the wire; 4 ms left no + // headroom for clock stretching on the shared (rail-gated) Murphy bus. + Wire.setTimeOut(10); touchDataEnabled = true; } #endif @@ -737,7 +752,8 @@ void InputManager::updateTouchFromIrq(const unsigned long now, static_cast(touchDownPoint.x); const int dy = static_cast(touchUpPoint.y) - static_cast(touchDownPoint.y); - if (absInt(dx) > TOUCH_TAP_SLOP_PX || absInt(dy) > TOUCH_TAP_SLOP_PX) { + const int slop = touchTapSlopPx(); + if (absInt(dx) > slop || absInt(dy) > slop) { touchMovedBeyondTapSlop = true; } } @@ -748,7 +764,12 @@ void InputManager::updateTouchFromIrq(const unsigned long now, if (touchPressed && now >= touchReleaseAt) { touchPressed = false; touchReleasedEvent = true; - lastTouchHeldDurationMs = now - touchDownPoint.timestamp; + // Measure to the last REAL sample (touchUpPoint), not to this expiry — + // the TOUCH_IRQ_PULSE_MS hold-over otherwise inflates every contact by + // ~120ms, which eats into the swipe time window and long-press thresholds. + lastTouchHeldDurationMs = (touchUpPoint.timestamp >= touchDownPoint.timestamp) + ? touchUpPoint.timestamp - touchDownPoint.timestamp + : now - touchDownPoint.timestamp; } } @@ -756,7 +777,11 @@ bool InputManager::readChsc6xPoint(TouchPoint &point) { const uint8_t addr = BoardConfig::ACTIVE.touch.i2cAddress; Wire.beginTransmission(addr); Wire.write(TOUCH_READ_COMMAND); - if (Wire.endTransmission(false) != 0) { + // Full STOP between the command write and the read: the Murphy M3's CHSC6x + // (and its ES8388 bus-mate) NACK repeated-start reads — hardware-probed; a + // stop-separated read works. The controller keeps its register pointer across + // the stop, so this is safe for the frame read. + if (Wire.endTransmission(true) != 0) { return false; } @@ -796,15 +821,51 @@ bool InputManager::decodeChsc6xFrame(const uint8_t *data, const size_t len, } const auto &t = BoardConfig::ACTIVE.touch; point.valid = true; - // Panel-native coordinates (the calibrated raw range, in the touch panel's - // own orientation); the app maps to its display/logical frame. See the touch - // note in the README. - point.x = mapTouchAxis(rawX, t.rawMinX, t.rawMaxX, t.rawMaxX - t.rawMinX); - point.y = mapTouchAxis(rawY, t.rawMinY, t.rawMaxY, t.rawMaxY - t.rawMinY); + // Digitizer mount correction, same scheme as the GT911 path: swap axes first + // (rotated sensor), then map with the panel-axis ranges (rawMin/Max describe + // the POST-swap axes), then per-axis flip. Previously this decoder ignored + // swapXY/flipX/flipY entirely — Murphy is the only CHSC6x board, so the gap + // went unnoticed until its transposed taps surfaced on hardware. + const uint16_t sx = t.swapXY ? rawY : rawX; + const uint16_t sy = t.swapXY ? rawX : rawY; + point.x = mapTouchAxis(sx, t.rawMinX, t.rawMaxX, t.rawMaxX - t.rawMinX); + point.y = mapTouchAxis(sy, t.rawMinY, t.rawMaxY, t.rawMaxY - t.rawMinY); + if (t.flipX) + point.x = static_cast((t.rawMaxX - t.rawMinX) - point.x); + if (t.flipY) + point.y = static_cast((t.rawMaxY - t.rawMinY) - point.y); point.timestamp = millis(); return true; } +// Gesture thresholds (TOUCH_TAP_SLOP_PX / TOUCH_SWIPE_MIN_PX) are tuned in the +// mapped units of the GT911 boards. On a small digitizer like the Murphy's +// (200x374, average span 287) the same pixel numbers demand 3-4x the physical +// finger travel, which swallows swipes into taps. Scale by the active +// digitizer's average span so gestures need the same physical fraction of the +// panel — but CAP at the tuned value so this never RAISES the threshold on a +// larger existing panel (e.g. the 960x540 GT911, span 749, which would +// otherwise jump +17%). Existing boards keep their tuned feel; only smaller +// panels scale down. +int InputManager::touchAxisSpanAvg() const { + const auto &t = BoardConfig::ACTIVE.touch; + const int xs = (t.rawMaxX > t.rawMinX) ? t.rawMaxX - t.rawMinX : 1; + const int ys = (t.rawMaxY > t.rawMinY) ? t.rawMaxY - t.rawMinY : 1; + return (xs + ys) / 2; +} + +int InputManager::touchTapSlopPx() const { + const int v = TOUCH_TAP_SLOP_PX * touchAxisSpanAvg() / GT911_REFERENCE_SPAN; + const int capped = v < TOUCH_TAP_SLOP_PX ? v : TOUCH_TAP_SLOP_PX; + return capped < 8 ? 8 : capped; +} + +int InputManager::touchSwipeMinPx() const { + const int v = TOUCH_SWIPE_MIN_PX * touchAxisSpanAvg() / GT911_REFERENCE_SPAN; + const int capped = v < TOUCH_SWIPE_MIN_PX ? v : TOUCH_SWIPE_MIN_PX; + return capped < 16 ? 16 : capped; +} + uint16_t InputManager::mapTouchAxis(uint16_t raw, const uint16_t rawMin, const uint16_t rawMax, const uint16_t outMax) const { @@ -820,21 +881,7 @@ uint16_t InputManager::mapTouchAxis(uint16_t raw, const uint16_t rawMin, void InputManager::beginGt911() { const auto &t = BoardConfig::ACTIVE.touch; - // Power the touch rail first (boards that gate it, e.g. Sticky's TOUCH_EN on - // GPIO42). Active-high + settle, before the reset dance and I2C probe; - // without this the GT911 never ACKs and touch is reported absent. No-op when - // unassigned. gpio_hold_dis first: the sleep path holds this pin LOW and the - // hold survives the deep-sleep wake reset; the HIGH write is a no-op until it - // is released. - if (t.powerEnable >= 0) { - gpio_hold_dis(static_cast(t.powerEnable)); - pinMode(t.powerEnable, OUTPUT); - // ON level: HIGH for active-high enables (Sticky), LOW for active-low (X4 - // Pro GPIO2). - digitalWrite(t.powerEnable, t.powerEnableActiveHigh ? HIGH : LOW); - delay(50); - } - + // Touch rail power is asserted by beginTouch() before this runs. if (t.sda >= 0 && t.scl >= 0) { Wire.begin(t.sda, t.scl, 400000); Wire.setTimeOut(10); @@ -998,7 +1045,7 @@ void InputManager::pollGt911(const unsigned long now) { static_cast(touchUpPoint.x) - static_cast(touchDownPoint.x); const int dy = static_cast(touchUpPoint.y) - static_cast(touchDownPoint.y); - if (absInt(dx) > TOUCH_TAP_SLOP_PX || absInt(dy) > TOUCH_TAP_SLOP_PX) { + if (absInt(dx) > touchTapSlopPx() || absInt(dy) > touchTapSlopPx()) { touchMovedBeyondTapSlop = true; } #ifdef TOUCH_PROBE_DEBUG