TFT_eSPI + LVGL on Xiao ESP32S3 + ST7789 SPI IPS with FT6236 cap touch.

To implement TFT_eSPI + LVGL on Xiao ESP32S3 + ST7789 SPI IPS with FT6236 cap touch.


First, follow previous exercise Xiao ESP32S3 display on 240*240 ST7789 SPI LCD/FT6236U cap. touch using TFT_eSPI in Arduino Framework, make sure both ST7789 and FT6236 works using TFT_eSPI library.

Install LVGL library in Arduino IDE Library Manager. Follow the video to prepare lv_conf.h file.

Then you can try LVGL example:
> File > Examples > lvgl > arduino > LVGL_Arduino

My example codes:

XS3_ST7789_TFT_eSPI_lvgl.ino
Create a button to toggle onboard LED.
/*
  TFT_eSPI + LVGL exercise
  run on Xiao ESP32S3 + 1.54" 240x240 ST7789V2 SPI IPS with FT6236 cap touch
  Create a button to toggle onboard LED.
  https://coxxect.blogspot.com/2025/10/tftespi-lvgl-on-xiao-esp32s3-st7789-spi.html
  
  modified from Examples > lvgl > arduino > LVGL_Arduino

  Library needed:
  - TFT_eSPI
  - LVGL

  Preparation:
  - Prepare TFT_eSPI in Arduino IDE Library Manager,
    follow previous exercise https://coxxect.blogspot.com/2025/08/xiao-esp32s3-display-on-240240-st7789.html,
    make sure both ST7789 and FT6236 works.
  - Install LVGL library in Arduino IDE Library Manager.
  - Go to lvgl library directory.
    Copy lv_conf_template.h as lv_conf.h into the Arduino Libraries directory.
  - To enable the content of the file:
    change the first #if 0 to #if 1
  - To use with TFT_eSPI, change the line:
    #define LV_USE_TFT_ESPI         1 //0

  To make it simple, haven't handle touch point adjustment for screen rotation.
  It work on rotation=0 only.
*/

#include <lvgl.h>
#include <TFT_eSPI.h>
#include <Wire.h>

#define CTP_INT 43
#define CTP_RST 44
#define FT6236_ADDR 0x38

/*Set to your screen resolution and rotation*/
#define TFT_HOR_RES   240
#define TFT_VER_RES   240
#define TFT_ROTATION  LV_DISPLAY_ROTATION_0

/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

/* LVGL calls it when a rendered image needs to copied to the display*/
void my_disp_flush( lv_display_t *disp, const lv_area_t *area, uint8_t * px_map)
{
    /*Call it to tell LVGL you are ready*/
    lv_display_flush_ready(disp);
}

/*Read the touchpad*/
void my_touchpad_read( lv_indev_t * indev, lv_indev_data_t * data )
{
    if (get_touch_count()>0){
        data->state = LV_INDEV_STATE_PRESSED;

        int x, y;
        
        Wire.beginTransmission(FT6236_ADDR);
        Wire.write(0x03);
        Wire.endTransmission();
        Wire.requestFrom(FT6236_ADDR, 2);
        x = (Wire.read() & 0x0f) << 8 | Wire.read();
        x = TFT_HOR_RES - x;
        
        Wire.beginTransmission(FT6236_ADDR);
        Wire.write(0x05);
        Wire.endTransmission();
        Wire.requestFrom(FT6236_ADDR, 2);
        y = (Wire.read() & 0x0f) << 8 | Wire.read();
        y = TFT_VER_RES - y;

        data->point.x = x;
        data->point.y = y;

    }else{
        data->state = LV_INDEV_STATE_RELEASED;
    }
}

int get_touch_count(){
    // Register 0x02;
    // Touch count, max 2
    Wire.beginTransmission(FT6236_ADDR);
    Wire.write(0x02);
    Wire.endTransmission();
    Wire.requestFrom(FT6236_ADDR, 1);
    
    return(Wire.read());
}

/*use Arduinos millis() as tick source*/
static uint32_t my_tick(void)
{
    return millis();
}

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, HIGH); //OFF 

    String LVGL_Arduino = "Hello Arduino! ";
    LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

    Serial.begin( 115200 );
    Serial.println( LVGL_Arduino );

    // Init I2C for FT6236 and CTR_RST
    Wire.begin();   // I2C
    delay(100);
    pinMode(CTP_RST, OUTPUT);
    digitalWrite(CTP_RST, HIGH);
    delay(100);
    digitalWrite(CTP_RST, LOW);
    delay(100);
    digitalWrite(CTP_RST, HIGH);
    delay(100);

    lv_init();

    /*Set a tick source so that LVGL will know how much time elapsed. */
    lv_tick_set_cb(my_tick);

    lv_display_t * disp;

    //Use TFT_eSPI interface
    /*TFT_eSPI can be enabled lv_conf.h to initialize the display in a simple way*/
    disp = lv_tft_espi_create(TFT_HOR_RES, TFT_VER_RES, draw_buf, sizeof(draw_buf));
    lv_display_set_rotation(disp, TFT_ROTATION);

    /*Initialize the (dummy) input device driver*/
    lv_indev_t * indev = lv_indev_create();
    lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); /*Touchpad should have POINTER type*/
    lv_indev_set_read_cb(indev, my_touchpad_read);

    /* Create LVGL content */
    lv_obj_t *label_top = lv_label_create(lv_scr_act());
    String version = "LVGL (V" + String(LVGL_VERSION_MAJOR) + "." + String(LVGL_VERSION_MINOR) + "." + String(LVGL_VERSION_PATCH) + ")";
    lv_label_set_text(label_top, version.c_str());
    lv_obj_align(label_top, LV_ALIGN_TOP_MID, 0, 0);

    lv_obj_t *label_bottom = lv_label_create(lv_screen_active());
    lv_label_set_text(label_bottom, "coXXect.blogspot.com");
    lv_obj_align(label_bottom, LV_ALIGN_BOTTOM_MID, 0, 0);

    // create a big toggle button that occupies the entire middle area
    lv_obj_t *btn = lv_btn_create(lv_scr_act());  // Create a button
    lv_obj_set_size(btn, LV_HOR_RES-100, LV_VER_RES - 100 - 30);  // Adjust size dynamically
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);  // Align to the center

    lv_obj_t *btn_label = lv_label_create(btn);  // Create label inside button
    lv_label_set_text(btn_label, "Toggle Me");  // Set label text
    lv_obj_center(btn_label);  // Center label inside button

    // Add event to toggle button text
    lv_obj_add_event_cb(btn, [](lv_event_t * e) {
      lv_obj_t * label = lv_obj_get_child((lv_obj_t *)lv_event_get_target(e), 0);
      const char * text = lv_label_get_text(label);
      
      //lv_label_set_text(label, strcmp(text, "ON") == 0 ? "OFF" : "ON");
      if (strcmp(text, "ON") == 0){
        lv_label_set_text(label, "OFF");
        digitalWrite(LED_BUILTIN, HIGH);
      }else{
        lv_label_set_text(label, "ON");
        digitalWrite(LED_BUILTIN, LOW);
      }

    }, LV_EVENT_CLICKED, NULL);

    Serial.println( "Setup done" );
}

void loop()
{
    lv_timer_handler(); /* let the GUI do its work */
    delay(5); /* let this time pass */
}


XS3_ST7789_TFT_eSPI_lv_slider.ino
Create a slider to control onboard LED brightness.
/*
  TFT_eSPI + LVGL lv_slider & lv_led exercise
  run on Xiao ESP32S3 + 1.54" 240x240 ST7789V2 SPI IPS with FT6236 cap touch
  Create a slider to control onboard LED brightness.
    https://coxxect.blogspot.com/2025/10/tftespi-lvgl-on-xiao-esp32s3-st7789-spi.html

  Library needed:
  - TFT_eSPI
  - LVGL

  Preparation:
  - Prepare TFT_eSPI in Arduino IDE Library Manager,
    follow previous exercise https://coxxect.blogspot.com/2025/08/xiao-esp32s3-display-on-240240-st7789.html,
    make sure both ST7789 and FT6236 works.
  - Install LVGL library in Arduino IDE Library Manager.
  - Go to lvgl library directory.
    Copy lv_conf_template.h as lv_conf.h into the Arduino Libraries directory.
  - To enable the content of the file:
    change the first #if 0 to #if 1
  - To use with TFT_eSPI, change the line:
    #define LV_USE_TFT_ESPI         1 //0

  To make it simple, haven't handle touch point adjustment for screen rotation.
  It work on rotation=0 only.
*/

#include <lvgl.h>
#include <TFT_eSPI.h>
#include <Wire.h>

#define CTP_INT 43
#define CTP_RST 44
#define FT6236_ADDR 0x38

/*Set to your screen resolution and rotation*/
#define TFT_HOR_RES   240
#define TFT_VER_RES   240
#define TFT_ROTATION  LV_DISPLAY_ROTATION_0

/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

String LVGL_Arduino;

/* LVGL calls it when a rendered image needs to copied to the display*/
void my_disp_flush( lv_display_t *disp, const lv_area_t *area, uint8_t * px_map)
{
    /*Call it to tell LVGL you are ready*/
    lv_display_flush_ready(disp);
}

/*Read the touchpad*/
void my_touchpad_read( lv_indev_t * indev, lv_indev_data_t * data )
{
    if (get_touch_count()>0){
        data->state = LV_INDEV_STATE_PRESSED;

        int x, y;
        
        Wire.beginTransmission(FT6236_ADDR);
        Wire.write(0x03);
        Wire.endTransmission();
        Wire.requestFrom(FT6236_ADDR, 2);
        x = (Wire.read() & 0x0f) << 8 | Wire.read();
        x = TFT_HOR_RES - x;
        
        Wire.beginTransmission(FT6236_ADDR);
        Wire.write(0x05);
        Wire.endTransmission();
        Wire.requestFrom(FT6236_ADDR, 2);
        y = (Wire.read() & 0x0f) << 8 | Wire.read();
        y = TFT_VER_RES - y;

        data->point.x = x;
        data->point.y = y;

    }else{
        data->state = LV_INDEV_STATE_RELEASED;
    }
}

int get_touch_count(){
    // Register 0x02;
    // Touch count, max 2
    Wire.beginTransmission(FT6236_ADDR);
    Wire.write(0x02);
    Wire.endTransmission();
    Wire.requestFrom(FT6236_ADDR, 1);
    
    return(Wire.read());
}

/*use Arduinos millis() as tick source*/
static uint32_t my_tick(void)
{
    return millis();
}

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);  // Set pin as output
    analogWrite(LED_BUILTIN, 255); // OFF

    LVGL_Arduino = "LVGL ";
    LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

    Serial.begin( 115200 );
    Serial.println( LVGL_Arduino );

    // Init I2C for FT6236 and CTR_RST
    Wire.begin();   // I2C
    delay(100);
    pinMode(CTP_RST, OUTPUT);
    digitalWrite(CTP_RST, HIGH);
    delay(100);
    digitalWrite(CTP_RST, LOW);
    delay(100);
    digitalWrite(CTP_RST, HIGH);
    delay(100);

    lv_init();

    /*Set a tick source so that LVGL will know how much time elapsed. */
    lv_tick_set_cb(my_tick);

    lv_display_t * disp;

    //Use TFT_eSPI interface
    /*TFT_eSPI can be enabled lv_conf.h to initialize the display in a simple way*/
    disp = lv_tft_espi_create(TFT_HOR_RES, TFT_VER_RES, draw_buf, sizeof(draw_buf));
    lv_display_set_rotation(disp, TFT_ROTATION);

    /*Initialize the (dummy) input device driver*/
    lv_indev_t * indev = lv_indev_create();
    lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); /*Touchpad should have POINTER type*/
    lv_indev_set_read_cb(indev, my_touchpad_read);

    /* Create LVGL content */
    lv_example_slider_1();

    Serial.println( "Setup done" );
}

void loop()
{
    lv_timer_handler(); /* let the GUI do its work */
    delay(5); /* let this time pass */
}

/*
    Modified examples from 
    LVGL Slider (lv_slider):
    https://docs.lvgl.io/master/details/widgets/slider.html

    LED (lv_led):
    https://docs.lvgl.io/master/details/widgets/led.html
*/
static void slider_event_cb(lv_event_t * e);
static lv_obj_t * slider_label;
lv_obj_t * led;

/**
 * A default slider with a label displaying the current value
 */
void lv_example_slider_1(void)
{
    /*Create a container with COLUMN flex direction*/
    lv_obj_t * cont_main = lv_obj_create(lv_screen_active());
    lv_obj_set_size(cont_main, TFT_HOR_RES-10, TFT_VER_RES-50);
    lv_obj_align(cont_main, LV_ALIGN_CENTER, 0, 0);
    lv_obj_set_flex_flow(cont_main, LV_FLEX_FLOW_COLUMN);

    lv_obj_t *label_top = lv_label_create(lv_screen_active());
    lv_label_set_text(label_top, "Arduino LVGL Exercise on ESP32S3");
    lv_obj_align(label_top, LV_ALIGN_TOP_MID, 0, 0);

    lv_obj_t *label_bottom = lv_label_create(lv_screen_active());
    lv_label_set_text(label_bottom, "coXXect.blogspot.com");
    lv_obj_align(label_bottom, LV_ALIGN_BOTTOM_MID, 0, 0);

    lv_obj_t *label_lvgl = lv_label_create(cont_main);
    lv_label_set_text(label_lvgl, LVGL_Arduino.c_str());

    /* Create LED*/
    led  = lv_led_create(cont_main);
    lv_obj_align(led, LV_ALIGN_CENTER, 0, 0);

    lv_led_set_color(led, lv_color_hex(0xffa500));
    lv_led_set_brightness(led, 0);

    /*Create a slider in the center of the display*/
    lv_obj_t * slider = lv_slider_create(cont_main);
    lv_obj_set_width(slider, 200);
    lv_obj_center(slider);
    lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

    lv_obj_set_style_anim_duration(slider, 2000, 0);
    /*Create a label below the slider*/
    slider_label = lv_label_create(lv_screen_active());
    lv_label_set_text(slider_label, "0%");

    lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);

}

static void slider_event_cb(lv_event_t * e)
{
    lv_obj_t * slider = lv_event_get_target_obj(e);
    char buf[8];
    int slider_value = (int)lv_slider_get_value(slider);

    lv_snprintf(buf, sizeof(buf), "%d%%", slider_value);
    lv_label_set_text(slider_label, buf);
    lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);

    int led_value = map(slider_value, 0, 100, 255, 0);
    analogWrite(LED_BUILTIN, led_value);

    int brightness_value = map(slider_value, 0, 100, 0, 255);
    lv_led_set_brightness(led, brightness_value);

}

Related:
~ Using Arduino_GFX_Library with LVGL, instead of TFT_eSPI.


Comments

Popular posts from this blog

Drive 320x240 ILI9341 SPI TFT using ESP32-S3 (NodeMCU ESP-S3-12K-Kit) using TFT_eSPI library, in Arduino Framework.

480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library