ESP32-C3 with integrated GC9A01 - cheap touch controller

Thanks, full uninstall and reinstall fixed it

1 Like

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.

1 Like

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.

1 Like

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:

1 Like
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 %)