This example demonstrates how to run LVGL (Light and Versatile Graphics Library) on an ESP32 with a 2.8‑inch TFT LCD touch screen. LVGL is an open‑source embedded GUI library offering rich widgets (buttons, sliders, charts, lists, etc.) and animations. This sketch integrates several official LVGL demos, including widget showcase, performance benchmark, keypad/encoder demo, music player UI, and stress test. Users can interact with the GUI via the touch screen to experience the power of LVGL.
LVGL_Demos.ino.#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
Note: This example does not use the
RTP_IRQpin; touch detection is done by pollingmy_touch.Pressed().
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
setup():// Uncomment one of these:
lv_demo_widgets(); // Widget showcase (recommended)
// lv_demo_benchmark(); // Performance benchmark
// lv_demo_keypad_encoder(); // Keypad/encoder demo
// lv_demo_music(); // Music player UI
// lv_demo_stress(); // Stress test
The LVGL demo layout depends on the selected demo, but generally follows LVGL’s standard style with a title bar, main content area, and bottom navigation. Refer to the LVGL documentation for detailed layouts.
⚠️ Important Notes:
lvgl libraries beforehand.my_disp_flush callback pushes LVGL’s draw buffer to the LCD.my_touchpad_read to obtain touch coordinates and passes them to LVGL’s input device driver.screenWidth/screenHeight definitions.setup().lv_demo_widgets(), which provides a rich set of widget examples for initial exploration.setCal() parameters.USE_LV_LOG non‑zero), LVGL logs will be output via Serial.RTP_IRQ is not used; touch detection is polled, which works for most applications.lv_demo_benchmark() automatically stops after completing the test.lv_demo_keypad_encoder) are designed for keypad/encoder input; touch support may be limited but still demonstrates LVGL’s input abstraction layer.This example is based on the ESP32‑WROOM‑32E with an ILI9341 TFT LCD and resistive touch. It integrates LVGL, using TFT_eSPI as the low‑level display driver and TFT_Touch as the touch input driver. LVGL display and input device callbacks are registered.
#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
RTP_IRQ is not defined because it is not used.my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
static const uint16_t screenWidth = 320;
static const uint16_t screenHeight = 240;
static lv_disp_draw_buf_t disp_buf;
static lv_color_t buf[screenWidth*30];
screenWidth and screenHeight define the resolution (landscape 320×240).disp_buf is the LVGL display buffer structure.buf is the actual buffer array of size screenWidth * 30 (320×30 pixels), taking about 19 KB of RAM. LVGL draws in chunks, refreshing 30 rows at a time.my_disp_flush() Display Flush Callbackvoid my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
my_lcd.setAddrWindow(area->x1, area->y1, w, h);
my_lcd.pushColors((uint16_t *)&color_p->full, w*h, true);
lv_disp_flush_ready(disp);
}
w and height h of the rectangle to update.my_lcd.setAddrWindow() to set the LCD's drawing window.my_lcd.pushColors() to push the pixel data (color_p) to the LCD. (uint16_t *)&color_p->full casts LVGL’s colour data to 16‑bit RGB565 format.lv_disp_flush_ready(disp) to notify LVGL that the flush is complete.#if USE_LV_LOG != 0
void my_print(const char * buf)
{
Serial.printf(buf);
Serial.flush();
}
#endif
USE_LV_LOG is defined and non‑zero, LVGL log messages are output via Serial for debugging.my_touchpad_read() Touch Read Callbackvoid my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{
bool touched = my_touch.Pressed();
if( !touched)
{
data->state = LV_INDEV_STATE_REL;
}
else
{
data->state = LV_INDEV_STATE_PR;
data->point.x = my_touch.X();
data->point.y = my_touch.Y();
}
}
my_touch.Pressed() to detect touch.data->state = LV_INDEV_STATE_REL (released).data->state = LV_INDEV_STATE_PR (pressed) and reads my_touch.X() and my_touch.Y() to fill the coordinates.void setup()
{
Serial.begin(115200);
String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
my_lcd.init();
my_lcd.fillScreen(0xFFFF);
my_lcd.setRotation(1);
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
lv_init();
delay(10);
#if USE_LV_LOG != 0
lv_log_register_print_cb(my_print);
#endif
lv_disp_draw_buf_init(&disp_buf, buf, NULL, screenWidth*30);
/* Initialize display driver */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = my_lcd.width();
disp_drv.ver_res = my_lcd.height();
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &disp_buf;
lv_disp_drv_register(&disp_drv);
/* Initialize input device driver */
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);
// Uncomment one demo
lv_demo_widgets();
// lv_demo_benchmark();
// lv_demo_keypad_encoder();
// lv_demo_music();
// lv_demo_stress();
}
Serial.begin(115200) for debug output.my_lcd.init(): initialise TFT LCD.my_lcd.fillScreen(0xFFFF): fill with white background (optional).my_lcd.setRotation(1): set landscape mode (320×240).my_touch.setCal() sets calibration values.lv_init() initialises the LVGL core.lv_log_register_print_cb(my_print).lv_disp_draw_buf_init(&disp_buf, buf, NULL, screenWidth*30) where buf is the pixel buffer, NULL means no second buffer (double buffering), and screenWidth*30 is the buffer size.hor_res and ver_res.flush_cb = my_disp_flush.draw_buf = &disp_buf.lv_disp_drv_register(&disp_drv) to register.LV_INDEV_TYPE_POINTER (pointer device, e.g., touch screen).read_cb = my_touchpad_read.lv_indev_drv_register(&indev_drv) to register.lv_demo_widgets().void loop()
{
lv_task_handler(); /* Let LVGL handle tasks (animations, events, etc.) */
delay(5);
}
lv_task_handler() is LVGL’s core task handler; it must be called regularly to update the UI, process animations, and handle input.delay(5) provides a short pause to avoid hogging the CPU. Adjust as needed.You can customise the code for different scenarios:
Uncomment the desired demo function in setup() and comment out the others:
// lv_demo_widgets();
lv_demo_benchmark(); // switch to benchmark
lv_demo_widgets(): showcases widgets – good for getting familiar.lv_demo_benchmark(): automatically measures rendering performance, displays frame rate.lv_demo_keypad_encoder(): demonstrates keypad/encoder interaction (touch may not cover all controls).lv_demo_music(): music player UI.lv_demo_stress(): stress test with high load.Modify my_lcd.setRotation() and my_touch.setRotation() parameters:
screenWidth/screenHeight.Change the number of rows in buf[screenWidth*30] (e.g., 30). Increasing may improve performance but uses more RAM. Typically 20‑40 rows is fine.
Define USE_LV_LOG as 1 (e.g., #define USE_LV_LOG 1) at the top, then view logs via Serial.
Refer to the calibration method and adjust the seven parameters in setCal().
LVGL supports theme customisation. After lv_disp_drv_register in setup(), add:
lv_theme_t * th = lv_theme_default_init(NULL, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), LV_THEME_DEFAULT_DARK, LV_FONT_DEFAULT);
lv_disp_set_theme(NULL, th); // apply to default display
LVGL supports multiple input devices; you can register keyboards, encoders, etc. Refer to the official documentation.
Change the delay(5) value in loop() or use a more precise timer.
No display or garbled screen
User_Setup.h is correctly configured (ILI9341, pins).delay(10) after lv_init() to ensure initialisation completes.Touch not responding or offset
setCal() calibration parameters.my_touch.Pressed() works (test with Serial print).Compilation errors: cannot find lvgl.h or lv_demos.h
LVGL, version 8.x or 9.x recommended).TFT_eSPI and TFT_Touch are installed.lv_demo_benchmark() stops responding after completion
Touch coordinates still inaccurate after calibration
setCal() matches the screen rotation.No LVGL log output
USE_LV_LOG is defined non‑zero and Serial.begin() is called.Poor performance (low frame rate)
screenWidth*20) to lower RAM usage, but it may increase refresh calls.lv_disp_draw_buf_init.