当前位置: 首页 > news >正文

LVGL 9.0移植到STM32F746G-DISCO与性能基准测试实战

最近帮团队把一套跑得好好的 LVGL 8.3 界面工程升级到 LVGL 9.0,结果一编译就冒出一大片报错:lv_disp_drv_t找不到、lv_disp_flush_ready参数不匹配、大量scr开头 API 全部失效。为什么会这样?因为 LVGL 9.0 不是 8.x 的小版本迭代,而是一次渲染引擎和驱动模型的整体重构。很多在 8.3 上成立的经验,到了 9.0 基本要重新验证。

更麻烦的是,团队手头正好有一块 STM32F746G-DISCO 开发板,主控是 Cortex-M7 216MHz,板载 4.3 寸 480x272 RGB 屏,理论上跑 GUI 很能打。但“理论上能打”和“实际跑得流畅”是两回事:LVGL 9.0 的渲染路径变了,缓冲策略变了,颜色格式也更复杂,如果不做基准测试,你根本不知道性能瓶颈到底卡在 CPU、DMA、显存带宽,还是驱动回调写得太粗糙。

这篇文章会以 STM32F746G-DISCO 为硬件平台,完整走一遍 LVGL 9.0 的移植流程,然后给出一个可以在自己项目里直接复用的性能基准测试方案。内容包括:LVGL 9.0 与 8.x 的核心差异、CubeMX 工程准备、显示驱动移植代码、FPS 统计方法、常见排查思路,以及从颜色格式到 DMA 优化的取舍建议。如果你正准备把手上的 STM32 项目升级到 LVGL 9.x,或者正在评估 M7 跑新版 LVGL 的主流 MCU 选型,这篇文章可以直接作为参考清单。

1. 这篇文章真正要解决的问题

先说一个现状:目前网上大多数“STM32 移植 LVGL”教程还是基于 8.3 甚至 8.0,照搬到 9.0 基本会编译失败。LVGL 9.0 把显示驱动从“注册一个驱动结构体”改成了“创建一个 display 对象”,把lv_disp_flush_readylv_disp_flush_is_last等系列 API 全部重命名;同时颜色格式从单一的LV_COLOR_DEPTH扩展为显式LV_COLOR_FORMAT_*枚举。这意味着以前“复制粘贴就能跑”的移植套路失效了。

第二个痛点是性能评估。很多开发者移植完 GUI 后,只凭肉眼判断“界面流畅不流畅”,一旦觉得卡,就盲目加 DMA、换双缓冲、调刷新频率,最后问题反而更多。真正靠谱的做法,是把渲染帧率、单帧绘制耗时、CPU 占用率和内存占用这四类指标量化,然后针对瓶颈做单一变量优化。这也是本篇文章要重点展示的部分。

第三个痛点是平台特殊性。STM32F746G-DISCO 这块板子带 RGB 接口 LCD 和板载 SDRAM,但很多教程默认你用的是 SPI 屏或 8080 并口屏。LTDC + SDRAM + 双缓冲的框架,和普通 SPI 屏幕的移植方式完全不同。如果照搬 SPI 屏教程,你在 flush 回调这一层就会卡住。

所以,这篇文章要解决的读者问题可以概括成三句话:

  • 在 STM32F746G-DISCO 上,LVGL 9.0 的移植标准步骤到底是什么?
  • 怎么样用数据验证“卡不卡”,而不是靠感觉?
  • 拿到基准数据后,有哪些优化手段可以用来提升实际渲染性能?

2. LVGL 9.0 与 STM32F746G-DISCO 的基础概念

2.1 STM32F746G-DISCO 的硬件底子

在动手移植前,先把硬件能力摸清楚。STM32F746G-DISCO 是 ST 官方 Discovery 系列评估板,核心特点如下表:

硬件模块关键信息对 LVGL 的作用
主控 MCUSTM32F746NGH6,Cortex-M7,最高 216MHz决定 CPU 渲染算力
内部 Flash/RAM1MB Flash / 320KB SRAMLVGL 代码与局部变量驻留
板载 LCD4.3 寸 TFT,480x272,RGB 接口显示输出,由 LTDC 驱动
板载 SDRAM约 8MB,16bit,具体型号以原理图为准显存与 LVGL draw buffer 的主要存放位置
调试接口板载 ST-LINK/V2下载程序与串口日志

从 GUI 角度看,这块板子最大的优势是:RGB 接口 LCD 可以直接接在 STM32 的 LTDC 外设上,配合 SDRAM 做显存,不需要像 SPI 屏那样频繁搬运整帧数据。刷新路径是“LTDC 从 SDRAM 显存读取像素 → 自动输出到 LCD”。LVGL 往显存区域绘制内容,LTDC 负责定时扫描,两者并行工作,这是流畅 UI 的硬件基础。

2.2 LVGL 9.0 是什么

LVGL(Light and Versatile Graphics Library)是一个开源的嵌入式图形库,提供控件系统、事件处理、动画、布局、图片解码和多套渲染后端。9.0 是 LVGL 在架构层面的一次大版本升级。它的目标不是修修补补,而是把 8.x 里长期积累的“显示驱动 + 渲染器”耦合设计拆开,让一套代码可以更灵活地适配不同 MCU 和屏幕接口。

LVGL 9.0 官方定位是“下一代架构”,但这也意味着它跟 8.x 的代码并不完全兼容。升级时不能简单地替换源码,而是需要理解新的 display 对象模型和渲染缓冲机制。

2.3 LVGL 8.x 与 9.0 的关键差异

对移植者来说,最需要关注的是驱动层的 API 变化。下面这张表整理了最容易踩坑的几个点:

对比维度LVGL 8.xLVGL 9.0影响
显示驱动注册lv_disp_drv_t+lv_disp_drv_register()lv_display_create()创建对象初始化代码必须重写
flush 回调签名(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)(lv_display_t *display, const lv_area_t *area, uint8_t *px_map)回调参数类型变化,数据指针变uint8_t *
刷新完成信号lv_disp_flush_ready(drv)lv_display_flush_ready(display)函数与参数更名
最后一帧判断lv_disp_flush_is_last(drv)lv_display_flush_is_last(display)函数更名
屏幕 APIlv_scr_act()lv_scr_load()lv_screen_active()lv_screen_load()常用 API 改名
颜色格式依赖LV_COLOR_DEPTHLV_COLOR_FORMAT_RGB565/RGB888/ARGB8888等显式枚举颜色配置更灵活,但需要显式设置

其中影响最大的是 flush 回调。8.x 里color_plv_color_t *,很多老代码直接按颜色深度做强转。9.0 改成uint8_t *px_map后,你必须知道当前缓冲区到底是什么颜色格式,再决定按 2 字节还是 4 字节处理,否则很容易写出“偶尔花屏”的 bug。

2.4 什么是性能基准测试,为什么要做

性能基准测试,就是用一组可重复、可量化的操作,测量系统在特定负载下的表现。对 GUI 来说,常用指标包括:

  • FPS(Frames Per Second):每秒完成多少次渲染刷新,直接关系肉眼流畅度。
  • 单帧绘制耗时:从 LVGL 开始绘制一帧到 flush 完成的毫秒数,反映每次交互的成本。
  • CPU 占用率:渲染、事件处理、布局计算占用 CPU 的比例,决定系统还能剩多少算力给业务逻辑。
  • 内存占用:LVGL 堆内存、draw buffer、控件对象内存的消耗,在 MCU 上通常是最稀缺的资源。

如果不做基准测试,单凭“看起来挺流畅”来评估,很容易掩盖两类问题:一是当前界面简单所以流畅,一旦叠加复杂控件就卡;二是虽然肉眼流畅,但 CPU 已经被绘图吃满,业务任务被拖慢。只有在固定环境和固定测试用例下跑数据,才能为后续优化提供判断依据。

3. 环境准备与工程创建

3.1 软件与硬件清单

开始移植前,先把工具链准备好。以下清单以本文使用的方案为例,版本请以你实际安装为准,但整体思路不变:

工具/材料用途
STM32F746G-DISCO 开发板硬件平台
一根 USB 线供电 + ST-LINK 调试下载
STM32CubeMX生成 HAL 工程和外设初始化代码
STM32CubeIDE 或 Keil MDK 或 IAR编译、下载、调试
LVGL 9.x 官方源码从 GitHub Releases 下载 9.0 系列稳定版
串口调试助手查看日志和性能数据

3.2 在 CubeMX 中创建基础工程

STM32F746G-DISCO 的移植第一步,是先把 LCD 和 SDRAM 底层跑通。不要一上来就加入 LVGL,否则底层有问题时,你很难判断是 LVGL 配置错误,还是 LTDC/SDRAM 初始化失败。

在 CubeMX 中按以下步骤创建工程:

第 1 步:选择 MCU 型号

打开 STM32CubeMX,选择STM32F746NGHx。这块板子对应的具体型号是 STM32F746NGH6,选择时以实际板载芯片为准。

第 2 步:配置时钟树

开启 HSE,配置 PLL,使 CPU 主频达到 216MHz。F746 的典型配置是 HSE 25MHz 外部晶振,通过 PLL 倍频到 216MHz。可以在 Clock Configuration 页面里直接输入 216,让 CubeMX 自动求解分频倍频参数。

第 3 步:启用 LTDC

在 Pinout 配置中启用 LTDC,并配置 Layer1:

  • 像素格式:RGB888 或 RGB565,两种都可以,本文后续示例按 RGB565 写,颜色格式需要和 LVGL 保持一致。
  • 屏幕尺寸:480x272。
  • 窗口位置:横向/纵向起点均为 0,铺满整个屏幕。
  • 时序参数(HSPW、HSBP、HSFP、VSPW、VSBP、VSFP):需要对照屏体数据手册填写,不同批次屏幕可能略有差异。

第 4 步:启用 FMC 并配置 SDRAM

启用 FMC 外设,配置 SDRAM Bank。F746G-DISCO 板载 SDRAM 通常是 16bit 位宽、容量约 8MB,具体 Bank、时序参数以开发板原理图和 SDRAM 数据手册为准。CubeMX 里需要配置列数、行数、突发长度、CAS 延迟、刷新周期等参数。这些参数配置错会导致 SDRAM 读写异常,症状是屏幕闪烁、花屏或程序硬错误。

第 5 步:配置串口调试输出

为了方便后续打印 FPS 数据,启用一个串口,比如 USART3,波特率 115200,使用板载 ST-LINK 的虚拟串口输出日志。

第 6 步:生成工程

在 Project Manager 中配置工程名称、工具链类型,然后生成代码。

3.3 先验证底层显示通路

移植 LVGL 之前,最稳妥的做法是先做一个“裸奔显示测试”。思路很简单:在 SDRAM 中选一段地址作为 LTDC 显存,把整段显存填充为纯色,如果屏幕能够稳定显示对应颜色,就证明“CPU → SDRAM → LTDC → LCD”这条链路是通的。

在 CubeMX 生成的main.c中,加入类似下面的测试代码:

// 文件路径:Core/Src/main.c // 假设 LTDC Layer1 的显存地址配置为 SDRAM 起始地址 #define LTDC_FB_ADDR 0xC0000000 #define LCD_WIDTH 480 #define LCD_HEIGHT 272 // 使用 16bit 颜色时需要 2 字节每像素 // 整屏填充为红色(RGB565 的红色是 0xF800) static void lcd_test_fill_red(void) { uint16_t *fb = (uint16_t *)LTDC_FB_ADDR; for (uint32_t i = 0; i < LCD_WIDTH * LCD_HEIGHT; i++) { fb[i] = 0xF800; } }

如果屏幕显示为整屏红色,说明 LTDC 和 SDRAM 通路正常。这一步比直接移植 LVGL 更利于调试,因为问题范围被限制在了底层显示链路上。

4. LVGL 9.0 移植到 STM32F746G-DISCO

底层显示链路验证通过后,就可以开始正式移植 LVGL 9.0。

4.1 获取 LVGL 9.0 源码

从 LVGL 官方 GitHub Releases 页面下载 9.0 系列稳定版源码,例如lvgl-9.0.0.zip。解压后将lvgl目录复制到你的工程目录下。源码目录结构大体如下:

lvgl/ ├── lv_conf.h.template # 配置文件模板 ├── lvgl.h # 主头文件 ├── src/ │ ├── core/ # 核心对象、事件、刷新 │ ├── draw/ # 渲染器 │ ├── display/ # 显示驱动抽象 │ ├── layouts/ # 布局 │ ├── libs/ # 第三方库绑定 │ ├── misc/ # 工具模块 │ ├── themes/ # 主题 │ ├── widgets/ # 控件 │ ├── drv/ # 驱动代码 │ └── others/ # 扩展

如果你用的是 STM32CubeIDE 且工程是 CMake 构建,可以直接在CMakeLists.txt中把lvgl目录添加到子目录,并链接lvgl目标。如果使用 Keil MDK,则需要把lvgl/src下所有.c文件(demosexamples可根据需要酌情添加)加入工程,并把lvgl目录和lvgl/src目录加入头文件搜索路径。

4.2 创建并配置 lv_conf.h

LVGL 的配置文件是lv_conf.h。把lvgl/lv_conf.h.template复制到工程目录,并改名为lv_conf.h。在编译选项中宏LV_CONF_H通常指向这个文件。默认情况下,模板文件里#if 0需要改成#if 1,LVGL 才会启用自定义配置。

下面是一份适合在 STM32F746G-DISCO 上起步的配置要点:

// 文件路径:lv_conf.h #define LV_COLOR_DEPTH 16 #define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_LEVEL_INFO // LVGL 内存池大小,单位是字节 // 具体值根据控件数量和界面复杂度调整 #define LV_MEM_SIZE (64U * 1024U) // 屏幕相关 #define LV_DEF_REFR_PERIOD 30 #define LV_DPI_DEF 96 // 如果没有触摸,保持输入设备相关模块最小配置 #define LV_USE_INDEV 1

说明几点:

  • LV_COLOR_DEPTH在 9.0 中仍然存在,但不能只靠它描述缓冲区的完整颜色模型。推荐同时使用显式的颜色格式 API(例如LV_COLOR_FORMAT_RGB565)来创建缓冲区,这样移植代码的意图最清晰。
  • LV_MEM_SIZE控制 LVGL 内部动态内存池。它和 draw buffer 不同,draw buffer 由你自己分配。起步时可以给 64KB,界面复杂后逐步调大。
  • LV_DEF_REFR_PERIOD默认 30ms,意味着 LVGL 每 30ms 最多触发一轮刷新。追求更高帧率时可以适当缩短这个值,但会消耗更多 CPU。

4.3 添加显示驱动移植文件

LVGL 源码自带的移植模板在lvgl/examples/porting目录下。把lv_port_disp_template.clv_port_disp_template.h复制到工程自己的 BSP 目录,并重命名为lv_port_disp.clv_port_disp.h

lv_port_disp.c中完成三件事:

  1. 创建一个lv_display_t对象。
  2. 设置 flush 回调。
  3. 分配 draw buffer 并设置给 display。

参考实现如下:

// 文件路径:BSP/lv_port_disp.c #include "lv_port_disp.h" #include "main.h" #include "string.h" #define LCD_WIDTH 480 #define LCD_HEIGHT 272 // LTDC Layer1 的显存地址,需要和 CubeMX 中配置一致 #define LTDC_FB_ADDR 0xC0000000 static lv_display_t *disp; // 用于 LVGL 渲染的彩色缓冲,建议放到 SDRAM,避免占用内部 SRAM LV_ATTRIBUTE_MEM_ALIGN static lv_color_t buf[LCD_WIDTH * LCD_HEIGHT / 10]; // flush 回调:LVGL 将渲染好的局部像素块通过这个函数交给显示器 static void lcd_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map) { uint16_t *fb = (uint16_t *)LTDC_FB_ADDR; uint16_t *src = (uint16_t *)px_map; uint32_t width = area->x2 - area->x1 + 1; uint32_t height = area->y2 - area->y1 + 1; uint32_t fb_stride = LCD_WIDTH; for (uint32_t y = 0; y < height; y++) { uint32_t dst_index = (area->y1 + y) * fb_stride + area->x1; uint32_t src_index = y * width; memcpy(&fb[dst_index], &src[src_index], width * 2); } lv_display_flush_ready(display); } void lv_port_disp_init(void) { // 确保底层 LTDC 和 SDRAM 已经在 main.c 中初始化完成 disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT); if (disp == NULL) { // 创建失败需要排查内存池是否过小 return; } lv_display_set_flush_cb(disp, lcd_flush_cb); // 设置 draw buffer,这里使用单缓冲 + 局部渲染模式 uint32_t buf_size = sizeof(buf); lv_display_set_buffers(disp, buf, NULL, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL); }

这段代码中有几个关键点需要解释。

第一,为什么显存地址是0xC0000000?这是 F746 的 FMC Bank2 地址,具体地址区间取决于 CubeMX 中 SDRAM Bank 的配置。如果配置的是 Bank1,地址会是0x60000000。所以这一项必须和你的实际工程一致,否则屏幕要么不显示,要么显示乱码。

第二,为什么直接用memcpy而不是 DMA2D?在最小实现里,先用memcpy能最快跑通流程。性能优化阶段可以再换成 DMA2D 或专用拷贝硬件。先把功能跑通,再优化性能,是 GUI 移植的基本原则。

第三,LV_DISPLAY_RENDER_MODE_PARTIAL表示什么?局部渲染模式让 LVGL 只在需要更新的区域进行绘制,然后把该区域通过 flush 回调送出。它比全屏缓冲更节省内存,但小区域反复刷新时可能会增加调用开销。9.0 中还有LV_DISPLAY_RENDER_MODE_DIRECTLV_DISPLAY_RENDER_MODE_FULL两种模式,后面的优化章节再展开。

4.4 在主循环中调用 LVGL 核心处理

LVGL 需要周期性调用lv_timer_handler()来处理动画、事件、布局和渲染任务。在裸机工程中,最简单的做法是在main()while(1)循环中不断调用:

// 文件路径:Core/Src/main.c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_LTDC_Init(); MX_FMC_Init(); MX_USART3_UART_Init(); lv_init(); lv_port_disp_init(); // 创建一个简单的标签验证图形库是否工作 lv_obj_t *label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "LVGL 9.0 OK"); lv_obj_center(label); while (1) { lv_timer_handler(); HAL_Delay(5); } }

需要注意:lv_screen_active()是 9.0 中的屏幕 API,8.x 里是lv_scr_act()。如果你习惯性地写成lv_scr_act(),编译直接报错,这也是 9.0 升级时最常见的报错之一。

另外,while(1)中不建议让lv_timer_handler()HAL_Delay加上固定 5ms 延迟,因为更合理的做法是让 LVGL 根据配置的刷新周期自行调度。上面这种写法仅用于快速验证。更推荐的做法是:

while (1) { lv_timer_handler(); }

如果担心 CPU 占用过高,可以通过lv_timer_handler的返回值计算距离下一次唤醒的时间,再调用低功耗延迟。但这属于进阶优化,工程阶段先用最简循环即可。

4.5 输入设备移植

LVGL 不是只能点按操作,它支持键盘、编码器、指针等多种输入设备。STM32F746G-DISCO 板载 LCD 本身不带触摸,但你可以用一个旋转编码器作为输入设备,验证 LVGL 的控件切换和焦点机制。

如果你手头确实有配套触摸屏,驱动思路是:

  1. 硬件初始化触摸控制器,通过 I2C 或 SPI 读取触摸坐标。
  2. 在 LVGL 中调用lv_indev_create()创建输入设备。
  3. 实现read_cb回调,把触摸坐标写入lv_indev_data_t结构体。

注意,9.0 中lv_indev_drv_t的注册方式和 8.x 也有差异。8.x 是lv_indev_drv_register(&indev_drv),9.0 改为lv_indev_create()后调用lv_indev_set_read_cb(),然后由该对象自动注册输入设备。这块建议直接参考 LVGL 官方示例中自带的端口模板。

在还没有触摸硬件的阶段,“控件能否正常显示与交互逻辑正确”可以通过编码器或按键来验证,不一定非要触摸屏。

5. 性能基准测试方案

移植完成后,界面能显示出来只是第一步。下面进入本文的核心:如何对 STM32F746G-DISCO 上的 LVGL 9.0 做性能基准测试。

5.1 测试指标体系

性能基准测试要覆盖四类数据:

指标说明对 UI 体验的影响
FPS每秒画面刷新次数影响流畅感,低于 24FPS 会感到明显卡顿
单帧绘制耗时LVGL 完成一次完整渲染并 flush 的耗时决定每帧之间 CPU 是否空闲
CPU 占用率渲染、事件、布局处理占 CPU 总时间的比例决定业务任务能否及时执行
内存占用LVGL 堆、draw buffer、控件对象的内存决定能承载多复杂的界面

其中 FPS 是最直观的指标,也是本文重点展示的测量项。内存占用可以通过 LVGL 自带的内存监控接口查询,CPU 占用率在裸机工程中可以用“绘图任务运行时长 / 总运行时长”估算。

5.2 用 flush 回调统计 FPS

前面提到,lv_display_flush_ready()每次被调用,都对应一块渲染区域的输出完成。因此在 flush 回调中增加计数,就可以近似得到“渲染帧完成次数”。每秒统计一次计数的变化量,就是当前场景的 FPS。

lv_port_disp.c中增加全局计数和日志输出:

// 文件路径:BSP/lv_port_disp.c volatile uint32_t g_flush_count = 0; static void lcd_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map) { g_flush_count++; // ... 原有的显存拷贝逻辑 ... lv_display_flush_ready(display); }

然后在主循环中每秒打印一次 FPS:

// 文件路径:Core/Src/main.c #include <stdio.h> uint32_t last_flush_count = 0; uint32_t last_tick = HAL_GetTick(); while (1) { lv_timer_handler(); if (HAL_GetTick() - last_tick >= 1000) { uint32_t current_count = g_flush_count; uint32_t fps = current_count - last_flush_count; char info[64]; snprintf(info, sizeof(info), "FPS: %lu\r\n", fps); HAL_UART_Transmit(&huart3, (uint8_t *)info, strlen(info), 100); last_flush_count = current_count; last_tick = HAL_GetTick(); } }

这个方案的原理是:LVGL 在每轮刷新周期中,如果屏幕上有区域需要重绘,就会调用 flush 回调把绘制结果送到显存。因此 flush 回调的调用频率和渲染帧率直接相关。如果屏幕完全静止,LVGL 不会触发刷新,FPS 会降到 0,这是正常的,因为静态画面不需要反复重绘。

5.3 设计不同负载的测试用例

为了让 FPS 数据有对比价值,需要设计几组不同负载的测试场景。下面给出三个典型的测试用例。

用例 1:空屏幕刷新

空屏幕下创建少量控件,不触发任何动态重绘,只观察系统空闲时的 flush 频率。它反映 LVGL 在无渲染负载时的调度开销。

用例 2:动态文本刷新

每秒更新一个 label 的文本,触发文字区域重绘。这是最常见的 UI 场景,能反映文字渲染和局部刷新的开销。

// 文件路径:Core/Src/main.c lv_obj_t *fps_label = lv_label_create(lv_screen_active()); lv_obj_set_style_text_font(fps_label, &lv_font_montserrat_24, 0); lv_obj_center(fps_label); uint32_t counter = 0; while (1) { lv_timer_handler(); if (HAL_GetTick() - last_update_ms >= 100) { last_update_ms = HAL_GetTick(); counter++; lv_label_set_text_fmt(fps_label, "Counter: %lu", counter); } }

用例 3:大量控件拖动

创建多个矩形或弧线控件,让它们位置或角度发生变化。这种测试能反映复杂绘图的性能瓶颈。更简单的方式是让一个较大的圆角矩形做位移动画,持续触发大范围重绘。

5.4 测试记录模板

把各组数据记录到一张表格里,方便比较不同配置的影响。下面是建议的记录模板:

测试用例颜色格式缓冲模式FPS单帧耗时(ms)CPU占用备注
空屏幕RGB565PARTIAL
动态文本RGB565PARTIAL
大量控件RGB565PARTIAL
空屏幕RGB888PARTIAL
动态文本RGB888PARTIAL

填写几组数据后,你就能看出颜色格式、缓冲模式对性能的影响有多大。这也为下一步优化提供了数据依据。

5.5 如何判断结果是否健康

不同 MCU 和不同屏幕分辨率下,FPS 的绝对值没有统一标准,但可以参考以下经验:

  • 如果 FPS 明显高于屏幕刷新率,说明渲染速度已经不是瓶颈,界面卡顿很可能是业务逻辑阻塞了主循环。
  • 如果 FPS 接近或低于 30,说明渲染路径需要优化。优先检查 draw buffer 是否过小、颜色格式是否带宽过大、flush 回调是否用了低效拷贝方式。
  • 如果 CPU 占用接近 100% 且 FPS 仍然不高,说明 HAL 底层或 LVGL 绘制设置需要调整,增加 DMA 是主要方向。

注意,这些只是参考经验,不同工程差别很大,重点是通过记录的测试数据持续对比,而不是追求某一个固定数字。

6. 性能优化方向与配置调优

拿到基准数据后,如果测试结果不理想,可以从下面几个方向逐个优化。每次只改一个变量,然后重新跑同样场景,这样能准确判断哪项改动真正有效。

6.1 选择合适的颜色格式

LVGL 9.0 能配置多种颜色格式。对于 STM32F746G-DISCO 的 4.3 寸 480x272 屏,最常见的两种选择是 RGB565 和 RGB888。

  • RGB565:每像素占 2 字节,LTDC 配置为 RGB565 后,显存带宽减半,刷新更快,但颜色数量少一些,可能有轻微色阶。
  • RGB888:每像素占 3 字节,色彩更细腻,但显存带宽和 SDRAM 占用都上升,相同场景下 FPS 通常会下降。

如果你的界面以图表、工业监控、文字为主,RGB565 视觉差别不大,性能收益却很明显。只有在颜色渐变要求高、界面有大量图片素材的场景,才建议使用 RGB888。

确保在 CubeMX 的 LTDC 配置中把 Layer 像素格式改成 RGB565,同时在 LVGL 侧用LV_COLOR_FORMAT_RGB565创建缓冲区,两边一致,否则会出现颜色错乱。

6.2 调整 draw buffer 大小与数量

draw buffer 是 LVGL 渲染时临时存放像素数据的空间。它的大小直接影响渲染效率。缓冲区太小,LVGL 就需要把一帧画面拆成很多小块分别绘制,每次绘制都要调用 flush 回调,导致整体渲染耗时上升。

lv_port_disp.c中,目前用的是:

static lv_color_t buf[LCD_WIDTH * LCD_HEIGHT / 10];

也就是只给了整屏 1/10 的缓冲。如果界面复杂,可以逐步加大到 1/4 屏甚至 1/2 屏。缓冲越大,单次能绘制的区域越大,渲染效率越高,但内存消耗也越大。这块需要根据你的 SDRAM 余量做平衡。

如果你希望进一步提高刷新流畅度,可以考虑双缓冲。双缓冲模式下,LVGL 可以在一块缓冲绘制的同时,让另一块缓冲提供给 LTDC 扫描,从机制上避免画面撕裂。但双缓冲占用的内存会翻倍,并且需要修改lv_display_set_buffers的参数。

参考双缓冲配置:

// 文件路径:BSP/lv_port_disp.c static lv_color_t buf1[LCD_WIDTH * LCD_HEIGHT / 4]; static lv_color_t buf2[LCD_WIDTH * LCD_HEIGHT / 4]; lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_DIRECT);

注意:LV_DISPLAY_RENDER_MODE_DIRECT表示显示缓冲区直接对应屏幕的一个完整区域,通常要求缓冲区大小等于屏幕分辨率。如果缓冲区只有 1/4 屏,需要使用PARTIAL模式。实际开发中,可以先从 PARTIAL 单缓冲起步,确认稳定后再尝试 DIRECT 双缓冲。

6.3 使用 DMA 或 DMA2D 提升拷贝效率

当前 flush 回调里用的是memcpy,它是按普通 CPU 指令逐字拷贝。对于 480x272 的屏幕,如果每次刷新区域很大,CPU 拷贝会比较耗时。在 STM32F746 上,可以使用 DMA2D 外设来做批量像素拷贝或颜色格式转换,把 CPU 从数据搬运中解放出来。

使用 DMA2D 需要额外的驱动代码和等待完成中断,比较复杂。一个折中方案是:先保留memcpy版本,然后在性能测试中观察 CPU 占用。如果 FPS 已经足够,就不需要引入 DMA2D;如果 FPS 不达标,再针对 flush 回调做 DMA2D 优化。

6.4 缩短刷新周期与减少无效刷新

LVGL 默认刷新周期是 30ms,也就是每 30ms 才触发一次刷新判断。如果把LV_DEF_REFR_PERIOD改小到 20ms 或 15ms,可以让 UI 响应更灵敏,但也会增加 CPU 负载。

另外,尽量避免对屏幕大范围对象做连续无效更新。比如说,lv_obj_set_pos每次移动控件都会触发重绘,如果你在while(1)中把位置设置成同一个值,LVGL 并不会自动去重。合理使用lv_obj_set_style_opa()或动画回调机制,能减少无效刷新。

6.5 关闭日志和优化编译选项

调试阶段开启LV_USE_LOG很必要,但正式性能测试时要关闭,因为串口输出本身会占用 CPU 时间。编译优化级别对性能也有显著影响。在 Keil 中设置-O2-O3较发布版本,即使同样的代码,FPS 也可能会提升 20% 以上。

7. 常见问题与排查思路

移植 LVGL 9.0 到 STM32F746G-DISCO 的过程中,最常遇到的问题集中在这几个方向。下面用表格整理常见的现象、原因和排查方法。

问题现象可能原因排查方式解决方案
编译报错:lv_disp_drv_t未定义代码基于 8.x 编写,没有适配 9.0 API搜索代码中使用lv_disp的位置改用lv_display_create()lv_display_set_flush_cb()
编译报错:lv_scr_act未定义9.0 将 scr 系列 API 改名为 screen查看报错文件,替换 APIlv_scr_act()lv_screen_active()lv_scr_load()lv_screen_load()
屏幕白屏或黑屏LTDC 显存地址不对 / SDRAM 未初始化 / 显存未填充先用纯色填充测试显存确认显存地址和 CubeMX 配置一致
屏幕花屏或颜色错乱LVGL 颜色格式和 LTDC 像素格式不一致检查两边颜色配置LVGL 用 RGB565,则 LTDC 也配置为 RGB565
屏幕能显示但点击无反应触摸控制器未初始化或输入设备未创建确认 I2C/SPI 通信,打印触摸原始坐标实现lv_indev_t并设置 read_cb
界面卡顿draw buffer 太小 / 颜色格式带宽过大 / 未开优化先记录 FPS 数值,再做单一变量优化调大缓冲、改用 RGB565、开启 DMA 和编译优化
运行后程序进入 HardFault数组或缓冲区越界 / LVGL 内存池不足查看调试器停止位置和调用栈调大LV_MEM_SIZE,检查 draw buffer 分配位置和大小

这里的排查顺序建议是:先确认底层显示通路,再确认 LTDC 颜色格式,再确认 LVGL 配置,最后才调性能。很多人一上来就在 LVGL 里反复找问题,实际上问题往往出在更底层。

8. 最佳实践与工程建议

8.1 锁定 LVGL 版本

LVGL 9.x 仍在快速迭代,不同小版本之间可能还有行为变化。项目里务必把源码固定到一个具体版本,比如lvgl-9.0.0,并且在 README 中记录版本号。千万不要在开发过程中频繁升级 LVGL 源码,否则会不断遇到新 API 改动,导致项目工期失控。

8.2 分离底层驱动与 GUI 层

在工程结构上,建议把 BSP(LTDC、SDRAM、串口、触摸)和 GUI(LVGL 移植、界面代码)分成不同目录。这样以后换平台或换屏幕时,只需要改 BSP 层和lv_port_disp.c,不需要改动界面层代码。

示例目录结构:

project/ ├── Core/
http://www.cnnetsun.cn/news/4320256.html

相关文章:

  • 用 Scrapy 爬取百家姓与姓氏源流数据:多源采集、数据清洗与结构化存储实战
  • 直播录像处理实战:FFmpeg转码切片与批量归档全流程
  • 266美元+四个AI模型,一天打造AI小镇应用:开源项目实战
  • 阿里编程题4星刷题体验:从算法建模到树状数组的实战解析
  • 【设计模式精讲】5.工厂方法(Factory Method)
  • S7-1200 MODBUS轮询库V15:多从站通信高效封装方案
  • YOLOv8农田作物倒伏识别系统:从环境搭建到部署实战解析
  • 2026 Agentic AI 智能体:让 AI 从“聊天“走向“自己干活“(MonkeyCode 实战)
  • OpenRouter 排障指南:API 网关原理、常见报错与 Claude Code 接入
  • 基于LSTM+CNN的光伏发电功率预测系统实战解析
  • Claude Code控制机械臂:从仿真到真机的安全实践
  • 专业的AI基座机构
  • 从字幕到Anki卡片:构建英语学习自动化流水线
  • 90%新手都踩的Python环境坑!版本冲突彻底解决指南 |数智码力
  • 【架构篇】科来网络流量分析审计系统
  • 数组名不是指针!一文搞懂C语言数组退化与sizeof陷阱
  • MATLAB语音滤波设计全解析:从加噪到频谱分析及滤波器实现
  • Runway AI峰会解读:AI视频生成从模型工具走向可控工作流
  • Duranta——一个开放的、研究级的RAN+UE参考协议栈
  • 1天速通计算机二级C语言:高频考点与上机题模板实战
  • VS Code 中只关闭 AI 生成提交消息而不禁用 Copilot 的完整指南
  • 毕业之家怎么用?论文初稿粘贴降重、对照查重报告针对性降重、定稿保格式,一篇说清
  • 基于STM32F103C8T6的步进电机控制与仿真完整教程
  • Grok Bot开发实战:从API接入到代购订单自动化
  • 815信号与系统考研真题解析:卷积、傅里叶与拉普拉斯变换全攻略
  • Unity游戏项目收尾实践:从玩法闭环到构建发布
  • 终端效率革命:用fd、fzf、bat和rg打造极速文件搜索与代码定位流水线
  • 跑团Replay制作全流程:从录音转写到AI立绘与批量合成
  • AI Agent安全边界:沙箱、权限与审批机制的工程实践
  • GLM 5.3 Flash接入效果差?智能体层才是决定上限的关键