三、LVGL 移植和配置(让屏幕和触摸用起来)

LVGL 是一个开源的嵌入式图形库,可以帮我们在屏幕上画出漂亮的按钮、文字、滑块等界面元素。要让它在你的 ESP32-S3 上跑起来,需要完成几步配置:添加库、修改配置文件、告诉 LVGL 如何把图像画到屏幕上、如何读取触摸坐标


1. 在 platformio.ini 中添加依赖(一键下载 LVGL 和驱动库)

打开项目里的 platformio.ini,在 lib_deps 后面加上这三行(如果原来有别的库,就换行添加):

ini

lib_deps = 
    lvgl/lvgl@^8.3.9          ; LVGL 图形库
    bodmer/TFT_eSPI@2.5.0     ; TFT 屏幕显示驱动(兼容各种 SPI 屏幕)
    fbiego/CST816S@^1.1.1     ; CST816S 触摸芯片驱动(如果你的触摸芯片是这个型号)

保存文件后,PlatformIO 会自动下载这三个库到项目的 .pio/libdeps 文件夹里。如果下载慢,参考之前的国内源方法。


2. 配置 LVGL 核心文件 lv_conf.h(告诉 LVGL 怎么运行)

LVGL 本身有很多可配置的选项,比如内存大小、是否使用操作系统、刷新速度等。这些配置放在一个叫 lv_conf.h 的文件里。

  • 找到模板文件:打开项目里的 .pio/libdeps/esp32-s3-devkitc-1/lvgl 文件夹,里面有个 lv_conf_template.h,把它复制一份到你的项目根目录(和 platformio.ini 同一层),并重命名为 lv_conf.h
    (如果根目录已经有了,直接打开修改)

  • 修改配置:用 VSCode 打开 lv_conf.h,找到下面几项并修改:

    • 第1步:启用 LVGL
      文件开头有一行 #if 0,把它改成 #if 1。这样 LVGL 的配置才会生效。

    • 第2步:让 LVGL 使用 Arduino 的内存管理函数
      搜索 LV_MEM_CUSTOM,把它设为 1,并取消下面两行的注释(或者直接加上):

      c
      
      #define LV_MEM_CUSTOM 1
      #define LV_MEM_CUSTOM_INCLUDE <Arduino.h>   // 使用 Arduino 的 malloc/free
    • 第3步:告诉 LVGL 如何获取系统时间(用于动画、长按等)
      搜索 LV_TICK_CUSTOM,设为 1,并添加:

      c
      
      #define LV_TICK_CUSTOM 1
      #define LV_TICK_CUSTOM_INCLUDE "Arduino.h"
      #define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis())   // 用 Arduino 的 millis() 作为时钟源
    • 其他配置保持默认即可,比如屏幕分辨率、颜色深度等后面会通过代码设置。

小提示:如果你的 ESP32-S3 有 8MB PSRAM,可以再开启 LV_MEM_SIZE 调大内存,但初学者先用默认就好。


3. 配置屏幕驱动 TFT_eSPI(告诉驱动你的屏幕型号和接线)

TFT_eSPI 是一个通用的 SPI 屏幕库,支持很多种屏幕驱动芯片(比如 ST7789、ILI9341)。我们需要修改它的配置文件,告诉它用哪种芯片、屏幕尺寸、以及和 ESP32-S3 的引脚连接。

  • 找到配置文件:打开 .pio/libdeps/esp32-s3-devkitc-1/TFT_eSPI 文件夹,里面有一个 User_Setup.h,用 VSCode 打开它。

  • 修改内容(建议先把原来的内容备份一下,然后按下面步骤修改):

    • 第1步:选择屏幕驱动芯片
      注释掉其他所有 #define ..._DRIVER 的行,只保留你的屏幕对应的那一行。比如你的屏幕是 ST7789 驱动的(常见于 240x320 的圆屏或方屏),就只保留:

      c
      
      #define ST7789_DRIVER
    • 第2步:设置屏幕分辨率和颜色顺序
      添加或确认以下几行(根据你的屏幕手册):

      c
      
      #define TFT_WIDTH  240   // 屏幕宽度(像素)
      #define TFT_HEIGHT 320   // 屏幕高度(像素)
      #define TFT_RGB_ORDER TFT_BGR   // 颜色顺序,如果颜色显示不对(比如红色变蓝色),就改成 TFT_RGB
      #define TFT_INVERSION_OFF        // 反色开关,如果屏幕颜色反了(白色变黑),就改成 TFT_INVERSION_ON
    • 第3步:设置 SPI 引脚(非常重要!要和你的硬件接线一致)
      找到 #define TFT_MOSI 等引脚定义的地方,改成你的实际接线。以常见的 ESP32-S3 开发板为例(假设 MOSI→7, SCLK→15, CS→4, DC→6, RST→5, BL→16):

      c
      
      #define TFT_MOSI 7
      #define TFT_SCLK 15
      #define TFT_CS   4
      #define TFT_DC   6
      #define TFT_RST  5
      #define TFT_BL   16            // 背光引脚
      #define TFT_BACKLIGHT_ON HIGH  // 背光点亮电平(通常是 HIGH)
    • 第4步:设置 SPI 通信速度
      在文件末尾附近,找到 #define SPI_FREQUENCY,可以设成 40000000(40MHz):

      c
      
      #define SPI_FREQUENCY  40000000

保存 User_Setup.h。TFT_eSPI 会自动使用这些配置。


4. 配置触摸驱动 CST816S(告诉驱动触摸芯片的 I2C 引脚和地址)

如果你的触摸芯片是 CST816S(常见于一些圆屏),需要修改它的头文件。

  • 找到头文件:打开 .pio/libdeps/esp32-s3-devkitc-1/CST816S 文件夹,找到 CST816S.h,用 VSCode 打开。

  • 修改 I2C 地址
    搜索 CST816S_ADDRESS,确保它被定义为 0x38(大部分 CST816S 是这个地址):

    c
    
    #define CST816S_ADDRESS 0x38

保存文件。后面我们会在代码里设置触摸的引脚(SDA、SCL、RST、IRQ)。


5. 编写主程序:注册显示和触摸设备(告诉 LVGL 怎么用它们)

现在我们把 LVGL、屏幕、触摸三者关联起来。LVGL 官方提供了一个 Arduino 例程,我们可以把它复制过来修改。

  • 复制例程:打开 .pio/libdeps/esp32-s3-devkitc-1/lvgl/examples/arduino/LVGL_Arduino 文件夹,把里面的 LVGL_Arduino.ino 文件内容全部复制,替换掉你自己 src/main.cpp 的内容。

  • 修改屏幕尺寸
    在文件开头,找到这两行:

    c
    
    static const uint16_t screenWidth  = 480;
    static const uint16_t screenHeight = 320;

    改成你的屏幕尺寸(比如 240x320):

    c
    
    static const uint16_t screenWidth  = 320;   // 注意:这里是宽度,如果屏幕是竖屏,可能宽320高240,根据你的实际调整
    static const uint16_t screenHeight = 240;

    注意:如果你的屏幕是竖屏(比如手机样式),通常宽度小于高度,这里填 width=240, height=320;如果是横屏,则反过来。请根据你的屏幕方向确定。

  • 添加触摸对象
    在文件开头,#include <lvgl.h> 下面,添加触摸库头文件和触摸对象:

    cpp
    
    #include <CST816S.h>
    CST816S touch(3, 18, 8, 46);   // 参数顺序:SDA, SCL, RST, IRQ(根据你的接线修改)
  • 修改触摸回调函数
    找到 my_touchpad_read 函数,将其内容替换为(根据 CST816S 的数据格式):

    cpp
    
    void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
        if (touch.available()) {                     // 如果有触摸
            data->state = LV_INDEV_STATE_PR;         // 按下状态
            // CST816S 返回的坐标可能需要调整方向,这里假设触摸的 x 对应屏幕的 y,且镜像
            data->point.x = 320 - touch.data.y;      // 根据你的实际情况调整映射关系
            data->point.y = touch.data.x;
        } else {
            data->state = LV_INDEV_STATE_REL;        // 释放状态
        }
    }

    注意:坐标映射需要根据屏幕和触摸的实际方向调整。上面的例子只是一种可能,如果触摸位置不对,你可能需要交换 x、y,或者用 touch.data.x 直接赋值。

  • 修改 setup() 中的触摸初始化
    找到 tft.setTouch(calData); 那行(大概在例程的 setup 后半部分),把它注释掉,换成:

    cpp
    
    // uint16_t calData[5] = { 275, 3620, 264, 3532, 1 };
    // tft.setTouch(calData);          // 这是电阻屏的校准,不需要
    touch.begin();                     // 初始化 CST816S
  • 最后,添加一个简单的界面来测试
    在 setup() 末尾(lv_timer_handler(); 之前)添加:

    cpp
    
    // 创建一个标签显示 "Hello"
    lv_obj_t *label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "Hello ESP32-S3 + LVGL!");
    lv_obj_align(label, LV_ALIGN_CENTER, 0, -20);
    
    // 创建一个按钮,点击时在串口打印信息
    lv_obj_t *btn = lv_btn_create(lv_scr_act());
    lv_obj_set_size(btn, 120, 50);
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 40);
    lv_obj_t *btn_label = lv_label_create(btn);
    lv_label_set_text(btn_label, "Click me");
    lv_obj_center(btn_label);
    
    // 给按钮添加事件回调
    lv_obj_add_event_cb(btn, [](lv_event_t *e) {
        if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
            Serial.println("Button clicked!");
        }
    }, LV_EVENT_CLICKED, NULL);
  • 保存文件,然后编译上传。如果一切顺利,屏幕会显示文字和一个按钮,触摸按钮时串口会打印 "Button clicked!"。


6. 常见问题排查

  • 屏幕不亮:检查背光引脚和 TFT_BL 配置是否正确,背光电平是 HIGH 还是 LOW

  • 屏幕显示花屏或颜色不对:尝试修改 TFT_RGB_ORDER 为 TFT_RGB,或者调整 TFT_INVERSION_OFF/ON

  • 触摸没反应:检查 I2C 引脚和地址,用 I2C 扫描程序确认设备地址是否为 0x38。坐标映射可能需要交换 x、y 或取反。

  • 编译报错:可能是某个库版本不兼容,可以尝试降低版本(比如 TFT_eSPI 用 2.5.0,LVGL 用 8.3.9)。


四、LVGL 部件和事件(开始画界面吧)

LVGL 提供了很多现成的“部件”(Widgets),就像手机 App 里的按钮、滑块、开关等。你只需要创建它们,设置属性,然后处理事件(比如点击、滑动)。

1. 常用部件一览(先认识几个常用的)

部件名称 作用 创建函数
Label(标签) 显示文字 lv_label_create(parent)
Button(按钮) 可点击的区域 lv_btn_create(parent)
Slider(滑块) 滑动调节数值 lv_slider_create(parent)
Switch(开关) 开/关切换 lv_switch_create(parent)
Checkbox(复选框) 多选选项 lv_checkbox_create(parent)
Textarea(文本框) 输入多行文字 lv_textarea_create(parent)
Image(图像) 显示图片 lv_img_create(parent)
Chart(图表) 显示曲线/柱状图 lv_chart_create(parent)
List(列表) 垂直列表 lv_list_create(parent)

每个部件创建时都需要指定一个“父对象”,通常是屏幕本身(lv_scr_act()),也可以是其他容器(比如面板)。

2. 创建、布局与样式(三步搞定一个部件)

2.1 创建对象
cpp

lv_obj_t *btn = lv_btn_create(lv_scr_act());   // 在屏幕上创建一个按钮
2.2 设置大小和位置
cpp

lv_obj_set_size(btn, 120, 50);                 // 宽度120,高度50
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);      // 居中对齐,偏移(0,0)

对齐方式有很多种,比如 LV_ALIGN_TOP_LEFT(左上角)、LV_ALIGN_BOTTOM_MID(底部中间)等。后面的两个数字是 x 和 y 方向的偏移像素。

2.3 在按钮上添加文字
cpp

lv_obj_t *label = lv_label_create(btn);        // 在按钮上创建标签
lv_label_set_text(label, "Click");              // 设置文字
lv_obj_center(label);                            // 让标签在按钮内居中
2.4 添加样式(可选)

如果你想改变按钮的颜色、圆角、阴影等,可以定义样式并应用:

cpp

static lv_style_t style;
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_color_hex(0xFF0000));   // 红色背景
lv_style_set_radius(&style, 10);                          // 圆角10像素
lv_obj_add_style(btn, &style, 0);                         // 应用样式到按钮

3. 事件回调机制(让部件动起来)

当用户点击按钮、滑动滑块时,LVGL 会产生一个“事件”。你需要为部件注册一个回调函数,在回调里判断事件类型并做出响应。

3.1 注册事件回调
cpp

lv_obj_add_event_cb(btn, my_event_cb, LV_EVENT_CLICKED, NULL);
  • btn:要监听的部件。

  • my_event_cb:回调函数名。

  • LV_EVENT_CLICKED:监听的事件类型(这里只监听点击,你也可以监听多种事件)。

  • NULL:用户数据(可传任意指针)。

3.2 编写回调函数
cpp

void my_event_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);   // 获取事件代码
    if (code == LV_EVENT_CLICKED) {
        Serial.println("Button clicked!");
    }
}
常用事件类型
  • LV_EVENT_CLICKED:点击完成(按下并释放)

  • LV_EVENT_PRESSED:刚按下

  • LV_EVENT_RELEASED:释放

  • LV_EVENT_VALUE_CHANGED:值改变(比如滑块滑动)

  • LV_EVENT_LONG_PRESSED:长按

4. 完整示例:一个带事件的按钮

把下面代码放到 loop() 之前(通常放在 setup() 里):

cpp

// 创建按钮
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 150, 60);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);

// 按钮上的文字
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Press me");
lv_obj_center(label);

// 添加点击事件
lv_obj_add_event_cb(btn, [](lv_event_t *e) {
    if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
        Serial.println("Button pressed!");
        // 可以在这里改变按钮文字等
        lv_obj_t *btn = (lv_obj_t*)lv_event_get_target(e);
        lv_obj_t *label = lv_obj_get_child(btn, 0); // 获取按钮的第一个子对象(就是label)
        lv_label_set_text(label, "Clicked!");
    }
}, LV_EVENT_CLICKED, NULL);

这里用了 C++ 的 Lambda 表达式([](lv_event_t *e){...}),可以让代码更紧凑。如果你不熟悉 Lambda,也可以像前面那样单独写一个函数。


5. 试试其他部件:滑块和开关

滑块示例
cpp
// 创建滑块
lv_obj_t *slider = lv_slider_create(lv_scr_act());
lv_obj_set_size(slider, 200, 10);
lv_obj_align(slider, LV_ALIGN_CENTER, 0, -40);

// 创建显示数值的标签
lv_obj_t *slider_label = lv_label_create(lv_scr_act());
lv_label_set_text(slider_label, "0");
lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_TOP_MID, 0, -10);

// 添加值改变事件
lv_obj_add_event_cb(slider, [](lv_event_t *e) {
    lv_obj_t *slider = lv_event_get_target(e);
    char buf[8];
    lv_snprintf(buf, sizeof(buf), "%d", lv_slider_get_value(slider));
    lv_obj_t *label = (lv_obj_t*)lv_event_get_user_data(e);
    lv_label_set_text(label, buf);
}, LV_EVENT_VALUE_CHANGED, slider_label);

开关示例
cpp

lv_obj_t *sw = lv_switch_create(lv_scr_act());
lv_obj_align(sw, LV_ALIGN_CENTER, 0, 50);

lv_obj_add_event_cb(sw, [](lv_event_t *e) {
    if (lv_event_get_code(e) == LV_EVENT_VALUE_CHANGED) {
        bool state = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
        Serial.println(state ? "ON" : "OFF");
    }
}, LV_EVENT_VALUE_CHANGED, NULL);

按键


6. 学习更多

  • LVGL 官方文档:https://docs.lvgl.io/(有详细的中文翻译)

  • 正点原子的视频教程(就是你截图的那个)也非常好,跟着做能快速上手。

现在,你已经掌握了 LVGL 的基本移植和部件使用方法,可以开始设计自己的智能终端界面了!如果在配置过程中遇到问题,多检查接线和配置文件,一步步排查。祝你开发顺利!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐