In many projects that requiring a user interface you see the uses of a TFT display. A popular and efficient approach is the ESP32 Cheap Yellow Display Board (ESP32-2432S028R). This solution is a cost-effective option that simplifies the creation of graphical user interfaces (GUIs) for IoT projects. This integrated design eliminates the need for complex PCB design or extensive hardware wiring, streamlining the development process since this is direct equimped with a ESP32.
This solution is ideally suited for integration with LVGL (Light and Versatile Graphics Library), a popular free and open-source graphics library designed to simplify embedded GUI development. LVGL offers an extensive collection of easy-to-use graphical elements, stunning visual effects, and a minimal memory footprint, making it the perfect choice for creating efficient and visually appealing user interfaces.
This beginners guide provides a step-by-step tutorial on getting started with the ESP32 Cheap Yellow Display Board (ESP32-2432S028R) using LVGL (Light and Versatile Graphics Library). For this tutorial project we are using the PlatformIO framework within Visual Studio Code.

When working with a standalone TFT Touchscreen Display 2.8 inch with an ILI9341 or ST7789 driver and want to uses LVGL (Light and Versatile Graphics Library) this guide can be a helpful starting point for your project.
The ESP32 Cheap Yellow Display Board – CYD (ESP32-2432S028R)
The ESP32-2432S028R is one of several options available from Yellow Display Boards, a popular choice among makers and hobbyists. The specific model used in this guide features a 2.8-inch TFT display with touch functionality, but other sizes are also available online, including 4.3″, 5.0″, and 7.0″ variants. These boards have gained recognition within the maker community as “Cheap Yellow Display” or CYD develop board, because of their affordability and versatility.
These boards are particularly convenient because they seamlessly integrate a TFT display with touch capabilities
and a popular developer microcontroller board, eliminating the need for manual wiring or creating a PCB. They have
proven to be extremely useful in my own projects and testing—thanks to their display and processing power, as well
as access to additional IO pins, which allows adding extra hardware as needed.
Next is shown the ESP32-2432S028R version 3 develop board

Back side

Front side
ESP32-2432S028R features
- Dimensions
- Module size 50.0×86.0mm
- Product weight: approximately 50g
- Connections
- Serial
- USB micro
- USB-C (only on the v3)
- Power
- Operating Voltage: 5V
- Power consumption: approximately 115mA
- Microcontroller ESP-WROOM-32
- Dual-core MCU, integrated WI-FI and Bluetooth functions
- Frequency can reach 240MHz
- 520KB SRAM, 448KB ROM, Flash size is 4MB
- TFT display ILI9341(v1,v2) or ST7789(v3)
- 2.8-inch color screen, support 16 BIT RGB 65K color display, display rich colors
- 240X320 resolution
- Backlight control circuit
- Onboard peripherals
- TF card interface for external storage
- RGB LED
- built-in LDR (light-dependent resistor)
- Speaker interface
- Extended IO
⚠️Different versions
There are different version of ESP32-2432S028R available on the marked. Version 3 has another type of display and an extra USB C port. See for details on the page about the Pinout ESP32 Cheap Yellow Display Board(CYD) ESP32-2432S028R
Where to buy?
There are several stores where you can buy the ESP32-2432S028R the most common is Aliexpress. (By using my link you support Kafkar)
The Light and Versatile Graphics Library (LVGL)
LVGL is a Light and Versatile Graphics Library, ideally suited for running on an ESP32 with a display. It boasts high speed and a low memory footprint. The library provides a robust set of UI building blocks, including buttons, charts, lists, sliders, images, and more. These elements can be easily combined and configured to create user interfaces for displaying information and enabling touch-based controls. The library offers extensive features beyond these core elements. For detailed information, please visit lvgl.io and consult the comprehensive documentation at docs.lvgl.io
Creating an example application with Platform.io
Create a new Platform.io project
To begin this project, we created a new empty Platform.io project in Visual Studio Code. Ensure that Visual Studio Code is installed with Platform.IO
For this guide, we named the project “ESP32-2432S028-Tutorial.” The board type selected was NodeMCU-32S, and the framework used was Arduino.

After pressing Finish, the project will be created. If the project is successfully created, you will find the file platform.ini
in the root directory of the project folder. This file will contain the necessary environment settings.
[env:nodemcu-32s]
platform = espressif32
board = nodemcu-32s
framework = arduino
Adding display Libraries to the project
The next step is to add the needed libraries To control the TFT Display,the Touchscreen using SPI communication protocol and the LVGL liberarye have to add the following Liberarys:
TFT_eSPI created by Bodmer [https://github.com/Bodmer/TFT_eSPI]
This is a TFT library optimized for the Raspberry Pi Pico (RP2040), STM32, ESP8266 and ESP32 that supports different driver chips
XPT2046_Touchscreen created by Paul Stoffregen [https://github.com/PaulStoffregen/XPT2046_Touchscreen]
This is a Touchscreen Library for XPT2046 Touch Controller Chip.
Adding these libraries can be done using the Platform.io library import function or by adding these libraries direct to your platform.ini
file
lib_deps =
bodmer/TFT_eSPI@^2.5.43
https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4
⚠️XPT2046_Touchscreen wrong version
The moment of writing this tutorial, the wrong version of the XPT2046_Touchscreen library is defined in Platform.io environment when installing this library via the library install function of platform.io.
To ensure that you install version 1.4. Adding the line: https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4
to your platform.ini file will ensure that the correct version is downloaded form github direct.
Configure TFT_eSPI
To control the TFT display, the TFT_eSPI library need to be configured correct to control the TFT screen and enable display functionality.
The TFT_eSPi library offers predefined user setups, but currently not support for the ESP32-2432S028R board. To address this, I have prepared specific setup files that can be used. Download these files on GitHub Setup_ESP32_2432S028R_ILI9341.h
, Setup_ESP32_2432S028R_ST7789.
h
Place the configuration files in the same folder as the main.cpp These files contain the correct IO ports configuration for the TFT display.
Depending on the version of the used ESP32-2432S028R development board, you have to select the correct configuration file. For v1 and v2 use Setup_ESP32_2432S028R_ILI9341.h
For v3 use Setup_ESP32_2432S028R_ST7789.h
See more details on Pinout ESP32 Cheap Yellow Display Board(CYD) ESP32-2432S028R
For this selection, you need to add the following lines to your platform.io
and un-comment the one needed for your setup
build_flags =
;###############################################################
; TFT_eSPI library setting here (no need to edit library files):
;###############################################################
-D USER_SETUP_LOADED=1 ;Set this settings as valid
;-include src/Setup_ESP32_2432S028R_ILI9341.h ;for version 1 and version 2
-include src/Setup_ESP32_2432S028R_ST7789.h ;for version 3
Integrating LVGL into Your Project
The next step involves integrating the LVGL library to enable its functionality within your project. This can be achieved through the PlatformIO library import feature or by directly adding the library dependencies to your platformio.ini
file. The lib_deps
section should then resemble the following:
lib_deps =
bodmer/TFT_eSPI@^2.5.43
https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#v1.4
lvgl/lvgl@^9.2.2
The next step is to extend the build_flags
with the following directives:
-D LV_USE_TFT_ESPI
: This crucial flag enables hardware acceleration and efficient display rendering when using LVGL with ESP32 and TFT_eSPI.-D LV_CONF_INCLUDE_SIMPLE
: This directive instructs the LVGL library to use a simplified method for including thelv_conf.h
file. Consequently, you can place thelv_conf.h
file directly within your project structure, alongside your application code, rather than within the library’s directory.
The build_flags
section will then resemble the following:
build_flags =
-D USER_SETUP_LOADED=1
;-include src/Setup_ESP32_2432S028R_ILI9341.h ;for version 1 and version 2
-include src/Setup_ESP32_2432S028R_ST7789.h ;for version 3
-D LV_USE_TFT_ESPI
-D LV_CONF_INCLUDE_SIMPLE
Add serial port speed to Platform.IO
The application utilizes the serial connection to output (debugging) information. It operates at a baud rate of 115200. This rate must also be specified in the platformio.ini
file to ensure the terminal is configured with the same data speed.
monitor_speed = 115200
Integrating the Application Code
The final step involves integrating the application code into your project. You can achieve this by either replacing your existing main.cpp
file with the code from main.cpp or copying the code directly into your main.cpp
.
Alternatively, you can download the complete project from GitHub: https://github.com/Kafkar/ESP32_2432S028R-LVGL-tutorial. Download the project as a .zip file from here.
Once you’ve integrated the code, build and upload the application to your ESP32_2432S028R. You can then test it.
Upon startup, the display will show a test screen featuring labels, LEDs, buttons, and a slider.

The top button allows you to toggle the LEDs on and off. The center button enables you to switch between the left and right LED. By interacting with the slider at the bottom, you can adjust the percentage value.

Example Application Explained (main.cpp)
Includes
The application starts with including the needed liberaries. beside the TFT_eSPI.h and the XPT2046_Touchscreen.h needs also the generic SPI.h to be include. This liberary is used by the TFT_eSPI.h and the XPT2046_Touchscreen.h.
#include <Arduino.h>
#include <SPI.h>
// include the installed LVGL- Light and Versatile Graphics Library - https://github.com/lvgl/lvgl
#include <lvgl.h>
// include the installed "TFT_eSPI" library by Bodmer to interface with the TFT Display - https://github.com/Bodmer/TFT_eSPI
#include <TFT_eSPI.h>
// include the installed the "XPT2046_Touchscreen" library by Paul Stoffregen to use the Touchscreen - https://github.com/PaulStoffregen/XPT2046_Touchscreen
#include <XPT2046_Touchscreen.h>
Instance TFT_eSPI
To use the TFT_eSPI in the application, it should be instantiated. This uses the configured setup file.
If you are not using the ESP32_2432S028R but another display, then you have to check in the setup file if the correct IO ports are used to connect to the display
// Create a instance of the TFT_eSPI class
TFT_eSPI tft = TFT_eSPI();
Instance XPT2046 touchscreen
To use the touchscreen function, the driver should be configured. Therefore, the IO ports used by the touchscreen have to be defined. See for more IO details the Pinout ESP32 Cheap Yellow Display Board(CYD) ESP32-2432S028R
// Set the pius of the xpt2046 touchscreen
#define XPT2046_IRQ 36 // T_IRQ
#define XPT2046_MOSI 32 // T_DIN
#define XPT2046_MISO 39 // T_OUT
#define XPT2046_CLK 25 // T_CLK
#define XPT2046_CS 33 // T_CS
After the configuration, the driver has to be instantiated.
// Create a instance of the SPIClass and XPT2046_Touchscreen classes
SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);
Global definitions and variables
The test program is using some global variable. It start with the defines of the screen size.
// Define the size of the TFT display
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
// Define the size of the buffer for the TFT display
#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
The next set of variables are needed for transferring data between some functions.
// Touchscreen coordinates: (x, y) and pressure (z)
int x, y, z;
// Create variables for the LVGL objects
lv_obj_t *led1;
lv_obj_t *led3;
lv_obj_t * btn_label;
// Create a variable to store the LED state
bool ledsOff = false;
bool rightLedOn = true;
// Create a buffer for drawing
uint32_t draw_buf[DRAW_BUF_SIZE / 4];
The setup function
At the end of the main.cpp you will find the two main functions that are default created bij the android framwork we are using. these are the setup function and the loop function.
void setup() {
String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.begin(115200);
Serial.println(LVGL_Arduino);
// Start LVGL
lv_init();
// Start the SPI for the touchscreen and init the touchscreen
touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
touchscreen.begin(touchscreenSPI);
// Set the Touchscreen rotation in landscape mode
// Note: in some displays, the touchscreen might be upside down, so you might need to set the rotation to 0: touchscreen.setRotation(0);
touchscreen.setRotation(2);
// Create a display object
lv_display_t *disp;
// Initialize the TFT display using the TFT_eSPI library
disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
// Initialize an LVGL input device object (Touchscreen)
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
// Set the callback function to read Touchscreen input
lv_indev_set_read_cb(indev, touchscreen_read);
// Function to draw the GUI (text, buttons and sliders)
lv_create_main_gui();
}
The loop function
Following the setup, the loop
function executes continuously. It exclusively calls the lv_task_handler()
and lv_tick_inc()
functions from the LVGL library. The lv_task_handler()
function is responsible for executing all scheduled LVGL tasks.
The lv_tick_inc()
function serves as LVGL’s system tick, providing a measure of elapsed time for animations and other time-sensitive tasks. The parameter passed to lv_tick_inc()
should represent the time elapsed since the previous call. In this example, we utilize a 5-millisecond interval and incorporate a 5-millisecond delay within the loop
function.
void loop() {
lv_task_handler(); // let the GUI do its work
lv_tick_inc(5); // tell LVGL how much time has passed
delay(5); // let this time pass
}
function read the touchdata
void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) {
// Checks if Touchscreen was touched, and prints X, Y and Pressure (Z)
if(touchscreen.tirqTouched() && touchscreen.touched()) {
// Get Touchscreen points
TS_Point p = touchscreen.getPoint();
// Calibrate Touchscreen points with map function to the correct width and height
x = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
y = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);
z = p.z;
data->state = LV_INDEV_STATE_PRESSED;
// Set the coordinates
data->point.x = x;
data->point.y = y;
}
else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
Event handeler LED on/off button
// Callback that is triggered when btn2 is clicked/toggled
static void event_handler_btn2(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * obj = (lv_obj_t*) lv_event_get_target(e);
if(code == LV_EVENT_VALUE_CHANGED) {
LV_UNUSED(obj);
if(ledsOff==true){
if(lv_obj_has_state(obj, LV_STATE_CHECKED)==true)
{
lv_led_off(led1);
lv_led_on(led3);
lv_label_set_text(btn_label, "Left");
rightLedOn = true;
}
else
{
lv_led_on(led1);
lv_led_off(led3);
lv_label_set_text(btn_label, "Right");
rightLedOn = false;
}
//LV_LOG_USER("Toggled %s", lv_obj_has_state(obj, LV_STATE_CHECKED) ? "on" : "off");
}
}
}
Eventhandler LED control
static void event_handler(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * obj = (lv_obj_t*)lv_event_get_target(e);
if(code == LV_EVENT_VALUE_CHANGED) {
LV_UNUSED(obj);
if(lv_obj_has_state(obj, LV_STATE_CHECKED)==true)
{
if(rightLedOn==true)
{
lv_led_off(led1);
lv_led_on(led3);
}
else
{
lv_led_off(led3);
lv_led_on(led1);
}
ledsOff = true;
}
else
{
lv_led_off(led1);
lv_led_off(led3);
ledsOff = false;
}
//LV_LOG_USER("State: %s\n", lv_obj_has_state(obj, LV_STATE_CHECKED) ? "On" : "Off");
}
}
Evendhandler slider
static lv_obj_t * slider_label;
// Callback that prints the current slider value on the TFT display and Serial Monitor for debugging purposes
static void slider_event_callback(lv_event_t * e) {
lv_obj_t * slider = (lv_obj_t*) lv_event_get_target(e);
char buf[8];
lv_snprintf(buf, sizeof(buf), "%d%%", (int)lv_slider_get_value(slider));
lv_label_set_text(slider_label, buf);
lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
}
Setting up the graphical user interface
void lv_create_main_gui(void) {
/*Create a LED and switch it OFF*/
led1 = lv_led_create(lv_screen_active());
lv_obj_align(led1, LV_ALIGN_CENTER, -100, 0);
lv_led_set_brightness(led1, 50);
lv_led_set_color(led1, lv_palette_main(LV_PALETTE_LIGHT_GREEN));
lv_led_off(led1);
/*Copy the previous LED and switch it ON*/
led3 = lv_led_create(lv_screen_active());
lv_obj_align(led3, LV_ALIGN_CENTER, 100, 0);
lv_led_set_brightness(led3, 250);
lv_led_set_color(led3, lv_palette_main(LV_PALETTE_LIGHT_GREEN));
lv_led_on(led3);
// Create a text label aligned center on top ("Hello, Kafkar.com!")
lv_obj_t * text_label = lv_label_create(lv_screen_active());
lv_label_set_long_mode(text_label, LV_LABEL_LONG_WRAP); // Breaks the long lines
lv_label_set_text(text_label, "Hello, Kafkar.com!");
lv_obj_set_width(text_label, 150); // Set smaller width to make the lines wrap
lv_obj_set_style_text_align(text_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(text_label, LV_ALIGN_CENTER, 0, -90);
lv_obj_t * sw;
sw = lv_switch_create(lv_screen_active());
lv_obj_add_event_cb(sw, event_handler, LV_EVENT_ALL, NULL);
lv_obj_add_flag(sw, LV_OBJ_FLAG_EVENT_BUBBLE);
lv_obj_align(sw, LV_ALIGN_CENTER, 0, -60);
lv_obj_add_state(sw, LV_STATE_CHECKED);
// Create a Toggle button (btn2)
lv_obj_t *btn2 = lv_button_create(lv_screen_active());
lv_obj_add_event_cb(btn2, event_handler_btn2, LV_EVENT_ALL, NULL);
lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_height(btn2, LV_SIZE_CONTENT);
btn_label = lv_label_create(btn2);
lv_label_set_text(btn_label, "Left");
lv_obj_center(btn_label);
// Create a slider aligned in the center bottom of the TFT display
lv_obj_t * slider = lv_slider_create(lv_screen_active());
lv_obj_align(slider, LV_ALIGN_CENTER, 0, 60);
lv_obj_add_event_cb(slider, slider_event_callback, LV_EVENT_VALUE_CHANGED, NULL);
lv_slider_set_range(slider, 0, 100);
lv_obj_set_style_anim_duration(slider, 2000, 0);
// Create a label below the slider to display the current slider value
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);
}