Root cause, established by on-device elimination over one long session: SD-card WRITE bursts electrically disturb the T-Display P4's AMOLED -- the whole-screen blue flash of #167. The display stack itself measured clean at every layer while flashes were visible: framebuffer content (flush tracer: every band black), DSI bridge underrun interrupt (never fired), DSI host protocol errors incl. the host payload-buffer underflow (never latched), DCS/brightness traffic and XL9535 expander writes (silent), disp on/off (never). The flash survived every digital derate -- 4->1-bit SD bus, 20->10 MHz SD clock, 1000->750 Mbps DSI lane rate -- on every card tried, at any brightness (including near-zero, i.e. the panel is being upset at the hardware level, not showing content), yet vanished the moment the card was removed or left unwritten. The card's rail is switched off the same 3V3 domain as the panel (XL9535 SD_EN), so its program-current spikes reach the panel and no firmware setting can filter them. Streaming tile writes never flashed; rapid open-write-close store bursts always did. Meck-P4 not flashing proves nothing about the board: it never writes the SD. Fix: the hot store (contacts/channels/identity/blobs/prefs-file/chat history) lives on the INTERNAL 'storage' partition; the SD keeps bulk, gentle writers (map tiles, backups). The internal partition is mounted as LittleFS, not FAT: the P4's FAT layer has documented broken metadata and the v3 migration proved its writes are no better (a 102 KB contacts3 "copied" onto it read back as 0 B; on LittleFS the same copy reads back byte-exact and loads all 689 contacts). esp_littlefs looks partitions up by label with SUBTYPE_ANY, so the ex-FAT partition converts in place on first boot (formatOnFail) -- no partition-table change, OTA-safe. One-time v4 migration (lessons v1-v3 encoded): substance-gated (only from a card whose contacts3 beats the internal one -- a fresh test card once donated a blank store), whitelist-only copy (v2 filled the partition with junk before contacts3 landed), both historical card layouts probed (root vs /meshcomod), marker written only after the copy, card never written (it remains the rollback). Also in this change, kept as opt-in diagnostics (dormant without their env gates): WADA_P4_POKE_TRACE (DCS/expander/disp-on-off tracing + a DSI host error-register poller), WADA_SD_KHZ / WADA_SD_1BIT (SD primary-rung derates), WADA_DSI_MBPS (lane-rate override; default back at the vendor 1000 -- the 750 experiment changed nothing and is documented in the driver). Verified on device: no flash on the contact-churn trigger with the store internal (the previously 100%-reproducing trigger), 689/689 contacts after migration, tiles still on SD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
5.9 KiB
C++
132 lines
5.9 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
#include "Xl9535.h"
|
|
#include <Arduino.h>
|
|
#include <Wire.h>
|
|
|
|
// 9535 register addresses
|
|
static constexpr uint8_t REG_OUT0 = 0x02; // out1 = 0x03
|
|
static constexpr uint8_t REG_CFG0 = 0x06; // cfg1 = 0x07 (bit: 1=input, 0=output)
|
|
|
|
void Xl9535::writeReg(uint8_t reg, uint8_t val) {
|
|
Wire.beginTransmission(_addr);
|
|
Wire.write(reg);
|
|
Wire.write(val);
|
|
Wire.endTransmission();
|
|
}
|
|
|
|
uint8_t Xl9535::readReg(uint8_t reg) {
|
|
Wire.beginTransmission(_addr);
|
|
Wire.write(reg);
|
|
if (Wire.endTransmission(false) != 0) return 0xFF;
|
|
Wire.requestFrom((int)_addr, 1);
|
|
return Wire.available() ? Wire.read() : 0xFF;
|
|
}
|
|
|
|
bool Xl9535::begin(uint8_t sda, uint8_t scl, uint8_t addr) {
|
|
_addr = addr;
|
|
Wire.begin(sda, scl);
|
|
Wire.setClock(400000);
|
|
// probe
|
|
Wire.beginTransmission(_addr);
|
|
_ok = (Wire.endTransmission() == 0);
|
|
if (!_ok) { Serial.printf("[XL9535] not found at 0x%02X (SDA=%d SCL=%d)\n", _addr, sda, scl); return false; }
|
|
// start with outputs high, everything as output (we'll flip the few inputs in powerOnSequence)
|
|
_out[0] = _out[1] = 0xFF;
|
|
_cfg[0] = _cfg[1] = 0x00; // all outputs by default
|
|
writeReg(REG_OUT0, _out[0]); writeReg(REG_OUT0 + 1, _out[1]);
|
|
writeReg(REG_CFG0, _cfg[0]); writeReg(REG_CFG0 + 1, _cfg[1]);
|
|
Serial.println("[XL9535] up");
|
|
return true;
|
|
}
|
|
|
|
void Xl9535::setDir(uint8_t io, bool input) {
|
|
uint8_t port, bit; split(io, port, bit);
|
|
if (input) _cfg[port] |= (1 << bit);
|
|
else _cfg[port] &= ~(1 << bit);
|
|
writeReg(REG_CFG0 + port, _cfg[port]);
|
|
}
|
|
|
|
void Xl9535::write(uint8_t io, bool high) {
|
|
uint8_t port, bit; split(io, port, bit);
|
|
if (high) _out[port] |= (1 << bit);
|
|
else _out[port] &= ~(1 << bit);
|
|
#if defined(TDP4_POKE_TRACE)
|
|
// #167 hunt: any expander write while the panel streams is a glitch suspect (this is an
|
|
// UNLOCKED shadow RMW -- a cross-task race can transiently drop SCREEN_RST or a rail bit).
|
|
printf("[XLW] %lu io=%u v=%d out=%02X%02X\n", (unsigned long)millis(), io, (int)high, _out[1], _out[0]);
|
|
#endif
|
|
writeReg(REG_OUT0 + port, _out[port]);
|
|
}
|
|
|
|
bool Xl9535::read(uint8_t io) {
|
|
uint8_t port, bit; split(io, port, bit);
|
|
return (readReg(port /*in0=0x00,in1=0x01*/) >> bit) & 1;
|
|
}
|
|
|
|
void Xl9535::sx1262Reset() {
|
|
write(IO_SX1262_RST, false); delay(2);
|
|
write(IO_SX1262_RST, true); delay(5);
|
|
}
|
|
|
|
void Xl9535::powerOnSequence() {
|
|
if (!_ok) return;
|
|
// inputs: touch INT, ext-sensor INT, RTC INT, SX1262 DIO1
|
|
setDir(IO_TOUCH_INT, true);
|
|
setDir(IO_EXT_SENSOR_INT, true);
|
|
setDir(IO_RTC_INT, true);
|
|
setDir(IO_SX1262_DIO1, true);
|
|
|
|
// Power-rail bring-up — replicated EXACTLY from the tested-working LilyGo/Meck-P4 screen_lvgl
|
|
// sequence. The rails are power-CYCLED with 200 ms settling and end in specific states (VCCA LOW,
|
|
// 5V HIGH, 3V3 LOW); "just set them all high" leaves the AMOLED dark. Do not reorder / drop delays.
|
|
setDir(IO_VCCA_EN, false); setDir(IO_5V_EN, false); setDir(IO_3V3_EN, false);
|
|
setDir(IO_C6_EN, false); // C6 enable is an output — reset-pulsed at the end of this sequence
|
|
// GPS wake: HIGH = L76K awake (LilyGo's l76k example writes it HIGH, commented "turn off sleep").
|
|
// We used to park it LOW = module in sleep -> UART silent -> "no GPS detected" (SD_EN's twin).
|
|
write(IO_GPS_WAKE, true);
|
|
|
|
write(IO_VCCA_EN, false); // VCCA -> LOW (final)
|
|
write(IO_5V_EN, true); delay(200); // 5V power-cycle: HIGH -> LOW -> HIGH
|
|
write(IO_5V_EN, false); delay(200);
|
|
write(IO_5V_EN, true);
|
|
write(IO_3V3_EN, false); delay(200); // 3V3 power-cycle: LOW -> HIGH -> LOW
|
|
write(IO_3V3_EN, true); delay(200);
|
|
write(IO_3V3_EN, false);
|
|
delay(200);
|
|
|
|
// Board peripherals we still need (Meck's display-only example omits these): the SX1262.
|
|
// SD_EN (IO15) is an ACTIVE-LOW load-switch enable for the SD slot's VDD (pad-sweep proven
|
|
// 2026-07-15: SD_EN high -> all six SD pads clamp LOW through the unpowered card's ESD diodes,
|
|
// even against the P4's internal pull-ups; SD_EN low or Hi-Z -> all pads high, card powered).
|
|
// Meck never touches it because the XL9535 powers up all-inputs (Hi-Z) and the board strap
|
|
// defaults the switch ON — but OUR begin() parks every pin output-HIGH, which turned the slot
|
|
// OFF and made every mount fail (0x107 OCR timeout / 0x109 CRC). Drive it LOW, always.
|
|
write(IO_SD_EN, false); // SD slot power ON (active-low)
|
|
write(IO_SX1262_RST, true); // park high; radio_init pulses it
|
|
// Antenna select: park on the on-board antenna (RF1) before the radio exists, so the first
|
|
// transmit after any boot cannot key the PA into a possibly-empty external socket. LilyGo's own
|
|
// XL9535 init preloads this same level as its safe state. Choosing the external antenna is a
|
|
// deliberate, confirmed, session-only action from Settings -> Radio.
|
|
write(IO_RF_SWITCH, INTERNAL_LEVEL);
|
|
write(IO_TOUCH_RST, true); // release touch (HI8561 driver probes it)
|
|
// NB: SCREEN_RST is intentionally NOT touched here — RM69A10Display::begin() sequences it with
|
|
// the DSI-PHY LDO (HIGH->LOW->HIGH, 200 ms each), exactly as the working Meck-P4 app_main does.
|
|
|
|
// ESP32-C6 reset + enable: pulse C6_EN (IO14) HIGH->LOW->HIGH (100 ms each) + ~1 s so the C6 boots
|
|
// its ESP-AT firmware, which the c6_at component then drives over SDIO (AT-based Wi-Fi/BLE). C6_EN IS
|
|
// the reset (no separate line). Doing it here means the C6 is booting while the rest of the app inits.
|
|
#ifdef TDP4_C6_FLASH_HELPER
|
|
// C6-FLASH HELPER: do NOT reset-pulse the C6. C6_EN stays HIGH (powered) from begin()'s default, so
|
|
// the C6 has power but is never yanked out of a user-held ROM download mode by a P4 (re)boot.
|
|
Serial.println("[XL9535] power-on done — C6 reset SKIPPED (flash-helper build; C6 left free)");
|
|
#else
|
|
write(IO_C6_EN, true); delay(100);
|
|
write(IO_C6_EN, false); delay(100);
|
|
write(IO_C6_EN, true); delay(100);
|
|
Serial.println("[XL9535] power-on sequence done (C6 enabled for ESP-AT)");
|
|
#endif
|
|
}
|
|
|
|
#endif // HAS_TDISPLAY_P4
|