LilyGO T-Display S3 Long — ESPHome Screen Support

This board has no existing HA Community topic, so this write-up covers the hardware, the ESPHome drivers used, the LVGL quirks this panel needed, and the SY6970 PMU integration

Screen spec

Component Part Notes
MCU ESP32-S3R8 Native WiFi, octal PSRAM, 16MB flash, ESP-IDF framework
Display panel AXS15231B, QSPI 3.4" bar panel, native geometry 180 (W) x 640 (H), driven at 90 degrees rotation for a 640x180 logical landscape bar
Touch controller CST3530, capacitive I2C, shares the bus with the PMU
PMU SY6970 I2C address 0x6A, Li-ion battery charger with onboard STAT LED
Backlight LEDC PWM GPIO1, monochromatic light entity

The QSPI bus uses GPIO17 for clock and GPIO13/18/21/14 for data; the display’s chip-select is GPIO12 and reset is GPIO16. Pin assignments come from LilyGO’s own example firmware for this board.

Drivers used

Everything here is a stock ESPHome component; no custom C++ driver was needed for the display, touch, or PMU.

Peripheral ESPHome component Config
Display mipi_spi model: AXS15231, native dims 180x640, rotation: 90
Touch touchscreen: axs15231 I2C, shares touchscreen_bus with the PMU
PMU sy6970 I2C address 0x6a, on touchscreen_bus
Backlight output: ledc + light: monochromatic GPIO1

The AXS15231B has no hardware axis-swap bit, so ESPHome applies the 90-degree rotation as a software LVGL-level rotation rather than a native panel rotation. That single fact drives the touch quirk in the next section.

Modifications made (LVGL quirks)

Touch calibration must target the panel’s native, unrotated axes

ESPHome’s LVGL component rotates incoming touch coordinates itself when rotation: 90 is set: for a native width W, it computes new_x = old_y and new_y = W - old_x - 1. That means the touchscreen component’s own calibration.x_min/x_max/y_min/y_max has to describe the display’s native, unrotated geometry (180 wide x 640 tall) - not the logical 640x180 canvas the LVGL pages are laid out on.

The first attempt calibrated against the logical 640x180 canvas instead, and it showed up on hardware as touch feeling squeezed to one side, with only one button tile responding and the Y reading staying at 0 until the far edge of the screen was touched. Raw touch-coordinate logging plus reading the LVGL rotation source pinned this down. The fix:

touchscreen:
- platform: axs15231
  transform:
    mirror_x: false
    mirror_y: true
    swap_xy: true
  calibration:
    x_min: 0
    x_max: 180
    y_min: 0
    y_max: 640

swap_xy is needed because the sensor’s raw X/Y come out swapped relative to which native axis each should scale against; mirror_x/mirror_y were confirmed by on-hardware button testing until tiles landed on the correct side.

Narrow tiles need a taller label clamp

This panel’s grid is 4 columns x 1 row across a 640px-wide bar, so each tile is only about 160px wide. The date/time tile’s label config (labelLines) was left at 1 when this device profile was first written, while every other device profile in the catalog uses 2 (or 3 for double-height tiles). At 160px, a date string with a month name wraps to a second line, and with labelLines: 1 the label’s clamp height was computed for one line only, so the wrapped month got clipped instead of shown. Setting labelLines: 2 for this device gave the clamp enough height for both lines.

PMU updates

What was added

  • A sy6970 PMU config on the shared touch I2C bus: 1000mA input current limit, 4200mV charge voltage, 500mA charge current, 128mA precharge current, charging disabled by default.
  • Diagnostic sensors: binary vbus_connected, charging, charge_done; text bus_status, charge_status.
  • A template switch to enable/disable charging, exposed to Home Assistant, defaulting to off.

The original motivation was simple: stop the flashing green STAT LED since I don’t use a battery

The I2C watchdog problem

The SY6970 has a built-in I2C communication watchdog (register REG07, bits 5:4, WATCHDOG[1:0]; power-on default is 01 = 40 seconds). If the host never resets it, the chip silently reverts several registers back to power-on defaults, including the charge-enable bit (REG03 CHG_CONFIG) and the STAT LED disable bit (REG07 STAT_DIS). ESPHome’s sy6970 component never touches this register, so charging and the LED would both quietly re-enable roughly 40 seconds after every boot or after any toggle from Home Assistant - matching exactly what was observed on hardware.

The fix

The stock component never services this watchdog, so the fix belongs in the component. I raised it upstream:

  • ESPHome: esphome/esphome#19424 adds an i2c_watchdog_timeout option (DISABLED, 40S, 80S, 160S, default 40S) and resets the watchdog automatically on every successful update, so the chip’s safety net stays enabled. A config error is raised if update_interval is not shorter than the timeout. reset_i2c_watchdog() is also available from lambdas.
  • Docs: esphome/esphome.io#7412

Credits

This device profile is built entirely on top of EspControl by Jonathan Tenniswood - the firmware, LVGL control-panel UI, and web configurator that make this screen usable at all. Docs: jtenniswood.github.io/espcontrol.

Pin assignments for the display, touch, and PMU come from LilyGO’s own example firmware: Xinyuan-LilyGO/T-Display-S3-Long (examples/*/pins_config.h).

A basic, EspControl-independent ESPHome config (LVGL hello-world example) was also contributed back to that same LilyGO repo, on the T-Display-S3-Long-cst3530 branch: examples/ESPHome_LVGL_HelloWorld. It’s a good starting point for anyone who wants bare ESPHome + LVGL on this board without pulling in the full EspControl package.

The display and touch drivers are the stock ESPHome mipi_spi (AXS15231 model) and axs15231 touchscreen components; PMU support uses ESPHome’s native sy6970 component. No custom ESPHome components were written for this board.

This work was originally submitted upstream as a PR to EspControl; the maintainer closed it since they don’t have this exact panel to test for regressions on. The device profile lives on this fork branch for anyone else picking up this board: willumpie82/espcontrol, add-lilygo-t-display-s3-long.

Hello,

Nice project. I still have one of these boards lying around and I’m already using espcontrol.
How can I compile/integrate this device in espcontrol? I tried using the esphome.yaml but that doesn’t work.

Gr. Remco

Hello,

Iam a step further, the following yaml file compiles:

substitutions:
  name: "espcontrol-kitchen"
  friendly_name: "EspControl Kitchen"
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
packages:
  api_encryption:
    url: https://github.com/jtenniswood/espcontrol/
    file: common/addon/api_encryption_dynamic.yaml
    refresh: 1sec
  setup:
    url: https://github.com/willumpie82/espcontrol
    ref: add-lilygo-t-display-s3-long
    file: devices/lilygo-t-display-s3-long/packages.yaml
    refresh: 1sec

But at the end I get an error:

/Users/remco/esphome/.esphome/.remote_builds/R_mT_zCn/.esphome/packages/945fee8c/devices/lilygo-t-display-s3-long/../../common/addon/time.yaml: In lambda function:
/Users/remco/esphome/.esphome/.remote_builds/R_mT_zCn/.esphome/packages/945fee8c/devices/lilygo-t-display-s3-long/../../common/addon/time.yaml:432:21: error: 'class esphome::sntp::SNTPComponent' has no member named 'set_timezone'; did you mean 'set_timeout'?
432 |             id(panel_time).set_timezone(posix);
|                     ^~~~~~~~~~~~
|                     set_timeout
/Users/remco/esphome/.esphome/.remote_builds/R_mT_zCn/.esphome/packages/945fee8c/devices/lilygo-t-display-s3-long/../../common/addon/time.yaml:433:29: error: 'class esphome::homeassistant::HomeassistantTime' has no member named 'set_timezone'; did you mean 'set_timeout'?
433 |             id(homeassistant_time).set_timezone(posix);
|                             ^~~~~~~~~~~~
|                             set_timeout
/Users/remco/esphome/.esphome/.remote_builds/R_mT_zCn/.esphome/packages/945fee8c/devices/lilygo-t-display-s3-long/../../common/addon/time.yaml: In lambda function:
/Users/remco/esphome/.esphome/.remote_builds/R_mT_zCn/.esphome/packages/945fee8c/devices/lilygo-t-display-s3-long/../../common/addon/time.yaml:192:21: error: 'class esphome::sntp::SNTPComponent' has no member named 'set_timezone'; did you mean 'set_timeout'?
192 |               id(panel_time).set_timezone(posix);
|                     ^~~~~~~~~~~~
|                     set_timeout
/Users/remco/esphome/.esphome/.remote_builds/R_mT_zCn/.esphome/packages/945fee8c/devices/lilygo-t-display-s3-long/../../common/addon/time.yaml:193:29: error: 'class esphome::homeassistant::HomeassistantTime' has no member named 'set_timezone'; did you mean 'set_timeout'?
193 |               id(homeassistant_time).set_timezone(posix);

This is fixed in the latest EspControl control. But not in your fork.

Gr. Remco

just checkout my fork

willumpie82/espcontrol, add-lilygo-t-display-s3-long

I did that, But your branch does not compile anymore on esphome 2026.9. The errors are identified in the original repo and fixed in the latter versions. But your fork is from before that time so it does not compile anymore.

Gr. Remco

Ah I see,

should be fixed now (you can use my main branch now)