Mastering ESP32-2432S028R with LVGL: The Ultimate Beginner’s Guide in Platform.io

Feb 25, 2025 | Smart home, Boat Automation, Electronics

Device Device License Language Build GitHub Release

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

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

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 the lv_conf.h file. Consequently, you can place the lv_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);
}