ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ZYNQ平台LVGL高刷新率UI优化:从卡顿到满帧的实践指南

ZYNQ平台LVGL高刷新率UI优化:从卡顿到满帧的实践指南 先说个很多人在实际项目里会遇到的现象ZYNQ是双核Cortex-A9主频能到667MHz甚至1GHzLVGL又是为嵌入式量身定做的轻量级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只有一块全屏bufferLVGL绘制完成后直接交给显示控制器。绘制期间屏幕还在读这块buffer就会出现画面撕裂。部分缓冲Partial Buffer一块比较小的bufferLVGL分块绘制、分块提交内存占用小但每帧要多次搬运速度最慢。全双缓冲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里的数据强制写回DDRVDMA才能拿到正确内容。这是最简单、最稳妥的做法代价是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。改成16msLVGL的刷新周期就对齐了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_initlv_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%编译器加-mfpuneon即可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颜色双缓冲编译优化代码层裁剪就足够跑到60FPSCPU占用还能控制在50%以下。这种情况下上PL侧加速是纯属浪费时间。什么时候才考虑PL侧或GPU两类场景一是屏幕分辨率到了1920x1080二是有大量视频/图片要同时显示。这种情况下A9的软件渲染确实压不住再考虑上VDMA多路视频叠加或者外挂GPU也不迟。在ZYNQ上做高刷新UI更理智的顺序是先把软件层优化做尽再评估硬件加速。软件层还没做好的时候就上硬件大概率连硬件该做哪一块都说不清楚。6. 实测对比ZYNQ平台整套优化前后的帧率变化6.1 测试环境与测量方法把前面的理论串起来我在自己手头的一块ZYNQ-7020板卡上做了完整测试。测试条件如下硬件ZYNQ-7020PS主频667MHzDDR3 1GB屏幕800x480 RGB888接口通过AXI VDMA AXI4-Stream to Video Out驱动系统裸机standaloneLVGL 8.3测试界面一个综合页面含一条动态折线图、一个含10个item的滚动列表、两个开关控件、一个数值跳动标签测量方式LVGLLV_USE_PERF_MONITOR的FPS读数持续运行60秒取平均值6.2 各优化阶段帧率对比数据优化阶段平均FPSCPU占用表现默认单缓冲ARGB8888刷新周期30ms2288%明显卡顿撕裂严重双缓冲VDMA搬运3476%无撕裂但帧率仍不够色深改RGB565刷新周期16ms4562%肉眼可接受轻微迟滞双核分工LVGL独占核0业务逻辑丢核15247%基本流畅代码层裁剪动画错峰、删阴影、图片C数组化6041%满帧操作跟手这个数据很说明问题双缓冲解决的是撕裂真正把帧率从45拉到60的反而是双核分工代码层裁剪这种看似不起眼的调整。6.3 分辨率与帧率目标下的配置组合建议根据这次实测加上过往项目经验我整理了一份配置建议可以直接作为选型参考屏幕规格帧率目标推荐配置320x240以下60FPS单缓冲都够但建议双缓冲CPU开销极低800x48060FPSRGB565双缓冲VDMA动画错峰够用1024x60045-50FPS加NEON编译业务逻辑移到核11920x108030FPS软件渲染到极限考虑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卡启动制作的步骤。这套内容在论坛里被问过很多次到时候我把每一步操作和踩过的坑都摊开来讲。
返回列表