diff --git a/examples/simple_secure_chat_ui/main.cpp b/examples/simple_secure_chat_ui/main.cpp index 05364b45d..98ef7dffb 100644 --- a/examples/simple_secure_chat_ui/main.cpp +++ b/examples/simple_secure_chat_ui/main.cpp @@ -900,10 +900,74 @@ void configureDisplay() { lcd.fillScreen(0x000000u); } +// ── ROOT CAUSE NOTE ────────────────────────────────────────────────────────── +// Upstream LovyanGFX 1.2.7 does NOT understand the IO_EXPANDER pin encoding +// (only the mverch67 fork does). Our SCIndicatorDisplay.h sets +// cfg.pin_cs = 4 | IO_EXPANDER (= 0x44 = 68) +// During Panel_ST7701_Base::init(), LovyanGFX calls +// lgfx::gpio_lo(pin_cs) // assert CS +// ... send SPI init commands (gamma, voltages, RGB666, sleep-out, etc.) ... +// lgfx::gpio_hi(pin_cs) // deassert CS +// gpio_lo(68) on ESP32-S3 (max GPIO 48) is a silent no-op → CS is NEVER +// actually asserted → all SPI init commands are sent into the void → +// the ST7701 never receives its init sequence. +// +// This worked SOMETIMES because: +// - on soft reboot the ST7701 retained state from the previous boot +// - on cold boot from charged caps it was in some semi-init state +// - on cold boot from discharged caps it stayed in factory state → stripes +// +// FIX: manually assert TCA9535 P0.4 (real LCD CS) LOW before lcd.begin() +// and deassert it HIGH after. CS stays LOW for the entire LovyanGFX init, +// so all SPI commands actually reach the ST7701. The internal gpio_lo/hi +// calls in LovyanGFX become harmless no-ops (they target a non-existent +// GPIO 68, but our manual TCA9535 CS state is preserved). +// ──────────────────────────────────────────────────────────────────────────── + +static void sensecap_lcd_cs_assert() { + const uint8_t TCA = 0x20; + // Config Port 0: bit 4 (LCD CS) = OUTPUT, bit 5 (LCD RESX) = OUTPUT, + // others = INPUT. 0xCF = 1100 1111 (bits 4,5 cleared = output). + Wire.beginTransmission(TCA); + Wire.write(0x06); // CONFIG_P0 + Wire.write(0xCF); + uint8_t e1 = Wire.endTransmission(); + + // Output Port 0: bit 4 LOW (CS asserted), bit 5 HIGH (RESX deasserted), + // all other latches HIGH. 0xEF = 1110 1111. + Wire.beginTransmission(TCA); + Wire.write(0x02); // OUTPUT_P0 + Wire.write(0xEF); + uint8_t e2 = Wire.endTransmission(); + + Serial.printf("[lcd_cs] assert LOW: cfg_err=%u out_err=%u\n", e1, e2); +} + +static void sensecap_lcd_cs_deassert() { + const uint8_t TCA = 0x20; + // Output Port 0: all HIGH (CS deasserted, RESX deasserted). + Wire.beginTransmission(TCA); + Wire.write(0x02); // OUTPUT_P0 + Wire.write(0xFF); + uint8_t e = Wire.endTransmission(); + Serial.printf("[lcd_cs] deassert HIGH: out_err=%u\n", e); +} + void initializeDisplay() { ESP_LOGI(TAG, "Initializing display..."); + + // Hold the LCD CS LOW manually for the entire lcd.begin() duration so the + // ST7701 actually receives the SPI init sequence. See ROOT CAUSE NOTE above. + sensecap_lcd_cs_assert(); + delay(5); + bool ok = lcd.begin(); Serial.printf("[display] lcd.begin() = %s w=%d h=%d\n", ok ? "OK" : "FAIL", lcd.width(), lcd.height()); + + // Release LCD CS now that the SPI init sequence is complete. Subsequent + // RGB pixel data goes via the parallel bus and does not need CS. + sensecap_lcd_cs_deassert(); + lcd.setBrightness(255); // Fallback: force GPIO 45 HIGH in case LovyanGFX Light_PWM LEDC setup failed. pinMode(45, OUTPUT); diff --git a/src/helpers/esp32/SenseCapHAL.h b/src/helpers/esp32/SenseCapHAL.h index b2f901316..a063462a7 100644 --- a/src/helpers/esp32/SenseCapHAL.h +++ b/src/helpers/esp32/SenseCapHAL.h @@ -9,11 +9,15 @@ // Pin encoding matches LovyanGFX convention: (pin_index | IO_EXPANDER) // IO_EXPANDER = 0x40 (defined as build flag in platformio.ini) // -// SX1262 expander pin assignments (TCA9535 Port 0): -// pin 0 = NSS (EXPANDER_IO_RADIO_NSS) -// pin 1 = RESET (EXPANDER_IO_RADIO_RST) -// pin 2 = BUSY (EXPANDER_IO_RADIO_BUSY) -// pin 3 = DIO1 (EXPANDER_IO_RADIO_DIO_1) +// TCA9535 Port 0 pin assignments: +// pin 0 = NSS (EXPANDER_IO_RADIO_NSS) — SX1262 chip-select +// pin 1 = RESET (EXPANDER_IO_RADIO_RST) — SX1262 hardware reset +// pin 2 = BUSY (EXPANDER_IO_RADIO_BUSY) — SX1262 busy signal +// pin 3 = DIO1 (EXPANDER_IO_RADIO_DIO_1) — SX1262 IRQ +// pin 4 = display SPI CS — ST7701 chip-select (must stay OUTPUT HIGH) +// pin 5 = display RESX — ST7701 hardware reset (LovyanGFX cfg.pin_rst) +// pin 6 = touch INT — FT5x06 interrupt (input, not used in SW) +// pin 7 = touch RST — FT5x06 reset (not used in SW) // // DIO1 interrupt: TCA9535 /INT output → GPIO 42 (IO_EXPANDER_IRQ) // The /INT pin fires FALLING when any input changes. @@ -85,23 +89,22 @@ public: // Both this HAL and the touch-read callback must use the same handle. void setMutex(SemaphoreHandle_t m) { _mutex = m; } - // Call once Wire is running. - // Configures TCA9535 Port 0 radio pins (bits 0-3) only. - // Bits 4-7 are preserved exactly as LovyanGFX left them - // (bit 4 = display SPI CS, must stay OUTPUT HIGH so the - // display ignores any SPI clock pulses from the radio). + // Call once Wire is running, AFTER lcd.begin() has completed. + // Configures TCA9535 Port 0 radio pins (bits 0-3) and locks bit 4 (display CS). + // Bits 5-7 are preserved exactly as LovyanGFX left them. void initExpander() { // Read the current register values set by LovyanGFX uint8_t cur_out = readReg(0x02); uint8_t cur_cfg = readReg(0x06); - // Bits 0-3 (radio pins): inputs initially, output latch HIGH (de-asserted) - // RadioLib will reconfigure them as needed. - // Bit 4 (display SPI CS): OUTPUT HIGH — permanently de-asserted. - // LovyanGFX leaves this pin as INPUT after lcd.begin(), - // which lets it float. When radio SPI transactions drive - // GPIO 41/48, a floating CS can corrupt the ST7701 display. - // Bits 5-7 (other): preserved exactly as LovyanGFX left them. + // Bits 0-3 (radio): inputs initially; RadioLib reconfigures as needed. + // Output latch HIGH = de-asserted. + // Bit 4 (display CS): OUTPUT HIGH — permanently de-asserted. + // LovyanGFX leaves this as INPUT after lcd.begin(); + // a floating CS allows radio SPI to corrupt the ST7701. + // Bit 5 (display RESX): preserved as OUTPUT HIGH — lcd.begin() already pulsed + // it LOW→HIGH to reset the ST7701 before init commands. + // Bits 6-7 (touch INT/RST): preserved as LovyanGFX left them (inputs). _out0 = (cur_out & 0xE0) | 0x1F; // bits 0-4 = HIGH, bits 5-7 = preserved _cfg0 = (cur_cfg & 0xE0) | 0x0F; // bits 0-3 = input, bit 4 = OUTPUT, bits 5-7 = preserved diff --git a/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h b/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h index 07682d8b2..2f2b0029d 100644 --- a/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h +++ b/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h @@ -58,12 +58,15 @@ public: cfg.offset_x = 0; cfg.offset_y = 0; cfg.offset_rotation = 0; + // pin_rst stays at default (no-connect). Upstream LovyanGFX cannot + // toggle expander pins. The ST7701 RESX (TCA9535 bit 5) is pulsed + // manually by sensecap_lcd_reset_pulse() in main.cpp before lcd.begin(). _panel_instance.config(cfg); } { auto cfg = _panel_instance.config_detail(); - cfg.pin_cs = 4 | IO_EXPANDER; + cfg.pin_cs = 4 | IO_EXPANDER; // TCA9535 bit 4 = ST7701 chip-select cfg.pin_sclk = 41; cfg.pin_mosi = 48; cfg.use_psram = 1;