之前在 STM32 项目里做 GUI 界面,大部分资料都停留在 LVGL 8.x 版本。等我把工程切到 LVGL 9.0 后,发现原来的lv_disp_drv_t、lv_disp_buf_t初始化方式已经变了,网上很多代码直接编译不过。加上 STM32F746G-DISCO 板卡带 RGB 接口 LCD 和 SDRAM,很多读者都想知道这块板子跑 LVGL 9.0 到底能到多少帧、CPU 占用大概什么水平,这正好是一个很适合做性能基准测试的平台。
本文会围绕 STM32F746G-DISCO 上的 LVGL 9.0 移植流程展开,从工程准备、LVGL 9.0 显示驱动接入、心跳与刷新回调处理,到基准测试方法与常见坑位,都会完整梳理一遍。无论你是准备在 STM32 上入门 LVGL,还是想评估新版本性能后再决定是否升级,这篇实战笔记都能给你一个可复用的参考路径。
需要说明的是,LVGL 版本迭代比较快,不同开发板的 LCD 时序和引脚配置也有差异,所以下面代码会以“思路 + 关键示例”为主,具体引脚和屏幕参数以你自己生成的工程为准。
1. 移植背景与核心概念
1.1 STM32F746G-DISCO 为什么适合跑 LVGL
STM32F746G-DISCO 是 ST 官方 Discovery 系列开发板,主控芯片是 STM32F746NGH6,基于 ARM Cortex-M7 内核,主频最高可以到 216MHz。Cortex-M7 相比老一代 Cortex-M4,在流水线、分支预测和内存访问上有明显改进,而且 STM32F746 内部带了硬件浮点单元 FPU,这对图形运算里大量使用的坐标计算、颜色混合、透明度处理都有帮助。
这块板子还有一个很关键的特性:板载了 RGB 接口的 TFT-LCD,分辨率是 480x272,同时板上有外部 SDRAM 可以用来做显存和 LVGL 缓冲区。RGB 接口 LCD 和 SPI 接口屏不太一样,RGB 屏需要一个持续的像素时钟来刷新画面,通常由 LTDC 外设驱动,而 LTDC 可以从 SDRAM 中直接读取显存数据并输出到屏幕。因此,LVGL 负责“画好每一帧画面”,LTDC 负责“把画面送到屏幕”,SDRAM 则承担 framebuffer 和 LVGL 渲染缓冲区的存放。
这套组合非常适合做 LVGL 性能测试,因为显存路线是全内存映射的方式,渲染完成后再由硬件刷新,不存在 SPI 屏那种逐字节发送的瓶颈,测出来的 FPS 更接近 MCU 的“真实渲染能力”。
1.2 LVGL 9.0 相比 8.x 的主要变化
LVGL 9.0 是一个大版本升级,API 变化比较明显。如果以前用的是 8.x,升级到 9.0 时不能只替换源码,很多初始化逻辑都需要调整。
比较明显的变化包括:
- 显示设备接口从
lv_disp_drv_t变成了lv_display_t,创建显示器的入口变成了lv_display_create()。 - 缓冲区注册从
lv_disp_drv_register()改成了lv_display_set_buffers()。 - flush 回调使用
lv_display_set_flush_cb()来注册,并且在刷新完成以后调用lv_display_flush_ready()。 - 颜色格式支持更灵活,比如
LV_COLOR_FORMAT_RGB565、LV_COLOR_FORMAT_ARGB8888,并且不再像 8.x 那样只依赖LV_COLOR_DEPTH一种全局颜色深度。 - 输入设备接口也做了调整,从
lv_indev_drv_t变成了lv_indev_create()的方式。 - 内置了大量重构后的绘制单元(Draw Unit)架构,为后续多线程渲染和硬件加速做了铺垫。
这些变化意味着,网上大量基于 8.x 的移植代码在 9.0 上会直接报错。本文后面的示例全部基于 LVGL 9.0 的 API 来写。
1.3 性能基准测试到底在测什么
LVGL 性能基准测试并不是简单看一个“帧率数字”就够了。真正有参考价值的测试,至少要包含三个维度:
第一是 FPS,也就是每秒能刷新多少帧画面。FPS 越高,界面滑动和动画越流畅。但 FPS 会受场景复杂度影响很大,全屏图片缩放和纯色矩形填充的 FPS 完全不同。
第二是 CPU 占用率,也就是lv_timer_handler()执行一轮所花的时间占整个调度周期的比例。如果 FPS 很高但 CPU 已经 90% 以上,说明系统已经没有余量处理业务逻辑。
第三是单帧渲染时间。通过记录lv_timer_handler()前后时间戳,可以算出每一帧的平均渲染耗时和最大耗时。这个指标对判断动画掉帧最直接。
后续第 5 节会专门说明如何基于 LVGL 9.0 的 perf monitor 和自定义计时方式来做这些测量。
2. 环境准备与工程结构
2.1 硬件与软件清单
在开始移植之前,先把环境准备好。
硬件方面:
- STM32F746G-DISCO 开发板。
- 一根 USB 线,用于供电和下载调试。
- 可选:逻辑分析仪或者串口,用于输出测试数据。
软件方面:
- STM32CubeIDE 或者 Keil MDK,两者都可以。
- STM32CubeMX,用于生成基础工程,尤其是 SDRAM、LTDC、GPIO 的初始化代码。
- LVGL 9.0 源码,建议直接从 GitHub 官方仓库获取 release 分支。
- STM32F7 HAL 库,CubeMX 生成工程时会自动带入。
版本方面建议统一:如果你的开发环境是 STM32CubeIDE,就使用它自带的 HAL 版本;LVGL 9.0 建议使用最新的 9.x release,因为 9.0 初期版本还有一些小问题,后续的 9.1、9.2 会有修复。本文写作时以 9.x 主线 API 为准,具体版本号请以你拉取代码时为准。
2.2 获取 LVGL 9.0 源码
LVGL 源码建议直接克隆官方仓库:
git clone --branch release/v9.2 https://github.com/lvgl/lvgl.git如果 GitHub 访问不方便,可以从官方镜像或者本地包管理工具获取。这里版本号只是示例,你可以选择你需要的 release。拿到源码以后,LVGL 本体只需要几个关键目录:
src目录,这是 LVGL 的核心源码。lvgl.h,总头文件。lv_conf_template.h,配置模板,复制后改名为lv_conf.h。
注意不要把整个仓库全部加入编译,只要把src目录加入 include path 和编译路径即可。
2.3 最小工程结构
这里建议的工程结构如下:
STM32F746G_LVGL9/ ├── Core/ │ ├── Inc/ │ ├── Src/ │ └── Startup/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F7xx_HAL_Driver/ ├── LVGL/ │ ├── lv_conf.h │ ├── lvgl.h │ ├── src/ │ └── ... ├── App/ │ ├── app_lvgl.c │ ├── app_lvgl.h │ ├── lcd_driver.c │ ├── lcd_driver.h │ └── ... └── STM32F746G_DISCO.iocApp目录用来放我们自己写的 LVGL 适配层,比如lcd_driver.c负责底层 LCD 操作,app_lvgl.c负责 LVGL 初始化和主循环调度。这样把“硬件驱动”和“LVGL 应用”分离开,后续做性能测试和代码维护都更方便。
3. LVGL 9.0 移植核心配置
3.1 lv_conf.h 的基本配置
LVGL 的裁剪和功能开关都集中在lv_conf.h文件里。把lv_conf_template.h复制一份到工程中,改名为lv_conf.h,然后确保在编译选项或者头文件路径中能找到它。
lv_conf.h中几个关键项需要重点关注:
// LVGL 总开关 #define LV_USE_OS LV_OS_NONE // 颜色深度 #define LV_COLOR_DEPTH 16 // 开启性能监视器 #define LV_USE_PERF_MONITOR 1 // 开启内置 Benchmark 示例 #define LV_USE_DEMO_BENCHMARK 1 // 内存配置 #define LV_MEM_SIZE (64U * 1024U)LV_COLOR_DEPTH设置为 16 表示使用 RGB565 颜色格式,这种格式占内存小,渲染也快。如果你的界面需要更丰富的颜色表现,可以改成 32,但是帧率和内存占用会明显增加。
LV_USE_PERF_MONITOR是 LVGL 自带的性能显示工具,开启后界面上会显示 FPS 和 CPU 占用率。这个适合开发调试阶段,量产时可以关闭。
LV_USE_DEMO_BENCHMARK是 LVGL 自带的 Benchmark 演示程序,用来测试多项图形绘制性能,后面性能测试章节会用到。
3.2 心跳 Tick 实现
LVGL 需要一个毫秒级的时间基准,用来处理动画、时间相关事件。在裸机环境下,我们可以用任意一个定时器来实现,比如 SysTick。
假设你已经有一个HAL_GetTick()或者自定义的毫秒计数函数:
static uint32_t my_tick_get(void) { return HAL_GetTick(); } void app_lvgl_init(void) { lv_init(); lv_tick_set_cb(my_tick_get); }这里lv_tick_set_cb()是 LVGL 9.0 设置 tick 来源函数。注意这里传的是一个“获取当前毫秒数”的回调函数,而不是直接传一个 tick 值。
3.3 创建 Display 与缓冲区设置
LVGL 9.0 不再使用lv_disp_drv_t这个旧结构体,而是通过lv_display_create()来创建一个显示器对象。
static lv_color_t buf1[LVGL_BUF_SIZE]; static lv_color_t buf2[LVGL_BUF_SIZE]; void app_lvgl_init(void) { // 先初始化 LVGL lv_init(); lv_tick_set_cb(my_tick_get); // 创建显示器 lv_display_t *disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT); lv_display_set_buffers(disp, buf1, buf2, LVGL_BUF_SIZE * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_PARTIAL); lv_display_set_flush_cb(disp, lcd_flush_cb); // 之后可以创建 UI 对象 }lv_display_set_buffers()的第三个参数是缓冲区大小,单位是字节。LVGL 9.0 的 API 定义中,缓冲区大小参数是uint32_t buf_size,指的是缓冲区的字节大小,而不是颜色数量。为了安全,建议写成sizeof(buf1)的形式。
LV_DISPLAY_RENDER_MODE_PARTIAL是部分渲染模式,LVGL 只渲染屏幕上需要更新的区域。对于中小内存的单片机来说,这种模式更实用。如果改成LV_DISPLAY_RENDER_MODE_FULL,LVGL 会一次渲染整帧,需要较大的缓冲区。
3.4 Flush 回调实现
Flush 回调是 LVGL 与 LCD 驱动之间的桥梁。LVGL 渲染完成后会调用这个回调,把图像数据交给 LCD 驱动。
void lcd_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); // 把 px_map 中的数据拷贝到 LTDC 当前层显存对应区域 lcd_copy_area(area->x1, area->y1, w, h, (uint16_t *)px_map); // 通知 LVGL 刷新完成 lv_display_flush_ready(disp); }注意:lv_display_flush_ready()必须在真正完成数据拷贝或交给 DMA 后调用。如果你的底层拷贝是异步的,比如使用 DMA2D,那么应该在 DMA 传输完成中断里调用lv_display_flush_ready()。如果拷贝是同步的,则可以在拷贝函数返回后直接调用。
3.5 输入设备接入(可选)
STM32F746G-DISCO 板载了触摸屏,但触摸 IC 的驱动需要根据具体型号编写。LVGL 9.0 的输入设备接入方式如下:
lv_indev_t *indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, touch_read_cb);touch_read_cb需要填充lv_indev_data_t结构体:
static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) { static lv_point_t last_point = {0, 0}; if (touch_get_point(&last_point)) { >// 文件路径:App/app_lvgl.c #include "app_lvgl.h" #include "lvgl.h" #include "lcd_driver.h" #define LCD_WIDTH 480 #define LCD_HEIGHT 272 #define LVGL_BUF_SIZE (LCD_WIDTH * 40) static lv_color_t buf1[LVGL_BUF_SIZE]; static lv_color_t buf2[LVGL_BUF_SIZE]; void app_lvgl_init(void) { lv_init(); lv_tick_set_cb(HAL_GetTick); lv_display_t *disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL); lv_display_set_flush_cb(disp, lcd_flush_cb); }这里缓冲区设置为LCD_WIDTH * 40,也就是 40 行像素大小。这个值不是固定的,后续性能测试可以尝试不同大小的缓冲区。
4.3 接入 LTDC 层显存地址
Flush 回调中的关键操作,是把 LVGL 渲染好的区域拷贝到 LTDC 当前正在显示的画面层。
对于 STM32F746G-DISCO,LTDC 的 Layer0 通常配置为使用 SDRAM 的某一段地址作为帧缓冲。每次 flush 时,需要把 LVGL 输出的像素数据覆盖到帧缓冲的对应区域。
// 文件路径:App/lcd_driver.c #define LCD_FB_ADDR 0xC0000000 // 以实际工程配置为准 #define LCD_PIXEL_WIDTH 480 #define LCD_PIXEL_HEIGHT 272 void lcd_copy_area(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h, uint16_t *pixels) { uint16_t *dst = (uint16_t *)(LCD_FB_ADDR + (y1 * LCD_PIXEL_WIDTH + x1) * 2); uint16_t *src = pixels; for (uint16_t row = 0; row < h; row++) { memcpy(dst, src, w * 2); dst += LCD_PIXEL_WIDTH; src += w; } }这种逐行拷贝方式实现简单,但性能一般。如果开启了 DMA2D,可以改用 DMA2D 的内存到内存拷贝模式,把拷贝过程交给硬件执行,具体会在第 7 节说明。
4.4 主循环调用 lv_timer_handler
LVGL 的运行依赖于周期性调用lv_timer_handler()。在裸机环境中,主循环可以这样写:
// 文件路径:main.c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_FMC_Init(); MX_LTDC_Init(); lcd_init(); app_lvgl_init(); // 创建一个测试界面 create_demo_ui(); while (1) { lv_timer_handler(); HAL_Delay(5); } }这里lv_timer_handler()会处理 LVGL 内部的所有任务,包括界面更新、动画、输入设备检测等。加上HAL_Delay(5)是为了降低 CPU 占用,但也会影响最高帧率。性能测试时,可以考虑改成 CPU 空转或者使用更短的延时。
4.5 HelloWorld 验证
LVGL 初始化完成后,先创建一个最简单的界面,验证移植是否成功。
void create_demo_ui(void) { lv_obj_t *label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "LVGL 9.0 on STM32F746"); lv_obj_center(label); }如果屏幕正常显示文字,说明 LVGL 的 tick、display、flush、缓冲区配置基本都是通的。这个时候可以继续做性能测试。
5. 性能基准测试实战
5.1 性能指标与测试方法
性能基准测试需要先明确几个指标的计算方式。
帧率 FPS 的测量方法:在lcd_flush_cb中每调用一次说明 LVGL 完成了一次局部画面刷新,但局部刷新不等于完整帧。因此更合理的做法是:统计lv_timer_handler()一次完整执行中,画面从开始渲染到 flush 完成的过程耗时,或者直接借助 LVGL 内部的 perf monitor。
CPU 占用率测量方法:在一段时间内统计lv_timer_handler()执行耗时占比。比如每 100ms 统计一次,假如lv_timer_handler()累计执行了 60ms,那么 CPU 占用率大约是 60%。LVGL 自带的 perf monitor 已经实现了这个逻辑。
单帧渲染时间测量方法:在调用lv_timer_handler()之前记录一个时间戳,调用完成后再次记录,两者相减就是该轮渲染耗时。注意lv_timer_handler()并不一定每轮都会触发渲染,如果界面上没有变化,它处理完定时器任务后就会快速返回。
5.2 启用 LVGL 自带 Perf Monitor
LVGL 9.0 内置了性能监视器,只要在lv_conf.h中开启:
#define LV_USE_PERF_MONITOR 1编译运行后,在屏幕左上角或右下角会显示类似以下信息:
FPS: 45 CPU: 32%这个数字是 LVGL 自己统计的,包含界面渲染和内部定时任务开销。调试阶段这个功能非常方便,不需要额外写代码就能看到大概性能水平。
不过要注意,perf monitor 本身也会消耗一定绘制资源,而且它在屏幕上绘制文字和背景本身也会增加一点点渲染负载。测出来的数据用于相对比较没有问题,但如果是发布版本,建议关闭。
5.3 使用官方 Benchmark Demo
LVGL 官方提供了 benchmark 演示,用来测试不同绘制场景的性能。在lv_conf.h中开启:
#define LV_USE_DEMO_BENCHMARK 1然后调用:
lv_demo_benchmark();这个 demo 会自动运行一系列测试场景,包括矩形、边框、圆弧、文字、图片缩放、阴影等。每一轮完成后会给出一个分数,并且可以统计平均渲染时间。
使用 benchmark demo 的一个好处是测试场景标准化,同一个 demo 在不同开发板、不同配置之间跑出来的数据具有可比性。缺点是它测的是 LVGL 渲染性能的“上限”,实际业务界面的复杂度千差万别。
5.4 自定义多场景对比测试
除了官方 benchmark,建议针对自己的业务场景做几组对比测试。常见的对比维度有:
第一,缓冲区大小对比。分别设置缓冲区为 10 行、20 行、40 行、全屏高度,测量相同界面的 FPS 和 CPU 占用。
第二,颜色格式对比。在 RGB565 和 ARGB8888 之间切换,测量全屏图像填充和半透明控件的渲染耗时。
第三,渲染模式对比。对比LV_DISPLAY_RENDER_MODE_PARTIAL和LV_DISPLAY_RENDER_MODE_FULL,观察帧率和画面撕裂情况。
第四,DMA2D 开关对比。没有 DMA2D 时,Flush 使用 CPU 循环拷贝;开启 DMA2D 后,Flush 将拷贝交给硬件,对比 CPU 占用率的变化。
自定义测试代码示例:
// 测试某个操作的单帧耗时 uint32_t t0 = DWT->CYCCNT; lv_obj_t *obj = lv_obj_create(lv_screen_active()); lv_obj_set_size(obj, 200, 100); lv_obj_center(obj); lv_timer_handler(); uint32_t t1 = DWT->CYCCNT; float cost_ms = (float)(t1 - t0) / 216000000.0f * 1000.0f;注意 DWT->CYCCNT 需要在启动时使能:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;这种方法统计的是 CPU 周期数,精度比HAL_GetTick()高很多,适合做短时间片段的性能对比。
5.5 测试结果记录与对比思路
建议做一张测试记录表,这样后续优化效果一目了然:
| 测试场景 | 颜色格式 | 缓冲区大小 | 渲染模式 | 平均 FPS | CPU 占用 | 最大单帧耗时 |
|---|---|---|---|---|---|---|
| 官方 Benchmark | RGB565 | 40 行 | PARTIAL | 待测 | 待测 | 待测 |
| 官方 Benchmark | ARGB8888 | 40 行 | PARTIAL | 待测 | 待测 | 待测 |
| 自定义页面 A | RGB565 | 全屏 | FULL | 待测 | 待测 | 待测 |
这里的数值因工程而异,不建议直接套用网上的成绩。不同 CubeMX 版本、不同编译器优化选项、不同 LTDC 像素时钟都会影响结果。关键是保持测试条件一致,每次只改变一个变量,这样优化才有参考意义。
6. 常见问题与排查
6.1 白屏或者花屏
白屏通常意味着 LTDC 没有正常工作,或者显存地址没有数据。排查时按下面顺序来:
先确认 SDRAM 是否正常。向显存地址写入一个纯色值,比如全红0xF800,看屏幕是否显示红色。如果还是白屏,优先检查 FMC SDRAM 的初始化时序。
再确认 LTDC 层配置。LTDC 需要正确配置 Layer 的窗口位置、像素格式、显存地址和颜色格式。STM32F746G-DISCO 的 RGB 屏颜色格式通常是 RGB888,而 LVGL 的缓冲区可能是 RGB565,两者不一致会导致显示颜色错乱或花屏。
最后确认像素时钟。如果屏幕规格是 60Hz 刷新率,像素时钟和时序参数由 CubeMX 计算。手动改错一个参数就可能导致画面偏移或者花屏。
6.2 刷新闪烁和撕裂
闪烁一般是由于 LVGL 缓冲区太小,导致渲染和显示不同步。可以通过开启双缓冲区减轻:
lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);双缓冲区下,LVGL 可以在一个缓冲区渲染的同时,把另一个缓冲区的内容交给 LCD 刷新。
撕裂则是 LTDC 正在读取显存时,LVGL 修改了同一块显存区域造成的。要彻底避免撕裂,需要做到刷新和渲染同步,通常可以等待垂直同步信号,或者在 LTDC 配置中开启 FIFO Underrun 中断。对于大部分工业界面,轻微的撕裂影响不大,优先保证帧率。
6.3 内存不足或者 HardFault
LVGL 的lv_conf.h中LV_MEM_SIZE默认可能比较小,如果创建了大量控件或者图片,会出现内存分配失败。现象是运行一段时间后界面卡死或者直接 HardFault。
排查方法:开启 LVGL 内存监控函数lv_mem_monitor(),在串口输出内存使用情况。
lv_mem_monitor_t mon; lv_mem_monitor(&mon); printf("used: %d / %d\n", mon.used_size, mon.total_size);HardFault 也可能发生在 Flush 回调中,比如缓冲区地址没有对齐,或者 SDRAM 地址越界写入。建议在 HardFault_Handler 中解析故障栈,确认 PC 指针是否落在lcd_copy_area或memcpy中。
6.4 API 版本不兼容报错
LVGL 9.0 升级后,旧代码编译报错很常见。最典型的是找不到lv_disp_drv_t、lv_disp_buf_t,或者调用了lv_disp_drv_register()。
解决办法是统一按照 9.0 的新 API 来写。不要混用 8.x 的旧接口,也不要把不同版本的源码混在一个工程里。如果从旧项目升级,优先参考官方 migration guide,检查lv_conf.h中是否有已经不存在的宏。
还有一种情况是lv_conf.h找不到,这个一般是头文件路径没配好。LVGL 源码和lv_conf.h需要同时加入编译器的 include path。
6.5 Cortex-M7 Cache 一致性问题
STM32F746 属于 Cortex-M7,带有 D-Cache。如果开启了 D-Cache,并且 DMA2D 或 LTDC 在访问 SDRAM 中的显存,CPU 写进缓存的数据可能没有及时同步到内存,导致画面显示错乱。
解决方案有两个方向:
一是对显存所在区域配置 MPU,设置为 Non-cacheable 属性。这种方式最简单,但 SDRAM 读写性能会下降。
二是手动维护 Cache 一致性。在 DMA 传输前调用SCB_CleanDCache()清除数据缓存,在 DMA 读回数据后调用SCB_InvalidateDCache()使缓存失效。
对于性能测试来说,建议先不开 D-Cache,或者关闭 SDRAM 区域的 Cache,等整体跑通后再根据性能瓶颈决定是否启用。
7. 性能优化建议与工程实践
7.1 合理设置缓冲区大小与渲染模式
缓冲区大小对性能影响很明显。缓冲区太小,LVGL 需要把一帧画面拆成很多小块分别渲染,每块都要经历一次 Flush,渲染次数增多。缓冲区太大,又会导致内存占用过高,甚至无法分配。
实际项目中可以从“屏幕总像素数”的角度估算:480x272 的 RGB565 全屏缓冲区需要480 * 272 * 2 = 261120字节,约 255KB。如果全部放在内部 RAM,已经接近芯片极限,所以一般放在外部 SDRAM,或者在内部 RAM 中只放少量行缓冲。
一个值得尝试的配置是 1/4 屏缓冲区,也就是 480x68 的 RGB565 缓冲,约 65KB,放在外部 SDRAM 中。这个容量在流畅度和内存占用之间比较均衡。
7.2 颜色格式选择
RGB565 是嵌入式 GUI 中性价比最高的颜色格式,占内存小,渲染时颜色处理简单,性能通常比 ARGB8888 高出不少。
如果界面必须使用半透明效果和抗锯齿,可以考虑 ARGB8888,但要注意开启半透明后,LVGL 的混合计算量会显著增加。对于 STM32F746G-DISCO 这种 Cortex-M7 平台,复杂界面上 ARGB8888 的帧率明显低于 RGB565。
建议:默认使用 RGB565,只在特定页面需要丰富色彩时再局部使用 ARGB8888 或者图片资源,而不是全工程统一切换。
7.3 使用 DMA2D 加速拷贝和填充
DMA2D 是 STM32 系列中专门用来做 2D 图形数据传输的外设,支持内存到内存拷贝、内存填充、像素格式转换等功能。在 LVGL 的 Flush 回调中,可以把memcpy替换为 DMA2D 操作。
一个最简单的 DMA2D 拷贝示例:
void lcd_copy_area_dma2d(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h, uint16_t *pixels) { uint32_t dst = LCD_FB_ADDR + (y1 * LCD_PIXEL_WIDTH + x1) * 2; uint32_t src = (uint32_t)pixels; DMA2D->CR = DMA2D_CR_MODE_MEM2MEM; DMA2D->FGMAR = src; DMA2D->BGMAR = dst; DMA2D->FGOR = 0; DMA2D->BGOR = LCD_PIXEL_WIDTH - w; DMA2D->FGPFCC = DMA2D_INPUT_RGB565; DMA2D->BGPFCC = DMA2D_INPUT_RGB565; DMA2D->NLR = (h << 16) | w; DMA2D->CR |= DMA2D_CR_START; while (DMA2D->CR & DMA2D_CR_START); }这种方式把“内存拷贝”从 CPU 搬运到 DMA2D。不过这里还有一个细节:在 Flush 回调中使用阻塞等待while会卡住 CPU,虽然拷贝本身没有占用 CPU 计算,但等待期间 CPU 也不能做其他事。更好的做法是使用 DMA2D 中断,在传输完成中断里调用lv_display_flush_ready()。
7.4 结合 FreeRTOS 提升响应性
如果系统里还有其他业务逻辑,比如网络协议栈、传感器读取、按键扫描,建议把 LVGL 放到 FreeRTOS 任务中。LVGL 9.0 支持配置操作系统接口层:
#define LV_USE_OS LV_OS_FREERTOS开启后,LVGL 可以使用 FreeRTOS 的互斥锁和信号量来保护内部状态,也便于后续多核或并行渲染扩展。
在实际项目中,可以单独创建一个lvgl_task,优先级适中,循环调用lv_timer_handler()并设置合适的延时。业务逻辑放在其他任务中,通过 LVGL 的消息队列与界面任务通信,避免直接在中断或非 LVGL 上下文中操作控件。
要注意的是,LVGL 并不是线程安全的。如果多个任务同时调用 LVGL API,可能会出现不可预料的崩溃。即使用了 FreeRTOS 适配层,也建议所有界面操作统一放到 LVGL 任务中执行。
7.5 工程化建议
在正式做性能测试和移植时,有几个工程实践可以坚持一下。
第一,组件裁剪。LVGL 9.0 默认开启的功能很多,如果只是做简单仪表盘,完全用不到图表、日历、键盘等组件。在lv_conf.h中关闭不需要的组件,可以减少内存占用和编译时间。
比如:
#define LV_USE_CHART 0 #define LV_USE_CALENDAR 0 #define LV_USE_KEYBOARD 0第二,编译器优化等级。开发调试阶段建议使用-O0,性能测试阶段建议切换到-O2或-Os。优化等级对 LVGL 渲染性能影响非常明显,有时候帧率能提升 20% 以上。
第三,日志分级。调试时打开 LVGL 日志:
#define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_LEVEL_WARN发布时关闭日志,避免日志打印占用串口和 CPU 时间。
第四,备份测试记录。每次优化后及时保存 FPS、CPU、内存使用数据,配合章节 5.5 的表格记录,避免优化方向反复横跳。
8. 总结与下一步
这篇文章从 STM32F746G-DISCO 的硬件特点说起,梳理了 LVGL 9.0 的新接口变化,完整演示了从 CubeMX 创建工程、LCD 点亮、LVGL 初始化、心跳和 Flush 回调接入的全过程。然后围绕性能基准测试,介绍了 FPS、CPU 占用、单帧耗时三个核心指标,以及如何使用 perf monitor、官方 Benchmark demo 和自定义计时来做测量。
在移植过程中最值得留意的几个坑,一个是 LVGL 9.0 的 API 与 8.x 差异大,不要直接复用旧代码;另一个是 SDRAM 显存区域的 Cache 一致性,Cortex-M7 的 D-Cache 使用不当会带来画面错乱;还有 Flush 回调应尽量缩短执行时间,有条件时用 DMA2D 或双缓冲来改善显示体验。
性能优化是一条需要反复测试和验证的路。建议先把官方 Benchmark 跑通,记录基准数据,再依次调整颜色格式、缓冲区大小、渲染模式、DMA2D 加速和编译器优化等级,每一步都用同一测试场景做对比。不要盲目追求高分,先搞清楚当前界面是 CPU 渲染瓶颈还是数据拷贝瓶颈,再针对性地优化。
如果你也正在做 STM32 + LVGL 9.0 的移植,建议先从一个最简 Demo 跑通,再做性能优化。这样后续遇到问题,至少能缩小排查范围。觉得这篇文章对你有帮助的话,可以先收藏备用,等实际测试中遇到问题再回来对照排查思路。