From e54c289e6242c02a3928b678ee36d7abeee6f4e0 Mon Sep 17 00:00:00 2001 From: Enygma <94997734+EnygmaINC@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:40:37 -0500 Subject: [PATCH 1/5] Add lcdwiki-es3c35p board: 3.5" ST77922 QSPI display with built-in touch New board env for the 3.5" ESP32-S3 ST77922 (320x480) module: - display via the new Arduino_ST77922_1L4 driver (DisplayDrivers submodule) - TFT_DC=-1 / TFT_IPS=0 to stop pre_compiler.h's CYD defaults clobbering TFT_CS/TFT_SCLK - new TOUCH_CTRL_ST77922 driver (I2C 0x55, 16-bit regs, whole-block read) - SD_MMC 4-bit, backlight PWM, WS2812 LED, audio amp held off Verified on hardware: display, touch, SD, all four rotations. Env is left commented out of default_envs and the CI matrix. Unverified: audio, battery sense, deep sleep. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/main.yml | 1 + boards/lcdwiki-es3c35p/connections.md | 98 ++++++++++++++++++++++ boards/lcdwiki-es3c35p/interface.cpp | 112 +++++++++++++++++++++++++ boards/lcdwiki-es3c35p/platformio.ini | 101 ++++++++++++++++++++++ lib/DisplayDrivers | 2 +- platformio.ini | 1 + src/hal/inputs/touch.cpp | 115 ++++++++++++++++++++++++-- 7 files changed, 424 insertions(+), 6 deletions(-) create mode 100644 boards/lcdwiki-es3c35p/connections.md create mode 100644 boards/lcdwiki-es3c35p/interface.cpp create mode 100644 boards/lcdwiki-es3c35p/platformio.ini diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2758611a..8f0af6f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,6 +91,7 @@ - { env: "NM-CYD-C5" } - { env: "CYD-3248W535C" } - { env: "CYD-4827S043R" } + # - { env: "lcdwiki-es3c35p" } - { env: "lilygo-t-dongle-s3-tft" } - { env: "lilygo-t-dongle-c5-tft" } - { env: "lilygo-t-display-S3-touch" } diff --git a/boards/lcdwiki-es3c35p/connections.md b/boards/lcdwiki-es3c35p/connections.md new file mode 100644 index 00000000..58c44d35 --- /dev/null +++ b/boards/lcdwiki-es3c35p/connections.md @@ -0,0 +1,98 @@ +# lcdwiki-es3c35p pinout + +3.5" ESP32-S3 (16MB flash, 8MB PSRAM), ST77922 panel over QSPI with the +capacitive touch block built into the same controller IC. Sibling of +[ES3C28P](../ES3C28P/connections.md) (2.8", standard SPI). +Sold rebadged, e.g. "Hosyond ESP32-S3 Touchscreen Module 3.5"" (Amazon B0H28X8SQ4). + +Sources: jlmeredith/ES3C35P `docs/HARDWARE.md`, `78/xiaozhi-esp32`'s +`boards/lcdwiki-es3c35p/`, and LCDwiki's published Arduino examples +(github.com/ydedox/st77922, Example_01 for the display, `esp_lcd_st77922.c` in +Example_08 for the touch register map). + +## Display -- ST77922, QSPI, no DC pin + +| Pin | GPIO | Notes | +| --- | ---- | ----- | +| CS | 10 | | +| SCLK | 12 | | +| D0 | 11 | QSPI SIO0 / MOSI | +| D1 | 13 | QSPI SIO1 / MISO | +| D2 | 14 | QSPI SIO2 / WP | +| D3 | 9 | QSPI SIO3 / HD | +| RST | -- | No reset pin; the display half needs none. | +| BL | 41 | Backlight, active HIGH (LEDC PWM) | + +All six QSPI pins are the ESP32-S3's FSPI IOMUX pins, which the driver requires +(`SPICOMMON_BUSFLAG_IOMUX_PINS`). If SPI bus init ever fails with "not using iomux +pins", check that `TFT_CS`/`TFT_SCLK` really are 10/12 at compile time. + +**`TFT_DC=-1` and `TFT_IPS=0` are load-bearing.** `include/pre_compiler.h` has a +"CYD defaults" block that silently redefines `TFT_CS`/`TFT_SCLK` (to 15/14) when a +board defines none of `TFT_DC`, `TFT_IPS`, `TFT_COL_OFS*`, `TFT_MOSI`. A QSPI board +has no DC or MOSI pin, so it must opt out explicitly, as `platformio.ini` does. + +Driven through `Arduino_ST77922_1L4` (`TFT_DATABUS_N=6` / +`TFT_DISPLAY_DRIVER_N=52`, `lib/DisplayDrivers/src/panels/Arduino_ST77922_1L4.h`), +a raw ESP-IDF `spi_master` driver modelled on the vendor's own Example_01 -- +Arduino_GFX's `Arduino_ESP32QSPI` hardcodes every phase onto 4 lines, while this +panel wants register writes on 1 line (opcode 0x02, address `reg << 8`) and pixel +data on 4 (opcode 0x32, address 0x003C00). The init table is the vendor's active +`st77922_lcd_init[]`, unmodified. + +Panel is native 320x480 **portrait**. `ROTATION=1` is the landscape mounting; the +panel's MADCTL does not remap addressing, so rotation is done in software by +`USE_CANVAS` (Arduino_Canvas) plus the driver's transform. Orientation and colour +order (RGB, no inversion) are confirmed correct on hardware at rotation 1. + +## Touch -- built into the ST77922, I2C 0x55, NOT FT6x36-compatible + +| Pin | GPIO | Notes | +| --- | ---- | ----- | +| SDA | 38 | shared with the ES8311 audio codec (addr 0x18) | +| SCL | 39 | | +| RST | 48 | idles **LOW**; the touch half does not answer until it is released (pulsed in `_setup_gpio()`). The display does not need it. | +| INT | 47 | active-low hint; the driver also polls at ~60 Hz | + +`TOUCH_CTRL_ST77922` is a self-contained driver in `src/hal/inputs/touch.cpp` +(SensorLib has none). 16-bit big-endian register addresses, repeated-start reads: +`0x0010` info byte (bit 3 = report available), `0x0009` max touches, `0x0014` +report block of `maxTouches x 8` bytes (`[valid:1|rsvd:1|x_h:6] x_l y_h y_l area +intensity rsvd rsvd`). **The whole block must be read in one transfer** -- reading +only the first record leaves the controller returning the same stale point forever. +Reports raw panel-native portrait coordinates (x 0-319, y 0-479), so the host-side +`SwapXY`/`MirrorX`/`MirrorY` table in `interface.cpp` applies. Rotation 1 is verified +on hardware (swap + mirror Y); rows 0/2/3 were also verified by the user. + +## SD card -- native 4-bit SDIO, dedicated bus + +| Pin | GPIO | +| --- | ---- | +| CLK | 5 | +| CMD | 4 | +| D0 | 6 | +| D1 | 7 | +| D2 | 2 | +| D3 | 3 | + +Not shared with anything else on this board. Verified working with a card inserted. +With no card the boot log reports `sdmmc_init_ocr ... 0x107` / "Failed to mount +SDCARD", which is expected. + +## Other pins + +| Function | GPIO | Notes | +| -------- | ---- | ----- | +| WS2812 status LED | 40 | Single LED, driven via `neopixelWrite()` | +| SC8002B audio amp shutdown | 1 | Active LOW; held HIGH (disabled) at boot | +| BOOT button | 0 | Only physical key; doubles as Esc | +| Battery sense | *unknown* | Not documented in any source consulted; `ANALOG_BAT_PIN` is left unset. | +| Free/unclaimed | 45, 46 | Per HARDWARE.md | + +## Status + +Verified on real hardware (COM6, 2026-11-09): display (orientation, colours, +backlight PWM), capacitive touch (menu navigation, on-screen keyboard / Wi-Fi +password entry), SD card, and all four rotations (display and touch, switched at +runtime from the Launcher Rotation menu). Not yet verified: audio, battery +sense, deep sleep / wake. diff --git a/boards/lcdwiki-es3c35p/interface.cpp b/boards/lcdwiki-es3c35p/interface.cpp new file mode 100644 index 00000000..15c4e759 --- /dev/null +++ b/boards/lcdwiki-es3c35p/interface.cpp @@ -0,0 +1,112 @@ +#include "hal/bright/bright.h" +#include "hal/device.h" +#include "hal/inputs/touch.h" +#include "idf/launcher_platform.h" +#include "powerSave.h" +#include +#include + +// lcdwiki-es3c35p -- ESP32-S3 + 3.5" ST77922 (QSPI) with the capacitive touch +// block built into the same controller IC (I2C, address 0x55, 16-bit register +// map -- not FT6x36-compatible; see TOUCH_CTRL_ST77922 in touch.cpp). Sibling of +// ES3C28P, which this file mirrors closely; see that board for the same +// pattern with a standard-SPI panel and a standard-address touch chip. +// The SD card is wired to the SDIO interface, not SPI; the pins live in +// platformio.ini and sd_functions.cpp mounts it in 4-bit mode from there. + +// The touch block answers at 0x55; passed to hal_touch_init() explicitly. +#define TOUCH_I2C_ADDR 0x55 + +// BOOT button (GPIO0) -- the only physical key on the module, used as Esc. +#define BOOT_BTN 0 + +static DeviceTouch touchCfg() { + DeviceTouch cfg; + cfg.pin_sda = TOUCH_SDA; + cfg.pin_scl = TOUCH_SCL; + // The reset line is pulsed once in _setup_gpio(), before the display starts. + // It only affects the touch half of the chip, but the touch driver must not + // pulse it again after init anyway, so it is left to _setup_gpio(). + cfg.pin_rst = -1; + cfg.pin_irq = TOUCH_INT; + // rotation: 0 1 2 3 + bool swapXY[4] = {false, true, false, true}; + bool mirrorX[4] = {false, false, true, true}; + bool mirrorY[4] = {false, true, true, false}; + for (int i = 0; i < 4; i++) { + cfg.SwapXY[i] = swapXY[i]; + cfg.MirrorX[i] = mirrorX[i]; + cfg.MirrorY[i] = mirrorY[i]; + } + return cfg; +} + +void _setup_gpio() { + // The ST77922 is one chip doing both display and touch, and the touch half's + // reset line (GPIO 48, "TOUCH_RST") idles LOW on this board: the chip does + // not answer on I2C (0x55) until it is pulsed. Release it once, up front. + // This is for touch only -- the display half works without it. + pinMode(TOUCH_RST, OUTPUT); + digitalWrite(TOUCH_RST, LOW); + delay(20); + digitalWrite(TOUCH_RST, HIGH); + delay(200); + + // Backlight on right away (active HIGH), before the panel is initialised; + // the brightness PWM in _post_setup_gpio() takes over the pin afterwards. + pinMode(TFT_BL, OUTPUT); + digitalWrite(TFT_BL, HIGH); + + // Audio codec disabled by default (Audio_EN is active LOW) + pinMode(AUDIO_EN_PIN, OUTPUT); + digitalWrite(AUDIO_EN_PIN, HIGH); + + // WS2812 status LED off. neopixelWrite() ships with the Arduino core, so the + // board needs no external LED library. + neopixelWrite(RGB_LED_PIN, 0, 0, 0); + + // BOOT button doubles as the Esc key + launcherGpioInputPullup(BOOT_BTN); +} + +void _post_setup_gpio() { +#ifndef ST77922_BRINGUP_TEST + // Backlight PWM -- must be done after tft.init() + hal_bright_attach(TFT_BL); + hal_bright_set(TFT_BL, bright); +#endif + + // Capacitive touch, at this board's nonstandard I2C address. + hal_touch_init(touchCfg(), TOUCH_I2C_ADDR); +} + +void _setBrightness(uint8_t brightval) { +#ifndef ST77922_BRINGUP_TEST + hal_bright_set(TFT_BL, brightval); +#else + (void)brightval; // bring-up: backlight held solid on by _setup_gpio() +#endif +} + +void InputHandler(void) { + static long tm = launcherMillis(); + if (launcherMillis() - tm > 250 || LongPress) { + LTouchPoint t; + checkPowerSaveTime(); + if (hal_touch_read(touchCfg(), t)) { + tm = launcherMillis(); + if (!hal_touch_apply(t)) return; + } else if (launcherGpioRead(BOOT_BTN) == LOW) { + tm = launcherMillis(); + if (!wakeUpScreen()) { + AnyKeyPress = true; + EscPress = true; + } + } + } +} + +void powerOff() { + neopixelWrite(RGB_LED_PIN, 0, 0, 0); + esp_deep_sleep_start(); +} diff --git a/boards/lcdwiki-es3c35p/platformio.ini b/boards/lcdwiki-es3c35p/platformio.ini new file mode 100644 index 00000000..f29b5e9b --- /dev/null +++ b/boards/lcdwiki-es3c35p/platformio.ini @@ -0,0 +1,101 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/platformio.ini/page/projectconf.html + +##################### lcdwiki-es3c35p (ESP32-S3 + 3.5" ST77922 QSPI + built-in cap touch) #################### +; 3.5" ESP32-S3 IPS LCD Display Module, 320x480, quad-SPI (no DC pin) ST77922 +; panel, sold rebadged as "Hosyond ESP32-S3 Touchscreen Module 3.5"" (Amazon +; B0H28X8SQ4) among others. Sibling of ES3C28P (2.8", standard 4-wire SPI) -- +; see boards/ES3C28P for that board's config, most of which this one mirrors. +; https://www.lcdwiki.com/3.5inch_ESP32-S3_Display +; +; NOT BUILDABLE YET: TFT_DATABUS_N=6 / TFT_DISPLAY_DRIVER_N=52 +; (Arduino_ST77922_1L4, lib/DisplayDrivers/src/backends/Arduino_ST77922_1L4.h) +; is new and has not been compiled or flashed as part of Launcher. Its panel +; bring-up logic is ported from a version of this exact driver that WAS +; confirmed working on real ES3C35P hardware outside this repo -- see that +; file's header comment -- but the Launcher integration itself is untested. +[env:lcdwiki-es3c35p] +board = esp32s3 +board_build.partitions = support_files/custom_16Mb.csv +build_src_filter = ${env.build_src_filter} +<../boards/lcdwiki-es3c35p> +build_flags = + ${env.build_flags} + -DBOARD_HAS_PSRAM + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + + -DDEVICE_NAME='"lcdwiki-es3c35p"' + -DOTA_TAG='"lcdwiki_es3c35p"' + + ; Panel native size is 320x480 portrait; ROTATION=1 is the confirmed + ; landscape mounting (see Arduino_ST77922_1L4.h's rotation-transform note). + -DROTATION=1 + + -DHAS_TOUCH=1 + -DTOUCH_CTRL_ST77922=1 # self-contained driver in src/hal/inputs/touch.cpp (NOT FT6x36-compatible) + ; TFT -- ST77922 320x480 over QSPI, no DC pin. This is NOT the same bus + ; as ES3C28P's Arduino_HWSPI (0): this panel's command phase is a single + ; QSPI line while its data phase is 4, which Arduino_GFX's own QSPI bus + ; (TFT_DATABUS_N=1) cannot do -- see Arduino_ST77922_1L4.h. + ; Draw into a full-screen PSRAM canvas and flush it in one transfer. Without + ; it every pixel is its own SPI transaction (~20 s to paint the boot screen). + ; The canvas also owns rotation, which suits a panel that can't rotate itself. + -DUSE_CANVAS=1 + -D TFT_DATABUS_N=6 # Arduino_ST77922_1L4 (QSPI, single-line command phase) + -D TFT_DISPLAY_DRIVER_N=52 # Arduino_ST77922_1L4 + -DTFT_WIDTH=320 + -DTFT_HEIGHT=480 + ; QSPI has no DC pin, but defining TFT_DC (even as -1) is what stops + ; include/pre_compiler.h's "CYD defaults" block from overriding TFT_CS/TFT_SCLK + ; below with 15/14 -- it fires when none of TFT_DC/TFT_IPS/TFT_COL_OFS*/TFT_MOSI is set. + -DTFT_DC=-1 + -DTFT_IPS=0 + -DTFT_CS=10 + -DTFT_SCLK=12 + -DTFT_D0=11 + -DTFT_D1=13 + -DTFT_D2=14 + -DTFT_D3=9 + -DTFT_RST=-1 ; no reset pin -- RST is tied to CHIP_PU on this board (see connections.md) + -DTFT_BL=41 + + ; Backlight PWM (TFT_BRIGHT_CHANNEL/Bits/FREQ come from pre_compiler.h) + -DDIMMER_SETUP=20 + + ; Capacitive touch -- built into the ST77922 itself, I2C 0x55 with a 16-bit + ; register map; shared I2C bus with the ES8311 codec. + -DTOUCH_SDA=38 + -DTOUCH_SCL=39 + -DTOUCH_RST=48 + -DTOUCH_INT=47 + + ; SD card -- native 4-bit SDIO, not shared with anything else on this board. + -DUSE_SD_MMC=1 + -DSDM=SD_MMC + -DSD_MMC_4BIT=1 + -DPIN_SD_CLK=5 + -DPIN_SD_CMD=4 + -DPIN_SD_D0=6 + -DPIN_SD_D1=7 + -DPIN_SD_D2=2 + -DPIN_SD_D3=3 + + ; WS2812 status LED, driven by the core's neopixelWrite() -- no extra library + -DRGB_LED_PIN=40 + + ; SC8002B audio amplifier shutdown line (active LOW). Held off at boot; + ; the I2S pins are unused by the launcher. + -DAUDIO_EN_PIN=1 + + ; No battery-sense pin is documented for this board yet -- ANALOG_BAT_PIN + ; is left unset until one is confirmed on real hardware (see connections.md). + +lib_ignore = + XPowersLib diff --git a/lib/DisplayDrivers b/lib/DisplayDrivers index 79d2dc75..fafb0ccd 160000 --- a/lib/DisplayDrivers +++ b/lib/DisplayDrivers @@ -1 +1 @@ -Subproject commit 79d2dc757d0684a3f63ab027920edd152972f93f +Subproject commit fafb0ccd947e9bc206d4d65309abcfeed2136d99 diff --git a/platformio.ini b/platformio.ini index b3163407..a5d64e51 100644 --- a/platformio.ini +++ b/platformio.ini @@ -47,6 +47,7 @@ default_envs = ; headless-xiao-esp32s3-16mb ; headless-xiao-esp32c5-8mb ; heltec-lora32-expansion-v2 + ; lcdwiki-es3c35p ; lilygo-t5-epaper-s3-pro ; lilygo-t-deck ; lilygo-t-deck-plus diff --git a/src/hal/inputs/touch.cpp b/src/hal/inputs/touch.cpp index 5e9c6490..b25b07fc 100644 --- a/src/hal/inputs/touch.cpp +++ b/src/hal/inputs/touch.cpp @@ -153,10 +153,88 @@ struct TouchDrvAXS5106L { void reset() {} }; static TouchDrvAXS5106L touch; + +#elif defined(TOUCH_CTRL_ST77922) +#include +// ST77922 (lcdwiki-es3c35p). The touch block lives inside the display controller +// and is NOT FT6x36-compatible: register addresses are 16 bits wide (MSB first) +// and reads use a repeated start. SensorLib has no driver for it, so, like +// AXS5106L above, the whole driver lives here. Layout (from the vendor's +// esp_lcd_touch_st77922): +// 0x0010 info byte: bit 3 = "a coordinate report is available" +// 0x0014 8-byte report per touch: [valid:1|rsvd:1|x_h:6] x_l y_h y_l area intensity rsvd rsvd +// Coordinates are reported raw (panel-native portrait), so the host-side +// SwapXY/MirrorX/MirrorY path applies, same as FT6X36/AXS5106L. +#define ST77922_TOUCH_ADDRESS 0x55 +#define ST77922_REG_FW_VERSION 0x0000 +#define ST77922_REG_MAX_XY 0x0005 // max X (2 bytes), max Y (2 bytes), max touches (1 byte) +#define ST77922_REG_MAX_TOUCHES 0x0009 +#define ST77922_MAX_TOUCHES 10 // upper bound for the on-stack report buffer +#define ST77922_REG_INFO 0x0010 +#define ST77922_REG_REPORT 0x0014 +#define ST77922_INFO_WITH_COORD 0x08 +#define ST77922_REPORT_VALID 0x80 + +struct TouchDrvST77922 { + TwoWire *wire = nullptr; + int8_t irq = -1; + uint8_t addr = ST77922_TOUCH_ADDRESS; + uint32_t lastPoll = 0; + + bool readReg(uint16_t reg, uint8_t *buf, size_t len) { + if (!wire) return false; + wire->beginTransmission(addr); + wire->write((uint8_t)(reg >> 8)); + wire->write((uint8_t)(reg & 0xFF)); + if (wire->endTransmission(false) != 0) return false; // repeated start, no STOP + if (wire->requestFrom((uint8_t)addr, len) < len) return false; + for (size_t i = 0; i < len; ++i) buf[i] = (uint8_t)wire->read(); + return true; + } + + bool getPoint(int16_t *x, int16_t *y, uint8_t) { + // INT is an active-low hint; poll at ~60Hz when it is not asserted so a + // module that never pulls it low still works. + const uint32_t now = launcherMillis(); + if (irq >= 0 && launcherGpioRead(irq) != LOW && (uint32_t)(now - lastPoll) < 16) return false; + lastPoll = now; + + uint8_t info = 0; + if (!readReg(ST77922_REG_INFO, &info, 1)) return false; + if (!(info & ST77922_INFO_WITH_COORD)) return false; + // Read the whole report block in one transfer, as the vendor driver does + // (max-touches x 8 bytes): the controller does not refresh its report + // buffer if only the first record is read, so a partial read leaves the + // same stale point reported forever. + uint8_t maxTouches = 0; + if (!readReg(ST77922_REG_MAX_TOUCHES, &maxTouches, 1)) return false; + if (maxTouches == 0 || maxTouches > ST77922_MAX_TOUCHES) maxTouches = ST77922_MAX_TOUCHES; + uint8_t r[ST77922_MAX_TOUCHES * 8]; + if (!readReg(ST77922_REG_REPORT, r, (size_t)maxTouches * 8)) return false; + int p = -1; + for (uint8_t i = 0; i < maxTouches; ++i) { + if (r[i * 8] & ST77922_REPORT_VALID) { + p = i * 8; + break; + } + } + if (p < 0) return false; + *x = (int16_t)(((r[p] & 0x3F) << 8) | r[p + 1]); + *y = (int16_t)((r[p + 2] << 8) | r[p + 3]); +#ifdef ST77922_TOUCH_DEBUG + launcherConsolePrintf("[ST77922 touch] raw x=%d y=%d\n", (int)*x, (int)*y); +#endif + return true; + } + + void reset() {} +}; +static TouchDrvST77922 touch; #endif #if defined(TOUCH_CTRL_GT911) || defined(TOUCH_CTRL_CST8XX) || defined(TOUCH_CTRL_FT6X36) || \ - defined(TOUCH_CTRL_GT9895) || defined(TOUCH_CTRL_HI8561) || defined(TOUCH_CTRL_AXS5106L) + defined(TOUCH_CTRL_GT9895) || defined(TOUCH_CTRL_HI8561) || defined(TOUCH_CTRL_AXS5106L) || \ + defined(TOUCH_CTRL_ST77922) #include static TwoWire &wireFor(const DeviceTouch &cfg) { return cfg.i2c_bus ? *static_cast(cfg.i2c_bus) : Wire; @@ -271,6 +349,31 @@ bool hal_touch_init(const DeviceTouch &cfg, uint8_t i2c_addr, bool xpt_shared_sp uint8_t probe[AXS5106L_REPORT_LEN]; return touch.readReport(probe); } +#elif defined(TOUCH_CTRL_ST77922) + (void)xpt_shared_spi; + if (cfg.pin_sda >= 0 && cfg.pin_scl >= 0) wireFor(cfg).begin(cfg.pin_sda, cfg.pin_scl); + wireFor(cfg).setClock(400000); + // The touch half of the ST77922 idles in reset on this board; the board's + // _setup_gpio() releases that line (GPIO 48) before this runs, so pin_rst + // stays -1 here and touchResetPulse() only handles an optional reset_cb. + touchResetPulse(cfg); + if (cfg.pin_irq >= 0) launcherGpioInputPullup(cfg.pin_irq); + touch.wire = &wireFor(cfg); + touch.irq = cfg.pin_irq; + touch.addr = i2c_addr ? i2c_addr : ST77922_TOUCH_ADDRESS; + // Probe with real register reads and report what the chip says about itself. + { + uint8_t ver = 0, info[5] = {0}; + if (!touch.readReg(ST77922_REG_FW_VERSION, &ver, 1) || !touch.readReg(ST77922_REG_MAX_XY, info, sizeof(info))) { + launcherConsolePrintf("[ST77922 touch] no answer at 0x%02X\n", touch.addr); + return false; + } + launcherConsolePrintf( + "[ST77922 touch] fw=%u maxX=%u maxY=%u maxTouches=%u\n", ver, (info[0] << 8) | info[1], + (info[2] << 8) | info[3], info[4] + ); + return true; + } #else (void)cfg; (void)i2c_addr; @@ -282,7 +385,7 @@ bool hal_touch_init(const DeviceTouch &cfg, uint8_t i2c_addr, bool xpt_shared_sp bool hal_touch_read(const DeviceTouch &cfg, LTouchPoint &out) { #if defined(TOUCH_CTRL_XPT2046) || defined(TOUCH_CTRL_GT911) || defined(TOUCH_CTRL_CST8XX) || \ defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_GT9895) || defined(TOUCH_CTRL_HI8561) || \ - defined(TOUCH_CTRL_AXS5106L) + defined(TOUCH_CTRL_AXS5106L) || defined(TOUCH_CTRL_ST77922) uint8_t r = rotation & 0x03; int16_t screenW, screenH; panelSize(screenW, screenH); @@ -296,11 +399,12 @@ bool hal_touch_read(const DeviceTouch &cfg, LTouchPoint &out) { if (!touch.touched()) return false; auto p = touch.getPointScaled(); int16_t x = p.x, y = p.y; -#elif defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_AXS5106L) +#elif defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_AXS5106L) || defined(TOUCH_CTRL_ST77922) int16_t x = 0, y = 0; if (!touch.getPoint(&x, &y, 1)) return false; #endif -#if defined(TOUCH_CTRL_XPT2046) || defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_AXS5106L) +#if defined(TOUCH_CTRL_XPT2046) || defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_AXS5106L) || \ + defined(TOUCH_CTRL_ST77922) if (cfg.SwapXY[r]) { int16_t t = x; x = y; @@ -354,7 +458,8 @@ bool hal_touch_read_raw(LTouchPoint &out) { out.pressed = true; return true; #elif defined(TOUCH_CTRL_GT911) || defined(TOUCH_CTRL_CST8XX) || defined(TOUCH_CTRL_FT6X36) || \ - defined(TOUCH_CTRL_GT9895) || defined(TOUCH_CTRL_HI8561) || defined(TOUCH_CTRL_AXS5106L) + defined(TOUCH_CTRL_GT9895) || defined(TOUCH_CTRL_HI8561) || defined(TOUCH_CTRL_AXS5106L) || \ + defined(TOUCH_CTRL_ST77922) int16_t x = 0, y = 0; if (!touch.getPoint(&x, &y, 1)) return false; out.x = x; From e421109bcb64b4f31ac69379422c300ee82c8299 Mon Sep 17 00:00:00 2001 From: Enygma <94997734+EnygmaINC@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:55:19 -0500 Subject: [PATCH 2/5] lcdwiki-es3c35p: enable battery sense on GPIO 8 GPIO 8 is a 1:2 divider off the LiPo rail. Found by an ADC scan of the unclaimed pins and checked against a multimeter (2.007 V x 2 = 4.01 V vs 3.99-4.00 V measured while charging). Default ANALOG_BAT_MULTIPLIER of 2.0 applies, so only ANALOG_BAT_PIN=8 is needed. Co-Authored-By: Claude Sonnet 5.5 --- boards/lcdwiki-es3c35p/connections.md | 7 ++++--- boards/lcdwiki-es3c35p/platformio.ini | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/boards/lcdwiki-es3c35p/connections.md b/boards/lcdwiki-es3c35p/connections.md index 58c44d35..5b52547b 100644 --- a/boards/lcdwiki-es3c35p/connections.md +++ b/boards/lcdwiki-es3c35p/connections.md @@ -86,7 +86,7 @@ SDCARD", which is expected. | WS2812 status LED | 40 | Single LED, driven via `neopixelWrite()` | | SC8002B audio amp shutdown | 1 | Active LOW; held HIGH (disabled) at boot | | BOOT button | 0 | Only physical key; doubles as Esc | -| Battery sense | *unknown* | Not documented in any source consulted; `ANALOG_BAT_PIN` is left unset. | +| Battery sense | 8 | ADC1, 1:2 resistor divider (`ANALOG_BAT_PIN=8`, default `ANALOG_BAT_MULTIPLIER` 2.0). Found by an ADC scan of the unclaimed pins; verified against a multimeter (ADC 2.007 V x 2 = 4.01 V vs 3.99-4.00 V measured while charging). With USB attached the charger holds the rail near 4.1 V, so it reads high and Launcher shows the charging icon. | | Free/unclaimed | 45, 46 | Per HARDWARE.md | ## Status @@ -94,5 +94,6 @@ SDCARD", which is expected. Verified on real hardware (COM6, 2026-11-09): display (orientation, colours, backlight PWM), capacitive touch (menu navigation, on-screen keyboard / Wi-Fi password entry), SD card, and all four rotations (display and touch, switched at -runtime from the Launcher Rotation menu). Not yet verified: audio, battery -sense, deep sleep / wake. +runtime from the Launcher Rotation menu), and battery sense (GPIO 8, checked +against a multimeter with a 3.7 V 850 mAh LiPo). Not yet verified: audio, +deep sleep / wake. diff --git a/boards/lcdwiki-es3c35p/platformio.ini b/boards/lcdwiki-es3c35p/platformio.ini index f29b5e9b..95f2cec6 100644 --- a/boards/lcdwiki-es3c35p/platformio.ini +++ b/boards/lcdwiki-es3c35p/platformio.ini @@ -94,8 +94,10 @@ build_flags = ; the I2S pins are unused by the launcher. -DAUDIO_EN_PIN=1 - ; No battery-sense pin is documented for this board yet -- ANALOG_BAT_PIN - ; is left unset until one is confirmed on real hardware (see connections.md). + ; Battery sense: GPIO 8 reads ~2.0 V with a ~4.0 V LiPo attached, i.e. a 1:2 + ; divider (Launcher's default ANALOG_BAT_MULTIPLIER of 2.0 applies). Found by an + ; ADC scan and confirmed against a multimeter (see connections.md). + -DANALOG_BAT_PIN=8 lib_ignore = XPowersLib From 4c88c25e782c1f28538ff443e5b92fddf607f139 Mon Sep 17 00:00:00 2001 From: Enygma <94997734+EnygmaINC@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:00:28 -0500 Subject: [PATCH 3/5] lcdwiki-es3c35p: make Turn-off wake on BOOT and switch the backlight off powerOff() was copied from ES3C28P and overrode Launcher's default, which enables the GPIO0 wake source. It called esp_deep_sleep_start() with no wake source, so only a reset or power cycle could bring the board back, and the backlight pad was left to float. Now enable ext0 wake on GPIO0 (BOOT), latch the backlight pin low for the sleep and release it in _setup_gpio(). Touch cannot wake deep sleep: its INT (GPIO47) is not an RTC pad. Verified on hardware: screen fully dark, BOOT wakes, touch does not. Co-Authored-By: Claude Sonnet 5.5 --- boards/lcdwiki-es3c35p/connections.md | 8 ++++++-- boards/lcdwiki-es3c35p/interface.cpp | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/boards/lcdwiki-es3c35p/connections.md b/boards/lcdwiki-es3c35p/connections.md index 5b52547b..8b3b7165 100644 --- a/boards/lcdwiki-es3c35p/connections.md +++ b/boards/lcdwiki-es3c35p/connections.md @@ -95,5 +95,9 @@ Verified on real hardware (COM6, 2026-11-09): display (orientation, colours, backlight PWM), capacitive touch (menu navigation, on-screen keyboard / Wi-Fi password entry), SD card, and all four rotations (display and touch, switched at runtime from the Launcher Rotation menu), and battery sense (GPIO 8, checked -against a multimeter with a 3.7 V 850 mAh LiPo). Not yet verified: audio, -deep sleep / wake. +against a multimeter with a 3.7 V 850 mAh LiPo), and Turn-off / deep sleep +(screen fully dark, wakes on BOOT; tested on USB + battery). Touch cannot wake +it: the touch INT is GPIO 47, which is not an RTC pad, so BOOT (GPIO 0) is the +only wake source. `powerOff()` in `interface.cpp` latches the backlight pin low +for the sleep and releases it in `_setup_gpio()`. Not yet verified: audio, and +sleep on battery alone (USB unplugged). diff --git a/boards/lcdwiki-es3c35p/interface.cpp b/boards/lcdwiki-es3c35p/interface.cpp index 15c4e759..a8c17583 100644 --- a/boards/lcdwiki-es3c35p/interface.cpp +++ b/boards/lcdwiki-es3c35p/interface.cpp @@ -4,6 +4,8 @@ #include "idf/launcher_platform.h" #include "powerSave.h" #include +#include +#include #include // lcdwiki-es3c35p -- ESP32-S3 + 3.5" ST77922 (QSPI) with the capacitive touch @@ -42,6 +44,11 @@ static DeviceTouch touchCfg() { } void _setup_gpio() { + // Coming back from powerOff(): release the backlight pin hold it set, or the + // digitalWrite() below would be ignored and the screen would stay dark. + gpio_deep_sleep_hold_dis(); + gpio_hold_dis((gpio_num_t)TFT_BL); + // The ST77922 is one chip doing both display and touch, and the touch half's // reset line (GPIO 48, "TOUCH_RST") idles LOW on this board: the chip does // not answer on I2C (0x55) until it is pulsed. Release it once, up front. @@ -108,5 +115,18 @@ void InputHandler(void) { void powerOff() { neopixelWrite(RGB_LED_PIN, 0, 0, 0); + + // Backlight off and latched off for the duration of deep sleep; the pad would + // otherwise float once the LEDC peripheral loses power. Released in _setup_gpio(). + pinMode(TFT_BL, OUTPUT); + digitalWrite(TFT_BL, LOW); + gpio_hold_en((gpio_num_t)TFT_BL); + gpio_deep_sleep_hold_en(); + + // The BOOT button (GPIO0, an RTC pad) is the only possible wake source: the + // touch interrupt is on GPIO47, which is not an RTC GPIO, so a touch cannot + // wake a deep sleep. Without this the board only restarts via reset / power. + esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, LOW); + vTaskDelay(pdMS_TO_TICKS(200)); esp_deep_sleep_start(); } From 632d1ea0c5d3e5483d37cf2a3ede54087f33d5d7 Mon Sep 17 00:00:00 2001 From: Enygma <94997734+EnygmaINC@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:01:44 -0500 Subject: [PATCH 4/5] lcdwiki-es3c35p: document audio as out of scope Launcher has no sound output on any board; the codec and amp are only kept quiet at boot. Co-Authored-By: Claude Sonnet 5.5 --- boards/lcdwiki-es3c35p/connections.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/boards/lcdwiki-es3c35p/connections.md b/boards/lcdwiki-es3c35p/connections.md index 8b3b7165..46d252d5 100644 --- a/boards/lcdwiki-es3c35p/connections.md +++ b/boards/lcdwiki-es3c35p/connections.md @@ -99,5 +99,9 @@ against a multimeter with a 3.7 V 850 mAh LiPo), and Turn-off / deep sleep (screen fully dark, wakes on BOOT; tested on USB + battery). Touch cannot wake it: the touch INT is GPIO 47, which is not an RTC pad, so BOOT (GPIO 0) is the only wake source. `powerOff()` in `interface.cpp` latches the backlight pin low -for the sleep and releases it in `_setup_gpio()`. Not yet verified: audio, and -sleep on battery alone (USB unplugged). +for the sleep and releases it in `_setup_gpio()`. Not yet verified: sleep on +battery alone (USB unplugged). + +Audio is out of scope: Launcher has no sound output on any board (no tone/I2S/ +buzzer code in `src/`). The ES8311 codec (I2C 0x18) and SC8002B amp are only +kept quiet -- the amp enable (GPIO 1) is held off at boot -- and are untested. From 700e403eddb2b7df6f1e6592022798e6d1d3d89b Mon Sep 17 00:00:00 2001 From: Enygma <94997734+EnygmaINC@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:07:26 -0500 Subject: [PATCH 5/5] lcdwiki-es3c35p: remove bring-up leftovers, enable in CI matrix Remove the ST77922_BRINGUP_TEST / ST77922_TOUCH_DEBUG guards, refresh the stale "not buildable yet" header, and enable the env in the CI matrix now that it builds and is verified on hardware (per boards/NEW_BOARD_GUIDE.md). Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/main.yml | 2 +- boards/lcdwiki-es3c35p/interface.cpp | 10 +--------- boards/lcdwiki-es3c35p/platformio.ini | 11 +++++------ lib/DisplayDrivers | 2 +- src/hal/inputs/touch.cpp | 15 ++++++++------- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f0af6f0..bcc7c781 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,7 +91,7 @@ - { env: "NM-CYD-C5" } - { env: "CYD-3248W535C" } - { env: "CYD-4827S043R" } - # - { env: "lcdwiki-es3c35p" } + - { env: "lcdwiki-es3c35p" } - { env: "lilygo-t-dongle-s3-tft" } - { env: "lilygo-t-dongle-c5-tft" } - { env: "lilygo-t-display-S3-touch" } diff --git a/boards/lcdwiki-es3c35p/interface.cpp b/boards/lcdwiki-es3c35p/interface.cpp index a8c17583..c4348674 100644 --- a/boards/lcdwiki-es3c35p/interface.cpp +++ b/boards/lcdwiki-es3c35p/interface.cpp @@ -77,23 +77,15 @@ void _setup_gpio() { } void _post_setup_gpio() { -#ifndef ST77922_BRINGUP_TEST // Backlight PWM -- must be done after tft.init() hal_bright_attach(TFT_BL); hal_bright_set(TFT_BL, bright); -#endif // Capacitive touch, at this board's nonstandard I2C address. hal_touch_init(touchCfg(), TOUCH_I2C_ADDR); } -void _setBrightness(uint8_t brightval) { -#ifndef ST77922_BRINGUP_TEST - hal_bright_set(TFT_BL, brightval); -#else - (void)brightval; // bring-up: backlight held solid on by _setup_gpio() -#endif -} +void _setBrightness(uint8_t brightval) { hal_bright_set(TFT_BL, brightval); } void InputHandler(void) { static long tm = launcherMillis(); diff --git a/boards/lcdwiki-es3c35p/platformio.ini b/boards/lcdwiki-es3c35p/platformio.ini index 95f2cec6..7ea48e0f 100644 --- a/boards/lcdwiki-es3c35p/platformio.ini +++ b/boards/lcdwiki-es3c35p/platformio.ini @@ -15,12 +15,11 @@ ; see boards/ES3C28P for that board's config, most of which this one mirrors. ; https://www.lcdwiki.com/3.5inch_ESP32-S3_Display ; -; NOT BUILDABLE YET: TFT_DATABUS_N=6 / TFT_DISPLAY_DRIVER_N=52 -; (Arduino_ST77922_1L4, lib/DisplayDrivers/src/backends/Arduino_ST77922_1L4.h) -; is new and has not been compiled or flashed as part of Launcher. Its panel -; bring-up logic is ported from a version of this exact driver that WAS -; confirmed working on real ES3C35P hardware outside this repo -- see that -; file's header comment -- but the Launcher integration itself is untested. +; The panel is driven by Arduino_ST77922_1L4 (TFT_DATABUS_N=6 / +; TFT_DISPLAY_DRIVER_N=52, lib/DisplayDrivers/src/panels/Arduino_ST77922_1L4.h), +; and the built-in touch block by TOUCH_CTRL_ST77922 (src/hal/inputs/touch.cpp). +; Tested on hardware: display, touch, SD, all four rotations, battery sense, +; Turn-off / wake. See connections.md. [env:lcdwiki-es3c35p] board = esp32s3 board_build.partitions = support_files/custom_16Mb.csv diff --git a/lib/DisplayDrivers b/lib/DisplayDrivers index fafb0ccd..95c1db90 160000 --- a/lib/DisplayDrivers +++ b/lib/DisplayDrivers @@ -1 +1 @@ -Subproject commit fafb0ccd947e9bc206d4d65309abcfeed2136d99 +Subproject commit 95c1db9003a3b3de541ca148328f3c23e6056269 diff --git a/src/hal/inputs/touch.cpp b/src/hal/inputs/touch.cpp index b25b07fc..b3b874b2 100644 --- a/src/hal/inputs/touch.cpp +++ b/src/hal/inputs/touch.cpp @@ -221,9 +221,6 @@ struct TouchDrvST77922 { if (p < 0) return false; *x = (int16_t)(((r[p] & 0x3F) << 8) | r[p + 1]); *y = (int16_t)((r[p + 2] << 8) | r[p + 3]); -#ifdef ST77922_TOUCH_DEBUG - launcherConsolePrintf("[ST77922 touch] raw x=%d y=%d\n", (int)*x, (int)*y); -#endif return true; } @@ -364,13 +361,17 @@ bool hal_touch_init(const DeviceTouch &cfg, uint8_t i2c_addr, bool xpt_shared_sp // Probe with real register reads and report what the chip says about itself. { uint8_t ver = 0, info[5] = {0}; - if (!touch.readReg(ST77922_REG_FW_VERSION, &ver, 1) || !touch.readReg(ST77922_REG_MAX_XY, info, sizeof(info))) { + if (!touch.readReg(ST77922_REG_FW_VERSION, &ver, 1) || + !touch.readReg(ST77922_REG_MAX_XY, info, sizeof(info))) { launcherConsolePrintf("[ST77922 touch] no answer at 0x%02X\n", touch.addr); return false; } launcherConsolePrintf( - "[ST77922 touch] fw=%u maxX=%u maxY=%u maxTouches=%u\n", ver, (info[0] << 8) | info[1], - (info[2] << 8) | info[3], info[4] + "[ST77922 touch] fw=%u maxX=%u maxY=%u maxTouches=%u\n", + ver, + (info[0] << 8) | info[1], + (info[2] << 8) | info[3], + info[4] ); return true; } @@ -403,7 +404,7 @@ bool hal_touch_read(const DeviceTouch &cfg, LTouchPoint &out) { int16_t x = 0, y = 0; if (!touch.getPoint(&x, &y, 1)) return false; #endif -#if defined(TOUCH_CTRL_XPT2046) || defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_AXS5106L) || \ +#if defined(TOUCH_CTRL_XPT2046) || defined(TOUCH_CTRL_FT6X36) || defined(TOUCH_CTRL_AXS5106L) || \ defined(TOUCH_CTRL_ST77922) if (cfg.SwapXY[r]) { int16_t t = x;