
之前在 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 为什么适合跑 LVGLSTM32F746G-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_FULLLVGL 会一次渲染整帧需要较大的缓冲区。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-DISCOLTDC 的 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_handlerLVGL 的运行依赖于周期性调用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 MonitorLVGL 9.0 内置了性能监视器只要在lv_conf.h中开启#define LV_USE_PERF_MONITOR 1编译运行后在屏幕左上角或右下角会显示类似以下信息FPS: 45 CPU: 32%这个数字是 LVGL 自己统计的包含界面渲染和内部定时任务开销。调试阶段这个功能非常方便不需要额外写代码就能看到大概性能水平。不过要注意perf monitor 本身也会消耗一定绘制资源而且它在屏幕上绘制文字和背景本身也会增加一点点渲染负载。测出来的数据用于相对比较没有问题但如果是发布版本建议关闭。5.3 使用官方 Benchmark DemoLVGL 官方提供了 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 测试结果记录与对比思路建议做一张测试记录表这样后续优化效果一目了然测试场景颜色格式缓冲区大小渲染模式平均 FPSCPU 占用最大单帧耗时官方 BenchmarkRGB56540 行PARTIAL待测待测待测官方 BenchmarkARGB888840 行PARTIAL待测待测待测自定义页面 ARGB565全屏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 内存不足或者 HardFaultLVGL 的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 跑通再做性能优化。这样后续遇到问题至少能缩小排查范围。觉得这篇文章对你有帮助的话可以先收藏备用等实际测试中遇到问题再回来对照排查思路。