From 6f6b11720374460deddc642901f15c51695819b2 Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Wed, 29 Jul 2026 20:38:50 +0800 Subject: [PATCH 1/9] Murphy M3: wire real 4-bit SDMMC SD bus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The profile carried a pre-audio-recovery SPI pin guess (39/13/40/10) that conflicts with the proven I2S (39/40/41/42) and I2C (13) owners, plus NO_SDMMC — so SD never worked on the M3. The stock firmware mounts the card via SD_MMC (4-bit), and the reverse schematic confirms the wiring: D1=14 D0=15 CLK=16 CMD=17 D3=18 D2=21, with the card rail gated by GPIO10 through an AO3401 P-FET (active-LOW enable). SdPins now carries only the active-low powerEnable=10 (the existing SdmmcBlockDevice pulse-HIGH-then-hold-LOW mount sequence matches this gate exactly), the sdmmc field owns the bus at width 4, and FREEINK_SD_SDMMC auto-enables for MURPHY. Co-Authored-By: Claude Fable 5 --- libs/hardware/BoardConfig/include/BoardConfig.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index ae06e2a8..308f75b0 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -262,7 +262,7 @@ // 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 // Serial log transport hint for consumer firmware. Boards can share the same MCU @@ -815,7 +815,12 @@ 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 @@ -823,13 +828,10 @@ constexpr BoardProfile MURPHY_M3 = { PIN_UNASSIGNED, {TouchController::Chsc6x, 13, 12, 44, 45, 0x2e, 24, 224, 24, 398, false, 0, true, 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, + {16, 17, 15, 14, 21, 18, 4}, // 4-bit SDMMC: CLK=16 CMD=17 D0=15 D1=14 D2=21 D3=18 NO_GAUGE}; // --- de-link (X4-class GDEQ0426T82 panel on ESP32-S3) — SSD1677 + frontlight --- From 348246395940daf78d8435a05db7934094fe3bc3 Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Wed, 29 Jul 2026 22:00:33 +0800 Subject: [PATCH 2/9] Murphy M3: real OEM waveform LUTs, touch power gate, I2C bus rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display (fixes heavy ghosting): - The DEFAULT LUT bank was the OEM's five per-mode voltage-config blocks (0f8f4f/...) misread as waveforms — no kick phase, no DC balance, so residual charge accumulated on every refresh. Replaced with the OEM three-phase set (kick -> settle -> drive) extracted from the v525 dump, and the FAST bank with the community-sdk destination-drive-only derivation. All five registers take 42-byte payloads (the 56-byte figure came from address-gap guessing). - The driver now always writes the same buffer to DTM1 and DTM2 (OEM scheme). The differential branch was dead on single-buffer builds and empirically leaves pixels half-flipped on this panel. Touch (fixes touch dead on M3): - GPIO45 is a rail gate (CH442E + AO3401, ACTIVE-LOW), not a controller reset: moved from TouchConfig.reset to powerEnable with powerEnableActiveHigh=false. Hardware-probed: touch and the ES8388 codec vanish from the bus with GPIO45 high. - Touch rail power-up hoisted from beginGt911() into beginTouch() so CHSC6x boards get it too. - CHSC6x frame read uses a stop-separated transaction: this panel's controller NACKs repeated-start reads (hardware-probed). - Wire timeout 4ms -> 10ms for headroom on the shared gated bus. Board: - GPIO43 gates the ENTIRE I2C rail (touch/ES8388/RX8010/AHT30); carried as power.latch0 so holdPowerRails() asserts it at boot. - uiScale set to 0.6 (240x416 logical canvas at ~130 PPI) as advisory. Co-Authored-By: Claude Fable 5 --- .../src/driver/Uc8253MurphyDriver.cpp | 22 ++--- .../FreeInkDisplay/src/lut/Uc8253MurphyLuts.h | 91 ++++++++++--------- .../BoardConfig/include/BoardConfig.h | 21 ++++- .../InputManager/src/InputManager.cpp | 38 ++++---- 4 files changed, 96 insertions(+), 76 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index a1211284..64bc7b20 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -40,7 +40,7 @@ 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 + {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) 8, // promote FAST -> full every 8 refreshes }; return cfg; @@ -167,18 +167,14 @@ void Uc8253MurphyDriver::display(EpdBus& bus, const uint8_t* fb, const uint8_t* 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); - writePlane(bus, CMD_DTM2, fb); - } + // OEM 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 these 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/lut/Uc8253MurphyLuts.h b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h index b3cd86c4..eabe7629 100644 --- a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h +++ b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h @@ -2,75 +2,78 @@ #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. 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. +// +// History: an earlier revision of this header transcribed the manufacturer +// "M3 LUT.txt" blocks (0f8f4f / 4f8f0f / ...) as waveforms. The v525 dump +// analysis re-classified those five blocks as per-mode VOLTAGE CONFIG data +// (POWER_SETTING / VCOM_DC arguments), not LUTs — driving the panel with them +// flashes without properly latching and ghosts badly (community-sdk 59bc0e5 +// hit the same failure). Do not resurrect them here. 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->white) - 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, +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_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, - 0x00, 0x00, 0x00, 0x00, 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 (-> 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/LUTW (white->black) - 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, +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/LUTB (black->black) - 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, +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 / "FAST_REFRESH" (DU) ---------------------------------------------- +// --- FAST / non-flashing (destination-drive phase only) ---------------------- 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, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 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, 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_FAST[MURPHY_LUT_LEN_WW] = { // R21 WW -> W + 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, 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_FAST[MURPHY_LUT_LEN_BW] = { // R22 BW -> W + 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, 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_FAST[MURPHY_LUT_LEN_WB] = { // R23 WB -> B + 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, 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_FAST[MURPHY_LUT_LEN_BB] = { // R24 BB -> B + 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index 308f75b0..56f09395 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -826,13 +826,30 @@ constexpr BoardProfile MURPHY_M3 = { PIN_UNASSIGNED, // batteryChargeStatus: not identified 3.030303f, // stock firmware scales ADC by 0.0016 / 0.33, implying a 1:0.33 divider 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. + {TouchController::Chsc6x, 13, 12, 44, PIN_UNASSIGNED, 0x2e, 24, 224, 24, 398, false, 0, true, + false, /*powerEnable=*/45, false, false, false, false, /*powerEnableActiveHigh=*/false}, {48, 25000, 10, true}, MURPHY_AUDIO, NO_LEDS, NO_FLIP, {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_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}}; // --- 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/src/InputManager.cpp b/libs/hardware/InputManager/src/InputManager.cpp index 90a23124..00af181e 100644 --- a/libs/hardware/InputManager/src/InputManager.cpp +++ b/libs/hardware/InputManager/src/InputManager.cpp @@ -669,6 +669,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 +690,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 @@ -756,7 +770,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; } @@ -820,21 +838,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); From f36354014addeb7d58dcf7e6998b664c80a54865 Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Wed, 29 Jul 2026 22:34:54 +0800 Subject: [PATCH 3/9] Murphy M3: touch mount transform, scaled gestures, OEM-only refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Touch coordinate fix (taps were transposed): - decodeChsc6xFrame now applies the swapXY/flipX/flipY mount transform, same scheme as the GT911 path — it previously ignored those flags entirely (Murphy is the only CHSC6x board, so it went unnoticed). - MurphyM3 TouchConfig: swapXY=true with post-swap ranges (x 24/398 long axis, y 24/224 short axis), flipY=true. The digitizer reports raw x over the short axis but the framebuffer is landscape 416x240, so without the swap a normalized short-axis fraction was multiplied by the long screen dimension — every tap landed transposed. Gesture usability on small digitizers: - TOUCH_TAP_SLOP_PX / TOUCH_SWIPE_MIN_PX are tuned in GT911 (800x480) mapped units; on the Murphy's 200x374 space they demanded 3-4x the physical finger travel, absorbing swipes into taps. New touchTapSlopPx()/touchSwipeMinPx() scale by the digitizer's average span (GT911 boards unchanged). Matters doubly on the M3: the left-edge back-swipe is the device's only Back, and the top-edge swipe its only reader menu. - Touch held-time now measures to the last real sample instead of the TOUCH_IRQ_PULSE_MS hold-over expiry, which inflated every contact by ~120ms and ate into the swipe window and long-press thresholds. Display: ghostClearInterval 1 — every refresh runs the OEM three-phase GC bank. The OEM has no fast waveform on the mode-0 path; our single-phase FAST bank is DC-unbalanced by construction and leaves drifting afterimages (hardware-confirmed). Also corrected the LUT header history note: the five manufacturer blocks are the OEM's ALTERNATE LUT bank (0x17/0xA5 partial path package), not voltage config. Co-Authored-By: Claude Fable 5 --- .../src/driver/Uc8253MurphyDriver.cpp | 7 ++- .../FreeInkDisplay/src/lut/Uc8253MurphyLuts.h | 14 +++-- .../BoardConfig/include/BoardConfig.h | 9 ++- .../InputManager/include/InputManager.h | 5 ++ .../InputManager/src/InputManager.cpp | 55 ++++++++++++++++--- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index 64bc7b20..ed227fbe 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -41,7 +41,12 @@ const Uc8253MurphyConfig& uc8253MurphyDefaultConfig() { {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}, // 42 each (OEM writes ten 42-byte payloads) - 8, // promote FAST -> full every 8 refreshes + // 1 = every refresh runs the OEM three-phase GC bank, matching stock + // behavior exactly (the OEM has NO fast waveform on the mode-0 path; our + // single-phase FAST bank is DC-unbalanced by construction and leaves + // afterimages that drift over seconds — hardware-confirmed). Raise this + // only after a genuinely DC-balanced fast waveform exists. + 1, }; return cfg; } diff --git a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h index eabe7629..cb4e501c 100644 --- a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h +++ b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h @@ -17,12 +17,14 @@ // accumulates — the driver promotes every MURPHY_GHOST_CLEAR_INTERVAL'th fast // refresh to a DEFAULT one to flush it. // -// History: an earlier revision of this header transcribed the manufacturer -// "M3 LUT.txt" blocks (0f8f4f / 4f8f0f / ...) as waveforms. The v525 dump -// analysis re-classified those five blocks as per-mode VOLTAGE CONFIG data -// (POWER_SETTING / VCOM_DC arguments), not LUTs — driving the panel with them -// flashes without properly latching and ghosts badly (community-sdk 59bc0e5 -// hit the same failure). Do not resurrect them here. +// History: an earlier revision of this header carried the manufacturer +// "M3 LUT.txt" blocks (0f8f4f / 4f8f0f / ...) as the default bank. Those five +// blocks ARE LUTs — but they are the OEM's ALTERNATE bank, a package deal with +// the data-driven init (0x82=0x07, 0x50=0xD7) and the 0x17/0xA5 partial-window +// 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). Adopting them means porting +// the whole alternate init+trigger path, not swapping tables. namespace freeink { diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index 56f09395..fe0993c7 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -830,8 +830,13 @@ constexpr BoardProfile MURPHY_M3 = { // 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. - {TouchController::Chsc6x, 13, 12, 44, PIN_UNASSIGNED, 0x2e, 24, 224, 24, 398, false, 0, true, - false, /*powerEnable=*/45, false, false, false, false, /*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}, MURPHY_AUDIO, NO_LEDS, diff --git a/libs/hardware/InputManager/include/InputManager.h b/libs/hardware/InputManager/include/InputManager.h index 49beb357..8314e427 100644 --- a/libs/hardware/InputManager/include/InputManager.h +++ b/libs/hardware/InputManager/include/InputManager.h @@ -221,6 +221,8 @@ class InputManager { bool readChsc6xPoint(TouchPoint &point); bool decodeChsc6xFrame(const uint8_t *data, size_t len, TouchPoint &point) const; + int touchTapSlopPx() const; + int touchSwipeMinPx() const; uint16_t mapTouchAxis(uint16_t raw, uint16_t rawMin, uint16_t rawMax, uint16_t outMax) const; void beginGt911(); @@ -287,6 +289,9 @@ class InputManager { 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 to the active + // digitizer's span 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 unsigned long TOUCH_SWIPE_MAX_MS = 700; diff --git a/libs/hardware/InputManager/src/InputManager.cpp b/libs/hardware/InputManager/src/InputManager.cpp index 00af181e..cf38c96c 100644 --- a/libs/hardware/InputManager/src/InputManager.cpp +++ b/libs/hardware/InputManager/src/InputManager.cpp @@ -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) @@ -751,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; } } @@ -762,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; } } @@ -814,15 +821,45 @@ 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 (800x480-class, average axis span 640). On a +// small digitizer like the Murphy's (200x374, average 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 everywhere; GT911 boards are unchanged. +int InputManager::touchTapSlopPx() 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; + const int v = TOUCH_TAP_SLOP_PX * ((xs + ys) / 2) / 640; + return v < 8 ? 8 : v; +} + +int InputManager::touchSwipeMinPx() 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; + const int v = TOUCH_SWIPE_MIN_PX * ((xs + ys) / 2) / 640; + return v < 16 ? 16 : v; +} + uint16_t InputManager::mapTouchAxis(uint16_t raw, const uint16_t rawMin, const uint16_t rawMax, const uint16_t outMax) const { @@ -1002,7 +1039,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 From a7f017caec53a96fb1d425e9502c6524313f31dc Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Wed, 29 Jul 2026 22:55:01 +0800 Subject: [PATCH 4/9] Murphy M3: correct VBAT divider, USB-presence charging indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Battery percentage was nonsense because the profile carried a stock- firmware-derived x3.0303 divider guess: the ADC pin reads ~2.07V with a charging pack, which x3.03 puts at an impossible 6.3V. The reverse schematic's 680k/680k divider (x2.0) yields 4.15V — a textbook charging LiPo. Hardware-verified via the probe build's per-second VBAT log. Charging status: GPIO47 (the only unaccounted pin) probe-read static across plug/unplug, so the TP4054's CHRG pin drives only its LED and no status GPIO exists. New FREEINK_USB_PRESENCE_AS_CHARGING (default: only Murphy) reports USB-host presence instead, detected with zero wiring via the USB-Serial/JTAG SOF frame counter, which advances at 1kHz only while a host is attached. Used only when batteryChargeStatus is unassigned. Touch mount transform is now hardware-confirmed: all four corner taps land exactly (probe log, 2026-07-29). Co-Authored-By: Claude Fable 5 --- .../BatteryMonitor/src/BatteryMonitor.cpp | 41 +++++++++++++++++++ .../BoardConfig/include/BoardConfig.h | 17 +++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp index 7262e1a1..765f42df 100644 --- a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp +++ b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp @@ -11,9 +11,39 @@ #include #include +#if FREEINK_USB_PRESENCE_AS_CHARGING +#include "soc/usb_serial_jtag_reg.h" +namespace { +// 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). +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 +#endif + #if FREEINK_BATTERY_I2C_GAUGE #include + // Minimal, dependency-free I2C fuel-gauge read for boards that carry one (e.g. // LilyGo T5 S3: BQ27220 gauge + BQ25896 charger). Standard TI command registers; // the gauge reports true battery state, so no ADC pin or divider is involved. @@ -342,6 +372,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 = usbHostPresent(); + } +#endif } return status; } @@ -394,7 +430,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 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 fe0993c7..0468d47e 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -265,6 +265,15 @@ #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 + // Serial log transport hint for consumer firmware. Boards can share the same MCU // but expose logs differently: LilyGo T5 S3 is monitored over native USB CDC, // while Sticky bring-up is more reliable through the IDF/ROM console path. @@ -823,8 +832,12 @@ constexpr BoardProfile MURPHY_M3 = { {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, // 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 From e0ce40da5d08fbb3f7b3288440f0bcc3508d3684 Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Wed, 29 Jul 2026 23:07:43 +0800 Subject: [PATCH 5/9] Move USB-presence detection to BoardConfig as freeink::usbHostPresent() Consumer firmware needs the same signal BatteryMonitor's fallback uses: CrossPoint's charging bolt reads its HalGPIO::isUsbConnected(), which returns constant false on boards with no VBUS-detect GPIO. A shared inline helper lets both layers report USB-host presence from the USB-Serial/JTAG SOF counter. Co-Authored-By: Claude Fable 5 --- .../BatteryMonitor/src/BatteryMonitor.cpp | 32 ++----------------- .../BoardConfig/include/BoardConfig.h | 30 +++++++++++++++++ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp index 765f42df..b0e01055 100644 --- a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp +++ b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp @@ -11,34 +11,6 @@ #include #include -#if FREEINK_USB_PRESENCE_AS_CHARGING -#include "soc/usb_serial_jtag_reg.h" -namespace { -// 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). -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 -#endif #if FREEINK_BATTERY_I2C_GAUGE #include @@ -375,7 +347,7 @@ BatteryMonitor::Status BatteryMonitor::readStatus() const { #if FREEINK_USB_PRESENCE_AS_CHARGING else { status.chargingKnown = true; - status.charging = usbHostPresent(); + status.charging = freeink::usbHostPresent(); } #endif } @@ -432,7 +404,7 @@ bool BatteryMonitor::isCharging() const { if (_chargeStatusPin < 0) { #if FREEINK_USB_PRESENCE_AS_CHARGING // No status GPIO exists on this board — report USB presence instead. - return usbHostPresent(); + return freeink::usbHostPresent(); #else return false; #endif diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index 0468d47e..b3c2b59a 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -274,6 +274,36 @@ #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 // but expose logs differently: LilyGo T5 S3 is monitored over native USB CDC, // while Sticky bring-up is more reliable through the IDF/ROM console path. From 5d7150d11384b356e161bddc009b10af96165bf6 Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Thu, 30 Jul 2026 01:12:43 +0800 Subject: [PATCH 6/9] Murphy M3: no-op displayGray, supportsGrayscale capability, fast cadence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PanelDriver's displayGray DEFAULT refreshes the caller's fb with the FAST bank — but at that point fb holds a GRAY PLANE, not a frame. Every grayscale-capable driver overrides it; the Murphy driver (B/W only, no grayscale hooks) inherited the default, so every EPUB/TXT/XTC page and grayscale sleep cover ended with an extra refresh of the MSB plane: a near-solid-black page (hardware-reported). displayGray is now a no-op on Murphy. New PanelDriver::supportsGrayscale() (default true; Murphy false) is exposed through FreeInkDisplay so consumers can skip building gray planes entirely on B/W-only panels — 4-level grayscale AA was tried on this panel and abandoned (asymmetric VSH/VSL rails, per the Murphy repo findings). ghostClearInterval 1 -> 6: with the correct OEM banks in place, the non-flashing FAST waveform returns for page turns/UI, with every 7th refresh promoted to the three-phase GC flash to clear residue. Co-Authored-By: Claude Fable 5 --- .../FreeInkDisplay/include/FreeInkDisplay.h | 2 ++ libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp | 4 ++++ .../FreeInkDisplay/src/driver/PanelDriver.h | 5 +++++ .../src/driver/Uc8253MurphyDriver.cpp | 14 ++++++++------ .../FreeInkDisplay/src/driver/Uc8253MurphyDriver.h | 8 ++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/libs/display/FreeInkDisplay/include/FreeInkDisplay.h b/libs/display/FreeInkDisplay/include/FreeInkDisplay.h index 3cda5a40..ab0480c2 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 deb0ef64..09a316dc 100644 --- a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp +++ b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp @@ -770,6 +770,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 ed227fbe..0577851a 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -41,12 +41,14 @@ const Uc8253MurphyConfig& uc8253MurphyDefaultConfig() { {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}, // 42 each (OEM writes ten 42-byte payloads) - // 1 = every refresh runs the OEM three-phase GC bank, matching stock - // behavior exactly (the OEM has NO fast waveform on the mode-0 path; our - // single-phase FAST bank is DC-unbalanced by construction and leaves - // afterimages that drift over seconds — hardware-confirmed). Raise this - // only after a genuinely DC-balanced fast waveform exists. - 1, + // 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). The FAST bank is single-phase + // and DC-unbalanced by construction — the OEM has no fast waveform at all + // — so the cadence is the whole anti-ghosting story: lower = cleaner, + // higher = fewer flashes. 6 ≈ the usual e-reader page-turn compromise. + // (Interval semantics: N fast refreshes pass, the N+1th promotes.) + 6, }; return cfg; } diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h index 03877640..b15a2096 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h @@ -65,6 +65,14 @@ 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); From c347e80ecd9b8f75572cba280c407e81d81191ac Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Thu, 30 Jul 2026 01:56:19 +0800 Subject: [PATCH 7/9] Murphy M3: stronger fast waveform, tighter ghost-clear cadence Reading at ghostClearInterval=6 left a distracting residue between GC flashes (hardware-reported). Two levers: the FAST bank's destination drive now runs two identical groups instead of one (stronger latch, less residue per non-flashing refresh, update slightly longer), and the GC promotion tightens to every 5th refresh. Co-Authored-By: Claude Fable 5 --- .../src/driver/Uc8253MurphyDriver.cpp | 5 +++-- .../FreeInkDisplay/src/lut/Uc8253MurphyLuts.h | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index 0577851a..3a68968a 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -46,9 +46,10 @@ const Uc8253MurphyConfig& uc8253MurphyDefaultConfig() { // flash that clears accumulated residue). The FAST bank is single-phase // and DC-unbalanced by construction — the OEM has no fast waveform at all // — so the cadence is the whole anti-ghosting story: lower = cleaner, - // higher = fewer flashes. 6 ≈ the usual e-reader page-turn compromise. + // higher = fewer flashes. 4 after hardware feedback (6 left a + // distracting residue while reading). // (Interval semantics: N fast refreshes pass, the N+1th promotes.) - 6, + 4, }; return cfg; } diff --git a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h index cb4e501c..f1658db7 100644 --- a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h +++ b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h @@ -13,7 +13,9 @@ // ghosting. // // FAST is the community-sdk non-flashing derivation (commit 684effb): the -// destination-drive phase alone. No inversion sweep, so residual charge +// 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. // @@ -59,23 +61,23 @@ constexpr uint8_t MURPHY_LUT_24_DEFAULT[MURPHY_LUT_LEN_BB] = { // R24 BB (-> bl // --- 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 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_21_FAST[MURPHY_LUT_LEN_WW] = { // R21 WW -> W - 0x01, 0x88, 0x88, 0x88, 0x88, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 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_FAST[MURPHY_LUT_LEN_WB] = { // R23 WB -> B - 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 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_FAST[MURPHY_LUT_LEN_BB] = { // R24 BB -> B - 0x01, 0x48, 0x48, 0x48, 0x48, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 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}; From 13e7c5bd2b0868378c1f765dc6096339fa96e8d9 Mon Sep 17 00:00:00 2001 From: mr-tbot Date: Thu, 30 Jul 2026 02:44:21 +0800 Subject: [PATCH 8/9] Murphy M3: OEM alternate/partial refresh as the fast path; 1.5s sleep hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FAST now runs the OEM's real quick-refresh package (Ghidra FUN_420389ec else-branch / FUN_42038b60 / FUN_42038f74): the data-driven init variant (0x82=0x07, 0x50=0xD7, pointer-fed 0x00/0x01/0x06/0x61 payloads — recovered from the app0 segment dump the findings doc cites, byte-exact in the v525 binary too), the five-payload ALT LUT bank (56/42/56/42/42), and the 0x17/0xA5 auto trigger (internal PON->DRF->POF; no window bytes exist in the OEM path). Differential planes per the doc: prev->DTM1, new->DTM2, falling back to both-planes-new on single-buffer builds. GC promotion every Nth fast refresh retained. Escape hatch: -DFREEINK_MURPHY_OEM_PARTIAL=0 restores the synthetic fast bank. Known open point (FIXME'd): the OEM ALT loader's steady-state 0x22/0x23 mapping is crossed vs the findings doc's array names (one-shot RTC flag branch); shipped the steady-state arrangement — swap bw/wb in ALT_BANK if changed pixels invert on hardware. Input: CONFIRM_POWER_HOLD_MS 400 -> 1500 on Murphy — the shared confirm/power pin is the primary select there, and 400ms fired sleep on merely deliberate presses (hardware-reported). Co-Authored-By: Claude Fable 5 --- .../src/driver/Uc8253MurphyDriver.cpp | 163 +++++++++++++++--- .../src/driver/Uc8253MurphyDriver.h | 31 ++-- .../FreeInkDisplay/src/lut/Uc8253MurphyLuts.h | 47 ++++- .../InputManager/include/InputManager.h | 6 +- 4 files changed, 210 insertions(+), 37 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index 3a68968a..ee237963 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,6 +42,50 @@ 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() { @@ -43,11 +95,12 @@ const Uc8253MurphyConfig& uc8253MurphyDefaultConfig() { {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). The FAST bank is single-phase - // and DC-unbalanced by construction — the OEM has no fast waveform at all - // — so the cadence is the whole anti-ghosting story: lower = cleaner, - // higher = fewer flashes. 4 after hardware feedback (6 left a - // distracting residue while reading). + // 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, }; @@ -68,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 @@ -143,6 +196,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; @@ -157,10 +255,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) { @@ -173,13 +270,39 @@ void Uc8253MurphyDriver::display(EpdBus& bus, const uint8_t* fb, const uint8_t* _fastRefreshCount = 0; } - loadLut(bus, useFast ? _cfg.fast : _cfg.def); +#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 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 these 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. + // 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); diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.h index b15a2096..73e193df 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 }; @@ -75,10 +84,12 @@ class Uc8253MurphyDriver : public PanelDriver { 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 fillPlane(EpdBus& bus, uint8_t command, uint8_t fillByte); 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 f1658db7..a4792dc0 100644 --- a/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h +++ b/libs/display/FreeInkDisplay/src/lut/Uc8253MurphyLuts.h @@ -19,14 +19,13 @@ // accumulates — the driver promotes every MURPHY_GHOST_CLEAR_INTERVAL'th fast // refresh to a DEFAULT one to flush it. // -// History: an earlier revision of this header carried the manufacturer -// "M3 LUT.txt" blocks (0f8f4f / 4f8f0f / ...) as the default bank. Those five -// blocks ARE LUTs — but they are the OEM's ALTERNATE bank, a package deal with -// the data-driven init (0x82=0x07, 0x50=0xD7) and the 0x17/0xA5 partial-window +// 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). Adopting them means porting -// the whole alternate init+trigger path, not swapping tables. +// (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 { @@ -81,4 +80,40 @@ constexpr uint8_t MURPHY_LUT_24_FAST[MURPHY_LUT_LEN_BB] = { // R24 BB -> B 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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_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_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_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_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}; + } // namespace freeink diff --git a/libs/hardware/InputManager/include/InputManager.h b/libs/hardware/InputManager/include/InputManager.h index 8314e427..9635ba03 100644 --- a/libs/hardware/InputManager/include/InputManager.h +++ b/libs/hardware/InputManager/include/InputManager.h @@ -282,7 +282,11 @@ 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 on the shared confirm/power pin before it means POWER (sleep). + // On boards where that pin is also the PRIMARY select button (Murphy M3's + // middle key), 400ms fires on merely deliberate presses — hardware feedback + // showed accidental sleeps — so those boards demand a real 1.5s hold. + static constexpr unsigned long CONFIRM_POWER_HOLD_MS = FREEINK_DEVICE_MURPHY ? 1500 : 400; // Touch timing / protocol constants (ported from the Murphy M3 CHSC6x // driver). From fb5e7d7ffa063b5d54bf1ec04e2338f867013140 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Mon, 3 Aug 2026 23:03:01 -0400 Subject: [PATCH 9/9] Make touch gesture thresholds board-configurable Move confirm/power button hold time from compile-time constant to per-board config, allowing boards with shared confirm/power buttons to set longer holds. Cap touch gesture threshold scaling to prevent raising thresholds on larger panels - only smaller panels scale down to maintain consistent physical gesture distances across different digitizer sizes. --- .../BatteryMonitor/src/BatteryMonitor.cpp | 2 -- .../BoardConfig/include/BoardConfig.h | 8 ++++- .../InputManager/include/InputManager.h | 16 +++++---- .../InputManager/src/InputManager.cpp | 34 +++++++++++-------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp index b0e01055..a0c18e63 100644 --- a/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp +++ b/libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp @@ -11,11 +11,9 @@ #include #include - #if FREEINK_BATTERY_I2C_GAUGE #include - // Minimal, dependency-free I2C fuel-gauge read for boards that carry one (e.g. // LilyGo T5 S3: BQ27220 gauge + BQ25896 charger). Standard TI command registers; // the gauge reports true battery state, so no ADC pin or divider is involved. diff --git a/libs/hardware/BoardConfig/include/BoardConfig.h b/libs/hardware/BoardConfig/include/BoardConfig.h index b3c2b59a..7bc29de9 100644 --- a/libs/hardware/BoardConfig/include/BoardConfig.h +++ b/libs/hardware/BoardConfig/include/BoardConfig.h @@ -627,6 +627,11 @@ 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. + unsigned long confirmPowerHoldMs = 400; }; constexpr TouchConfig NO_TOUCH = {TouchController::None, @@ -897,7 +902,8 @@ constexpr BoardProfile MURPHY_M3 = { // 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}}; + {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 9635ba03..2f7dd601 100644 --- a/libs/hardware/InputManager/include/InputManager.h +++ b/libs/hardware/InputManager/include/InputManager.h @@ -221,6 +221,7 @@ 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, @@ -282,11 +283,10 @@ 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; - // Hold time on the shared confirm/power pin before it means POWER (sleep). - // On boards where that pin is also the PRIMARY select button (Murphy M3's - // middle key), 400ms fires on merely deliberate presses — hardware feedback - // showed accidental sleeps — so those boards demand a real 1.5s hold. - static constexpr unsigned long CONFIRM_POWER_HOLD_MS = FREEINK_DEVICE_MURPHY ? 1500 : 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). @@ -294,10 +294,12 @@ class InputManager { 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 to the active - // digitizer's span so small panels need the same physical finger travel. + // 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 cf38c96c..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); } @@ -839,25 +839,31 @@ bool InputManager::decodeChsc6xFrame(const uint8_t *data, const size_t len, } // Gesture thresholds (TOUCH_TAP_SLOP_PX / TOUCH_SWIPE_MIN_PX) are tuned in the -// mapped units of the GT911 boards (800x480-class, average axis span 640). On a -// small digitizer like the Murphy's (200x374, average 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 everywhere; GT911 boards are unchanged. -int InputManager::touchTapSlopPx() const { +// 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; - const int v = TOUCH_TAP_SLOP_PX * ((xs + ys) / 2) / 640; - return v < 8 ? 8 : v; + 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 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; - const int v = TOUCH_SWIPE_MIN_PX * ((xs + ys) / 2) / 640; - return v < 16 ? 16 : v; + 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,