在 STM32H743 上部署 LVGL:源码移植、显示触摸适配与 UI 设计

本文以 野火 STM32H743XI EVK Pro 为硬件平台,以 LVGL v8.3.11 + Keil MDK 为软件环境,记录从准备 LVGL 源码、加入工程,到适配显示、触摸和系统时基的完整过程,并介绍使用 AI 与 SquareLine Studio 1.6.1 设计个人 UI 的两种路线。

一、硬件平台与移植前提

本文使用的硬件配置如下:

项目 配置
开发板 野火 STM32H743XI EVK Pro
显示屏 7 英寸 LCD
分辨率 800 × 480
显示接口 24 bit RGB 接口
触摸方式 电容触摸,支持 5 点触控
供电要求 开发板外接 DC 12 V 电源

供电提示: 使用 7 英寸屏时,应给开发板接入 DC 12 V 电源。供电不足可能造成屏幕显示异常,也可能导致 MCU 无法与调试器稳定通信。

在移植 LVGL 之前,建议先确认现有裸机工程已经满足以下条件:

  1. LCD 初始化正常,可以稳定显示点、线、圆或色块等基本图形。
  2. 触摸驱动正常,可以读取按下状态和触摸坐标。
  3. 屏幕分辨率、像素格式和显存地址已经在 BSP 中正确配置。

LVGL 的显示和输入设备最终都要建立在现有 LCD、触摸驱动之上。如果底层驱动尚未跑通,后续很难判断问题来自 BSP 还是 LVGL 适配层。

二、下载并整理 LVGL v8.3.11 源码

2.1 选择 LVGL 版本

在 GitHub 中搜索 LVGL,进入官方源码仓库:

请添加图片描述

图 1:LVGL GitHub 源码仓库

点击仓库右侧的 Releases,选择需要的版本下载。本文使用 LVGL v8.3.11,主要考虑到 v8 系列资料较多、移植接口成熟,并且与本文使用的 lv_port_disp.clv_port_indev.c 模板一致。

版本说明: 本文代码基于 LVGL v8。LVGL v9 的显示、输入设备注册 API 已发生变化,不能直接照搬本文接口。如果准备使用 LVGL Pro 等新工具,应先确认工具支持的 LVGL 主版本。

2.2 提取移植所需文件

下载并解压源码包后,本文保留以下内容:

  • src
  • examples
  • demos
  • lvgl.h
  • lv_conf_template.h

源码包的原始目录如下:

请添加图片描述

图 2:LVGL Release 解压后的目录

新建一个名为 LVGL 的文件夹,将上述文件和目录复制进去。整理完成后的顶层目录如下:

请添加图片描述

图 3:整理后的 LVGL 顶层目录

随后进入 examples 目录,只保留 porting 文件夹。将 porting 中 6 个模板文件名里的 _template 去掉,例如:

  • lv_port_disp_template.clv_port_disp.c
  • lv_port_disp_template.hlv_port_disp.h
  • lv_port_indev_template.clv_port_indev.c
  • lv_port_indev_template.hlv_port_indev.h
  • lv_port_fs_template.clv_port_fs.c
  • lv_port_fs_template.hlv_port_fs.h

处理后的 porting 目录如下:

请添加图片描述

图 4:移除 _template 后的 porting 文件

最后,将 lv_conf_template.h 重命名为 lv_conf.h

demos 并不是 LVGL 内核运行的必需项。如果不使用官方 Demo,不要把其中的源文件和大体积资源加入 Keil 工程,以免增加编译时间和 Flash 占用。

三、将 LVGL 加入 STM32 工程

3.1 放置源码目录

将整理好的 LVGL 文件夹复制到 STM32 工程根目录。对于 CubeMX 生成的工程,可以将它放在与 .ioc 文件同级的位置:

请添加图片描述

图 5:将 LVGL 文件夹放入工程根目录

3.2 在 Keil 中创建分组

打开 Keil,在工程目标上右键,选择 Manage Project Items

请添加图片描述

图 6:打开 Manage Project Items

在弹出的窗口中创建用于管理 LVGL 代码的 Group:

请添加图片描述

图 7:在 Keil 中创建 LVGL 分组

本文按功能划分为以下 4 个逻辑分组:

  • LVGL_conf:LVGL 配置文件和总头文件。
  • LVGL_porting:显示、触摸等硬件适配代码。
  • LVGL_src:LVGL 内核源码。
  • LVGL_MyGUI:用户界面代码,可按实际项目选用。

创建完成后的工程分组如下:

请添加图片描述

图 8:LVGL 工程分组

Keil 中的 Group 是逻辑分组,并不等同于磁盘上的真实文件夹。

3.3 添加源码文件

选中 LVGL_conf,点击 Add Files

请添加图片描述

图 9:通过 Add Files 添加 LVGL 文件

lv_conf.hlvgl.h 添加到 LVGL_conf。随后采用同样的方法:

  1. examples/porting 中使用到的 .c 文件加入 LVGL_porting
  2. src 及其所有子目录中的 .c 文件加入 LVGL_src
  3. 如果有自己的 UI 源码,将其加入 LVGL_MyGUI

src 下包含 100 多个源文件,需要逐个子目录确认,避免漏加 .c 文件。

3.4 配置头文件搜索路径

打开 Options for Target

请添加图片描述

图 10:打开 Options for Target

进入 C/C++ → Include Paths,点击右侧的省略号按钮添加路径:

请添加图片描述

图 11:配置 Keil Include Paths

至少加入以下目录,具体相对路径应以工程结构为准:

LVGL
LVGL\src
LVGL\examples\porting

同时不要忘记加入 LCD BSP 和触摸驱动头文件所在目录。

四、配置 lv_conf.h

打开重命名后的 lv_conf.h,先将文件开头用于启用配置内容的条件编译从 0 改为 1

#if 1 /* Set it to "1" to enable content */

随后检查颜色深度:

#define LV_COLOR_DEPTH 16

这里的 16 仅适用于 LVGL 使用 RGB565 的情况。屏幕物理接口是 24 bit RGB,并不代表 LVGL 缓冲区一定要配置成 24/32 bit;最终应以 LTDC 图层像素格式、BSP 画点函数参数和显存格式为准。三者不一致时,通常会出现颜色异常或图像错位。

五、适配 LVGL 显示接口

5.1 启用 lv_port_disp.c

打开 lv_port_disp.c,将文件顶部的:

#if 0

修改为:

#if 1

然后加入项目 LCD 驱动头文件:

请添加图片描述

图 12:启用 lv_port_disp.c 并包含 LCD 头文件

5.2 设置屏幕分辨率

DEFINES 区域将水平、垂直分辨率改成实际值。本文为 800 × 480:

#define MY_DISP_HOR_RES 800
#define MY_DISP_VER_RES 480

请添加图片描述

图 13:设置 LVGL 屏幕分辨率

5.3 选择显示缓冲区方案

继续向下找到 lv_port_disp_init()。模板提供了 3 种缓冲区示例:

请添加图片描述

图 14:LVGL 显示缓冲区示例

  • Example 1:单个局部缓冲区。 内存占用最小,适合先完成移植验证。
  • Example 2:两个局部缓冲区。 可以配合 DMA/DMA2D 在刷新时并行绘制,性能通常更好。
  • Example 3:两个全屏缓冲区。 刷新效率高,但内存开销最大。

以 RGB565 为例,800 × 480 的单个全屏缓冲区约占:

800 × 480 × 2 Byte = 768000 Byte ≈ 750 KiB

两个全屏缓冲区约占 1.5 MiB,通常需要外部 SDRAM。本文先采用 Example 1 跑通显示链路。

5.4 实现 disp_flush()

找到 disp_flush(),在模板循环中调用现有 LCD 画点函数。下面代码仅展示适配思路,LCD_DrawPixel() 的函数名和颜色参数必须根据实际 BSP 修改:

static void disp_flush(lv_disp_drv_t *disp_drv,
                       const lv_area_t *area,
                       lv_color_t *color_p)
{
    for (int32_t y = area->y1; y <= area->y2; y++) {
        for (int32_t x = area->x1; x <= area->x2; x++) {
            /* 仅当 LV_COLOR_DEPTH == 16 且 BSP 接收 RGB565 时可直接使用 full */
            LCD_DrawPixel(x, y, color_p->full);
            color_p++;
        }
    }

    /* 无论采用哪种刷新方式,完成后都必须通知 LVGL */
    lv_disp_flush_ready(disp_drv);
}

性能提示: 逐像素调用 BSP 函数适合验证显示链路,但 800 × 480 屏幕上会比较慢。正式项目建议让 disp_flush() 直接复制矩形像素块到帧缓冲区,或使用 DMA2D 加速。若启用了 STM32H7 的 D-Cache,还要处理帧缓冲区的 Cache 一致性。

至此,LVGL 显示设备的基础适配完成。

六、适配 LVGL 触摸输入

6.1 启用 lv_port_indev.c

打开 lv_port_indev.c,同样将顶部的 #if 0 改为 #if 1,并包含项目触摸驱动头文件。

本文只使用触摸屏作为 Pointer 输入设备,因此可以在 lv_port_indev_init() 中保留触摸相关注册,关闭模板中未使用的 Mouse、Keypad、Encoder 和 Button 相关代码:

请添加图片描述

图 15:关闭未使用的输入设备模板

6.2 接入按下状态与坐标读取函数

继续找到以下两个触摸适配函数:

请添加图片描述

图 16:触摸按下状态与坐标适配函数

本文的 Goodix 触摸驱动对外提供:

  • GTP_Get_Is_Pressed_For_LVGL():返回当前是否按下。
  • GTP_Get_XY_For_LVGL():读取当前触摸坐标。

因此可以按下面的方式接入:

static bool touchpad_is_pressed(void)
{
    return GTP_Get_Is_Pressed_For_LVGL();
}

static void touchpad_get_xy(lv_coord_t *x, lv_coord_t *y)
{
    int16_t temp_x;
    int16_t temp_y;

    GTP_Get_XY_For_LVGL(&temp_x, &temp_y);
    *x = (lv_coord_t)temp_x;
    *y = (lv_coord_t)temp_y;
}

触摸读取回调只负责上报状态与坐标,不建议在这里调用 LCD_DrawPixel() 等绘图函数,否则可能与 LVGL 的刷新过程互相干扰。

6.3 从触摸中断定位底层处理链路

不同屏幕使用的触摸控制器和 BSP 不同,函数名不会完全一致。可以从外部中断入口开始查找触摸处理逻辑。本文驱动中的调用关系为:

GTP_IRQHandler()
    └── GTP_TouchProcess()
            └── Goodix_TS_Work_Func()

触摸中断处理函数如下图所示:

请添加图片描述

图 17:Goodix 触摸中断处理函数

继续进入 GTP_TouchProcess(),即可找到更深一层的 Goodix 数据处理函数:

请添加图片描述

图 18:Goodix 触摸数据处理函数

如果 LVGL 能检测到按下但坐标不正确,应重点检查屏幕旋转、X/Y 交换、坐标镜像和分辨率映射。

七、在 main.c 中初始化 LVGL

7.1 包含头文件

main.c 中加入:

#include "lvgl.h"          /* LVGL 核心接口 */
#include "lv_port_disp.h"  /* 显示设备适配 */
#include "lv_port_indev.h" /* 输入设备适配 */

7.2 按顺序完成初始化

先完成时钟、GPIO、SDRAM、LTDC、LCD 和触摸等底层外设初始化,再初始化 LVGL:

/* LCD、触摸等底层硬件初始化完成后 */
lv_init();
lv_port_disp_init();
lv_port_indev_init();

推荐的初始化顺序可以概括为:

HAL/系统时钟
    → SDRAM、LTDC、LCD
    → 触摸驱动
    → lv_init()
    → lv_port_disp_init()
    → lv_port_indev_init()
    → 创建 UI

八、为 LVGL 提供 1 ms 系统时基

LVGL 需要一个持续递增的系统时基,用于动画、输入设备长按、定时器等内部功能。本文使用 1 ms 定时器中断,在中断中调用:

lv_tick_inc(1);

以 TIM6 为例:

void TIM6_DAC_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim6);
    lv_tick_inc(1);
}

请添加图片描述

图 19:在 TIM6 中断中调用 lv_tick_inc(1)

注意事项:

  1. TIM6 的实际中断周期必须是 1 ms;如果周期不是 1 ms,传给 lv_tick_inc() 的数值也要相应调整。
  2. CubeMX 工程应将自定义代码放入 USER CODE 区域,防止重新生成工程时被覆盖。
  3. 如果工程已经通过其他可靠方式向 LVGL 提供 Tick,不要重复调用。

九、创建测试控件并运行任务处理器

9.1 创建按钮和开关

在进入 while 循环之前、完成 LVGL 初始化之后,加入以下测试代码:

lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 160, 80);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, -50);

lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, "Touch Me!");
lv_obj_center(label);

/* 创建开关。触摸正常时,点击后状态会自动切换 */
lv_obj_t *sw = lv_switch_create(lv_scr_act());
lv_obj_align(sw, LV_ALIGN_CENTER, 0, 50);

9.2 周期调用 lv_timer_handler()

在主循环中持续调用 LVGL 任务处理器:

while (1) {
    lv_timer_handler();
    HAL_Delay(5);
}

lv_timer_handler() 负责处理界面刷新、动画和输入事件。裸机工程中加入一个短延时可以避免 CPU 一直空转;如果使用 FreeRTOS,可以在 LVGL 任务中改用 osDelay()

十、编译、烧录与触摸验证

完成以上配置后重新编译并烧录程序。界面应显示一个按钮和一个开关:

请添加图片描述

图 20:LVGL 测试界面的初始状态

点击开关后,其状态发生切换,说明显示刷新和触摸输入链路均已正常工作:

请添加图片描述

图 21:触摸后开关状态发生切换

十一、进一步设计个人 UI

完成 LVGL 的基础移植后,就可以开始构建实际项目界面。本文新增两条常用路线:

  1. 使用 AI 根据需求和 HTML 原型生成原生 LVGL C 代码。
  2. 使用 SquareLine Studio 完成可视化设计并导出 LVGL 工程代码。

无论采用哪种方式,都不能改变 LVGL 的基础运行链路:lv_tick_inc() 仍然负责系统时基,lv_timer_handler() 仍然需要在主循环或 RTOS 任务中周期执行。

11.1 路线一:使用 AI 生成 LVGL UI

得益于 LVGL 的开源生态和 C API,可以先与 AI 沟通界面布局、交互逻辑和视觉风格,让 AI 生成一个 HTML 原型;确认原型满足需求后,再要求代码能力较强的 AI 将原型转换为 LVGL 的 .c.h 文件。

推荐向 AI 明确提供以下约束:

  • LVGL 版本:本文为 v8.3.11。
  • 屏幕分辨率:800 × 480。
  • 颜色深度:与 lv_conf.h 中的 LV_COLOR_DEPTH 保持一致。
  • 运行环境:STM32H743,不存在浏览器、HTML 或 CSS 运行时。
  • 输入方式:触摸屏,交互控件应直接显示在界面上。
  • 资源限制:说明可用 Flash、内部 SRAM 和外部 SDRAM 容量。

HTML 在这里是界面和交互原型,不会直接运行在 STM32 上。最终代码必须转换为真实的 LVGL 控件、样式、事件回调和资源声明。

将生成的 .c.h 文件放入工程目录,并添加到 Keil 的 UI 分组中:

请添加图片描述

图 22:将 AI 生成的 UI 源文件加入工程

main.c 中包含生成的头文件,并在 lv_init()、显示端口和输入端口初始化完成后,调用生成代码提供的 UI 初始化函数:

#include "AI_Code_UI.h"

/* LCD、触摸和 LVGL 初始化完成后 */
lv_init();
lv_port_disp_init();
lv_port_indev_init();

AI_Code_UI_Init(); /* 以生成文件中的实际初始化函数名为准 */

如果生成代码中的函数名不是 AI_Code_UI_Init(),应以头文件声明为准进行替换。UI 初始化完成后,原有 lv_tick_inc()lv_timer_handler() 的位置保持不变。

11.2 在 AI 生成的 UI 中加入真实图片

如果 UI 需要显示 Logo、图标或背景图,应提前要求 AI 在代码中预留对应图片资源接口。随后使用 LVGL 官方 Image Converter 将图片转换为 C 数组:

  1. 选择与项目 LV_COLOR_DEPTH 相匹配的颜色格式。
  2. 将图片导出为 .c 数组文件。
  3. 把生成的 .c 文件加入 Keil 工程。
  4. 在使用图片的源文件中通过生成的图片描述符或 LV_IMG_DECLARE() 进行引用。

如果只保留了图片声明,却没有把转换后的 .c 文件加入工程,链接阶段通常会出现图片符号未定义的问题。

11.3 路线二:使用 SquareLine Studio 设计 UI

本文使用 SquareLine Studio 1.6.1。打开软件后可以看到项目和示例选择界面:

请添加图片描述

图 23:SquareLine Studio 1.6.1 主界面

进入 Examples,选择一个分辨率和界面结构接近目标屏幕的示例并双击打开:

请添加图片描述

图 24:选择 SquareLine Studio 示例工程

示例分辨率只作为起点。打开工程后,进入 File → Project Settings

请添加图片描述

图 25:配置 SquareLine Studio Project Settings

重点检查以下项目:

  • LVGL 主版本与目标工程一致,本文应选择 LVGL v8 对应配置。
  • Horizontal Resolution 设置为 800
  • Vertical Resolution 设置为 480
  • 导出路径指向一个便于管理的独立目录。
  • 图片、字体和颜色格式与嵌入式工程配置一致。

设置完成后点击 APPLY CHANGES。软件会根据新配置重新生成界面,然后通过导出菜单生成 UI 文件:

请添加图片描述

图 26:从 SquareLine Studio 导出 UI 文件

导出的目录通常包含:

  • components
  • fonts
  • images
  • screens
  • ui.cui.h
  • ui_events.cui_events.h
  • ui_helpers.cui_helpers.h

请添加图片描述

图 27:SquareLine Studio 导出的 UI 目录

11.4 将 SquareLine 导出代码加入 Keil

在 STM32 工程中创建一个独立 UI 文件夹,例如 LVGL_MyGUI,将 SquareLine 导出的全部 UI 代码复制到该目录:

请添加图片描述

图 28:将 SquareLine 导出文件放入工程目录

随后在 Keil 中完成以下配置:

  1. 将导出目录及 screensimagesfontscomponents 等子目录中的所有 .c 文件加入 LVGL_MyGUI 分组。
  2. 将 UI 根目录及需要的子目录加入 Include Paths。
  3. main.c 中包含 ui.h
  4. lv_port_disp_init()lv_port_indev_init() 之后、进入主循环之前调用 ui_init()

Keil 中加入 UI 文件后的结构如下:

请添加图片描述

图 29:在 Keil 中加入 SquareLine UI 文件

初始化代码示例:

#include "ui.h"

lv_init();
lv_port_disp_init();
lv_port_indev_init();
ui_init();

while (1) {
    lv_timer_handler();
    HAL_Delay(5);
}

编译并烧录后,SquareLine 设计的界面即可运行在开发板上:

请添加图片描述

图 30:SquareLine UI 在 STM32H743 开发板上的运行效果

SquareLine 重新导出时可能覆盖生成文件。业务逻辑最好放在事件文件、用户代码文件或独立模块中,避免直接修改会被重复生成的代码。更深入的控件使用、动画和事件配置,可以继续参考 SquareLine Studio 专项教程。

十二、常见问题排查

12.1 编译时找不到 lvgl.h

检查 Keil 的 Include Paths 是否包含 LVGL,并确认路径是相对于工程文件的正确路径。

12.2 lv_conf.h 没有生效

确认文件已从 lv_conf_template.h 重命名为 lv_conf.h,文件顶部的配置开关已改成 #if 1,并且编译器能够从 Include Paths 找到该文件。

12.3 屏幕白屏或刷新停止

依次检查:

  1. DC 12 V 供电、背光和 LCD 底层初始化是否正常。
  2. lv_init()lv_port_disp_init() 是否已经调用。
  3. disp_flush() 是否真正写入屏幕或帧缓冲区。
  4. 刷新结束时是否调用了 lv_disp_flush_ready()

12.4 颜色异常

检查 LV_COLOR_DEPTH、LTDC 图层像素格式、LCD BSP 接收的颜色格式以及 RGB565 字节序是否一致。

12.5 触摸无响应或坐标颠倒

检查输入设备是否注册为 LV_INDEV_TYPE_POINTER,按下状态是否正确返回,以及坐标是否需要进行 X/Y 交换、镜像或旋转映射。

12.6 程序能运行但刷新很慢

逐像素调用 LCD_DrawPixel() 的开销较大。确认功能正确后,可将 disp_flush() 改为帧缓冲区块复制,并结合 DMA2D、双局部缓冲区和 Cache 维护优化性能。

12.7 UI 代码编译时报图片或字体符号未定义

确认 SquareLine 或 Image Converter 生成的资源 .c 文件已经加入 Keil,而不仅仅是添加了头文件。还要检查 imagesfonts 等目录是否加入 Include Paths。

12.8 加入 SquareLine UI 后 Flash 空间不足

优先检查生成的图片和字体资源。删除未使用的资源、减少图片尺寸或颜色深度、缩小字体字符范围,并避免同时编译无关的 demosexamples

十三、总结

在 STM32H743 上部署并构建 LVGL 界面的关键,不是简单地把源码加入工程,而是打通以下 5 条链路:

  1. 源码与配置链路:LVGL 内核源码、lv_conf.h 和编译路径正确。
  2. 显示链路:LVGL 输出区域能够写入 LCD 或帧缓冲区,并在完成后调用 lv_disp_flush_ready()
  3. 输入链路:触摸驱动能够向 LVGL 提供稳定的按下状态和坐标。
  4. 运行时链路lv_tick_inc() 提供系统时基,lv_timer_handler() 周期运行。
  5. UI 应用链路:AI 或 SquareLine 生成的界面代码、图片和字体资源完整加入工程,并在正确的初始化阶段创建界面。

当按钮能够显示、开关能够响应触摸并正确切换状态时,LVGL 的基础移植就完成了;当 AI 或 SquareLine 生成的界面也能稳定运行时,应用层 UI 链路就已经打通。后续可以继续接入 DMA2D、双缓冲和资源优化,进一步提升刷新性能与界面复杂度。


关键词: STM32H743、LVGL、Keil、LCD、Goodix、SquareLine Studio、AI UI、嵌入式 GUI

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐