Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
107 changes: 107 additions & 0 deletions boards/lcdwiki-es3c35p/connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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 | 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

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), 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: 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.
124 changes: 124 additions & 0 deletions boards/lcdwiki-es3c35p/interface.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#include "hal/bright/bright.h"
#include "hal/device.h"
#include "hal/inputs/touch.h"
#include "idf/launcher_platform.h"
#include "powerSave.h"
#include <Wire.h>
#include <driver/gpio.h>
#include <esp_sleep.h>
#include <interface.h>

// 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() {
// 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.
// 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() {
// Backlight PWM -- must be done after tft.init()
hal_bright_attach(TFT_BL);
hal_bright_set(TFT_BL, bright);

// Capacitive touch, at this board's nonstandard I2C address.
hal_touch_init(touchCfg(), TOUCH_I2C_ADDR);
}

void _setBrightness(uint8_t brightval) { hal_bright_set(TFT_BL, brightval); }

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);

// 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();
}
102 changes: 102 additions & 0 deletions boards/lcdwiki-es3c35p/platformio.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
; 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
;
; 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
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

; 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
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading