Got the LilyGO T-QT ESP32-S3 display working

Recently I ordered the LilyGO T-QT ESP32-S3 to turn it into a small alarm clock and maybe display some essential info about the house. It took a couple of hours to get it to work, since it is running the GC9107 which is not a standard module in ESPHome.

I’ll leave my config here for

  1. Anyone else trying or hoping to get it up and running.
  2. Maybe some advice on essential but pretty stupid backlight turnoff on boot?

Prerequisites

  1. The initial flash of the module must be done with your browser, esphome flasher, does not work, this is ESP32s3-specific.
  2. You need a font called OCRAEXT.ttf in your fonts directory. Of course you can use another one, but if you do so, change the font-section accordingly.
substitutions:
  name: "your-esp-name"
  friendly_name: "Your ESP Name"

esphome:
  name: "${name}"
  platformio_options:
    board_build.mcu: esp32s3
    board_build.name: "Espressif ESP32-S3-T-QT"
    board_build.upload.flash_size: "4MB"
    board_build.upload.maximum_size: 4194304
    board_build.vendor: "LilyGO"
  on_boot:
    - priority: 800
      then:
        - lambda: |-
            id(disp).enable();
            id(disp).transfer_byte(0x11);
            id(disp).disable();
    - priority: -100
      then:
        - switch.turn_off: backlight


esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# Enable logging
logger:

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

ota:
  password: "PASSWORD"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${friendly_name} Fallback Hotspot"
    password: "FALLBACK PASSWORD"

captive_portal:

spi:
  clk_pin: GPIO3
  mosi_pin: GPIO2


font:
  - file: "fonts/OCRAEXT.ttf"
    id: fontocra
    size: 42

switch:
  - platform: gpio
    pin:
      number: GPIO10
      mode:
        output: True
    name: "Display Backlight"
    id: backlight
    entity_category: diagnostic

time:
  - platform: homeassistant
    id: home_time
    timezone: Europe/Amsterdam

display:
  - platform: st7789v
    model: Custom
    width: 128
    height: 128
    offset_width: 0
    offset_height: 0
    backlight_pin: GPIO10
    cs_pin: GPIO5
    dc_pin: GPIO6
    reset_pin: GPIO1
    id: disp
    rotation: 180
    lambda: |-
      it.strftime(0, 0, id(fontocra), TextAlign::TOP_LEFT, "%H:%M", id(home_time).now());
      it.line(0, 50, 128, 50);

Resulting in:

So this one is pretty similar to T-Display-S3 and T-Embed. Compared to T-Embed:

  • You need the same priority 800 labda to send 0x11 to
  • You don’t need GPIO46 to turn peripherals on.
  • You do need to turn backlight OFF (!) at priority -100 (maybe earlier, but it doesn’t work at 800), otherwise the screen will remain off.

The code will also create a diagostic backlight switch in your HA, which is usefull for debugging, but otherwise pretty stupid. Especially since is must be off to be on. (Yeah, I could invert it probably)

Credits:

11 Likes

So, I made some improvements, basically answering my own question.

  1. Instead of defining GPIO10 as a switch, you can define it as a (inverted) PWM led light, which gives you the possibility to dim! and switch off the backlight.
  2. Make sure to update to the latest version of ESPHome. I’ve spend hours trying to get dimming to work. Then I found out my outdated version was suffering from some kind of PWM-bug for the ESP32S3.
  3. After this, the on_boot turn_off can be removed.
  4. You can also remove the backlight_pin from the display-settings
  5. I made some small adjustments to offset_width and _height.

Result:

substitutions:
  name: "your-esp-name"
  friendly_name: "Your ESP Name"

esphome:
  name: "${name}"
  platformio_options:
    board_build.mcu: esp32s3
    board_build.name: "Espressif ESP32-S3-T-QT"
    board_build.upload.flash_size: "4MB"
    board_build.upload.maximum_size: 4194304
    board_build.vendor: "LilyGO"
  on_boot:
    - priority: 800
      then:
        - lambda: |-
            id(disp).enable();
            id(disp).transfer_byte(0x11);
            id(disp).disable();

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# Enable logging
logger:

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

ota:
  password: "PASSWORD"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${friendly_name} Fallback Hotspot"
    password: "FALLBACK PASSWORD"

captive_portal:

spi:
  clk_pin: GPIO3
  mosi_pin: GPIO2

font:
  - file: "fonts/OCRAEXT.ttf"
    id: fontocra
    size: 42

output:
  - platform: ledc
    pin: GPIO10
    inverted: true
    id: backlightdim

light:
  - platform: monochromatic
    output: backlightdim
    name: "Backlight"

time:
  - platform: homeassistant
    id: home_time
    timezone: Europe/Amsterdam

display:
  - platform: st7789v
    model: Custom
    width: 128
    height: 128
    offset_width: 1
    offset_height: 2
    cs_pin: GPIO5
    dc_pin: GPIO6
    reset_pin: GPIO1
    id: disp
    rotation: 180
    lambda: |-
      it.strftime(0, 0, id(fontocra), TextAlign::TOP_LEFT, "%H:%M", id(home_time).now());
      it.line(0, 50, 127, 50);
6 Likes

Hi JelleVeeKay

Thanks for your post, I was looking for exactly this.

As I’m a beginner in this area, would it be possible to explain this specific step a little more indeep?
Whatever I tried, I end-up that the S3 reboots in the middle of the initial flash and is stuck/blocking every other action.

Only a full-flash with the original LilyGo bin can unblock it (first unplug, press & hold boot and replug, flash).

Hi Michael,
I don’t know what you have tried and if your problem is the same as mine

I experienced problems trying to do initial flash with esphomeflasher. I got “Invalid header: 0xffffffff” messages. For me it only worked by flashing through the browser (which took me a while, because I run ESPHome Docker version, which I assume you don’t):

afbeelding

If you select that option, it will guide you through the process.

(I got pointed into this direction here: ESP32-C3 Bluetooth Proxy invalid header: 0xffffffff - #3 by nymare it is about the ESP32 C3, but since the result was the same, I just tried the same solution for the S3)

Ah, finally I go it … I do have a slightly different model of the display. :roll_eyes:
So it can’t work 1:1, but the hint of flashing it once in http://web.esphome.io/ is still very true.

In my case, this worked at the end:
GitHub - landonr/lilygo-tdisplays3-esphome: tdisplay s3 170x320 running esphome using patched tft_espi

Same code is working for m5tack AtomS3
Thanks!

Here is a working example for AtomS3

substitutions:
  device_name: atom_s3_0

esphome:
  name: $device_name
  on_boot:
    - priority: 800
      then:
        - lambda: |-
            id(disp).enable();
            id(disp).transfer_byte(0x11);
            id(disp).disable();

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
    version: 2.0.3
    platform_version: 5.1.0
  variant: esp32s3

packages:
  common: !include common/common.yaml
  restart: !include common/restart.yaml
  syslog: !include common/syslog.yaml
  time: !include common/time.yaml
  uptime: !include common/uptime.yaml
  version: !include common/version.yaml
  web_server: !include common/web_server.yaml
  wifi: !include common/wifi.yaml
  wifi_signal: !include common/wifi_signal.yaml

#
logger:
  level: VERBOSE
  baud_rate: 115200
  deassert_rts_dtr: true

i2c:
  - id: bus_a
    sda: GPIO38
    scl: GPIO39

sensor:
  - platform: mpu6886
    address: 0x68
    accel_x:
      name: "$device_name MPU6886 Accel X"
    accel_y:
      name: "$device_name MPU6886 Accel Y"
    accel_z:
      name: "$device_name MPU6886 Accel z"
    gyro_x:
      name: "$device_name MPU6886 Gyro X"
    gyro_y:
      name: "$device_name MPU6886 Gyro Y"
    gyro_z:
      name: "$device_name MPU6886 Gyro z"
    temperature:
      name: "$device_name MPU6886 Temperature"

spi:
  clk_pin: GPIO17
  mosi_pin: GPIO21

display:
  - platform: st7789v
    id: disp
    model: Custom
    backlight_pin: GPIO16
    cs_pin: GPIO15
    dc_pin: GPIO33
    reset_pin: GPIO34
    height: 128
    width: 128
    offset_height: 2
    offset_width: 1
    eightbitcolor: true
    lambda: |-
      // it.fill(my_red);
      int maxX = it.get_width();
      int maxY = it.get_height();
      bool drawGird = false;
      if (drawGird)
        {
        for (int X=0; X <= maxX; X=X+maxX/8)
        {
          it.line(X,0,X,maxY,id(dark_gray));
        }
        for (int Y=0; Y <= maxY; Y=Y+maxY/8)
        {
          it.line(0,Y,maxX,Y,id(dark_gray));
        }

        for (int X=0; X <= maxX; X=X+maxX/4)
        {
          it.line(X,0,X,maxY,id(my_gray));
        }
        for (int Y=0; Y <= maxY; Y=Y+maxY/4)
        {
          it.line(0,Y,maxX,Y,id(my_gray));
        }
      }
      it.rectangle(0, 0, 10, 10);
      it.rectangle(maxX-10, 0, 10, 10);
      it.rectangle(maxX-10, maxY-10, 10, 10);
      it.rectangle(0, maxY-10, 10, 10);

      it.strftime(maxX/2, maxY/2, id(roboto), TextAlign::CENTER, "%H:%M", id(home_time).now());

font:
  - file: "gfonts://Roboto"
    id: roboto
    size: 42

switch:
- id: ${device_name}_backlight
  name: ${device_name}_backlight
  platform: gpio
  pin: GPIO16
  restore_mode: RESTORE_DEFAULT_ON
  icon: mdi:lightbulb

time:
  - platform: homeassistant
    id: home_time
    timezone: Europe/Moscow

binary_sensor:
  - platform: gpio
    pin: GPIO41
    name: "${device_name}_button"
    id: "${device_name}_button"
    on_press:
      then:
        - switch.toggle: ${device_name}_backlight
3 Likes

I tweaked the code a bit to provide dimmable control over the backlight on the M5 AtomS3. And with ESPHome version 2023.4.1, I didn’t need to specify the version, platform_version, or variant in the esp32 section.

output:
  - platform: ledc
    pin: GPIO16
    min_power: 0.80
    max_power: 0.99
    zero_means_zero: true
    id: backlightdim

light:
  - platform: monochromatic
    output: backlightdim
    restore_mode: RESTORE_DEFAULT_ON
    name: "Backlight"

1 Like

I have tried your config but for me display just blinks shows 00:00 (or current time) and does not show anything, if i uncomment id(disp).disable(); then it does not show anything at all

why can that be? :slight_smile:

substitutions:
  device_name: EH Elektroměr
  device_id: eh_elektromer
  hostname: eh-elektromer
  comment: Elektroměr, esp32-s3 (T-QT), DIN mount

esphome:
  name: $hostname
  comment: $comment
  platformio_options: 
    # board_build.extra_flags:
    #   - "-DARDUINO_USB_MODE=0"
    #   - "-DARDUINO_USB_CDC_ON_BOOT=1"
    board_build.mcu: esp32s3
    board_build.name: "Espressif ESP32-S3-T-QT"
    board_build.upload.flash_size: "4MB"
    board_build.upload.maximum_size: 4194304
    board_build.vendor: "LilyGO"
  on_boot:
    - priority: 800
      then:
        - lambda: |-
            id(disp).enable();
            id(disp).transfer_byte(0x11);
            id(disp).disable();

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: ${hostname} Fallback

api:
ota:
web_server:
  version: 2

preferences:
  flash_write_interval: 7min

output:
  - platform: ledc
    pin: GPIO10
    inverted: true
    id: backlightdim
light:
  - platform: monochromatic
    output: backlightdim
    name: "Backlight"
    id: ${device_id}_backlight
font:
  - file: "fonts/OCRAEXT.ttf"
    id: fontocra
    size: 42
  - file: "gfonts://Roboto"
    id: roboto
    size: 20
spi:
  clk_pin: GPIO3
  mosi_pin: GPIO2
display:
  - platform: st7789v
    model: Custom
    width: 128
    height: 128
    offset_width: 1
    offset_height: 2
    cs_pin: GPIO5
    dc_pin: GPIO6
    reset_pin: GPIO1
    id: disp
    rotation: 180
    lambda: |-
      it.strftime(0, 0, id(fontocra), TextAlign::TOP_LEFT, "%H:%M", id(my_time).now());
      it.line(0, 50, 127, 50);
#      it.printf(0, 60, id(roboto), "T1: %1", id(${device_id}_T1_total).state);
#      it.printf(0, 90, id(roboto), "T2: %1", id(${device_id}_T2_total).state);

binary_sensor:
  - platform: gpio
    pin: GPIO47
    name: "${device_name}_button"
    id: "${device_id}_button"
    on_press:
      then:
        - light.toggle: ${device_id}_backlight

time:
  - platform: sntp
    id: my_time
    timezone: Europe/Prague
############################################################################

esp32:
  board: esp32-s3-devkitc-1
  variant: ESP32S3
  framework: 
    type: arduino
    # type: esp-idf
    # sdkconfig_options:
    #   CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
    #   CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y

logger:
  level: VERY_VERBOSE
  logs:
    uart: DEBUG
    web_server: INFO
    wifi: INFO
    app: INFO
    ota: INFO
    wifi_esp32: INFO
    scheduler: INFO
    mdns: DEBUG
    web_server_idf: WARN
    bluetooth_proxy: DEBUG
    esp32_ble: DEBUG
    json: DEBUG
    sensor: DEBUG
    esp32_ble_tracker: debug
    api.service: debug
    obis: debug
  #baud_rate: 0
  #hardware_uart: USB_SERIAL_JTAG

############################################################################
  
uart:
  id: uart_bus
  tx_pin: 16
  rx_pin: 17
  rx_buffer_size: 2048  
  baud_rate: 300
  data_bits: 7
  stop_bits: 1
  parity: EVEN
  # debug:
  #   direction: BOTH
  #   dummy_receiver: false
  #   after:
  #     delimiter: "\n"
  #   sequence:
  #     - lambda: UARTDebug::log_hex(direction, bytes, ':');

external_components:
  - source: github://evlo/esphome_ZPA-ZE312
    components: [obis]
    refresh: 0s
  - source: github://pr#3500
    components:
      - web_server
      - web_server_idf
      - web_server_base
      - captive_portal

obis:
  uart_id: uart_bus

interval:
  - interval: 60sec
    then:
      - uart.write:
          id: uart_bus
          data: [0x2F, 0x3F, 0x21, 0x0D, 0x0A]

sensor:
  - platform: obis
    name: "${device_name} L1"
    channel: "21.8.0"
    unit_of_measurement: kWh
    accuracy_decimals: 3
    device_class: energy
    state_class: total_increasing
    icon: "mdi:lightning-bolt"
    id: ${device_id}_L1_total
  - platform: obis
    name: "${device_name} L2"
    channel: "41.8.0"
    unit_of_measurement: kWh
    accuracy_decimals: 3
    device_class: energy
    state_class: total_increasing
    icon: "mdi:lightning-bolt"
    id: ${device_id}_L2_total
  - platform: obis
    name: "${device_name} L3"
    channel: "61.8.0"
    unit_of_measurement: kWh
    accuracy_decimals: 3
    device_class: energy
    state_class: total_increasing
    icon: "mdi:lightning-bolt"
    id: ${device_id}_L3_total
  - platform: obis
    name: "${device_name} T1 in total"
    channel: "1.8.1"
    unit_of_measurement: kWh
    accuracy_decimals: 3
    device_class: energy
    state_class: total_increasing
    icon: "mdi:home-lightning-bolt"
    id: ${device_id}_T1_total
  - platform: obis
    name: "${device_name} T2 in total"
    channel: "1.8.2"
    unit_of_measurement: kWh    
    accuracy_decimals: 3
    device_class: energy
    state_class: total_increasing
    icon: "mdi:home-lightning-bolt"
    id: ${device_id}_T2_total
############################################################################
  - platform: wifi_signal
    name: "${device_name} WiFi Signal"
    update_interval: 10s
    id: wifi_signal_db
    icon: "mdi:wifi"
  - platform: uptime
    name: "${device_name} Uptime"
    update_interval: "600s"
    entity_category: "diagnostic"
    icon: "mdi:clock-outline"
  - platform: internal_temperature
    name: CPU Temperature

text_sensor:
  - platform: version
    name: "${device_name} ESPHome Version"
    hide_timestamp: true
    icon: "mdi:numeric"
  - platform: obis
    channel: "0.0.0"
    name: "${device_name} Elektroměr Serial Number"
    icon: "mdi:barcode"
############################################################################
  - platform: version
    name: "${device_name} ESPHome Version"
    hide_timestamp: true
    icon: "mdi:numeric"
  - platform: wifi_info
    ip_address:
      name: "${device_name} IP Address"
      disabled_by_default: False
      icon: "mdi:wifi-settings"
    mac_address:
      name: "${device_name} MAC"
      icon: "mdi:wifi-marker"
    scan_results:
      name: "${device_name} last wifi scan"
      icon: "mdi:wifi-sync"
    
button:
  - platform: restart
    name: "${device_name} Restart"
    icon: "mdi:restart"

bluetooth_proxy:
  active: true

I also tried directly copy your config without any alterations and it behaves the same

UPDATE: ok, with your config after a while it works. And if I just wait my copy works too.

Even after removing all obis stuff, it still restarts constantly

ideas?

Hard to say. I wonder if it even has anything to do with the display. Have you tried uploading a more or less empty firmware? Ruling out power/wifi issues?

well it seems to be time related issue, when i removed time display it did work

The st7789v component is now deprecated. Instead, I’ve found this works for me for AtomS3 that I’ve just recently (2024-04-05) purchased.

esp32:
  #board: m5stack-atoms3
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 8MB
  framework:
    type: esp-idf

output:
  - id: backlight_output
    platform: ledc
    frequency: 500Hz
    pin: 16
    min_power: 0.3
    max_power: 1
    zero_means_zero: true

light:
  - output: backlight_output
    id: backlight
    name: Backlight
    platform: monochromatic
    icon: mdi:brightness-7

display:
  - id: lcd_display
    platform: ili9xxx
    dimensions:
      height: 128
      width: 128
      offset_height: 1
      offset_width: 2
    model: st7789v
    data_rate: 80MHz
    cs_pin: GPIO15
    dc_pin: GPIO33
    reset_pin: GPIO34
    invert_colors: true
    transform:
      mirror_x: true
      mirror_y: true

This is using the ESP-IDF framework which I’m given to understand is the recommended one for ESP32S3. We use the transform key instead of rotation to do the 180⁰ in hardware instead of software. The offsets are swapped compared to using the st778v component. We have to invert the colours too and using the m5stack-recommended 500Hz PWM for the backlight I find that 0.3 for the minimum duty cycle gives a usable 0-100% backlight brightness range.

Note that I do not have to send command 0x11 (SLPOUT) to the display controller at start-up - I assume the ili9xxx/st7789v driver must be doing that for us.

FYI I can get 20fps (50ms refresh) for simple graphics-primitive-based animations with the auto-clear turned off.

2 Likes

I’ve now bought another AtomS3 and have used the arduino framework type with otherwise the same configuration I’ve posted previously and it works fine too. (I prefer the arduino framework anyway - never liked the way ESP-IDF builds everything and lets God/the linker sort them out.)

1 Like