LVGL_IDF3.5: 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 3.5-inch ESP32 touchscreen application, and reuses APIs such as TFT_eSPI, Wire, Serial, and millis() through the Arduino-ESP32 3.3.8 component. In app_main(), the program initializes the ILI9488 LCD, XPT2046 touch, DHT20, LVGL 9.1.0, and the UI exported from SquareLine Studio 1.6.1, then continuously handles the interface in a FreeRTOS task loop, updates temperature and humidity every 2 seconds, and controls GPIO25 based on the ON/OFF button.
After the firmware is flashed, the LCD displays the background, the two sensor readings, and two image buttons. When the learner clicks 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 callback uses a calibration array to map the raw samples to 480×320 landscape coordinates. This lesson verifies the complete chain from ESP-IDF component management, LCD refresh, and touch reading through to UI events and hardware output.
Reference materials:
2. Learning Objectives¶
- Be able to reconfigure, build, flash, and monitor a project for the
esp32target using ESP-IDF 5.5.4. - Be able to explain the relationship among the ESP-IDF
app_main(), the Arduino compatibility layer, and the FreeRTOS task loop. - Be able to explain the execution order of LVGL partial refresh, touch calibration, and SquareLine UI initialization.
- Be able to verify that the DHT20 updates the label every 2 seconds and controls GPIO25 through the touch button.
- Be able to locate common faults based on build logs, backlight, UI, touch, and sensor behavior.
3. What You Need to Prepare¶
- One 3.5-inch ESP32 touchscreen development board, targeting the classic
esp32. - One USB data cable that supports both power delivery and data transfer.
- One DHT20 temperature and humidity module, connected to GPIO22 (SDA) and GPIO21 (SCL).
- One LED module, connected to GPIO25; if using the board's interface label, connect it to the GPIO_D interface.
- VS Code, the Espressif IDF extension, and ESP-IDF 5.5.4, or an ESP-IDF 5.5.4 terminal where
export.ps1has been run. - VS Code + ESP-IDF Extension: Click here for the installation tutorial.
- The complete project
ESP_IDF/LVGL_IDF/. - The Windows build path must contain only ASCII characters. Before starting the lab, copy
LVGL_IDFentirely to a pure-English path, for exampleC:/esp_projects/LVGL_IDF/; do not build directly in the course-material path that contains Chinese characters. - The first-time configuration requires downloading or verifying managed components over the network; even when
managed_components/already exists, the dependency lock file should remain the source of truth.
4. Software Operation Steps¶
The following steps use the Espressif IDF extension for VS Code as the primary interface. When using it for the first time, complete the steps in order, and do not flash directly before the target chip and serial port have been set.
- In VS Code, select File > Open Folder to open the project folder.
- Run the
Build -> Deletecommand to completely clear the build cache and path files left over from the old project and avoid compilation conflicts.
- Make sure
ESP-IDF 5.5.4is 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 target chip name in the status bar, or run ESP-IDF: Set Espressif Device Target, and select
esp32from the list.
- Select a configuration suitable for the classic ESP32, such as ESP-WROVER-KIT 3.3V. This lesson uses UART flashing; OpenOCD is not involved in writing, but selecting the correct configuration prevents the extension from repeatedly prompting that the configuration is missing.
- 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 prompt that the project build is complete.
- Confirm that the flashing method in the status bar is UART, then click the lightning-shaped Flash button; you can also run
idf.py -p COMx flash, replacingCOMxwith 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, confirm that SDA maps to GPIO22 and SCL maps to GPIO21, and connect the LED module to the interface labeled GPIO_D so that its signal line connects to IO25. Do not connect the signal line to a power pin, and do not hot-plug modules with power applied.
- Reconnect the USB cable that supports data transfer. After the upload completes and the board resets automatically, the screen backlight should turn on, and the interface should show temperature, humidity, and the two buttons ON and OFF.
- Use your finger to tap ON and OFF in turn, and observe whether the LED lights up and goes out accordingly, while checking 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 ESP-IDF Entry Starts the Arduino Component¶
// ESP-IDF enters the Arduino-compatible application through the C-linkage hook.
extern "C" void app_main() {
// Initialize the Arduino component before using millis(), Wire, or drivers.
initArduino();
// Register the hardware, LVGL callbacks, and generated UI objects.
setup();
while (true) {
// Run one application pass, then yield to other FreeRTOS work.
loop();
vTaskDelay(pdMS_TO_TICKS(5));
}
}
The ESP-IDF startup task calls app_main() in C-linkage form. Because the project disables Arduino's auto-start, initArduino() must be executed first; otherwise millis(), Wire, and the Arduino driver environment are unavailable. The 5 ms FreeRTOS delay yields the CPU while keeping LVGL responsive. This reflects the idea that "IDF manages tasks while the Arduino component provides compatible APIs."
6.2 Component Dependency Locking¶
# Pin Arduino-ESP32 to the version used by this ESP-IDF course.
dependencies:
espressif/arduino-esp32: "==3.3.8"
main/idf_component.yml pins Arduino-ESP32 to 3.3.8, and the component manager generates dependencies.lock and managed_components accordingly. Deleting the lock file may re-resolve dependencies; the course release package should keep the lock file and verify compatibility before updating over the network.
6.3 Periodic DHT20 Reading¶
// Limit the relatively slow sensor transaction to one update every two seconds.
if (millis() - last_update > 2000) {
update_sensor_values();
// Start the next measurement interval after the update completes.
last_update = millis();
}
The sensor reading is limited to about once every 2 seconds, while lv_timer_handler() still executes every cycle. This avoids blocking the UI with long delays. If the temperature and humidity log stops but touch still responds, check the DHT20 and the timing branch first; if everything stops, check whether the task is running or whether the device has restarted.
6.4 Touch State Edge Logging¶
// Report only state transitions so a long press does not flood the serial log.
if (pressed && !was_pressed) {
Serial.printf("Touch DOWN: %u, %u\n", touchX, touchY);
} else if (!pressed && was_pressed) {
Serial.println("Touch UP");
}
// Save this sample for comparison with the next touch poll.
was_pressed = pressed;
was_pressed saves the previous sample state and prints a log only on the press or release edge. A long press will not flood the serial port. When the UI can display but there is no log, check whether CONFIG_TOUCH_CS=12, the touch calibration, and #ifdef TOUCH_CS are in effect.
6.5 LVGL Partial Screen Refresh¶
// Use one eighth of a frame so partial rendering reduces RAM consumption.
#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 8)
lv_display_set_buffers(disp, draw_buf, NULL, sizeof(draw_buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
The buffer occupies ⅛ of a frame, and LVGL renders in segments using partial mode. Increasing the buffer raises memory usage, while reducing it increases the number of transfers. When encountering screen corruption or partial areas not refreshing, first verify the RGB565 byte order, the ILI9488, and the SPI configuration.
6.6 Sensor Labels and Serial Port Linkage¶
// Write the latest sensor values into the labels created by ui_init().
lv_label_set_text_fmt(temp_label, "%d", temperature);
lv_label_set_text_fmt(humi_label, "%d", humidity);
The two labels are bound to the generated objects after ui_init(), and thereafter every sensor update changes both the UI and the serial output. When the backlight is on but the labels stay at --, it means the UI has been created but the first 2-second update has not completed yet, or the DHT20 read chain is abnormal.
6.7 GPIO25 Output¶
// Apply the shared state written by the SquareLine ON/OFF event callbacks.
digitalWrite(25, led ? HIGH : LOW);
The SquareLine button callback changes led, and the loop drives GPIO25 according to that value. The IDF version initializes to a low level, which differs from the start-up level in the Arduino main lesson; for acceptance, the criterion should be the change after a button click.
7. UI Asset Creation and Integration¶
This section uses SquareLine Studio 1.6.1 to demonstrate the UI creation process again; the operation steps and illustrations refer to Elecrow's official SquareLine Studio tutorial. The course already provides the exported UI files, so beginners can read this section first to understand the flow, then directly use the files in the project to complete the build. When creating a new project, you must select LVGL 9.1.0 and use a resolution of 480×320.
How to download SquareLine Studio: https://www.elecrow.com/wiki/Create_LVGL_UI_with_SquareLine_Studio.html.
- Launch SquareLine Studio 1.6.1, click Create, and select the Arduino with TFT_eSPI template for the Arduino platform. This template is responsible for generating the UI file framework suitable for Arduino and TFT_eSPI projects.
Note: When the Arduino framework is selected, SquareLine Studio shows only the Arduino with TFT_eSPI option. It generates template code suitable for TFT_eSPI, but SquareLine Studio also supports other graphics libraries; when switching to other hardware, you need to modify the display code according to the actual library.
- Enter the project name, set the LVGL version to
9.1.0, the resolution to width480and height320, and the color depth to16 bit, then click CREATE. After creation, the canvas should be landscape 480×320.
-
Explanation: A
16 bitcolor depth can represent 65,536 colors using the RGB 5:6:5 pixel representation. Keep it 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 the Inspector on the right shows the Screen properties. All subsequent widgets are added to this page.
- In the Assets area, click ADD FILE TO ASSETS, and import
background.png,on.png, andoff.pngfromLVGL-Assets-480x320/in turn. After importing, you should see three thumbnails.
Note: Image assets support only the PNG format; the image pixel size should be smaller than the project screen size; a single image should not exceed 100 KB, and it is best to keep it 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.
- Select
backgroundin Bg Image. The canvas should immediately show the course background image, and the background should fully cover the 480×320 page.
- Click Label in the Widgets panel on the left to add
Label1toScreen1. This label will be updated by the program to the temperature value.
- Select
Label1, and adjust its X, Y position and width/height in Transform so that it sits in the temperature display area of the background image. You can also drag it first and then fine-tune with the values.
- In
Label1's STYLE (MAIN), change the text color to white and set an appropriate font size. The text on the canvas should be clearly visible and not exceed the background frame.
-
Right-click
Label1and select Duplicate to createLabel2, 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 fill in preview-friendly default numbers in Text. These are only design-time placeholder values and will be replaced by the DHT20 readings once the board runs.
-
Click Button in Widgets 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. The ON icon should appear on 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 an obvious color change so that during preview you can tell whether a button is pressed. In "Inspector" -> "Style Settings" -> "State", set the displayed background color to white, while in the "Checked" state it shows red. Set the same parameters for the "OFF" button. -
Select
Button1, and click ADD EVENT. -
Select "CLICKED" as the trigger condition, and choose the trigger event in "Action". It will be modified later in the generated program to implement LED control.
Note: Since the button ultimately controls the LED on/off, you can add any event here first to let the exported UI file generate the button event code framework; the LED control code will be modified in subsequent steps.
-
Finish 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 "OFF").
- Click Run.
- Open File > Project Settings, then configure the relevant settings for the exported files.
- Set the export directory to an easily locatable path using plain English characters, create a new
outputfolder, fill in and confirm that the LVGL Include Path islvgl.h, then click APPLY CHANGES to confirm.
Tip: After selecting Flat export, all output files will be placed in the same folder, so the program does not need to modify any file paths. If Flat export is not selected, files will be scattered across different folders, and the compiler may fail to recognize them automatically; usually you would need to modify the paths manually. Therefore, it is recommended to keep this option checked.
- Click Export > Export UI Files. After the export is complete,
ui.c,ui.h,ui_Screen1.c, the event files, helper files, and the image array file should appear in the target directory.
- Copy all exported UI files (
.cand.h) into the UI folder under the Components directory of the project, and update them to the lvgl 9.1 version. Incomponents/UI/CMakeLists.txt, make sure these files are included in the build.
8. Experimental Results¶
After flashing and resetting, the LCD first displays black. About 300 ms later, GPIO27 goes high, the backlight turns on, and the 480×320 UI loads. The temperature and humidity integers are displayed slightly left of center on the screen, and the ON and OFF image buttons are displayed on the right. When the DHT20 is functioning normally, the labels update about every 2 seconds; if the values stay unchanged over a short period because the environment has not changed, that is normal behavior.
Tap the ON button on the screen, and the LED lights up; tap the OFF button, and the LED turns off.
ON -> LED lights up.
OFF -> LED turns off.
















































