Skip to content

2.4_2.8inch_Lesson01_LVGL_Light_Control

1. Course Introduction

In this lesson, we use VS Code + PlatformIO to open and run the LVGL light-control UI project for the CrowPanel Advance 2.4inch. The program initializes the ST7789 display, FT5x06 touch, LVGL 9.1.0, and the UI exported from SquareLine Studio. After the screen displays the light bulb, the On, and the Off buttons, you can control the light bulb/LED connected to GPIO18 by touching the buttons.

The software focus of this lesson is the PlatformIO project workflow: opening the project, inspecting platformio.ini, confirming the source file structure, selecting the serial port, compiling, uploading, and opening the serial monitor. The UI creation part can follow the same set of SquareLine Studio steps, but the exported UI files need to be placed into src/ and include/ according to the PlatformIO project structure.

Reference materials:

2. Learning Objectives

  • Be able to install the PlatformIO IDE extension in VS Code and open the course project.
  • Be able to explain the role of platformio.ini, src/, include/, and images/ in this project.
  • Be able to complete the PlatformIO workflow of compiling, uploading, selecting the serial port, and opening the serial monitor.
  • Be able to understand the relationship between the LVGL display flush callback, the touch read callback, UI initialization, and the GPIO18 light-control event.
  • Be able to use SquareLine Studio to create the On / Off light-control interface and integrate the exported UI files into the PlatformIO project.

3. Preparations

3.1 Hardware

  • One CrowPanel Advance 2.4inch development board.
  • One light bulb/LED module for the light-control experiment; the course code uses GPIO18 to control it.
  • One USB data cable that supports data transfer.

3.2 Software and Versions

  • Visual Studio Code.
  • PlatformIO IDE extension.
  • PlatformIO project: 2.4_2.8_PlatfromIO.
  • LVGL 9.1.0.
  • LovyanGFX 1.2.25.
  • SquareLine Studio, for recreating or exporting the UI files.

Code and Resource Download

Code download: - PlatfromIO2.4_2.8

Resource download: - PlatformIO images

4. PlatformIO Software Operation Steps

  1. Open VS Code, search for and install PlatformIO IDE in Extensions.

Installing the PlatformIO IDE extension

  1. After installation, restart VS Code; the PlatformIO icon will appear on the left sidebar. Click the icon to enter PIO Home.

Opening PlatformIO Home

  1. In the Quick Access area of PIO Home, click Open Project.

Clicking Open Project

  1. In the folder selection dialog that appears, select the root directory of this lesson's project:
2.4_2.8_PlatfromIO

The platformio.ini file must be visible in that directory. After selecting, click to open the project.

image-20260730113539194

  1. Wait for VS Code's Explorer on the left to load the project files. Confirm that the following files and directories exist:
platformio.ini
src/main.cpp
src/ui.c
src/ui_events.c
src/ui_Screen1.c
src/ui_img_lamp_28_png.c
include/LovyanGFX_Driver.h
include/pins_config.h
include/lamp_control.h
include/ui.h
include/ui_events.h

image-20260730113651736

  1. Connect the external LED, and use a USB data cable to connect the CrowPanel Advance 2.4inch to a USB port on your computer.

IMG_8117

  1. Check the bottom status bar of VS Code and select the serial port corresponding to the development board. If Auto can identify it correctly, you can keep the automatic selection.

image-20260730115115097

  1. Click the Upload icon in the bottom status bar, and PlatformIO will automatically compile and upload the firmware. The first build will download dependency libraries, so it will take longer.

image-20260730115206646

image-20260730122318759

Note: This tutorial uses PlatformIO's platformio.ini to manage the board, framework, and library dependencies. Do not write the software steps as menu configuration for other IDEs, and do not manually modify the library source code downloaded in .pio/libdeps.

5. Hardware Operation Steps

  1. Connect the external LED, and use a USB cable that supports data transfer to connect the development board to the computer.

IMG_8117

  1. After flashing is complete, wait for the board to reset; the screen should display the light-control UI you just created.

Official screenshot: UI displayed after flashing

  1. Tap the ON and Off buttons on the screen, and observe whether the light bulb/LED on the UART1 interface toggles on and off.

Official screenshot: tapping On turns the light on

Official screenshot: tapping Off turns the light off

6. Key Code Explanation

6.1 PlatformIO Configuration

[platformio]
build_dir = ../../pio_build/2.4inch_LVGL_9.1
libdeps_dir = ${sysenv.USERPROFILE}/.platformio/libdeps/2.4inch_LVGL_9.1

[env:esp32-s3-devkitc-1]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip
board = esp32-s3-devkitc-1
framework = arduino
platform_packages =
    framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz
monitor_speed = 115200
board_build.flash_size = 16MB
board_build.partitions = huge_app.csv
board_build.arduino.memory_type = qio_opi
build_flags = 
    -DBOARD_HAS_PSRAM
    -DLV_CONF_INCLUDE_SIMPLE
    -I include
board_upload.flash_size = 16MB
lib_deps = 
    lvgl/lvgl@9.1.0
    lovyan03/LovyanGFX@1.2.25

platformio.ini is the core configuration file of the PlatformIO project. The current course code uses pioarduino's platform-espressif32 55.03.38, and fixes the Arduino-ESP32 Core to 3.3.8 via platform_packages, to avoid inconsistencies in library APIs or build behavior when different computers automatically install other versions.

build_dir places the build output in the parent pio_build directory, and libdeps_dir places the dependency libraries in the PlatformIO dependency directory under the user's home directory. This shortens the paths actually involved in compilation and linking, reducing link failures caused by Chinese paths or overly long paths on Windows. board_build.flash_size = 16MB, huge_app.csv, and board_build.arduino.memory_type = qio_opi correspond to the Flash/PSRAM configuration of the 2.4/2.8inch ESP32-S3 display boards used in this course. LV_CONF_INCLUDE_SIMPLE and -I include let LVGL read include/lv_conf.h from the project.

6.2 Display, Touch, and Pin Configuration

include/pins_config.h defines the display resolution and the light-control pin:

#define LCD_H_RES 320
#define LCD_V_RES 240
#define LED_PIN 18

LCD_H_RES and LCD_V_RES define the landscape resolution used by LVGL; both the 2.4inch and 2.8inch boards in this course create their UI at 320 x 240. LED_PIN is the GPIO ultimately controlled by the UI buttons, which is GPIO18 in this example.

include/LovyanGFX_Driver.h configures the ST7789 display and FT5x06 touch:

cfg.spi_host = SPI2_HOST;
cfg.freq_write = 80000000;
cfg.pin_sclk = 42;
cfg.pin_mosi = 39;
cfg.pin_dc = 41;

cfg.pin_cs = 40;
cfg.panel_width = 240;
cfg.panel_height = 320;
cfg.offset_rotation = 3;
cfg.invert = true;

cfg.i2c_addr = 0x38;
cfg.pin_sda = 15;
cfg.pin_scl = 16;
cfg.pin_int = 47;
cfg.freq = 400000;

The display is driven via SPI2, and the touch is read via I2C. offset_rotation, the touch coordinate range, and the coordinate transformation in my_touchpad_read() later together determine whether the display orientation and touch orientation are consistent. If the tap position is mirrored left-to-right or offset up-and-down, you should first check the rotation and coordinate transformation here.

6.3 Light-Control Functions

extern "C" void lamp_set_on(void) {
  digitalWrite(LED_PIN, HIGH);
  Serial.println("LED ON");
}

extern "C" void lamp_set_off(void) {
  digitalWrite(LED_PIN, LOW);
  Serial.println("LED OFF");
}

These two functions are implemented in src/main.cpp and declared in include/lamp_control.h. Because the ui_events.c exported by SquareLine Studio is a C file while the main program is a C++ file, extern "C" is used here to expose the interface so that the C file can properly call the light-control functions implemented in C++. After tapping a UI button, the program ultimately changes the GPIO18 level via digitalWrite() and prints LED ON or LED OFF to the serial port.

6.4 LVGL Clock and Drawing Buffer

static uint32_t my_tick(void) {
  return millis();
}

static constexpr uint32_t DRAW_BUFFER_LINES = 40;
static constexpr uint32_t DRAW_BUFFER_SIZE = LCD_H_RES * DRAW_BUFFER_LINES * sizeof(uint16_t);

buf = static_cast<uint8_t *>(heap_caps_malloc(DRAW_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
buf1 = static_cast<uint8_t *>(heap_caps_malloc(DRAW_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
if (buf == nullptr || buf1 == nullptr) {
  Serial.println("LVGL draw buffer allocation failed");
  while (true) {
    delay(1000);
  }
}

my_tick() provides the Arduino framework's millis() to LVGL, driving LVGL's timers, animations, and input handling. DRAW_BUFFER_LINES = 40 means each drawing buffer caches 40 rows of screen data, and the buffer size is 320 x 40 x 2 bytes because each RGB565 pixel occupies 2 bytes.

The buffer is allocated with heap_caps_malloc() and requires the MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL attributes. This allows LovyanGFX to push the pixel data rendered by LVGL directly to the LCD via DMA. If buffer allocation fails, the program prints an error to the serial port and stalls in a loop, making it easier to locate memory configuration or PSRAM/Flash configuration issues.

6.5 LVGL Display Flush Callback

static void my_disp_flush(lv_display_t *display, const lv_area_t *area, uint8_t *pixel_map) {
  gfx.startWrite();
  gfx.pushImageDMA(area->x1, area->y1,
                   area->x2 - area->x1 + 1,
                   area->y2 - area->y1 + 1,
                   reinterpret_cast<lgfx::rgb565_t *>(pixel_map));
  gfx.waitDMA();
  gfx.endWrite();
  lv_display_flush_ready(display);
}

After LVGL finishes rendering a region, it calls my_disp_flush(). area indicates the rectangular region that needs to be refreshed, and pixel_map is the RGB565 pixel data prepared by LVGL. The function sends this image to the screen via gfx.pushImageDMA(), gfx.waitDMA() waits for the DMA transfer to complete, and finally lv_display_flush_ready() must be called to notify LVGL that the current buffer can continue to be used. If this call is omitted, the interface may freeze and stop refreshing.

6.6 Touch Read Callback

static void my_touchpad_read(lv_indev_t *indev, lv_indev_data_t *data) {
  (void)indev;
  data->state = LV_INDEV_STATE_REL;
  if (gfx.getTouch(&touchX, &touchY)) {
    data->state = LV_INDEV_STATE_PR;
    data->point.x = constrain(static_cast<int32_t>(LCD_H_RES - 1) - touchX, 0, LCD_H_RES - 1);
    data->point.y = constrain(static_cast<int32_t>(touchY), 0, LCD_V_RES - 1);

    Serial.print("Data x ");
    Serial.println(data->point.x);
    Serial.print("Data y ");
    Serial.println(data->point.y);
  }
}

gfx.getTouch() reads the coordinates from the FT5x06 touch controller. When there is no touch, the state remains LV_INDEV_STATE_REL; when a touch is detected, the state changes to LV_INDEV_STATE_PR, and the raw coordinates are transformed to the current 320 x 240 landscape orientation. Here, LCD_H_RES - 1 - touchX corrects the X-axis direction so that the position the user taps corresponds to the position of the LVGL widget. The serial output Data x / Data y can be used to debug whether the touch is being read correctly.

6.7 setup() Initialization Flow

void setup()
{
  Serial.begin(115200);

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  gfx.init();
  gfx.initDMA();
  gfx.fillScreen(TFT_BLACK);

  lv_init();
  lv_tick_set_cb(my_tick);

  buf = static_cast<uint8_t *>(heap_caps_malloc(DRAW_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
  buf1 = static_cast<uint8_t *>(heap_caps_malloc(DRAW_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));

  lv_display_t *display = lv_display_create(LCD_H_RES, LCD_V_RES);
  lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
  lv_display_set_flush_cb(display, my_disp_flush);
  lv_display_set_buffers(display, buf, buf1, DRAW_BUFFER_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);

  lv_indev_t *input = lv_indev_create();
  lv_indev_set_type(input, LV_INDEV_TYPE_POINTER);
  lv_indev_set_read_cb(input, my_touchpad_read);

  pinMode(38, OUTPUT);
  digitalWrite(38, HIGH);

  gfx.fillScreen(TFT_BLACK);
  ui_init();

  Serial.println("Setup done");
}

The initialization order is the main thread for troubleshooting in this lesson: first initialize the serial port and GPIO18, then initialize the LovyanGFX display and DMA; next initialize LVGL, register the clock callback, allocate the drawing buffer, create the LVGL display device and set the color format, flush callback, and double buffering; then create the touch input device and register the touch read callback; finally turn on the GPIO38 backlight and call ui_init() to create the interface exported from SquareLine Studio.

If the backlight is not turned on, the program may already be running but the screen appears black; if ui_init() is not called, the screen will light up but the lamp control interface will not appear. LVGL 9.1 uses the new API set of lv_display_create(), lv_display_set_buffers(), and lv_indev_create(), which differs from the display/input registration method of LVGL 8.

6.8 UI Event File

void Lamp_on(lv_event_t * e)
{
    (void)e;
    lamp_set_on();
}

void Lamp_off(lv_event_t * e)
{
    (void)e;
    lamp_set_off();
}

src/ui_events.c is obtained by organizing the export from SquareLine Studio. Lamp_on / Lamp_off are event functions bound to UI buttons; inside these functions they call lamp_set_on() / lamp_set_off(), thereby connecting the UI click events to the GPIO18 hardware output. include/lamp_control.h is responsible for declaring these two interfaces so that the C file and C++ file can correctly call each other.

6.9 Main Loop Keeps LVGL Running

void loop()
{
  const uint32_t delay_ms = lv_timer_handler();
  delay(constrain(delay_ms, 1UL, 5UL));
}

lv_timer_handler() must be executed continuously for LVGL to refresh the interface, poll touch input, and trigger button events. The current code uses the suggested delay returned by LVGL and clamps it between 1 and 5 ms, keeping the interface responsive while preventing the main loop from spinning idle and consuming excessive CPU.

7. UI Production Operation Steps

The UI in this section is created using SquareLine Studio. The operation goal is to create a 320 x 240 LVGL 9.1.0 interface, add a light bulb image, an On button, and an Off button, and bind the Lamp_on and Lamp_off callback functions to the two buttons respectively. The exported UI files are finally copied into the PlatformIO project, and src/main.cpp is responsible for the display driver, touch driver, and LVGL execution.

Reference for downloading and basic installation of SquareLine Studio:

Create LVGL UI with SquareLine Studio

7.1 Create a SquareLine Project

  1. Open SquareLine Studio and click Create at the top. When creating a new project, you must select LVGL 9.1.0.
  2. In the board category, select the TFT_eSPI-compatible template category shown by SquareLine Studio.
  3. Select the TFT_eSPI project template.

19

  1. Confirm the resolution based on the screen size. The course projects for 2.4-inch and 2.8-inch use 320 x 240.

Official screenshot: resolution reference for different screen sizes

  1. In the Project Settings on the right, set the project name and path, and confirm the key parameters:
Resolution: 320 x 240
Color depth: 16 bit
LVGL version: 9.1.0
Theme: Light
Multilanguage: Disable

20

  1. After confirming the parameters, click CREATE to create the project.

Enterprise WeChat screenshot_17852958807489

7.2 Import Light Bulb Image Assets

  1. After entering the project editing interface, switch to the Assets area at the bottom.
  2. Click ADD FILE INTO ASSETS to import the light bulb image asset into the project resources.
  3. Of course, you can also choose other images you wish to use.
  4. Image asset download link: PlatformIO images

Official screenshot: open Assets and add image asset

  1. After the import is complete, drag the light bulb image asset onto the Screen1 canvas. Adjust the position of the image on the canvas so that it is on the left side of the screen, leaving space for the button area on the right.

Official screenshot: place the light bulb image on the canvas

7.3 Add the On Button

  1. In the left Widgets > BASIC, select Button.
  2. Drag the Button onto the right side of the canvas as the On button.

Official screenshot: add a Button widget

  1. Select the button and adjust its position and size in the Inspector on the right.

Official screenshot: adjust button size and position

  1. Expand STYLE (MAIN) > Background and set the button background color to orange-red; the official example color is FF5529.

Official screenshot: set the On button background color

  1. Add a Label from the left Widgets > BASIC and drag it into the button.

Official screenshot: add a Label to the button

  1. Select the Label and change the text to On in the text box on the right.

Official screenshot: set the Label text to On

  1. Adjust the Label style, set the text color to white, and choose an appropriate font size. Here we use montserrat 40.

Official screenshot: set the On text color and font size

  1. In the Hierarchy panel on the right, confirm that Label is a child control of Button.

Official screenshot: confirm the Label is under the Button

7.4 Copy and Create the Off Button

  1. In the Hierarchy panel, select the completed Button1.
  2. Open the more menu and select Copy.

Official screenshot: copy the On button

  1. Paste it as a new button on the canvas or in the hierarchy. Use Paste as child to complete the copy.

Official screenshot: paste the copied button

  1. Move the copied button below the On button.

Official screenshot: move the copied button

  1. Select the Label in the new button and change the text to Off.

Official screenshot: set the Label text to Off

  1. Adjust the position and background color of the two buttons as needed so that the On / Off buttons are arranged vertically on the right side.

Official screenshot: finished On and Off button styles

  1. 7.5 Set Button Events

  2. Select the On button and scroll the Inspector on the right to the EVENTS area.

  3. Click ADD EVENT to add an event.

Official screenshot: add an event for the On button

  1. Set Trigger to RELEASED.
  2. Select CALL FUNCTION for Action.
  3. Enter the Function name:
Lamp_on

Official screenshot: bind Lamp_on to the On button

  1. Select the Off button and add an event in the same way.
  2. Set Trigger to RELEASED, select CALL FUNCTION for Action, and enter the Function name:

    Note: The function name for Call function must match the function name in the exported UI project exactly. In this lesson, the ON button uses Lamp_on and the Off button uses Lamp_off; incorrect capitalization will cause the button to light up the LED.

Lamp_off

Official screenshot: bind Lamp_off to the Off button

  1. Click the play button in the top-right corner to enter preview mode. After adding, run the program. Check whether the On / Off buttons can be clicked normally.

Official screenshot: click the play button to enter preview

Official screenshot: Play mode active

7.5 Set the Export Path

  1. Click File > Project Settings to open the project settings.

Official screenshot: open Project Settings

  1. In FILE EXPORT, set the export path. In the example, Project Export Root points to the SquareLine project directory, and UI Files Export Path points to the Output folder.

Official screenshot: set the UI file export path

  1. Set LVGL Include Path to:
lvgl.h
  1. Check Flat export (exports all files to one folder), then click APPLY CHANGES.

Official screenshot: set lvgl.h, Flat export, and apply

Note: It is recommended to check Flat export so that the exported UI files are consolidated into one folder, making it less likely to miss files when copying to the PlatformIO project later.

7.6 Copy UI Files to the PlatformIO Project

After exporting is complete, copy by file type into the PlatformIO project:

.c files -> 2.4_2.8_PlatfromIO/src/
.h files -> 2.4_2.8_PlatfromIO/include/

Find the user interface source files and header files generated by SquareLine Studio.

image-20260730143454941

image-20260730143558464

After copying, run PlatformIO Upload again. If you only modified the UI files, you can also Build first to confirm that no header files, function names, or image resources are missing, then upload to the development board for verification.

8. Experimental Phenomena

Click the ON and Off buttons on the screen and observe whether the light bulb/LED on the UART1 interface toggles on and off.

Official screenshot: click On to turn the light on

Official screenshot: click Off to turn the light off

9. Code Download

Code download link: PlatfromIO2.4_2.8

Image asset download link: PlatformIO images