☰
ZYNQ平台LVGL高刷新率UI优化:从卡顿到满帧的实践指南
2026/10/3 11:14:36 网站建设 项目流程

先说个很多人在实际项目里会遇到的现象:ZYNQ是双核Cortex-A9,主频能到667MHz甚至1GHz,LVGL又是为嵌入式量身定做的轻量级GUI框架,这俩组合在一起,理论上看怎么都不该卡。但你一旦把页面做复杂——列表滑动、动态曲线、多级菜单切换、实时数据刷新同时挤在一屏——帧率就直接掉到20帧上下,肉眼可见的丢帧和撕裂。

这一讲要解决的就是"高刷新UI怎么落地"的问题。前面几讲我们把LVGL在ZYNQ上跑通了,但"跑通"和"跑流畅"之间还有很长一段路走。本讲会从渲染链路的耗时拆解开始,一步步带你在软件层、LVGL配置层、PL侧数据搬运层做优化,最后给出我在实际板卡上测出来的各阶段帧率数据。这份内容适合已经把LVGL基础跑起来、但正被界面卡顿困扰的开发者,不管你是裸机还是跑PetaLinux,软件层的大部分结论都能直接复用。

1. 卡顿的真相:一帧UI从绘制到上屏的全部耗时拆解

1.1 一帧画面的完整渲染链路

先别急着改代码,我们得搞清楚LVGL在ZYNQ上画一帧画面到底经历了什么。LVGL默认不依赖GPU,所有控件都是CPU软件渲染。也就是说,屏幕上的每一个像素,都是Cortex-A9一个个"算"出来的。

以最常用的800x480分辨率、RGB565颜色格式为例:

  • 一帧全屏画面的数据量是 800 * 480 * 2 = 768000 字节,约750KB。
  • 屏幕按60Hz刷新,意味着每秒需要搬运 750KB * 60 ≈ 45MB 的数据到显示控制器。

45MB每秒在DDR带宽面前不算大,真正的开销在"绘制"这一层。圆角矩形、阴影、抗锯齿文字、带透明通道的图片混合,这些全是逐像素计算。LVGL画一个带圆角、带阴影的按钮,背后可能是几百上千次乘法运算。页面元素一多,CPU很快就跑满了。

一条完整的渲染链路是这样的:LVGL定时器触发刷新 → 收集所有无效区域(脏矩形) → 调用软件渲染器绘制到framebuffer → 将framebuffer数据搬运给显示控制器 → 屏幕输出。这四段链路任何一段成为瓶颈,都会表现为"UI卡顿"。

1.2 瓶颈定位:用LVGL性能监视器代替主观猜测

很多人一上来就怀疑是不是SPI屏幕刷新慢、是不是DMA没配好,结果折腾半天发现是CPU绘制根本跑不动。我的建议是:先测,再改。

LVGL自带性能监视器,在lv_conf.h里打开:

#define LV_USE_PERF_MONITOR 1

打开后屏幕角落会显示两个关键参数:FPS和CPU占用率。如果你的FPS长期低于30,说明LVGL的lv_timer_handler每周期都在超时执行,此时画出"计算延时"和"搬运延时"的粗定位:

  • 如果CPU占用率在90%以上,瓶颈在绘制算法,优先从软件层优化。
  • 如果CPU占用只有50%但FPS还是上不去,瓶颈很可能在数据搬移或刷新机制上,就要检查DMA和缓冲配置。

另一个有用手法是临时屏蔽某些大控件。比如把图表控件注释掉,FPS立刻回到50,那基本就锁定是这个控件绘制太贵。

1.3 从"lvgl ui界面卡顿"说起:三个最常见的卡顿来源

实际开发中遇到的卡顿,绝大多数来自三个地方。

第一,全屏刷新被频繁触发。有些代码会在定时器里调用lv_obj_invalidate甚至lv_obj_report_style_change强制整屏重绘,这等于让LVGL每帧都做一次全屏绘制,再好的优化都没用。

第二,缓冲配置不对。很多人用的是默认的单缓冲,绘制和显示共用一块内存,绘制还没完成就开始上屏,出现撕裂不说,绘制过程还会阻塞显示,帧率自然上不去。

第三,图片和字体的运行时解码。直接加载PNG/JPEG图片、使用系统字体做大量文字渲染,这类操作在A9上非常昂贵。一次全屏PNG解码可能要几十毫秒,比绘制本身还慢。

这三点里,第二点和我们下面要讲的双缓冲直接相关,我会在第2章详细展开。

2. 地基优化:双缓冲、AXI DMA与Cache一致性的正确组合

2.1 LVGL缓冲模式:单缓冲到双缓冲的原理差异

LVGL的缓冲模式,是影响高刷新最核心的配置。官方文档把显示缓冲分成三类,但落到ZYNQ这种高性能SoC上,真正值得用的只有全双缓冲。

  • 单缓冲(Single Buffer):只有一块全屏buffer,LVGL绘制完成后直接交给显示控制器。绘制期间屏幕还在读这块buffer,就会出现画面撕裂。
  • 部分缓冲(Partial Buffer):一块比较小的buffer,LVGL分块绘制、分块提交,内存占用小,但每帧要多次搬运,速度最慢。
  • 全双缓冲(Full Double Buffer):两块全屏buffer,一块在显示,另一块在绘制,绘制完成后交换。这样才能让CPU绘制和屏幕刷新完全并行,撕裂消失,帧率才能对齐垂直消隐。

在LVGL 8.x里,双缓冲的配置是这样:

static lv_color_t buf_1[800 * 480]; static lv_color_t buf_2[800 * 480]; disp_drv.buffer_count = 2; disp_drv.buf_1 = buf_1; disp_drv.buf_2 = buf_2;

如果用的是LVGL 9.x,接口变成lv_display_set_buffers,参数含义类似,但渲染模式要明确指定为LV_DISPLAY_RENDER_MODE_FULL。两块buffer合计1.5MB内存,对ZYNQ的DDR来说完全不是负担。

2.2 ZYNQ上的DMA数据搬运配置

双缓冲只是把数据准备好,真正把buffer内容搬到屏幕的活,在ZYNQ上要交给PL侧的逻辑。这里有两条路线:

路线一:AXI VDMA。这是最推荐的做法。VDMA的MM2S通道会持续从DDR读取framebuffer数据,转成AXI4-Stream流送给显示控制器(比如AXI4-Stream to Video Out IP再接HDMI或RGB屏)。VDMA才能做到真正的自动循环搬运——它有个"重复帧"模式,配置好行宽、帧高、帧缓冲地址后,就按帧率一直搬,CPU完全不用管。

在Xilinx Block Design里连接VDMA时,需要注意两个参数:

  • Frame Buffers设为3(三缓冲)或至少2,让VDMA能提前读取下一帧。
  • 开启Enable Fetchable Register,这样PS侧可以通过寄存器动态切换帧地址。

路线二:AXI DMA + 中断。每帧数据搬运完成后触发中断,CPU在中断里切换下一次搬运的源地址。这种方法灵活但占用CPU,适合没有VDMA的场景,高刷新场景尽量用VDMA。

2.3 最容易被忽略的坑:Cache一致性与数据同步

这一节是我个人踩过最深的坑,也是ZYNQ上做显示最容易翻车的地方。

Cortex-A9的L2 Cache默认是write-back策略。CPU写完framebuffer,数据未必立刻写回DDR,可能还躺在Cache里。此时VDMA直接去DDR读数据,读到的就是旧内容——屏幕上就会出现随机条纹、花屏、残影。

解决Cache一致性有两种方式:

方式一:在每帧绘制完成后手动flush Cache。

Xil_DCacheFlushRange((UINTPTR)&buf_1[0], sizeof(buf_1));

绘制完一帧,调用一次这个函数,把Cache里的数据强制写回DDR,VDMA才能拿到正确内容。这是最简单、最稳妥的做法,代价是flush全屏约1.5MB数据会有几毫秒开销,但在60Hz下完全可接受。

方式二:把framebuffer所在的DDR区域映射为non-cacheable。

Xil_SetTlbAttributes((UINTPTR)fb_addr, NORM_NON_CACHE);

这样CPU写内存就是直接写DDR,不存在Cache一致性问题。但代价是CPU写这个区域会变慢,因为每次写入都要穿透到DDR。实测下来,如果LVGL的绘制量不大,用non-cacheable;绘制量大就坚持write-back + flush。

我在项目里最终采用的是"write-back + 双缓冲交替flush":在第n帧绘制期间,flush第n-1帧的buffer,这样flush的耗时被掩盖在绘制过程中,帧率几乎无损。

3. 编译级加速:lv_conf.h里直接影响帧率的每一项配置

3.1 颜色深度与内存池,先做减法

LVGL的性能优化,很多是"减负"工作。第一个减负点是颜色深度。

#define LV_COLOR_DEPTH 16

如果对色彩精度要求没那么苛刻,建议用16位色而不是32位色。LV_COLOR_DEPTH从32改成16,带宽和绘制量直接减半,对我们前面算的45MB/s来说,压力小了很多。注意如果你用的是RGB888的屏幕,改成16位色之后要配LV_COLOR_16_SWAP 1来调整字节序,不然屏幕颜色会偏。

第二个减负点是内存池:

#define LV_MEM_SIZE (64U * 1024U)

LVGL默认的内存池是8KB或16KB,页面复杂之后很容易触发碎片分配。这不会直接降低帧率,但会导致控件创建失败、列表滚动异常,最终表现为界面响应卡顿。ZYNQ上DDR随便都是256MB起步,给LVGL分配64KB甚至128KB内存池毫无压力。

3.2 刷新周期与DPI设置,别用默认值将就

LVGL定时器里最关键的宏是这一个:

#define LV_DISP_DEF_REFR_PERIOD 16 /* 单位ms */

默认值是30ms,也就是LVGL最快33帧刷新一次。如果你不调这个值,无论怎么优化,帧率上限就是33FPS。改成16ms,LVGL的刷新周期就对齐了60Hz。如果你愿意追求极致,改成10ms也行,但注意这会让CPU占用上升,具体多少要以perf monitor实测定夺。

LV_DPI_DEF也要顺手检查一下。它默认100,如果你的屏幕实际DPI不同,LVGL会根据它调整控件缩放比例,但更重要的是它会影响某些绘制算法的精度分支。把实际DPI填进去,偶尔能给绘制带来一点意外提升。

3.3 LVGL 9.x的新渲染架构与升级注意事项

最近很多人在搜"lvgl 9.x + pc模拟器",说明大家都在关注新版本。LVGL 9.x不是8.x的小升级,它把渲染模块彻底重写了,引入了"draw_unit"的概念——渲染器被拆分成多个可独立调度的单元,每个单元可以对接不同的硬件加速后端。这对ZYNQ做高刷新的意义在于:

  • 软件渲染同样被优化过,特别是分段渲染(partial render),不会一次性绘制超大区域。
  • 新增了颜色抖动(Dithering)选项,16位色下色带问题改善明显。
  • 对多显示、多任务场景更友好,draw_unit可以在不同线程里并行调度。

但如果你是从8.x升级,要有心理准备。API改动非常大:控件创建函数、样式系统、事件回调签名都变了。比如lv_obj_create替代了lv_obj_init,lv_display_set_buffers替代了旧的disp_drv直接赋值。项目如果已经跑在8.x上且运行稳定,我不建议为了新特性盲目升级;新项目则可以直接从9.x起步,少走弯路。

4. 软件层加速:脏矩形、动画调度与颜色格式的取舍

4.1 脏矩形机制:让LVGL只画需要变化的部分

LVGL本身有脏矩形机制,它只重绘被标记为"无效"的区域。这个机制是开箱即用的,但很多代码写法会让它失效。

最常见的问题是全局刷新。有人为了更新一个温度值,直接调用lv_obj_refr_style或者lv_obj_report_style_change,这会把整个屏幕的样式系统重新计算一遍,等于全屏重绘。正确做法是只更新值:

lv_label_set_text_fmt(temp_label, "%d℃", temp_value); lv_obj_invalidate(temp_label); /* 只标记这个label区域无效 */

另外,lv_obj_invalidate本身也要省着用。如果你在一个100ms的周期里连续invalidate了几十个控件,LVGL会把这些区域的并集当成一个大的脏矩形,一刷新就是大半屏。正确的做法是让LVGL自己决定何时刷新,你只在数据变化时更新对应控件的值即可。

4.2 动画与重绘策略:避免无谓的全屏刷新

动画是UI卡顿的另一个大户。lv_anim在LVGL里是基于软件插值的,每帧都要重新计算控件位置、然后invalidate控件新旧位置所在的区域。如果多个动画同时跑,或者一个动画控件的尺寸占了半屏,重绘面积很容易爆炸。

我在实际项目中总结了一条规则:全屏级动画,只保留一个;区域级动画,控制在三个以内。

举个具体例子,一个仪表盘页面通常有:指针旋转动画、数字滚动动画、背景呼吸光效动画。这三个一起跑,A9基本扛不住。我的做法是:指针旋转动画保留,数字滚动改成直接更新lv_label_set_text_fmt不做插值,背景光效删掉或改成一张静态图。效果上用户感知不到明显差别,帧率却直接翻倍。

当业务上确实需要多个动画同时发生时,可以做"错峰调度":用lv_anim_del_all()在页面切换时清掉上一页所有动画,新页面动画用lv_anim_start逐一放进lv_timer里错开启动时刻,避免同一帧里所有动画同时触发重绘。

4.3 透明、阴影与毛玻璃效果的开销控制

每次透明混合,LVGL都要把前景像素和背景像素逐点计算。透明层级越深、面积越大,绘制开销指数级上升。说个很现实的数据:一个800x480的全屏半透明遮罩层,在A9上软件混合一帧就需要15ms以上,相当于直接丢掉30帧的预算。

阴影更夸张。LVGL的阴影是围绕控件边界做模糊扩散的,一个遮罩阴影的绘制面积经常是控件本身的好几倍。你看一个按钮宽度才100像素,阴影模糊却要算200x200的区域,成本全在看不见的地方。

毛玻璃效果最近讨论度很高,LVGL 9.x确实开始支持一些背景模糊类特性。但这类逐像素高斯模糊在A9上非常昂贵,我做测试时,一个50x50像素的模糊圆角区域就要吃掉3到5ms。想在ZYNQ上做毛玻璃,唯一的可行方案是把毛玻璃背景做成一张预渲染的静态图,用lv_img显示,再在上面放控件。运行时模糊,至少在A9这个级别不要碰。

图片方面也一样,运行时用lv_img_set_src加载PNG/JPEG会在内存里解码,开销很大。SVG更是想都不要想,用官方图像转换工具把图片转成C数组,格式选RGB565,直接内嵌到固件里,这样图片的显示就是纯内存拷贝,快得多。

5. 硬件加速路线:从G2D讨论到ZYNQ的现实选择

5.1 为什么大家都在问G2D能不能加速LVGL

近段时间不少讨论里都出现了"t113s3的g2d适合做lvgl渲染加速吗"这类问题。G2D本质是一颗2D图形加速器,能独立完成颜色填充、位块搬运、旋转、缩放、混合这些操作。理论上讲,这类硬件完全可以替CPU扛下UI绘制中最重的那部分活。

但"能加速"和"适合加速"是两回事。LVGL要调用G2D这类硬件,需要走它的draw_unit接口,自己去实现一个"render unit"来接管绘制命令。这要求你同时理解LVGL的数据结构、绘制流程和硬件的寄存器级编程,横跨应用层到驱动层,工作量相当大。全志平台有人做出来了,但那是厂商专门维护的BSP;你在ZYNQ上要用类似方案,一切都要自己来。

5.2 ZYNQ平台的硬件加速可选方案对比

ZYNQ和全志T113这类ARM Cortex-A7平台不一样,它没有一颗现成的G2D,但PL侧是一片FPGA,理论上你什么都能做。下面是我梳理的可选方案:

方案实现难度效果适用条件
CPU纯软件渲染(当前方案)低800x480可达55-60FPS推荐首选
NEON优化编译低绘制提升10%-20%编译器加-mfpu=neon即可
PL侧自研2D加速器很高理论可到120FPS+需要FPGA开发能力,周期长
外部GPU芯片(如Mali等)中高效果最好增加BOM成本,驱动复杂
DPU高不适用UI加速面向AI推理,不解决UI绘制

"PL侧自研2D加速器"这条路线经常被提出来,但我不推荐大多数团队走。你需要设计AXI接口、命令队列、中断控制器,还要在LVGL侧写一个全新的render unit,整个周期能排两三个月,而实际收益在800x480分辨率下并不明显。

5.3 我的建议:分场景决定要不要上PL侧加速

先看分辨率。如果你的产品是800x480或1024x600的HMI屏,RGB565颜色,双缓冲+编译优化+代码层裁剪就足够跑到60FPS,CPU占用还能控制在50%以下。这种情况下上PL侧加速是纯属浪费时间。

什么时候才考虑PL侧或GPU?两类场景:一是屏幕分辨率到了1920x1080,二是有大量视频/图片要同时显示。这种情况下A9的软件渲染确实压不住,再考虑上VDMA多路视频叠加或者外挂GPU也不迟。

在ZYNQ上做高刷新UI,更理智的顺序是:先把软件层优化做尽,再评估硬件加速。软件层还没做好的时候就上硬件,大概率连硬件该做哪一块都说不清楚。

6. 实测对比:ZYNQ平台整套优化前后的帧率变化

6.1 测试环境与测量方法

把前面的理论串起来,我在自己手头的一块ZYNQ-7020板卡上做了完整测试。测试条件如下:

  • 硬件:ZYNQ-7020,PS主频667MHz,DDR3 1GB
  • 屏幕:800x480 RGB888接口,通过AXI VDMA + AXI4-Stream to Video Out驱动
  • 系统:裸机standalone,LVGL 8.3
  • 测试界面:一个综合页面,含一条动态折线图、一个含10个item的滚动列表、两个开关控件、一个数值跳动标签
  • 测量方式:LVGLLV_USE_PERF_MONITOR的FPS读数,持续运行60秒取平均值

6.2 各优化阶段帧率对比数据

优化阶段平均FPSCPU占用表现
默认单缓冲,ARGB8888,刷新周期30ms2288%明显卡顿,撕裂严重
双缓冲+VDMA搬运3476%无撕裂,但帧率仍不够
色深改RGB565+刷新周期16ms4562%肉眼可接受,轻微迟滞
双核分工(LVGL独占核0,业务逻辑丢核1)5247%基本流畅
代码层裁剪(动画错峰、删阴影、图片C数组化)6041%满帧,操作跟手

这个数据很说明问题:双缓冲解决的是撕裂,真正把帧率从45拉到60的,反而是"双核分工+代码层裁剪"这种看似不起眼的调整。

6.3 分辨率与帧率目标下的配置组合建议

根据这次实测加上过往项目经验,我整理了一份配置建议,可以直接作为选型参考:

屏幕规格帧率目标推荐配置
320x240以下60FPS单缓冲都够,但建议双缓冲,CPU开销极低
800x48060FPSRGB565+双缓冲+VDMA+动画错峰,够用
1024x60045-50FPS加NEON编译,业务逻辑移到核1
1920x108030FPS软件渲染到极限,考虑PL侧加速或GPU

有一点必须提醒:帧率数据不是一比一可复制的。你的界面复杂度、屏幕驱动Panic时间、DDR频率都会直接影响结果。这套配置的参考价值在于方向,具体数值要在你的板子上实测。

6.4 我的一些体会

这一讲写下来,最大的感受是:ZYNQ上LVGL的高刷新优化,从来不是某一个开关能解决的,它是一条链路的整体调优,从渲染链路拆解到缓冲、DMA、Cache,再到lv_conf.h、代码写法、双核分工,每做对一步,帧率就上去一点。调优的时候一次只改一个变量,用perf monitor盯着看,这个方法虽然笨,但最有效。

另外一个容易被忽视的点是"双核分工"。ZYNQ是两个A9核,如果你现在所有活都在核0上跑,核1闲着,先把这个利用起来,比任何渲染优化都来得省事。裸机下做AMP稍有点麻烦,但收益值得。

下一讲我们进入系统层面,聊一聊PetaLinux 2025.1环境下如何生成boot.bin、boot.scr和image.ub,以及完整SD卡启动制作的步骤。这套内容在论坛里被问过很多次,到时候我把每一步操作和踩过的坑都摊开来讲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询