ESP32-C3 with integrated GC9A01 - cheap touch controller

Thanks, full uninstall and reinstall fixed it

Anybody getting boot loop recently? I have not changed my code, seems to have happened at some point after ESPHome update. I’ve been reading something about version 2.0.14 being the last one working:
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
version: 2.0.14
but that doesn’t work for me, all kinds of issues compiling it.

My suggestion is to check out my updated code - I’ve gone over to

  framework:
    type: esp-idf
    version: recommended

as well as LVGL and - painful as converting over was - not looking back. Most of my stability issues were due to running out of memory, and migrating over to esp-idf has freed up a noticeable amount of memory making them more responsive. I do have one device that reboots every now and then, but as I have a number of them and the one has issues I think that is a hardware fault.

Thanks for the code, I got the device back to life now. Using lvgl now. Also the screen seems to be more responsive. I was using some early touchscreen and display platforms before. I was about to throw it away, but it’s usable again. I might still be tempted to get Waveshare ESP32-C6 1.47inch.

I’m getting an error suggesting the display is not initialized properly. Copilot says in such cases LVGL might still work, and it does, I can draw in LVGL even with that error still there.
But when not using LVGL the display is just covered with random dots and the display initialization error is also shown in logs.
I already tried data_rate: 4MHz and update_interval: 1s

substitutions:
  screenstart: ALWAYS_ON
  #GPIO pins for the LCD screen
  repin: GPIO1
  dcpin: GPIO2
  bkpin: GPIO3
  clpin: GPIO6
  mopin: GPIO7
  cspin: GPIO10
  # GPIO pins for the touch screen
  sdapin: GPIO4
  sclpin: GPIO5
  intpin: GPIO8

display:
  - platform: ili9xxx
    model: GC9A01A
    id: watchface
    invert_colors: true
    reset_pin: $repin
    cs_pin: $cspin
    dc_pin:
      number: $dcpin
      ignore_strapping_warning: true
    update_interval: 200ms
    pages:
      - id: page1
        lambda: |-
          it.image(0, 00, id(bathroom2));
spi:
  mosi_pin: $mopin
  clk_pin: $clpin
  miso_pin: GPIO8   # free pin, not connected

i2c:
  sda: $sdapin
  scl: $sclpin

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf
    version: recommended
[15:10:09.378][C][ili9xxx:089]: ili9xxx
[15:10:09.378][C][ili9xxx:089]:   Rotations: 0 °
[15:10:09.378][C][ili9xxx:089]:   Dimensions: 240px x 240px
[15:10:09.378][C][ili9xxx:090]:   Width Offset: 0
[15:10:09.378][C][ili9xxx:090]:   Height Offset: 0
[15:10:09.380][C][ili9xxx:099]:   Color mode: 16bit
[15:10:09.383][C][ili9xxx:108]:   Data rate: 8MHz
[15:10:09.385][C][ili9xxx:111]:   CS Pin: GPIO10
[15:10:09.388][C][ili9xxx:112]:   DC Pin: GPIO2
[15:10:09.391][C][ili9xxx:114]:   Color order: BGR
[15:10:09.391][C][ili9xxx:114]:   Swap_xy: NO
[15:10:09.391][C][ili9xxx:114]:   Mirror_x: YES
[15:10:09.391][C][ili9xxx:114]:   Mirror_y: NO
[15:10:09.391][C][ili9xxx:114]:   Invert colors: YES
[15:10:09.398][C][ili9xxx:124]:   => Failed to init Memory: YES!
[15:10:09.398][C][ili9xxx:362]:   Update Interval: 1.0s
[15:10:09.399][E][component:154]:   display is marked FAILED: unspecified
[15:10:09.452][C][light:088]: Light 'Display Backlight'
[15:10:09.454][C][light:091]:   Default Transition Length: 1.0s
[15:10:09.454][C][light:091]:   Gamma Correct: 2.80

Hmmmm… AI is a handy tool, and in general is getting better, but can still be misleading. First thing I’d do would be to do a ‘clean build files’ and recompile just in case. If the error is still there then can investigate further! :slight_smile:

[FWIW I just recompiled my code under esphome 2025.10.5 and HA 2025.11.2, and no errors…]

Use the mipi_spi driver instead of ili9xxx, it will cope better with limited memory as it can use a partial buffer.

Thanks - that fixed it.
First I replaced ili9xxx with mipi_spi with no other changes and LVGL still worked, but no more memory initialization error.
Then I replaced LVGL with Display Lambda to show a picture and that also worked.

Did you get this working?

I am looking to do something similar using Music Assistant.
If your willing to share your code that would be amazing.

As an aside, FWIW I think this has a lot of promise to help generate a decent looking display:

substitutions:
  friendly_name: Round Display
  screensaver: 30s 

esphome:
  name: round_display
  friendly_name: $friendly_name

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

logger:
  level: INFO
  baud_rate: 0

api:

ota:
  - platform: esphome

wifi:
  ssid: "123"
  password: "123123123"
  ap:
    ssid: "RDisplay Fallback Hotspot"
    password: "123123123"

captive_portal:

web_server:
  version: 3
  local: True
  log: False

time:
  - platform: sntp
    id: esptime
    timezone: "Europe/Moscow"
    servers:
     - time.apple.com
     - time.google.com
     - 1.pool.ntp.org

globals:
  - id: is_day
    type: bool
    restore_value: false
    initial_value: 'true'
  - id: touch_start_x
    type: int
    initial_value: '0'
  - id: touch_start_y
    type: int
    initial_value: '0'
  - id: touch_end_x
    type: int
    initial_value: '0'
  - id: touch_end_y
    type: int
    initial_value: '0'
  - id: touch_start_time
    type: unsigned long
    initial_value: '0'

sensor:
  - platform: scd4x
    update_interval: 20s
    co2:
      name: "SCD41 CO2"
      id: scd41_co2
    temperature:
      name: "SCD41 Temperature"
      id: scd41_temp
    humidity:
      name: "SCD41 Humidity"
      id: scd41_hum

  - platform: bh1750
    name: "BH1750 Illuminance"
    id: bh1750_lux
    address: 0x23
    update_interval: 20s

  - platform: bmp085
    temperature:
      name: "BMP180 Temperature"
      id: bmp180_temp
    pressure:
      name: "BMP180 Pressure"
      id: bmp180_pres
    update_interval: 20s

  - platform: htu21d
    model: htu21d
    temperature:
      name: "HTU21D Temperature"
      id: htu21d_temp
    humidity:
      name: "HTU21D Humidity"
      id: htu21d_hum
    update_interval: 20s

spi:
  mosi_pin: GPIO7
  clk_pin: GPIO6

i2c:
  sda: GPIO20
  scl: GPIO21
  frequency: 100kHz
  scan: true
  id: touchscreen_i2c

# Using native CST816 touchscreen platform
touchscreen:
  - platform: cst816
    id: my_touchscreen
    address: 0x15
    reset_pin: GPIO8
    skip_probe: true
    i2c_id: touchscreen_i2c
    transform:
      mirror_x: false
      mirror_y: false
      swap_xy: false
    on_touch:
      then:
        - lambda: |-
            ESP_LOGI("TOUCH", "=== TOUCH x=%d, y=%d ===", touch.x, touch.y);
            id(touch_start_x) = touch.x;
            id(touch_start_y) = touch.y;
            id(touch_end_x) = touch.x;
            id(touch_end_y) = touch.y;
            id(touch_start_time) = millis();
        - script.execute: screentime
    on_release:
      then:
        - lambda: |-
            ESP_LOGI("TOUCH", "=== RELEASE ===");
            unsigned long touch_duration = millis() - id(touch_start_time);
            int delta_x = id(touch_end_x) - id(touch_start_x);
            int delta_y = id(touch_end_y) - id(touch_start_y);
            int abs_delta_x = abs(delta_x);
            int abs_delta_y = abs(delta_y);
            
            ESP_LOGI("TOUCH", "Duration=%lu ms, delta_x=%d, delta_y=%d", 
                     touch_duration, delta_x, delta_y);
            
            // CST816 в polling mode генерирует только одно событие touch
            // Используем ДЛИТЕЛЬНОСТЬ касания и ПОЛОЖЕНИЕ для определения жестов:
            // - Касание в верхней части экрана (y < 60) = SWIPE UP
            // - Касание в нижней части (y > 180) = SWIPE DOWN
            // - Касание слева (x < 60) = SWIPE LEFT
            // - Касание справа (x > 180) = SWIPE RIGHT
            // - В центре экрана - клик или долгое нажатие
            
            int touch_x = id(touch_start_x);
            int touch_y = id(touch_start_y);
            
            if (touch_duration > 500) {
              // Длинное нажатие
              ESP_LOGI("TOUCH", "Gesture: LONG PRESS");
              id(my_touch_screen).publish_state("LONG PRESS");
            } else if (touch_y < 60) {
              // Верхняя часть экрана
              ESP_LOGI("TOUCH", "Gesture: SWIPE UP (top area)");
              id(my_touch_screen).publish_state("SWIPE UP");
            } else if (touch_y > 180) {
              // Нижняя часть экрана
              ESP_LOGI("TOUCH", "Gesture: SWIPE DOWN (bottom area)");
              id(my_touch_screen).publish_state("SWIPE DOWN");
            } else if (touch_x < 60) {
              // Левая часть экрана
              ESP_LOGI("TOUCH", "Gesture: SWIPE LEFT (left area)");
              id(my_touch_screen).publish_state("SWIPE LEFT");
            } else if (touch_x > 180) {
              // Правая часть экрана
              ESP_LOGI("TOUCH", "Gesture: SWIPE RIGHT (right area)");
              id(my_touch_screen).publish_state("SWIPE RIGHT");
            } else {
              // Центр экрана - короткое нажатие
              ESP_LOGI("TOUCH", "Gesture: SINGLE CLICK (center)");
              id(my_touch_screen).publish_state("SINGLE CLICK");
            }

# скрипт управления подсветкой
script:
  - id: screentime
    mode: restart
    then:
      - light.control: 
          id: back_light
          brightness: 100%
      - delay: $screensaver
      - if:
          condition:
            lambda: 'return id(is_day);'
          then:
            - light.control: 
                id: back_light
                brightness: 100%
          else:
            - light.control: 
                id: back_light
                brightness: 20%

text_sensor:
  - platform: template
    id: my_touch_screen
    name: "Touch Gesture"
    internal: true
    on_value:
      then:
        - script.execute: screentime
        - if:
            condition:
              text_sensor.state:
                id: my_touch_screen
                state: 'SINGLE CLICK' 
            then:
              - logger.log: "Single click detected"
        - if:
            condition:
              text_sensor.state:
                id: my_touch_screen
                state: 'SWIPE RIGHT'
            then:
              - display.page.show_next: watchface
              - component.update: watchface
              - logger.log: "Swiped right, next page"
        - if:
            condition:
              text_sensor.state:
                id: my_touch_screen
                state: 'SWIPE LEFT'
            then:
              - display.page.show_previous: watchface 
              - delay: 0.5s
              - component.update: watchface
              - logger.log: "Swiped left, previous page"
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'LONG PRESS'
                - display.is_displaying_page: page2   
            then:
              - logger.log: "Long press on page2"
              - display.page.show: page2
              - component.update: watchface

output:
  - platform: ledc
    pin: GPIO3
    id: gpio_3_backlight_pwm
    frequency: 1000Hz

light:
  - platform: monochromatic
    output: gpio_3_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: RESTORE_DEFAULT_ON

font:
  - file: "gfonts://Roboto"
    id: font_12
    size: 12
  - file: "gfonts://Roboto"
    id: font_16
    size: 16
  - file: "gfonts://Roboto"
    id: font_20
    size: 20
  - file: "gfonts://Roboto"
    id: font_24
    size: 24
  - file: "gfonts://Roboto"
    id: font_32
    size: 32
  - file: "gfonts://Roboto"
    id: font_64
    size: 64
  - file: "gfonts://Material Design Icons"
    id: icon_font_24
    size: 24
    glyphs: [
      "\U000F0E0E", # mdi:air-filter (CO2)
      "\U000F0E02", # mdi:thermometer (температура)
      "\U000F058E", # mdi:water-percent (влажность)
      "\U000F0F55", # mdi:gauge (давление)
      "\U000F0335", # mdi:brightness-6 (освещенность)
      "\U000F0954"  # mdi:clock-outline (время)
    ]
  - file: "gfonts://Material Design Icons"
    id: icon_font_16
    size: 16
    glyphs: [
      "\U000F0E0E", # mdi:air-filter (CO2)
      "\U000F0E02", # mdi:thermometer (температура)
      "\U000F058E", # mdi:water-percent (влажность)
      "\U000F0F55", # mdi:gauge (давление)
      "\U000F0335", # mdi:brightness-6 (освещенность)
      "\U000F0954"  # mdi:clock-outline (время)
    ]

color:
  - id: my_red
    hex: FF0000
  - id: my_green
    hex: 008000
  - id: my_blue
    hex: 0000FF
  - id: my_yellow
    hex: FFFF00
  - id: my_cyan
    hex: 00FFFF
  - id: my_white
    hex: FFFFFF

display:
  - platform: ili9xxx
    invert_colors: true
    model: GC9A01A
    id: watchface
    reset_pin: GPIO1
    cs_pin: GPIO10
    dc_pin: 
      number: GPIO2
      ignore_strapping_warning: true
    update_interval: 1s
    pages:
      - id: page1
        lambda: |-
          // CO2 с иконкой
          it.printf(60, 65, id(icon_font_24), id(my_yellow), TextAlign::CENTER, "\U000F0E0E");
          it.printf(180, 65, id(font_32), id(my_yellow), TextAlign::CENTER, "%.0f ppm", id(scd41_co2).state);
        
          // Время с иконкой
          it.printf(60, 120, id(icon_font_24), id(my_yellow), TextAlign::CENTER, "\U000F0954");
          it.strftime(180, 120, id(font_64), TextAlign::CENTER, "%H:%M:%S", id(esptime).now());
        
          // Температура с иконкой
          it.printf(60, 170, id(icon_font_24), id(my_yellow), TextAlign::CENTER, "\U000F0E02");
          it.printf(180, 170, id(font_32), id(my_yellow), TextAlign::CENTER, "%.1f°C", id(scd41_temp).state);
        
          // Влажность с иконкой
          it.printf(60, 205, id(icon_font_24), id(my_yellow), TextAlign::CENTER, "\U000F058E");
          it.printf(180, 205, id(font_32), id(my_yellow), TextAlign::CENTER, "%.1f%%", id(scd41_hum).state);
        
          it.circle(120, 120, 119, id(my_yellow));  
        
      - id: page2
        lambda: |-
          // CO2 с иконкой
          it.printf(60, 65, id(icon_font_24), id(my_red), TextAlign::CENTER, "\U000F0E0E");
          it.printf(180, 65, id(font_32), id(my_red), TextAlign::CENTER, "%.0f ppm", id(scd41_co2).state);
        
          // Время с иконкой
          it.printf(60, 120, id(icon_font_24), id(my_red), TextAlign::CENTER, "\U000F0954");
          it.strftime(180, 120, id(font_32), TextAlign::CENTER, "%H:%M:%S", id(esptime).now());
        
          // Температура HTU21D с иконкой
          it.printf(60, 170, id(icon_font_24), id(my_red), TextAlign::CENTER, "\U000F0E02");
          it.printf(180, 170, id(font_32), id(my_red), TextAlign::CENTER, "%.1f°C", id(htu21d_temp).state);
        
          // Влажность HTU21D с иконкой
          it.printf(60, 205, id(icon_font_24), id(my_red), TextAlign::CENTER, "\U000F058E");
          it.printf(180, 205, id(font_32), id(my_red), TextAlign::CENTER, "%.1f%%", id(htu21d_hum).state);
        
          it.circle(120, 120, 119, id(my_red)); 

      - id: page3
        lambda: |-
          // Заголовок
          it.printf(120, 15, id(font_20), id(my_cyan), TextAlign::CENTER, "ALL SENSORS");
        
          // SCD41 данные с иконками
          it.printf(20, 40, id(icon_font_16), id(my_white), TextAlign::CENTER, "\U000F0E0E");
          it.printf(50, 40, id(font_16), id(my_white), TextAlign::LEFT, "CO2: %.0f ppm", id(scd41_co2).state);
        
          it.printf(20, 58, id(icon_font_16), id(my_white), TextAlign::CENTER, "\U000F0E02");
          it.printf(50, 58, id(font_16), id(my_white), TextAlign::LEFT, "T: %.1f°C", id(scd41_temp).state);
        
          it.printf(140, 58, id(icon_font_16), id(my_white), TextAlign::CENTER, "\U000F058E");
          it.printf(170, 58, id(font_16), id(my_white), TextAlign::LEFT, "H: %.1f%%", id(scd41_hum).state);
        
          // HTU21D данные с иконками
          it.printf(20, 85, id(icon_font_16), id(my_green), TextAlign::CENTER, "\U000F0E02");
          it.printf(50, 85, id(font_16), id(my_green), TextAlign::LEFT, "HTU T: %.1f°C", id(htu21d_temp).state);
        
          it.printf(20, 103, id(icon_font_16), id(my_green), TextAlign::CENTER, "\U000F058E");
          it.printf(50, 103, id(font_16), id(my_green), TextAlign::LEFT, "HTU H: %.1f%%", id(htu21d_hum).state);
        
          // BMP180 данные с иконками
          it.printf(20, 130, id(icon_font_16), id(my_yellow), TextAlign::CENTER, "\U000F0E02");
          it.printf(50, 130, id(font_16), id(my_yellow), TextAlign::LEFT, "BMP T: %.1f°C", id(bmp180_temp).state);
        
          it.printf(20, 148, id(icon_font_16), id(my_yellow), TextAlign::CENTER, "\U000F0F55");
          it.printf(50, 148, id(font_16), id(my_yellow), TextAlign::LEFT, "P: %.0f hPa", id(bmp180_pres).state);
        
          it.printf(20, 175, id(icon_font_16), id(my_blue), TextAlign::CENTER, "\U000F0335");
          it.printf(50, 175, id(font_16), id(my_blue), TextAlign::LEFT, "Light: %.0f lx", id(bh1750_lux).state);
        
          // Время с иконкой
          it.printf(80, 210, id(icon_font_16), id(my_cyan), TextAlign::CENTER, "\U000F0954");
          it.strftime(160, 210, id(font_20), id(my_cyan), TextAlign::CENTER, "%H:%M:%S", id(esptime).now());
        
          // Рамка
          it.circle(120, 120, 119, id(my_cyan));


sun:
  latitude: 10.00
  longitude: 20.00

  on_sunrise:
    - then: 
        - globals.set:
            id: is_day
            value: 'true'
        - light.control:
            id: back_light
            brightness: 100%
        - logger.log: "Sunrise: brightness set to 100%"

  on_sunset:
    - elevation: -1
      then: 
        - globals.set:
            id: is_day
            value: 'false'
        - light.control:
            id: back_light
            brightness: 20%
        - logger.log: "Sunset: brightness set to 20%"

I spent the whole evening trying to get the display to work on the new version of esphome %)

Been trying to get a bluetooth proxy working and a display… really struggling, though i cant see any problems with my config? can anyone else

substitutions:
#GPIO pins for the LCD screen
  repin: GPIO1
  dcpin: GPIO2
# Note - you may see an error on compilation "WARNING GPIO2 is a Strapping PIN and should be avoided" - ignore this as you have no choice
# This error message may be removed via the "ignore_strapping_warning" option for the screen driver
  bkpin: GPIO3
  clpin: GPIO6
  mopin: GPIO7
  cspin: GPIO10
# GPIO pins for the touch screen
  sdapin: GPIO4
  sclpin: GPIO5
  intpin: GPIO8  

esphome:
  name: esphome-web-e90f20
  friendly_name: ESPHome Web e90f20

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf
    sdkconfig_options:
      # CRITICAL: Fix for C3 Memory crashes with Bluetooth + Display
      CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL: n
      CONFIG_BT_NIMBLE_MAX_CONNECTIONS: "2"
      CONFIG_ESP32C3_REV_MIN_3: "y"

globals:
  - id: last_device_name
    type: std::string
    initial_value: '"Scanning..." '

# Enable logging
# Change to avoid "Components should block for at most 20-30ms" warning messages in the log - an issue since 2023.7.0
# Not really a breaking change - it's an issue I suspect due to the device being slow and this error previously
# simply not being reported
logger:
  level: DEBUG #makes uart stream available in esphome logstream
  logs:
    component: ERROR
  #Turn off UART logging over RX/TX
  baud_rate: 0

#uart:
#  rx_pin: GPIO20
#  tx_pin: GPIO21
#  baud_rate: 9600
#  id: uart_bus

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

ota:
  - platform: esphome
    password: !secret ota_password

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esphome-Web-E90F20"
    password: ""


# Optimization: Move BLE settings to the 2026-standard component
esp32_ble:
  max_connections: 2

bluetooth_proxy:
  active: true

esp32_ble_tracker:
  scan_parameters:
    interval: 1100ms
    window: 1100ms
    active: true
  on_ble_advertise:
    - then:
        - lambda: |-
            if (x.get_name().length() > 0) {
              id(last_device_name) = x.get_name();
            }

spi:
  mosi_pin: $mopin
  clk_pin: $clpin

i2c:
  sda: $sdapin
  scl: $sclpin

font:
  - file: 'fonts/GoogleSans-Medium.ttf'
    id: font_16
    size: 16
  - file: 'fonts/GoogleSans-Medium.ttf'
    id: font_32
    size: 32

# Existing Display Fix: Use 8-bit color to save 50% RAM
display:
  - platform: ili9xxx
    model: GC9A01A
    id: screen
    color_palette: 8BIT 
    invert_colors: true
    reset_pin: $repin
    cs_pin: $cspin
    dc_pin:
      number: $dcpin
      ignore_strapping_warning: true
    update_interval: 1s
    lambda: |-
      // Use your defined fonts from your original YAML
      it.print(120, 40, id(font_16), TextAlign::CENTER, "BT Proxy Active");
      it.line(40, 60, 200, 60);
      it.printf(120, 100, id(font_16), TextAlign::CENTER, "Last Found:");
      it.printf(120, 140, id(font_32), TextAlign::CENTER, "%s", id(last_device_name).c_str());

# ... (Keep your wifi, api, ota, and touch components exactly as they were)

As you’re setting up bluetooth, make sure that the first time you set it up that you do it via a serial cable - otherwise the partitions won’t be created properly and could lead to weird issues. Otherwise, or just to be safe, add into the esp32: section

  partitions: "esp_part.csv"

and create a text file called “esp_part.csv” in cour config directory with the following in it:

padding, data, undefined,    ,       0x4000,
phy_init, data, phy,     ,        0x1000,
otadata,  data, ota,     ,        0x2000,
app0,     app,  ota_0,   ,        0x1C0000,
app1,     app,  ota_1,   ,        0x1C0000,
nvs,      data, nvs,     ,        0x6D000,

Separately, my hopes and prayers are with you. :wink: The bluetooth proxy stuff takes up a lot of memory to work reliably, and the device does not have a lot of ram. I did get it working but even when trimming everything down as much as I could the poor thing would struggle . I would at the very least put this in to track both uptime and available memory. If it drops below 80kb free to consider alternatives…

sensor:
  - platform: uptime
    name: "uptime"
  - platform: template
    name: "free memory"
    lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
    icon: "mdi:memory"
    entity_category: diagnostic
    state_class: measurement
    unit_of_measurement: "b"
    update_interval: 60s

Oh, and if you are having issues seeing anything on the screen, make sure that you are turning on the backlight. Check my sample code eg:

output:
  - platform: ledc
    pin: $bkpin
    id: gpio_3_backlight_pwm

light:
  - platform: monochromatic
    output: gpio_3_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: $screenstart

FWIW My bad. On further checking I found that the capacitor I had put in to smooth the power being fed to the device had failed. Replaced and all good again.

I don’t know if this will help anyone else, but I had a hell of a slog getting the Elecrow 1.28 inch CrowPanel to play nice with ESPhome. That’s this unit, btw: Crowpanel -ESP32 Display-1.28(R) inch 240*240 Round IPS Display Capacitive Touch SPI Screen

Compared to the rotary-knob model, the i2c tapdance was rather arcane.
Anyway, this works with ESPHOME 2026.2.1 - and works around the nigh-impossibility of finding a 240x240x16b contiguous buffer for the display.

Sorry, no touch. I was just happy to get the display running properly at last.

esphome:
  name: crowpanel-128
  on_boot:
    priority: 550.0
    then:
      - lambda: |-
          ESP_LOGD("boot", "Applying hard power sequences...");
          id(buzzer_pin).turn_off();
          id(lcd_power_en).turn_on();
          delay(250);
          id(lcd_backlight).turn_on();

esp32:
  board: esp32-c3-devkitm-1
  variant: esp32c3
  framework:
    type: arduino

spi:
  clk_pin: GPIO6
  mosi_pin: GPIO7
  id: spi_bus

i2c:
  sda: GPIO4
  scl: GPIO5
  id: bus_a

pi4ioe5v6408:
  id: io_ext
  address: 0x43

output:
  - platform: gpio
    pin: GPIO3
    id: buzzer_pin
  - platform: gpio
    pin:
      number: 2
      pi4ioe5v6408: io_ext
    id: lcd_backlight
  - platform: gpio
    pin:
      number: 3
      pi4ioe5v6408: io_ext
    id: lcd_power_en

display:
  - platform: ili9xxx
    model: GC9A01A
    id: my_display
    spi_id: spi_bus
    cs_pin: GPIO10
    dc_pin: GPIO2
    reset_pin:
      number: 4
      pi4ioe5v6408: io_ext
    invert_colors: true
    # Switched to BGR as red background indicated flipped color channels
    color_order: BGR
    dimensions:
      width: 240
      height: 240
    transform:
      mirror_x: true

# LVGL Component 
lvgl:
  # buffer_size: 10% of 240x240 is 5760. 
  # This only requires ~11KB of RAM, which is almost guaranteed to succeed.
  buffer_size: 10%
  displays:
    - my_display
  # LVGL draws widgets instead of raw lines/pixels in a lambda.
  widgets:
    - obj:
        height: 100%
        width: 100%
        bg_color: 0x0000FF # Blue background
        widgets:
          - label:
              text: "LVGL SUCCESS!"
              align: CENTER
              text_color: 0xFFFFFF # White text

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

api:
  max_connections: 2

ota:
  - platform: esphome

logger:
  level: DEBUG
  hardware_uart: USB_SERIAL_JTAG