LVGL_IDF_5.0: Touch UI, Temperature/Humidity Display, and LED Control under ESP-IDF 5.5.4¶
1. Course Introduction¶
This lesson uses ESP-IDF 5.5.4 to build a 5.0-inch ESP32 touchscreen application, and reuses Arduino_GFX, Wire, Serial, and millis() APIs through the Arduino-ESP32 3.3.10 component. The Arduino component task first calls setup() to initialize the 800×480 RGB LCD, touch, DHT20, LVGL 9.1.0, and the UI exported from SquareLine Studio 1.6.1, then runs a continuous FreeRTOS task loop that handles the interface, updates temperature and humidity every 2 seconds, and controls GPIO38 based on the ON/OFF button.
After the firmware is flashed, the LCD shows the background, two sensor values, and two image buttons. When the learner taps a button, the UI's C event code modifies the shared variable led, and the C++ main program immediately updates the LED output; the touch coordinates are mirrored along the X axis in landscape orientation so that the tap position aligns with the UI. This lesson verifies the complete chain from ESP-IDF component management, LCD refreshing, touch reading, to UI events and hardware output.
Reference materials:
2. Learning Objectives¶
- Be able to reconfigure, build, flash, and monitor a project for the
esp32s3target using ESP-IDF 5.5.4. - Be able to explain the relationship between ESP-IDF, the Arduino component
setup()/loop(), and FreeRTOS scheduling. - Be able to explain the execution order of LVGL partial refresh, touch coordinate mirroring, and SquareLine UI initialization.
- Be able to verify that the DHT20 updates the label every 2 seconds and controls GPIO38 through touch buttons.
- Be able to locate common faults based on build logs, backlight, UI, touch, and sensor behavior.
3. Preparation¶
-
One 5.0-inch ESP32 touchscreen development board, targeting ESP32-S3
esp32s3. -
One USB data cable that supports both power delivery and data transfer.
-
One DHT20 temperature and humidity module, connected to GPIO19 (SDA) and GPIO20 (SCL).
-
One LED module, connected to GPIO38.
-
VS Code, the Espressif IDF extension, and ESP-IDF 5.5.4, or an ESP-IDF 5.5.4 terminal where
export.ps1has already been executed. -
VS Code + ESP-IDF Extension: Click here for the installation tutorial link.
-
The complete project
CourseCode/ESP_IDF/LVGL_Control_LED_50/. -
The Windows build path must contain only ASCII characters. Before starting the experiment, copy
LVGL_Control_LED_50in full to an all-English path, for exampleC:/esp_projects/LVGL_Control_LED_50/; do not build directly inside the Chinese-character course-material path. -
Keep
components/DHT20,components/Arduino_GFX-master,components/uiand the LVGL 9 component,main/idf_component.yml,sdkconfig, anddependencies.lock. -
The first configuration requires downloading or verifying managed components over the network; even when
managed_components/already exists, the dependency lock file should remain authoritative.
4. Software Operation Steps¶
The following operations are performed primarily using the Espressif IDF extension in VS Code. Complete them in order on first use; do not flash directly while the target chip or serial port is not yet set.
- In VS Code, select File > Open Folder to open the project folder.
- Run the
Build -> Deletecommand to thoroughly clear the old project's leftover build cache and path files, avoiding compilation conflicts.
- Make sure ESP-IDF 5.5.4 is installed.
- Connect the development board with a USB data cable, click the port name in the status bar, and select the
COMport that actually appears for the board. The port number varies by computer; if unsure, unplug the board and observe which port disappears from the list.
- Click the chip icon in the VS Code status bar, run ESP-IDF: Set Espressif Device Target, and select
esp32s3to set the target chip.
- In the OpenOCD configuration item, select ESP32-S3 chip (via builtin USB-JTAG) to avoid the plugin repeatedly popping up prompts about missing configuration.
- Click the Build (wrench) button in the status bar, or run
idf.py buildin the terminal. The first build takes a long time; the bottom-right corner should show that the project build completed.
- Confirm that the flash method in the status bar is UART, then click the lightning-shaped Flash button; you can also run
idf.py -p COMx flash, whereCOMxis replaced with the actual port. After writing finishes, the board resets automatically.
5. Hardware Operation Steps¶
- With the power off, connect the DHT20 module to the board's IIC interface, and connect the LED module to the interface labeled GPIO_D.
- Reconnect the USB cable that supports data transfer. After upload completes and the board resets automatically, the screen backlight should light up, and the interface should show temperature, humidity, and the ON and OFF buttons.
- Tap ON and OFF in turn with your finger, observe whether the LED lights up and turns off accordingly, and check whether the serial monitor outputs the corresponding status. Do not press the screen with sharp or conductive objects.
ON -> LED lights up.
OFF -> LED turns off.
6. Key Code Explanation¶
6.1 Registering the Arduino, LVGL, and Hardware Components¶
# Build the application with every component used by its display and UI path.
idf_component_register(
SRCS "CrowPanel_ESP32_5.0.cpp"
INCLUDE_DIRS "."
REQUIRES
arduino-esp32
lvgl-9
ui
gt911-arduino-main
Arduino_GFX-master
DHT20
PCA9557
)
main/CMakeLists.txt declares the components the main program depends on, while main/idf_component.yml pins the Arduino-ESP32 component range:
# Pin the Arduino core range used to compile the C++ application component.
dependencies:
espressif/arduino-esp32: "^3.3.10"
If ui, DHT20, or Arduino_GFX-master is removed, the build stage will directly report missing header files or unresolved symbols. The first build requires the network to resolve managed components, after which dependencies.lock keeps versions consistent.
6.2 Configuring the 800×480 RGB Panel¶
// Board pins, update cadence, and display-buffer parameters used at runtime.
constexpr uint8_t kBacklightPin = 2;
constexpr uint8_t kLedPin = 38;
constexpr uint32_t kSensorUpdateIntervalMs = 2000;
constexpr uint32_t kDrawBufferRows = 48;
constexpr int32_t kPixelClockHz = 16000000;
constexpr size_t kBounceBufferPixels = 800 * 10;
// The LVGL display must use the panel's physical 800x480 resolution.
Arduino_RGB_Display *lcd = new Arduino_RGB_Display(
800 /* width */, 480 /* height */, rgb_bus, 0 /* rotation */, true /* auto_flush */);
These constants determine the backlight, LED, sensor period, LVGL partial buffer row count, and RGB clock. The width and height must remain 800×480; after modifying the pixel clock or porch parameters, you may see flickering, offset, or no display.
6.3 Allocating the PSRAM Draw Buffer and Registering LVGL¶
/**
* @brief Allocate LVGL draw memory and register display/input callbacks.
*
* Called once after the hardware display and touch controller are ready.
*/
void initialize_display_driver()
{
const uint32_t screen_width = lcd->width();
const uint32_t screen_height = lcd->height();
const size_t draw_buffer_size = screen_width * kDrawBufferRows * sizeof(uint16_t);
// Keep the partial draw buffer in PSRAM to preserve internal RAM.
lvgl_draw_buffer = static_cast<uint8_t *>(
heap_caps_malloc(draw_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (lvgl_draw_buffer == nullptr) {
halt("LVGL draw buffer allocation failed");
}
// Register the RGB565 partial renderer and its panel-copy callback.
lv_display_t *display = lv_display_create(screen_width, screen_height);
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
lv_display_set_flush_cb(display, display_flush);
lv_display_set_buffers(display, lvgl_draw_buffer, nullptr, draw_buffer_size,
LV_DISPLAY_RENDER_MODE_PARTIAL);
// Bind GT911 as the pointer device for this display.
lv_indev_t *touch_indev = lv_indev_create();
lv_indev_set_type(touch_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(touch_indev, touchpad_read);
lv_indev_set_display(touch_indev, display);
}
The draw buffer is explicitly allocated from PSRAM. On allocation failure, halt() stops subsequent execution so the serial port can retain a clear error message; continuing with a null pointer would cause a reset or exception.
6.4 Flush Callback and GT911 Input Callback¶
/**
* @brief Copy an LVGL partial-render buffer to the RGB display.
*/
void display_flush(lv_display_t *display, const lv_area_t *area, uint8_t *pixel_map)
{
// Convert LVGL's inclusive area into the bitmap dimensions expected by GFX.
const uint32_t width = area->x2 - area->x1 + 1;
const uint32_t height = area->y2 - area->y1 + 1;
auto *pixels = reinterpret_cast<uint16_t *>(pixel_map);
lcd->draw16bitRGBBitmap(area->x1, area->y1, pixels, width, height);
// Release LVGL's renderer only after the partial area has been copied.
lv_display_flush_ready(display);
}
/**
* @brief Convert the GT911 sample into an LVGL pointer event.
*/
void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data)
{
(void)indev;
// Default to released so an absent sample cannot leave a widget pressed.
data->state = LV_INDEV_STATE_RELEASED;
// Publish mapped GT911 coordinates only while a valid touch is present.
if (touch_has_signal() && touch_touched()) {
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = touch_last_x;
data->point.y = touch_last_y;
}
}
lv_display_flush_ready() is the draw-complete notification for this frame and must not be removed. The touch callback first writes RELEASED, then switches to PRESSED when a valid touch is detected, which prevents old coordinates from being continuously judged as pressed.
6.5 Periodically Updating the DHT20 and the LED¶
/**
* @brief Run LVGL timers, apply LED changes, and refresh sensor labels.
*
* Called continuously by the Arduino component task.
*/
void loop()
{
static uint32_t last_tick_ms = millis();
static uint32_t last_sensor_update_ms = 0;
const uint32_t now = millis();
// Advance LVGL with real elapsed time before processing its timers.
lv_tick_inc(now - last_tick_ms);
last_tick_ms = now;
lv_timer_handler();
delay(5);
// Touch-triggered LED requests are applied only when the state changes.
if (led != last_led_state) {
last_led_state = led;
digitalWrite(kLedPin, led != 0 ? HIGH : LOW);
Serial.printf("LED state changed: %d\n", led);
}
// Sensor work runs at a slower cadence so UI handling stays responsive.
if (now - last_sensor_update_ms >= kSensorUpdateIntervalMs) {
update_sensor_values();
last_sensor_update_ms = now;
}
}
LVGL is serviced every loop iteration, while the DHT20 is read only at the 2000 ms interval. The LED is written to GPIO38 only when the state changes; the serial log can be used to distinguish UI event problems from hardware output problems.
6.6 UI Events and C/C++ State Sharing¶
/** Request the C++ application loop to drive the LED output high. */
void TurnOn(lv_event_t * e)
{
(void)e;
led = 1;
}
/** Request the C++ application loop to drive the LED output low. */
void TurnOff(lv_event_t * e)
{
(void)e;
led = 0;
}
The SquareLine UI files are compiled as C, while the main program is C++. extern "C" lets both sides use the same led symbol; if omitted, an undefined reference will typically appear at the link stage.
7. UI Resource Creation and Integration¶
This section uses SquareLine Studio 1.6.1 to re-demonstrate how the interface is created. The course already provides the exported UI files; beginners can first read this section to understand the workflow, then use the files in the project directly to complete the build. When creating a new project, you must select LVGL 9.1.0;
How to download SquareLine Studio: https://www.elecrow.com/wiki/Create_LVGL_UI_with_SquareLine_Studio.html.
- Open SquareLine Studio 1.6.1, click Create at the top to create a new project, select 9.1 for the LVGL major version, and select Elecrow for the vendor category. In the template list, choose DIS07050H - ESP32 5inch HMI Display 800x480 RGB - Arduino-IDE, which fits the Elecrow CrowPanel 5-inch capacitive touchscreen, with preset parameters of 800×480 resolution and LVGL 9.1, then complete project creation. Note: This template is an Arduino project template designed specifically for the Elecrow CrowPanel. It directly generates LVGL UI code that matches the hardware. If you switch to a different screen model, you must select the corresponding hardware template and verify the resolution and color depth settings..
- Enter the project name, set the LVGL version to
9.1.0, the resolution to width800and height480, and the color depth to16 bit, then click CREATE. After creation, the canvas should be in landscape orientation at 800×480.
-
Note: A
16 bitcolor depth can represent 65,536 colors using an RGB 5:6:5 pixel format. Keep this consistent with the project's color configuration.. -
After the project opens, select
Screen1in the Screens panel on the left, and confirm that the central canvas is blank and that the Inspector on the right shows the Screen properties. All subsequent widgets will be added to this page.
- In the Assets area, click ADD FILE TO ASSETS and import
background.png,on.png, andoff.pngin sequence. After importing, you should see three thumbnails.
Note: Image assets support only the PNG format; the pixel dimensions of an image should be smaller than the project screen size; a single image should not exceed 100 KB, and is best kept within 30 KB, to avoid affecting display smoothness..
- Select
Screen1, expand STYLE SETTINGS > STYLE (MAIN) > Background on the right, and enable the background image setting.
- In Bg Image, select
background. The canvas should immediately display the course background image, and the background should fully cover the entire page.
- In the Widgets panel on the left, click Label to add
Label1toScreen1. This label will be updated by the program to display the temperature value.
- Select
Label1, and in Transform adjust the X and Y position and the width and height so that it sits in the temperature display area of the background image. You can also drag it first and then fine-tune using the numeric values.
- In
Label1's STYLE (MAIN), change the text color to white and set an appropriate font size. The text in the canvas should be clearly visible and should not extend beyond the background frame.
-
Right-click
Label1and select Duplicate to generateLabel2, then move it to the humidity display area. Keep the namesLabel1andLabel2, because the main program updates them by name. -
Select the two Labels respectively, and in Text enter default numbers for easy previewing. These are only design-time placeholder values; they will be replaced by DHT20 readings once the development board runs.
-
In Widgets, click Button to add
Button1, and drag it to the ON area on the right side of the interface. -
In Transform, set the size of
Button1and adjust its position so that the button aligns with the ON area in the background. -
Expand
Button1's STYLE (MAIN) > Background, and selecton.pngas the background image. An ON icon should appear in the canvas. -
Duplicate
Button1to getButton2, move it to the OFF area, and change the background image tooff.png. Keep the namesButton1andButton2to match the exported event functions. -
In the STATE settings of both buttons, check the
DEFAULTandPRESSEDstates. The pressed state can use a noticeable color change; in preview, you should be able to tell whether a button is pressed. In "Inspector" → "Style Settings" → "State", set the displayed background color to white, and in the "checked" state set it to red. Set the same parameters for the "OFF" button. -
Select
Button1, and click ADD EVENT. -
Select "CLICKED" as the trigger condition, and in "Action" select the trigger event. The LED control functionality will be implemented later by modifying the generated program.
Note: Since the button ultimately controls the LED on and off, you can add any event here to let the exported UI file generate the button event code framework; the LED control code will be modified in a later step..
-
Complete this event. Here, I choose to switch screens, that is, switch to the Screen1 screen.
-
Add the event to Button2 in the same way (state is "OFF").
-
Click Run.
-
Open File > Project Settings, then configure the relevant settings for the exported files.
-
Set the export directory to an easy-to-find path consisting of English characters only, create a new
outputfolder, confirm that the LVGL Include Path islvgl.h, and after confirming click APPLY CHANGES.Tip: After selecting Flat export, all output files are placed in the same folder, and the program does not need to modify file paths. If Flat export is not selected, the files are scattered across different folders and the compiler may not be able to recognize them automatically; usually you would then need to modify the paths manually, so it is recommended to keep it checked..
-
Click Export > Export UI Files. After the export completes,
ui.c,ui.h,ui_Screen1.c, the event files, helper files, and image array files should appear in the target directory. -
Copy all the exported UI
.cand.hfiles into the UI folder under the Components directory of the project, replacing them with the LVGL 9.1 version. Incomponents/UI/CMakeLists.txt, make sure these files are all included in the build.
8. Experimental Results¶
After power-on, the backlight turns on and the screen displays the background image, the temperature/humidity values, and the ON/OFF buttons.
After clicking ON, the LED corresponding to GPIO38 lights up.
After clicking OFF, it turns off.

















































