Getting the Lilygo T-Panel (Lite) to work

Ok, this is about the Lilygo T-Panel with a resolution of 460x460 or 480x480. Unfortunately, the documentation is already misleading at this point. I bought the T-Panel Lite, which is still advertised as having a capacitive touchscreen:

In the comments on Github, however, it was made clear that the Lite version does not have a built-in touchscreen.

There are 2 repos of the T-Panel on Github:

Unfortunately, only the first one is linked on the Lilygo website, so I initially tried to get the device to work with a completely incorrect configuration and pinmap.
Even now after numerous attempts, trial and error and minor successes, some basic things are still unclear to me, e.g. the resolution of the display, which according to the website is 460x460 for the T-Panel Lite, but in the code examples on Github the display is initialized with 480x480 - so you just don’t know what is actually correct.

I would therefore like to show my current intermediate status in the hope that someone can contribute something to get the T-Panel (Lite) up and running. Let’s start with what already works:

  • The ESP32s3 can be flashed and connected with ESPHome
  • WiFi signal and internal temperature can be read out and displayed in HomeAssistant
  • The display backlight can be activated and changed between 0% and 100% via a slider in HomeAssistant
  • When the display is on, it shows colored pixels that appear and disappear in pulses

And this is my current code:

esphome:
  name: lilygo-panel
  friendly_name: Lilygo Panel
  platformio_options:
    #set frequency to 80MHz
    board_build.f_flash: 80000000L
    board_build.flash_mode: qio
    board_build.flash_size: 16MB
    #board_build.arduino.memory_type: qio_qspi
            
esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf
    advanced:
      ignore_efuse_mac_crc: True
    #sdkconfig_options:
      #CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: y
      #CONFIG_ESP32S3_DATA_CACHE_64KB: y
      #CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
      #CONFIG_SPIRAM_RODATA: y
    sdkconfig_options:
      CONFIG_ESP32S3_SPIRAM_SUPPORT: y
      CONFIG_SPIRAM_CACHE_WORKAROUND: y
      CONFIG_ESP32S3_SPIRAM_USE: y
      CONFIG_ESP32S3_SPIRAM_SPEED_80M: y
      CONFIG_SPIRAM_BANKSWITCH_ENABLE: y
      #CONFIG_ESPTOOLPY_FLASHSIZE_16MB: y
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: y

logger:
  #level: DEBUG


# Enable Home Assistant API
api:
  encryption:
    key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

ota:
  - platform: esphome
    password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Lilygo-Panel Fallback Hotspot"
    password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

captive_portal:
    
sensor:
  - platform: wifi_signal
    name: WLAN Signal
    update_interval: 300s
  - platform: internal_temperature
    name: Interne Temperatur
    update_interval: 300s

#psram:
  #mode: octal
  #speed: 40MHz

output:
  - platform: ledc
    pin: 
      number: GPIO33
    id: ledc_gpio
#    frequency: 100Hz

light:
  - platform: monochromatic
    output: ledc_gpio
    name: "Display Helligkeit"
    id: display_backlight
    restore_mode: ALWAYS_ON
    entity_category: config

#Display
spi:
  - clk_pin: 36
    mosi_pin: 35

display:
  - platform: st7701s
    id: disp
    #auto_clear_enabled: false
    #update_interval: never
    #spi_mode: MODE3
    #color_order: BGR
    color_order: RGB
    dimensions:
      width: 480
      height: 480
    #invert_colors: true
    #transform:
    #  mirror_x: true
    #  mirror_y: true
    cs_pin: 14
    #reset_pin: 39
    de_pin: 38
    hsync_pin: 39
    vsync_pin: 40
    pclk_pin: 41
    hsync_pulse_width: 1
    hsync_front_porch: 20
    hsync_back_porch: 1
    vsync_pulse_width: 1
    vsync_front_porch: 30
    vsync_back_porch: 10
    #pclk_frequency:
    #pclk_inverted: False
    #init_sequence:
    #  - 1 # select canned init sequence number 1
    #  - [ 0xE0, 0x1F ]  # Set sunlight readable enhancement
    setup_priority: -100
    #init_sequence:
    #  - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x13]
    #  - [0xEF, 0x08]
    #  - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10]
    #  - [0xC0, 0x3B, 0x00]
    #  - [0xC1, 0x0B, 0x02]
    #  - [0xC2, 0x07, 0x02]
    #  - [0xCC, 0x10]
    #  - [0xCD, 0x08]
    #  - [0xB0, 0x02, 0x13, 0x1B, 0x0D, 0x10, 0x05, 0x08, 0x07, 0x07, 0x24, 0x04, 0x11, 0x0E, 0x2C, 0x33, 0x1D]
    #  - [0xB1, 0x05, 0x13, 0x1B, 0x0D, 0x11, 0x05, 0x08, 0x07, 0x07, 0x24, 0x04, 0x11, 0x0E, 0x2C, 0x33, 0x1D]
    #  - [0xE0, 0x00, 0x00, 0x02]
    #  - [0x11]
    #  - delay 120ms
    #  - [0x3A, 0x55]
    #  - [0x36, 0x08]
    #  - [0x29]
    #  - delay 120ms
    init_sequence:
      - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x13]  # INIT 1
      - [0xEF, 0x08]                           # INIT 2
      - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10]  # INIT 3
      - [0xC0, 0x3B, 0x00]                     # INIT 4
      - [0xC1, 0x0B, 0x02]                     # INIT 5
      - [0xC2, 0x30, 0x02, 0x37]               # INIT 6
      - [0xCC, 0x10]                           # INIT 7
      - [0xB0, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x09, 0x08, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18]  # Gamma Control Positive
      - [0xB1, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x08, 0x09, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18]  # Gamma Control Negative
      - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x11]  # INIT 8
      - [0xB0, 0x4D]                           # INIT 9
      - [0xB1, 0x33]                           # INIT 10
      - [0xB2, 0x87]                           # INIT 11
      - [0xB5, 0x4B]                           # INIT 12
      - [0xB7, 0x8C]                           # INIT 13
      - [0xB8, 0x20]                           # INIT 14
      - [0xC1, 0x78]                           # INIT 15
      - [0xC2, 0x78]                           # INIT 16
      - [0xD0, 0x88]                           # INIT 17
      - [0xE0, 0x00, 0x00, 0x02]               # INIT 18
      - [0xE1, 0x02, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x44, 0x44]  # INIT 19
      - [0xE2, 0x10, 0x10, 0x40, 0x40, 0xF2, 0xF0, 0x00, 0x00, 0xF2, 0xF0, 0x00, 0x00]  # INIT 20
      - [0xE3, 0x00, 0x00, 0x11, 0x11]        # INIT 21
      - [0xE4, 0x44, 0x44]                     # INIT 22
      - [0xE5, 0x07, 0xEF, 0xF0, 0xF0, 0x09, 0xF1, 0xF0, 0xF0, 0x03, 0xF3, 0xF0, 0xF0, 0x05, 0xED, 0xF0, 0xF0]  # INIT 23
      - [0xE6, 0x00, 0x00, 0x11, 0x11]        # INIT 24
      - [0xE7, 0x44, 0x44]                     # INIT 25
      - [0xE8, 0x08, 0xF0, 0xF0, 0xF0, 0x0A, 0xF2, 0xF0, 0xF0, 0x04, 0xF4, 0xF0, 0xF0, 0x06, 0xEE, 0xF0, 0xF0]  # INIT 26
      - [0xEB, 0x00, 0x00, 0xE4, 0xE4, 0x44, 0x88, 0x40]  # INIT 27
      - [0xEC, 0x78, 0x00]                     # INIT 28
      - [0xED, 0x20, 0xF9, 0x87, 0x76, 0x65, 0x54, 0x4F, 0xFF, 0xFF, 0xF4, 0x45, 0x56, 0x67, 0x78, 0x9F, 0x02]  # INIT 29
      - [0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F]  # INIT 30
      - [0x3A, 0x55]                           # Color Mode
      - [0x36, 0x08]                           # Memory Data Access Control
      - [0x11]                                 # Sleep Out
      - [0x29]                                 # Display On

    data_pins:
      blue:
        - 1         #b0
        - 2         #b1
        - 3         #b2
        - 4         #b3
        - 5         #b4
      green:
        - 6         #g0
        - 7         #g1
        - 8         #g2
        - 9         #g3
        - 10        #g4
        - 11        #g5
      red:
        - 12        #r0
        - 13        #r1
        - 42        #r2
        - 46        #r3
        - 45        #r4
    lambda: |-
      it.print(10, 10, id(normal), "Display Test");
     
font:
  - file: "fonts/MollenBold.otf"
    id: normal
    size: 24
    bpp: 4

As you can see, I’ve already tried a lot back and forth. This is probably why the code now contains some useless or incorrect lines of code.
Hoping that at least the pin assignments are correct, I suspect the main reason for the non-functioning display is its initialization sequence. However, I cannot rule out the possibility that something else is wrong, as the Lilygo documentation cannot be relied upon.

Maybe someone has another tip for me to get the display working.

That is typical of lilygo.

Hi @nickrout , I just got the T-Panel S3 RS-485, 480x480 and stumbled upon this trying to figure out what I could do…
Did you had any progress or luck so far?

I’ve been able to upload a test with Squareline studio, not esphome trials so far.

Thanks

I found this project for Arduino and it works well on T-Panel S3 (480x480 with RS485) not the Lite version, pins are different but sharing same MCU, same display driver and RGB panel:
https://github.com/jbanyer/squareline_lilygo_t_panel_starter.

So, the init sequence is the same as yours and it works fine on arduino.

Main difference from T-Panel Lite is the presence of I/O expander which gives access to SPI pins and LCD CS and LCD Reset.
I tried my configuration, no luck.

################################
# LILYGO T-PANEL S3 [with RS485], MCU: ESP32S3
#
# resources:
# official github: https://github.com/Xinyuan-LilyGO/T-Panel 
#
# A sample project TESTED with arduino: https://github.com/jbanyer/squareline_lilygo_t_panel_starter
#   pins: https://github.com/jbanyer/squareline_lilygo_t_panel_starter/blob/1d894c426c94dd30d8f70172eb76166fae314380/lib/config/pin_config.h#:~:text=pin_config.h
#   init sequence: https://github.com/jbanyer/squareline_lilygo_t_panel_starter/blob/1d894c426c94dd30d8f70172eb76166fae314380/lib/Arduino_GFX-1.4.6/src/display/Arduino_RGB_Display.h
#
# Some pins are connected to I/O espander: SPI CLK, SPI MOSI, LCD CS, LCD RESET
# see circuit https://github.com/Xinyuan-LilyGO/T-Panel/blob/6a1f2636dc2490d0c33338926ba862c3846202fd/project/T-Panel_V1.2.pdf

substitutions:
  device_name: "tpanel_studio"
  friendly_name: "Studio T-Panel"

esphome:
  name: tpanel_studio
  friendly_name: "$friendly_name"
  comment: "Lilygo T-Panel S3"
  project:
    name: "umbex.$device_name"
    version: "v0.12"  

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf
    sdkconfig_options:    # not sure these are necessary 
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
      CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
      CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
      CONFIG_SPIRAM_RODATA: y

psram:  # not sure these are necessary 
  mode: octal
  speed: 80MHz

logger:
  level: VERBOSE 

api:
  encryption:
    key: !secret api_password

ota:
  - platform: esphome
    password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  id: wifi_id

  ap:
    ssid: "esphome $device_name"
    password: !secret wifi_password

captive_portal:

# serial interface 
i2c:
  sda: GPIO17
  scl: GPIO18 

# some pins are connected to I/O espander, 
# see circuit https://github.com/Xinyuan-LilyGO/T-Panel/blob/6a1f2636dc2490d0c33338926ba862c3846202fd/project/T-Panel_V1.2.pdf
xl9535:
  - id: xl9535_hub
    address: 0x20

# SPI via I2C I/O expander
spi:
  clk_pin: 
    xl9535: xl9535_hub
    number: 15  
  mosi_pin: 
    xl9535: xl9535_hub
    number: 16

display:
  - platform: st7701s
    setup_priority: -100        # SPI load error if omitted
    id: disp
    auto_clear_enabled: false    # not sure
    update_interval: 60s        # or never
    spi_mode: MODE3             # not really sure but MODE0, MODE1, MODE2 does not work either
    color_order: RGB
    dimensions:
      width: 480
      height: 480
    invert_colors: false        # not sure 
    transform:
      mirror_x: true
      mirror_y: true
    cs_pin:                     # LCD_CS pin via I/O espander 
      xl9535: xl9535_hub
      number: 17
    de_pin: GPIO0               # DE pin not exists in circuit. so... NOT USED BY THIS DISPLAY? Assigning a free pin because required
    reset_pin:                 
      xl9535: xl9535_hub      
      number: 5
    # data_rate: 8MHz           # tested, no difference 
    pclk_frequency: 6MHz        
    pclk_inverted: true         # this seems to be inverted in jbanyer's source, with "10 /* pclk_active_neg"
    hsync_pin: GPIO39           # as in jbanyer's arduino test
    vsync_pin: GPIO40           # as in jbanyer's arduino test
    pclk_pin: GPIO41            # as in jbanyer's arduino test
    hsync_pulse_width: 2        # as in jbanyer's arduino test
    hsync_front_porch: 20       # as in jbanyer's arduino test
    hsync_back_porch: 0         # as in jbanyer's arduino test
    vsync_pulse_width: 8        # as in jbanyer's arduino test
    vsync_front_porch: 30       # as in jbanyer's arduino test
    vsync_back_porch: 1         # as in jbanyer's arduino test

    init_sequence:              # as in jbanyer's arduino test
      - [0x01] # Software Reset
      - delay 120ms
      - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x13]  # INIT 1
      - [0xEF, 0x08]                           # INIT 2
      - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10]  # INIT 3
      - [0xC0, 0x3B, 0x00]                     # INIT 4
      - [0xC1, 0x0B, 0x02]                     # INIT 5
      - [0xC2, 0x30, 0x02, 0x37]               # INIT 6
      - [0xCC, 0x10]                           # INIT 7
      - [0xB0, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x09, 0x08, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18]  # Gamma Control Positive
      - [0xB1, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x08, 0x09, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18]  # Gamma Control Negative
      - [0xFF, 0x77, 0x01, 0x00, 0x00, 0x11]  # INIT 8
      - [0xB0, 0x4D]                           # INIT 9
      - [0xB1, 0x33]                           # INIT 10
      - [0xB2, 0x87]                           # INIT 11
      - [0xB5, 0x4B]                           # INIT 12
      - [0xB7, 0x8C]                           # INIT 13
      - [0xB8, 0x20]                           # INIT 14
      - [0xC1, 0x78]                           # INIT 15
      - [0xC2, 0x78]                           # INIT 16
      - [0xD0, 0x88]                           # INIT 17
      - [0xE0, 0x00, 0x00, 0x02]               # INIT 18
      - [0xE1, 0x02, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x44, 0x44]  # INIT 19
      - [0xE2, 0x10, 0x10, 0x40, 0x40, 0xF2, 0xF0, 0x00, 0x00, 0xF2, 0xF0, 0x00, 0x00]  # INIT 20
      - [0xE3, 0x00, 0x00, 0x11, 0x11]        # INIT 21
      - [0xE4, 0x44, 0x44]                     # INIT 22
      - [0xE5, 0x07, 0xEF, 0xF0, 0xF0, 0x09, 0xF1, 0xF0, 0xF0, 0x03, 0xF3, 0xF0, 0xF0, 0x05, 0xED, 0xF0, 0xF0]  # INIT 23
      - [0xE6, 0x00, 0x00, 0x11, 0x11]        # INIT 24
      - [0xE7, 0x44, 0x44]                     # INIT 25
      - [0xE8, 0x08, 0xF0, 0xF0, 0xF0, 0x0A, 0xF2, 0xF0, 0xF0, 0x04, 0xF4, 0xF0, 0xF0, 0x06, 0xEE, 0xF0, 0xF0]  # INIT 26
      - [0xEB, 0x00, 0x00, 0xE4, 0xE4, 0x44, 0x88, 0x40]  # INIT 27
      - [0xEC, 0x78, 0x00]                     # INIT 28
      - [0xED, 0x20, 0xF9, 0x87, 0x76, 0x65, 0x54, 0x4F, 0xFF, 0xFF, 0xF4, 0x45, 0x56, 0x67, 0x78, 0x9F, 0x02]  # INIT 29
      - [0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F]  # INIT 30
      - [0x3A, 0x55]                           # Color Mode
      - [0x36, 0x08]                           # Memory Data Access Control
      - [0x11]                                 # Sleep Out
      - delay 120ms
      - [0x29]                                 # Display On

    data_pins:      # as in jbanyer's arduino test
      blue:
        - 1         #b0
        - 2         #b1
        - 3         #b2
        - 4         #b3
        - 5         #b4
      green:
        - 6         #g0
        - 7         #g1
        - 8         #g2
        - 9         #g3
        - 10        #g4
        - 11        #g5
      red:
        - 12        #r0
        - 13        #r1
        - 42        #r2
        - 46        #r3
        - 45        #r4
    lambda: |-
      it.print(10, 10, id(normal), id(alert_color), TextAlign::TOP_LEFT, "Hello World!");
     
font:
  - file: "fonts/Roboto-Regular.ttf"
    id: normal
    size: 20

color:
  - id: alert_color 
    red: 100%
    green: 20%
    blue: 20%
    
sensor:
  # Uptime sensor.
  - platform: uptime
    name: "$device_name uptime"
  - platform: homeassistant
    entity_id: sensor.vmc_studio_co2
    id: vmc_studio_co2
    on_value:
      - component.update: disp    # trying to force display update

time: 
  - platform: homeassistant  # Sync time with Home Assistant.
    id: homeassistant_time
    update_interval: 30s

output:
  - platform: ledc           # Display backlight 
    pin: 
      number: GPIO14  # ok 
    id: ledc_gpio
    frequency: 100Hz
    inverted: False

light:
  - platform: monochromatic   # Display backlight 
    output: ledc_gpio
    name: "$device_name Display light"
    id: display_backlight
    restore_mode: ALWAYS_ON
    entity_category: config

another config from @clydebarrow, unfortunately not working for me either

data pins for green are typos, but there are various more syntax errors when compiling unfortunately
- GPIO10 #g4
- GPIO11 #g5

That file hasn’t been tested - it’s basically a copy of the Waveshare 4" config that I’d started to modify.

thanks for confirming.

Got this device for the idea of using it as AP for OpenEPaperLink, but then decided against it and would rather use it as display.

But as it turns out, nobody got it yet working with esphome hence I’ll probably return it. A pity, it is nice hardware and the ESP32-H2 also just got supported by esphome with the last release

Here is a working config for the T-Panel S3 display. No touchscreen yet, that is going to need a new i2c component.

substitutions:
  name: lilygo-t-panel-s3
  friendly_name: Lilygo T-Panel S3

esphome:
  name: "${name}"
  platformio_options:
    board_build.flash_mode: dio

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

external_components:
  - source: github://pr#9892
    components: [mipi, mipi_rgb]
    refresh: 1h

logger:

psram:
  speed: 80MHz

i2c:
  - id: tp_i2c
    sda: 17
    scl: 18
    scan: true
    frequency: 400kHz

xl9535:
  id: expander

#touchscreen:
  #- platform: gt911
    #i2c_id: tp_i2c
    #id: my_touchscreen
    #update_interval: 16ms

light:
  - platform: monochromatic
    output: backlight_output
    name: LCD Backlight
    id: lcd_backlight
    restore_mode: ALWAYS_ON
    default_transition_length: 0s
    gamma_correct: 1.0

spi:
  - id: lcd_spi
    interface: software
    mosi_pin:
      xl9535:
      number: 16
    clk_pin:
      xl9535:
      number: 15

output:
  - platform: ledc
    id: backlight_output
    pin: 14
    frequency: 1220Hz
    inverted: false
    min_power: 15%

display:
  - platform: mipi_rgb
    model: t-panel-s3
    id: st7701s_disp

lvgl:

3 Likes

Sorry for the late reply but I would say that is false.
You can always rely on lilygo for having incorrect documentation.
I would even say they are very consistent, and reliable on it.

Both T-Panel S3 and T-Panel S3 Lite have 480x480. Lite has a case obscuring a little so it’s 460x460 visible.

I tried some of the code for the T-Panel S3 that is in this thread and ESPHome syntax seems to have changed too much for a simple rewrite :roll_eyes:

Might as well go back to just programming something in PlatformIO directly without ESPHome if it’s to be this much trouble.

1 Like

This took some time to figure out, but I’ve got it working (stable, backlight on, drawing to screen). Posting my complete config + notes in case it helps the next person.

What’s working

  • Custom ST7701S display driver (RGB, HSYNC/VSYNC mode)
  • XL9535 I/O expander on I²C 0x20
  • Backlight on PWM (GPIO14)
  • Simple test render (white background + red square)
  • Boot sequencing + first frame update (avoids watchdog hiccups)

Why these choices

  • PSRAM QUAD mode @ 80 MHz: avoids the octal-mode pin conflict with SD (uses GPIO35–37).
  • I²C logging disabled: prevents WDT stalls during heavy init.
  • Wi-Fi fast_connect + reboot_timeout: 0s: avoids spurious reboots if your AP is slow to respond.

Folder layout

/config/esphome/
  ├─ tpanel_display.yaml
  └─ components/
      └─ st7701s_tpanel/
          ├─ __init__.py
          ├─ st7701s_tpanel.h
          └─ st7701s_tpanel.cpp

Put the custom driver files in /config/esphome/components/st7701s_tpanel/.
The YAML below references it via external_components: → source: { type: local, path: components }.


[details=“Full config: /config/esphome/tpanel_display.yaml”]

# tpanel_display.yaml - Complete T-Panel S3 V1.3 Configuration
# Save as: /config/esphome/tpanel_display.yaml

esphome:
  name: tpanel
  friendly_name: T-Panel Display
  platformio_options:
    board_build.flash_mode: dio
    board_build.arduino.memory_type: qio_opi
    board_build.psram_type: opi
    board_build.f_flash: 80000000L
    board_build.flash_size: 16MB
  on_boot:
    - priority: -100  # Run after everything else
      then:
        - delay: 1s
        - light.turn_on:
            id: backlight
            brightness: 100%
        - delay: 2s
        - logger.log: "Triggering first display update..."
        - component.update: main_display

esp32:
  board: esp32-s3-devkitc-1
  variant: ESP32S3
  framework:
    type: esp-idf
    version: recommended
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"

      # PSRAM core enables
      CONFIG_ESP32S3_SPIRAM_SUPPORT: "y"
      CONFIG_SPIRAM: "y"
      CONFIG_SPIRAM_BOOT_INIT: "y"
      CONFIG_SPIRAM_TYPE_AUTO: "y"

      # Try QUAD mode instead of OCTAL (SD card uses GPIO35-37)
      CONFIG_SPIRAM_MODE_QUAD: "y"
      CONFIG_SPIRAM_SPEED_80M: "y"

      # Useful alloc settings
      CONFIG_SPIRAM_USE_MALLOC: "y"
      CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL: "16384"

      # Watchdog configuration - increase timeout
      CONFIG_ESP_TASK_WDT: "y"
      CONFIG_ESP_TASK_WDT_TIMEOUT_S: "30"
      CONFIG_ESP_TASK_WDT_PANIC: "y"
      CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0: "n"
      CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1: "n"

# External component - custom driver
external_components:
  - source:
      type: local
      path: components
    components: [st7701s_tpanel]

# PSRAM - Try QUAD mode instead of OCTAL since SD card uses GPIO35-37
psram:
  mode: quad  # Changed from octal
  speed: 80MHz

logger:
  level: DEBUG
  logs:
    component: ERROR
    i2c.idf: NONE  # CRITICAL: Disable I2C logging completely to prevent watchdog timeout
    i2c: NONE
    xl9535: WARN
    st7701s_tpanel: DEBUG  # Enable to see pin configuration

wifi:
  ssid: !secret wifi_2g_ssid
  password: !secret wifi_2g_password
  power_save_mode: none
  # Add these options to prevent WiFi watchdog timeout
  fast_connect: true  # Skip scanning if possible
  reboot_timeout: 0s  # Don't reboot on connection failure
  ap:
    ssid: "T-Panel-AP"
    password: "12345678"

captive_portal:

api:

ota:
  - platform: esphome

web_server:
  port: 80

# I2C Bus for XL9535 and Touch
i2c:
  sda: GPIO17
  scl: GPIO18
  frequency: 400kHz
  scan: true
  id: i2c_bus

# XL9535 I/O Expander (MUST be defined before display)
xl9535:
  - id: xl9535_hub
    address: 0x20

# Backlight control via PWM
output:
  - platform: ledc
    id: backlight_pwm
    pin: GPIO14
    frequency: 1000Hz  # Reduced from 20kHz for stability
    channel: 0  # Explicitly set LEDC channel
    inverted: false

light:
  - platform: monochromatic
    id: backlight
    name: "Display Backlight"
    output: backlight_pwm
    restore_mode: ALWAYS_ON
    default_transition_length: 0ms  # Changed from 250ms
    gamma_correct: 1.0  # No gamma correction

# Fonts
font:
  - file: "gfonts://Roboto"
    id: font_small
    size: 16
  - file: "gfonts://Roboto"
    id: font_main
    size: 24
  - file: "gfonts://Roboto"
    id: font_bold
    size: 32

# Custom ST7701S Display Component
display:
  - platform: st7701s_tpanel
    id: main_display
    xl9535_id: xl9535_hub
    color_order: RGB
    invert_colors: false
    update_interval: 5s
    auto_clear_enabled: true
    lambda: |-
      // Simple test: Fill entire screen with white
      it.fill(Color(255, 255, 255));
      
      // Big red square in center to verify
      it.filled_rectangle(140, 140, 200, 200, Color(255, 0, 0));

# Touch input (basic GPIO for now)
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO21
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Touch Detect"
    id: touch_pin
    on_press:
      - logger.log: "Touch detected!"
      - light.toggle: backlight

  - platform: status
    name: "System Status"
    id: system_status

# Sensors
sensor:
  - platform: wifi_signal
    name: "WiFi Signal"
    id: wifi_signal_sensor
    update_interval: 60s
    
  - platform: uptime
    name: "Uptime"
    id: uptime_sensor

# System controls
switch:
  - platform: restart
    name: "Restart Device"

button:
  - platform: template
    name: "Toggle Backlight"
    on_press:
      - light.toggle: backlight
  
  - platform: template
    name: "Backlight 100%"
    on_press:
      - light.turn_on:
          id: backlight
          brightness: 100%
  
  - platform: template
    name: "Backlight 50%"
    on_press:
      - light.turn_on:
          id: backlight
          brightness: 50%

Hiya,

Sorry but I can’t see the files

st7701s_tpanel.h
st7701s_tpanel.cpp

Am I missing something?

Thanks :slight_smile:

This seemed to work for me, but obviously the touch screen doesn’t work. Did you have any luck getting it working?

I have touch working. As a new user I have to wait until tomorrow to post the rest of the code.

Touch is working with the display (kind of). I’ve been chasing my tail on this one. I overwrote my original workign display setting and havent figured it out again, for the second time… I built a small diagnostic panel in the ESPHome Web UI to flip settings and narrow things down faster.

  • Touch (CST3240 @ 0x1A): IRQ proven, I²C reads OK.
  • Display (ST7701S 480Ă—480 RGB over IDF RGB panel): backlight OK, panel still black while I iterate timing/polarity.
  • Added a diagnostic WebUI (fill colors, test pattern, re-init button, IRQ monitor).
  • Looking for eyes on RGB timing/polarity and any known-good LilyGO T-Panel v1.3 ST7701S init nuances.

Hardware / pins (current):

  • I²C: SDA=17, SCL=18; XL9535 @ 0x20, CST3240 @ 0x1A, IRQ=21 (pull-up)
  • Backlight: 14 (LEDC ~19.5 kHz)
  • RGB: VSYNC=40 (HIGH), HSYNC=39 (HIGH), PCLK=41 (active on falling), DE disabled
  • Data (RGB565, B…G…R): B0…B4→1,2,3,4,5; G0…G5→6…11; R0…R4→12,13,42,46,45

What’s working now

  • Wi-Fi, OTA, PSRAM, LEDC backlight
  • I²C scan finds 0x20 and 0x1A
  • Touch IRQ toggles + basic readout confirmed
  • Driver logs are now verbose with a clear banner to verify the correct build is running

What I’m testing next

  • PCLK edge flip, HSYNC/VSYNC idle polarity A/B, small porch tweaks (HSYNC back porch 0↔10), PCLK 6↔8 MHz
  • Color order / MADCTL once the glass lights (blue/red swap previously observed)

Code 1 — Main ESPHome YAML (tpanel_display.yaml)

# tpanel_display.yaml - Updated with debug mode and alternative settings

# ========================================
# DIAGNOSTIC LAB CONSOLE
# Includes runtime tuning of all display parameters
# Comment out this line to disable the lab console
# ========================================
<<: !include display_lab_console.yaml

esphome:
  name: tpanel
  friendly_name: T-Panel Display
  platformio_options:
    board_build.flash_mode: dio
    board_build.arduino.memory_type: qio_opi
    board_build.psram_type: opi
    board_build.f_flash: 80000000L
    board_build.flash_size: 16MB
  on_boot:
    - priority: -100
      then:
        - delay: 1s
        - light.turn_on:
            id: backlight
            brightness: 100%
        - delay: 2s
        - logger.log: "Triggering first display update..."
        - component.update: main_display

wifi:
  ssid: !secret wifi_2g_ssid
  password: !secret wifi_2g_password
  power_save_mode: none
  fast_connect: true
  reboot_timeout: 0s
  ap:
    ssid: "T-Panel-AP"
    password: "12345678"


captive_portal:

api:

ota:
  - platform: esphome

web_server:
  port: 80

logger:
  level: VERBOSE
  logs:
    component: DEBUG  # Changed from ERROR to see component failures
    i2c.idf: INFO
    i2c: INFO
    xl9535: DEBUG  # Changed from INFO to see expander details
    st7701s_tpanel: VERBOSE  # Changed from DEBUG to see everything
    touchscreen: DEBUG  # Enable touch debugging
    cst816: VERBOSE  # Enable detailed CST816/CST3240 logging

psram:
  mode: quad
  speed: 80MHz

i2c:
  sda: GPIO17
  scl: GPIO18
  frequency: 400kHz
  scan: true
  id: i2c_bus

esp32:
  board: esp32-s3-devkitc-1
  variant: ESP32S3
  framework:
    type: esp-idf
    version: recommended
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
      CONFIG_ESP32S3_SPIRAM_SUPPORT: "y"
      CONFIG_SPIRAM: "y"
      CONFIG_SPIRAM_BOOT_INIT: "y"
      CONFIG_SPIRAM_TYPE_AUTO: "y"
      CONFIG_SPIRAM_MODE_QUAD: "y"
      CONFIG_SPIRAM_SPEED_80M: "y"
      CONFIG_SPIRAM_USE_MALLOC: "y"
      CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL: "16384"
      CONFIG_ESP_TASK_WDT: "y"
      CONFIG_ESP_TASK_WDT_TIMEOUT_S: "30"
      CONFIG_ESP_TASK_WDT_PANIC: "y"
      CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0: "n"
      CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1: "n"

external_components:
  - source:
      type: local
      path: components
    components: [st7701s_tpanel]
    refresh: always  # Force refresh on every build

xl9535:
  - id: xl9535_hub
    i2c_id: i2c_bus  # CRITICAL: Bind to the I2C bus we defined above
    address: 0x20

output:
  - platform: ledc
    id: backlight_pwm
    pin: GPIO14
    frequency: 19531Hz  # ~19.5 kHz to avoid audible whine/flicker
    channel: 0
    inverted: false

light:
  - platform: monochromatic
    id: backlight
    name: "Display Backlight"
    output: backlight_pwm
    restore_mode: ALWAYS_ON
    default_transition_length: 0ms
    gamma_correct: 1.0

font:
  - file: "gfonts://Roboto"
    id: font_roboto
    size: 24
  - file: "gfonts://Roboto"
    id: font_bold
    size: 32

# BGR color order (fixes purple/yellow test pattern issue)
display:
  - platform: st7701s_tpanel
    id: main_display
    xl9535_id: xl9535_hub
    color_order: BGR  # Panel swaps R/B channels, use BGR to compensate
    invert_colors: false
    update_interval: never  # Changed from 1s - disable auto-update during bring-up
    auto_clear_enabled: true
    lambda: |-
      # COMMENTED OUT during initial bring-up to avoid masking init issues
      # Uncomment after display is working
      # ESP_LOGD("display", "Drawing solid red fill");
      # it.fill(Color(255, 0, 0));  # Fill entire screen red
      pass  # Python-style no-op for empty lambda

# NOTE: CST3240 touch is wired at 7-bit address 0x1A; keep skip_probe during experiments
touchscreen:
  - platform: cst816
    id: touch_controller
    i2c_id: i2c_bus  # Use the same I2C bus as XL9535
    address: 0x1A  # CST3240 I2C 7-bit address
    interrupt_pin: GPIO21
    reset_pin:
      xl9535: xl9535_hub
      number: 4
      mode:
        output: true
      inverted: false
    skip_probe: true  # Skip chip ID detection (CST3240 may have different ID than CST816)
    on_touch:
      - logger.log:
          format: "Touch detected at (%d, %d)"
          args: ['touch.x', 'touch.y']

binary_sensor:
  - platform: status
    name: "System Status"
    id: system_status

  # Note: GPIO21 touch IRQ is managed by touchscreen component
  # Cannot add separate binary_sensor on same pin

button:
  # Backlight control buttons
  - platform: template
    name: "Toggle Backlight"
    on_press:
      - light.toggle: backlight

  - platform: template
    name: "Backlight 100%"
    on_press:
      - light.turn_on:
          id: backlight
          brightness: 100%

  - platform: template
    name: "Backlight 50%"
    on_press:
      - light.turn_on:
          id: backlight
          brightness: 50%

  - platform: template
    name: "Backlight 0% (Off)"
    on_press:
      - light.turn_off: backlight

  # Debug: Fill Red
  - platform: template
    name: "Fill RED"
    on_press:
      - lambda: |-
          id(main_display).fill_red();

  # Debug: Fill GREEN
  - platform: template
    name: "Fill GREEN"
    on_press:
      - lambda: |-
          id(main_display).fill_green();

  # Debug: Fill BLUE
  - platform: template
    name: "Fill BLUE"
    on_press:
      - lambda: |-
          id(main_display).fill_blue();

  # Debug: Fill WHITE
  - platform: template
    name: "Fill WHITE"
    on_press:
      - lambda: |-
          id(main_display).fill_white();

  # Test pattern
  - platform: template
    name: "Test Pattern (RGBY)"
    on_press:
      - logger.log: "Filling screen with test pattern (Red/Green/Blue/Yellow quadrants)"
      - lambda: |-
          id(main_display).fill_test_pattern();

  # Display update button
  - platform: template
    name: "Trigger Display Update"
    on_press:
      - logger.log: "Manually triggering display update"
      - component.update: main_display

sensor:
  - platform: uptime
    name: "Uptime"
    update_interval: 60s

  - platform: wifi_signal
    name: "WiFi Signal"
    update_interval: 60s

switch:
  - platform: restart
    name: "Restart Device"

Code 2 — Diagnostic WebUI include (display_lab_console.yaml)

# ========================================
# Display Lab Console - Full Diagnostic Suite
# ========================================
# This file provides runtime tuning of all display parameters
# Include in your main YAML with: <<: !include display_lab_console.yaml

# ========================================
# Staged Configuration State (Persisted across reboots)
# ========================================
globals:
  - id: g_pclk_mhz
    type: int
    restore_value: yes
    initial_value: '6'

  - id: g_pclk_edge
    type: int
    restore_value: yes
    initial_value: '1'   # 1=falling, 0=rising

  - id: g_hs_idle_low
    type: int
    restore_value: yes
    initial_value: '0'   # 0=HIGH, 1=LOW

  - id: g_vs_idle_low
    type: int
    restore_value: yes
    initial_value: '0'   # 0=HIGH, 1=LOW

  - id: g_h_front
    type: int
    restore_value: yes
    initial_value: '20'

  - id: g_h_pulse
    type: int
    restore_value: yes
    initial_value: '2'

  - id: g_h_back
    type: int
    restore_value: yes
    initial_value: '0'

  - id: g_v_front
    type: int
    restore_value: yes
    initial_value: '30'

  - id: g_v_pulse
    type: int
    restore_value: yes
    initial_value: '8'

  - id: g_v_back
    type: int
    restore_value: yes
    initial_value: '1'

  - id: g_rotation
    type: int
    restore_value: yes
    initial_value: '0'

  - id: g_bgr
    type: int
    restore_value: yes
    initial_value: '1'   # 0=RGB, 1=BGR

# ========================================
# SELECT Controls (Polarity & Color Order)
# ========================================
select:
  - platform: template
    name: "Lab: PCLK Edge"
    id: lab_pclk_edge
    options: ["Falling", "Rising"]
    optimistic: yes
    initial_option: "Falling"
    icon: mdi:clock-outline
    set_action:
      - lambda: 'id(g_pclk_edge) = (x == "Falling") ? 1 : 0;'

  - platform: template
    name: "Lab: HSYNC Idle"
    id: lab_hsync_idle
    options: ["HIGH", "LOW"]
    optimistic: yes
    initial_option: "HIGH"
    icon: mdi:sine-wave
    set_action:
      - lambda: 'id(g_hs_idle_low) = (x == "LOW") ? 1 : 0;'

  - platform: template
    name: "Lab: VSYNC Idle"
    id: lab_vsync_idle
    options: ["HIGH", "LOW"]
    optimistic: yes
    initial_option: "HIGH"
    icon: mdi:sine-wave
    set_action:
      - lambda: 'id(g_vs_idle_low) = (x == "LOW") ? 1 : 0;'

  - platform: template
    name: "Lab: Rotation"
    id: lab_rotation
    options: ["0°", "90°", "180°", "270°"]
    optimistic: yes
    initial_option: "0°"
    icon: mdi:screen-rotation
    set_action:
      - lambda: 'id(g_rotation) = (x=="0°")?0:(x=="90°")?1:(x=="180°")?2:3;'

  - platform: template
    name: "Lab: Color Order"
    id: lab_color_order
    options: ["RGB", "BGR"]
    optimistic: yes
    initial_option: "BGR"
    icon: mdi:palette
    set_action:
      - lambda: 'id(g_bgr) = (x=="BGR") ? 1 : 0;'

# ========================================
# NUMBER Controls (Timings)
# ========================================
number:
  # PCLK Frequency
  - platform: template
    name: "Lab: PCLK (MHz)"
    id: lab_pclk_mhz
    min_value: 2
    max_value: 20
    step: 1
    initial_value: 6
    optimistic: yes
    icon: mdi:speedometer
    mode: box
    set_action:
      - lambda: 'id(g_pclk_mhz) = (int)x;'

  # HSYNC Timings
  - platform: template
    name: "Lab: HS Front Porch"
    id: lab_h_front
    min_value: 0
    max_value: 80
    step: 1
    initial_value: 20
    optimistic: yes
    icon: mdi:arrow-right
    mode: box
    set_action:
      - lambda: 'id(g_h_front) = (int)x;'

  - platform: template
    name: "Lab: HS Pulse Width"
    id: lab_h_pulse
    min_value: 1
    max_value: 20
    step: 1
    initial_value: 2
    optimistic: yes
    icon: mdi:pulse
    mode: box
    set_action:
      - lambda: 'id(g_h_pulse) = (int)x;'

  - platform: template
    name: "Lab: HS Back Porch"
    id: lab_h_back
    min_value: 0
    max_value: 80
    step: 1
    initial_value: 0
    optimistic: yes
    icon: mdi:arrow-left
    mode: box
    set_action:
      - lambda: 'id(g_h_back) = (int)x;'

  # VSYNC Timings
  - platform: template
    name: "Lab: VS Front Porch"
    id: lab_v_front
    min_value: 0
    max_value: 80
    step: 1
    initial_value: 30
    optimistic: yes
    icon: mdi:arrow-down
    mode: box
    set_action:
      - lambda: 'id(g_v_front) = (int)x;'

  - platform: template
    name: "Lab: VS Pulse Width"
    id: lab_v_pulse
    min_value: 1
    max_value: 20
    step: 1
    initial_value: 8
    optimistic: yes
    icon: mdi:pulse
    mode: box
    set_action:
      - lambda: 'id(g_v_pulse) = (int)x;'

  - platform: template
    name: "Lab: VS Back Porch"
    id: lab_v_back
    min_value: 0
    max_value: 80
    step: 1
    initial_value: 1
    optimistic: yes
    icon: mdi:arrow-up
    mode: box
    set_action:
      - lambda: 'id(g_v_back) = (int)x;'

# ========================================
# BUTTON Controls (Apply & Test Tools)
# ========================================
button:
  # === Apply / Revert ===
  - platform: template
    name: "Lab: Apply Display Params"
    id: lab_apply
    icon: mdi:check-circle
    on_press:
      - lambda: |-
          using namespace esphome::st7701s_tpanel;
          TPanelRuntimeCfg cfg;
          cfg.pclk_hz        = id(g_pclk_mhz) * 1000000;
          cfg.pclk_active_neg= id(g_pclk_edge);
          cfg.hsync_idle_low = id(g_hs_idle_low);
          cfg.vsync_idle_low = id(g_vs_idle_low);
          cfg.h_front = id(g_h_front);
          cfg.h_pulse = id(g_h_pulse);
          cfg.h_back  = id(g_h_back);
          cfg.v_front = id(g_v_front);
          cfg.v_pulse = id(g_v_pulse);
          cfg.v_back  = id(g_v_back);
          cfg.rotation = id(g_rotation);
          cfg.bgr = id(g_bgr);
          id(main_display).set_staged(cfg);
          id(main_display).apply_now();
      # Auto-revert safety check (if VSYNC doesn't tick within 3s)
      - delay: 3s
      - lambda: |-
          if (id(main_display).vsync_count() == 0) {
            ESP_LOGW("lab", "No VSYNC ticks detected - reverting to defaults");
            id(main_display).revert_defaults();
          }

  - platform: template
    name: "Lab: Revert to Defaults"
    id: lab_revert
    icon: mdi:restore
    on_press:
      - lambda: 'id(main_display).revert_defaults();'

  # === Solid Color Fills ===
  - platform: template
    name: "Lab: Fill RED"
    id: lab_fill_red
    icon: mdi:square
    on_press:
      - lambda: 'id(main_display).fill_red();'

  - platform: template
    name: "Lab: Fill GREEN"
    id: lab_fill_green
    icon: mdi:square
    on_press:
      - lambda: 'id(main_display).fill_green();'

  - platform: template
    name: "Lab: Fill BLUE"
    id: lab_fill_blue
    icon: mdi:square
    on_press:
      - lambda: 'id(main_display).fill_blue();'

  - platform: template
    name: "Lab: Fill WHITE"
    id: lab_fill_white
    icon: mdi:square
    on_press:
      - lambda: 'id(main_display).fill_white();'

  - platform: template
    name: "Lab: Fill BLACK"
    id: lab_fill_black
    icon: mdi:square
    on_press:
      - lambda: 'id(main_display).fill_black();'

  # === Test Patterns ===
  - platform: template
    name: "Lab: Test Pattern (Quadrants)"
    id: lab_test_pattern
    icon: mdi:grid
    on_press:
      - lambda: 'id(main_display).fill_test_pattern();'

  - platform: template
    name: "Lab: Checkerboard"
    id: lab_checkerboard
    icon: mdi:checkerboard
    on_press:
      - lambda: 'id(main_display).fill_checkerboard(0xFFFF, 0x0000, 30);'

  - platform: template
    name: "Lab: Horizontal Gradient"
    id: lab_grad_h
    icon: mdi:gradient-horizontal
    on_press:
      - lambda: 'id(main_display).fill_gradient_h();'

  - platform: template
    name: "Lab: Vertical Gradient"
    id: lab_grad_v
    icon: mdi:gradient-vertical
    on_press:
      - lambda: 'id(main_display).fill_gradient_v();'

  - platform: template
    name: "Lab: Tearing Test (Moving Bar)"
    id: lab_tearing
    icon: mdi:play-speed
    on_press:
      - lambda: 'id(main_display).tearing_test();'

  # === Diagnostic Tools ===
  - platform: template
    name: "Lab: Power Cycle Panel"
    id: lab_power_cycle
    icon: mdi:power-cycle
    on_press:
      - lambda: 'id(main_display).power_cycle_panel();'

  - platform: template
    name: "Lab: I2C Scan"
    id: lab_i2c_scan
    icon: mdi:chip
    on_press:
      - lambda: 'id(main_display).i2c_scan_log();'

  - platform: template
    name: "Lab: XL9535 Dump"
    id: lab_xl9535_dump
    icon: mdi:file-document
    on_press:
      - lambda: 'id(main_display).xl9535_dump();'

# ========================================
# SENSOR Outputs (Health Monitoring)
# ========================================
sensor:
  - platform: template
    name: "Lab: VSYNC Count"
    id: lab_vsync_count
    update_interval: 1s
    icon: mdi:pulse
    unit_of_measurement: "frames"
    accuracy_decimals: 0
    lambda: 'return (float) id(main_display).vsync_count();'

# ========================================
# BINARY SENSOR Outputs (Status)
# ========================================
binary_sensor:
  - platform: template
    name: "Lab: Framebuffer Ready"
    id: lab_fb_ready
    device_class: connectivity
    icon: mdi:memory
    lambda: 'return id(main_display).fb_ready();'

# ========================================
# TEXT SENSOR Outputs (Error Reporting)
# ========================================
text_sensor:
  - platform: template
    name: "Lab: Last Error"
    id: lab_last_error
    icon: mdi:alert-circle
    lambda: |-
      const char* err = id(main_display).last_error();
      return err ? std::string(err) : std::string("None");
    update_interval: 5s

1 Like

Code 3 — Custom display driver (header) (custom_components/st7701s_tpanel/st7701s_tpanel.h)

// components/st7701s_tpanel/st7701s_tpanel.h
#pragma once

#include "esphome/components/display/display_buffer.h"
#include "esphome/components/xl9535/xl9535.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"

// Forward-declare the esp_lcd handle type so the header doesn't need IDF headers.
extern "C" {
  typedef struct esp_lcd_panel_t *esp_lcd_panel_handle_t;
}

namespace esphome {
namespace st7701s_tpanel {

enum : uint8_t {
  COLOR_ORDER_RGB = 0,
  COLOR_ORDER_BGR = 1,
};

// Runtime configuration structure for dynamic display parameter tuning
struct TPanelRuntimeCfg {
  int pclk_hz;               // Pixel clock in Hz (e.g. 6000000)
  bool pclk_active_neg;      // true = sample on falling edge, false = rising
  bool hsync_idle_low;       // false = HSYNC idle HIGH, true = idle LOW
  bool vsync_idle_low;       // false = VSYNC idle HIGH, true = idle LOW
  uint16_t h_front;          // Horizontal front porch
  uint16_t h_pulse;          // Horizontal pulse width
  uint16_t h_back;           // Horizontal back porch
  uint16_t v_front;          // Vertical front porch
  uint16_t v_pulse;          // Vertical pulse width
  uint16_t v_back;           // Vertical back porch
  uint8_t rotation;          // 0, 1, 2, 3 for 0°, 90°, 180°, 270°
  bool bgr;                  // false = RGB order, true = BGR order
};

// XL9535 pin assignments for T-Panel S3 display control (per LilyGO schematic)
#ifndef CS_PIN
#define CS_PIN  17  // XL9535_IO17 for ST7701S CS
#endif
#ifndef SCK_PIN
#define SCK_PIN 15  // XL9535_IO15 for ST7701S SCLK
#endif
#ifndef SDA_PIN
#define SDA_PIN 16  // XL9535_IO16 for ST7701S MOSI
#endif
#ifndef RST_PIN
#define RST_PIN 5   // XL9535_IO5 for ST7701S RST
#endif

class ST7701STPanelDisplay : public display::DisplayBuffer {
 public:
  // ==== Component overrides ====
  void setup() override;
  void dump_config() override;
  void loop() override {}  // not used
  // CRITICAL: Must run AFTER XL9535 (which uses IO=900), so use DATA (lower priority, runs later)
  float get_setup_priority() const override { return setup_priority::DATA; }
  void update() override;

  // ==== DisplayBuffer required overrides (note the *_internal names) ====
  void draw_absolute_pixel_internal(int x, int y, Color color) override;
  int get_width_internal() override { return 480; }
  int get_height_internal() override { return 480; }
  size_t get_buffer_length_() const;  // returns BYTES (removed override)
  display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }

  // ==== Setters (wired from Python) ====
  void set_xl9535(xl9535::XL9535Component *exp) { this->xl9535_ = exp; }
  void set_backlight_pin(uint8_t pin) { this->backlight_pin_ = pin; }
  void set_color_order(uint8_t order) { this->color_order_ = order; }
  void set_invert_colors(bool inv) { this->invert_colors_ = inv; }

  // ==== Public test/debug functions ====
  void fill_solid(uint16_t color);  // Fill entire screen with RGB565 color
  void fill_red() { fill_solid(0xF800); }
  void fill_green() { fill_solid(0x07E0); }
  void fill_blue() { fill_solid(0x001F); }
  void fill_white() { fill_solid(0xFFFF); }
  void fill_black() { fill_solid(0x0000); }
  void fill_test_pattern();  // Fill with quadrant pattern (R/G/B/Y)
  void fill_checkerboard(uint16_t color1, uint16_t color2, uint8_t square_size);
  void fill_gradient_h();  // Horizontal RGB gradient
  void fill_gradient_v();  // Vertical RGB gradient
  void tearing_test();  // Moving bar to detect tearing

  // ==== Runtime configuration API ====
  void set_staged(const TPanelRuntimeCfg &cfg);  // Stage new configuration
  void apply_now();  // Apply staged configuration (reinitialize RGB panel)
  void revert_defaults();  // Restore factory defaults and reinitialize
  TPanelRuntimeCfg get_active() const { return active_; }
  TPanelRuntimeCfg get_defaults() const;

  // ==== Diagnostic functions ====
  void power_cycle_panel();  // Hardware reset via XL9535
  void i2c_scan_log();  // Scan and log all I2C devices
  void xl9535_dump();  // Dump XL9535 register state
  bool fb_ready() const { return frame_buffer_ != nullptr; }
  int vsync_count() const { return vsync_count_; }
  const char* last_error() const { return last_error_; }

 protected:
  // ==== Internals ====
  bool allocate_framebuffer_();
  bool init_rgb_interface_();

  void reset_display_();
  void init_display_();
  void write_byte_(uint8_t value, bool is_data);
  void write_command_(uint8_t cmd);
  void write_data_(uint8_t data);

  // ==== State ====
  xl9535::XL9535Component *xl9535_{nullptr};

  // Backlight is on a native ESP32 GPIO (not the expander)
  uint8_t backlight_pin_{14};

  // Panel options
  uint8_t color_order_{COLOR_ORDER_RGB};
  bool invert_colors_{false};

  // Framebuffer (RGB565) and size in BYTES
  uint16_t *frame_buffer_{nullptr};
  size_t buffer_size_{0};

  // esp_lcd panel handle (created in init_rgb_interface_)
  esp_lcd_panel_handle_t panel_handle_{nullptr};

  bool setup_complete_{false};

  // Runtime configuration
  TPanelRuntimeCfg staged_;  // Configuration being edited
  TPanelRuntimeCfg active_;  // Currently applied configuration
  bool dirty_{false};  // True if staged differs from active

  // Diagnostics
  volatile int vsync_count_{0};  // Incremented by VSYNC ISR
  const char* last_error_{nullptr};  // Points to static string describing last error
  uint32_t tearing_bar_pos_{0};  // Position for tearing test animation
};

}  // namespace st7701s_tpanel
}  // namespace esphome
1 Like
  1. Is it in custom_components or components?
  2. I can’t find st7701s_tpanel.cpp either.

Great work though! Have you put it on a public Git somewhere? :smile: