Skip to content

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 esp32 target 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.ps1 has 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_IDF entirely to a pure-English path, for example C:/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.

  1. In VS Code, select File > Open Folder to open the project folder.

image-20260724100418176

image-20260724100409585

  1. Run the Build -> Delete command to completely clear the build cache and path files left over from the old project and avoid compilation conflicts.

image-20260724100559083

  1. Make sure ESP-IDF 5.5.4 is installed.

image-20260722151415231

  1. Connect the development board with a USB data cable, click the port name in the status bar, and select the COM port 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.

IMG_7893

image-20260724100834534

  1. Click the target chip name in the status bar, or run ESP-IDF: Set Espressif Device Target, and select esp32 from the list.

image-20260724100916509

  1. 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.

image-20260724100932585

  1. Click the Build (wrench) button in the status bar, or run idf.py build in the terminal. The first build takes a long time; the bottom-right corner should prompt that the project build is complete.

image-20260722152315413

image-20260724101433696

image-20260724101210028

  1. 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, replacing COMx with the actual port. After writing finishes, the board resets automatically.

image-20260724101614286

image-20260724101714735

5. Hardware Operation Steps

  1. 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.

IMG_7894

  1. 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.

The LVGL interface after firmware startup

  1. 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.

IMG_7895

OFF -> LED turns off.

IMG_7896

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.

  1. 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.

19

  1. Enter the project name, set the LVGL version to 9.1.0, the resolution to width 480 and height 320, and the color depth to 16 bit, then click CREATE. After creation, the canvas should be landscape 480×320.

image-20260723105847529

  • Explanation: A 16 bit color 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 Screen1 in 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.

21

  1. In the Assets area, click ADD FILE TO ASSETS, and import background.png, on.png, and off.png from LVGL-Assets-480x320/ in turn. After importing, you should see three thumbnails.

22

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.

  1. Select Screen1, expand STYLE SETTINGS > STYLE (MAIN) > Background on the right, and enable the background image setting.

23

  1. Select background in Bg Image. The canvas should immediately show the course background image, and the background should fully cover the 480×320 page.

24

  1. Click Label in the Widgets panel on the left to add Label1 to Screen1. This label will be updated by the program to the temperature value.

25

  1. 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.

26

  1. 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.

27

  1. Right-click Label1 and select Duplicate to create Label2, then move it to the humidity display area. Keep the names Label1 and Label2, because the main program updates them by name.

    28

  2. 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.

    29

  3. Click Button in Widgets to add Button1, and drag it to the ON area on the right side of the interface.

    30

  4. In Transform, set the size of Button1 and adjust its position so that the button aligns with the ON area in the background.

    31

  5. Expand Button1's STYLE (MAIN) > Background, and select on.png as the background image. The ON icon should appear on the canvas.

    32

  6. Duplicate Button1 to get Button2, move it to the OFF area, and change the background image to off.png. Keep the names Button1 and Button2 to match the exported event functions.

    33

    34

  7. In the STATE settings of both buttons, check the DEFAULT and PRESSED states. 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.

    35

    36

  8. Select Button1, and click ADD EVENT.

    37

  9. 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.

    38

    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.

  10. Finish this event. Here, I choose to switch screens, that is, switch to the Screen1 screen.

39

  1. Add the event to Button2 in the same way (state "OFF").

40

  1. Click Run.

41

  1. Open File > Project Settings, then configure the relevant settings for the exported files.

42

image-20260723110125815

  1. Set the export directory to an easily locatable path using plain English characters, create a new output folder, fill in and confirm that the LVGL Include Path is lvgl.h, then click APPLY CHANGES to confirm.

image-20260723191544052

image-20260723110236455

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.

  1. 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.

46

47

  1. Copy all exported UI files (.c and .h) into the UI folder under the Components directory of the project, and update them to the lvgl 9.1 version. In components/UI/CMakeLists.txt, make sure these files are included in the build.

image-20260724102857757

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.

IMG_7895

OFF -> LED turns off.

IMG_7896

9. Code Download