资讯动态

基于Zynq SoC与Vitis的SPI屏幕LVGL移植实践

发布时间:2026/10/20 6:22:19 来源:尧图企业网站定制
最近把一块SPI接口的小尺寸LCD接到了zynq soc平台上用vitis新版本工具链把lvgl跑了起来整个过程比想象中顺利但坑也不少。身边有几个同事都在问移植细节索性把这套从硬件到软件的完整流程整理出来方便后来人少走弯路。这篇内容围绕“zynq soc vitis工具链 SPI屏幕 lvgl移植”展开适合正在做低成本显示方案、工业面板、或者想在嵌入式平台快速搭建图形界面的开发者参考。1. 项目全貌zynq soc上跑lvgl为什么要走SPI屏幕这条路1.1 选型思路SPI屏幕不是妥协是场景匹配很多人一听到Zynq SoC第一反应就是接HDMI或者RGB接口的大屏毕竟PS端自带图形加速单元、PL端有足够的逻辑资源随便做个全高清输出都不难。但实际项目里大量场景根本用不到高分辨率尤其是在工业控制面板、仪器仪表、小型手持设备这类场合需要的是“低成本、低功耗、小尺寸、快速上线”这时候一颗3.5寸以下的SPI接口TFT屏就非常合适。SPI屏的优势在于连线少标准4线制SCLK、MOSI、CS、DC加上复位和背光总共也就六七个引脚在PL端可以非常方便地用AXI Quad SPI控制器或者纯逻辑的方式驱动。对比RGB屏动辄几十根数据线的走线压力SPI屏在PCB布局、信号完整性、逻辑资源占用上都有明显优势。当然SPI屏幕也有清晰的天花板带宽有限、刷新率上不去无法胜任动画丰富的UI交互。但LVGL的设计哲学恰恰是“轻量、高效”配合脏矩形刷新机制能在这种受限带宽下获得可以接受的交互体验。这次项目里用的屏幕是320x240分辨率、RGB565色深理论帧率能做到25帧左右实际跑简单界面时体感流畅。1.2 工具链选择为什么直接上Vitis新版过去在Zynq上做裸机开发大家习惯用老的SDK后来升级到Vitis工具链。这次使用的是较新的Vitis统一IDE版本它和传统的Eclipse式Vitis相比界面和操作逻辑变化比较大底层基于现代Web技术架构工程管理和编译速度都有提升。关键的一点是新版Vitis把“平台工程”这个概念做得更清晰了。硬件工程师从Vivado导出XSA文件之后软件工程师在Vitis里基于这个XSA创建平台然后应用工程直接关联平台整个流程在工程层面就规范化了。对我们做LVGL移植的人来说这意味着BSP驱动、链接脚本、编译器选项这些底层的活工具链已经帮你搭好框架你只需要把LVGL源码放进去写回调。1.3 整条软件栈的分工整条链路可以拆成三层来理解。最底层是硬件描述层PL端生成的SPI控制器、GPIO背光控制通过AXI总线映射到PS端地址空间。中间层是驱动层Vitis的BSP提供了SPI驱动函数XSpi_*系列我们基于它封装屏幕控制器IC比如常见的ST7789或ILI9341系列的初始化、写命令、写数据接口。最上层是LVGL图形库它不直接操作硬件而是通过我们提供的disp_flush回调函数把绘制好的像素数据交给屏幕驱动发送出去。理解了这三层关系整个移植过程就有了清晰的边界每一层只解决自己的问题出了问题也能快速定位。我自己见过不少人在LVGL移植上卡住最后发现是屏幕初始化序列不对这种情况在分层清晰的代码结构下排查起来要容易得多。2. 硬件准备与PL端SPI控制器的搭建2.1 读懂屏幕手册的时序参数拿到一块SPI屏幕第一件事不是写代码而是去读屏幕控制器IC的数据手册这一步偷懒后面全是坑。以常见的ST7789V为例重点关注几个参数SPI时钟最高频率、CPOL/CPHA模式、写命令和写数据的时序要求、复位脉宽。大多数这类屏幕支持Mode 0CPOL0, CPHA0和Mode 3CPOL1, CPHA1实际用下来ST7789在Mode 0下工作最稳定。SPI时钟方面手册给出的上限通常比较高但实际系统里要考虑信号完整性和走线长度保守起见我会取手册上限的25%~50%个人经验是30MHz到50MHz之间再高就得非常小心PCB布局了。提示屏幕控制IC的初始化序列千万不能想当然。不同批次、不同厂商的屏幕即使主控IC型号相同初始化代码也可能有细微差异。最稳妥的办法是直接找屏幕厂商要初始化示例代码或者在网上找一个同型号的成熟驱动库先点亮屏幕再考虑后续优化。2.2 PL端AXI Quad SPI IP配置Vivado里添加AXI Quad SPI IP配置成“Standard SPI Mode”或“Dual/Quad SPI Mode”对于普通TFT屏幕来说标准模式就够了接MOSI、SCLK、CS、DC数据/命令选择可以复用SS信号或者用额外GPIO控制。IP的几个关键配置项要留意参考时钟选择100MHz的AXI时钟SPI的SCLK分频可以设置从2到128比如目标是40MHz就用100MHz除以2得到50MHz接近目标。FIFO深度默认的128字节对LVGL这种大块刷新数据来说偏小建议改到512或1024。如果配了DMA超大块数据可以直接走DMA通道FIFO深度影响会小一些。从机选择如果用单块屏幕固定占用SS0就行如果后面还接了触摸或其他SPI设备就得注意从机选择信号和片选逻辑的映射关系。PL端还需要额外做一个背光控制GPIO和一个复位控制GPIO这两个用AXI GPIO IP就可以。在Vivado里连接好自动布线然后生成比特流、导出XSA文件。整个硬件搭建大约半小时就能搞定前提是你对Block Design的操作足够熟练。2.3 时钟与带宽的粗略计算在动手画板子或者调参数前算一笔带宽账很有必要。以320x240分辨率、RGB565格式为例单帧像素点320 × 240 76800每个像素两个字节单帧原始数据76800 × 2 153600字节 约1.2 Mbit如果SPI时钟40MHz传送1.2 Mbit理论耗时1.2M / 40M 约30ms这意味着即使LVGL部分刷新脏矩形方式整屏全刷一次也要30ms级别帧率上限就被锁死在30fps上下。如果在界面上做了大面积刷新动画实际帧率会降到10到15fps这是可以预见的。后面谈优化时会展开怎么在带宽受限的情况下尽量把体感帧率做上去。3. Vitis工具链下的平台工程与屏幕驱动编写3.1 在Vitis统一IDE中创建Platform和应用工程新版Vitis和传统版本的工程操作逻辑有较大区别但核心流程不变先基于XSA创建Platform工程再基于Platform创建应用工程。具体步骤打开Vitis新建Platform工程选择由Vivado生成的XSA文件工具链会自动解析硬件描述生成对应的BSP驱动。在Platform工程里能看到ps7_cortexa9_0这个处理器核的BSP设置项可以配置驱动库、串口波特率、堆栈大小等。应用工程创建时关联刚建好的Platform选择空应用模板然后手动把LVGL源码和屏幕驱动源码加进去。这里有个建议记得把BSP里的标准和扩展库都打开LVGL依赖标准输入输出的地方不多但printf调试输出全靠串口驱动没开的话调试会很难受。注意新版Vitis编译速度虽然快但第一次构建时如果LVGL源码没做裁剪全量编译可能耗时较长。可以考虑先把LVGL的demo关掉只保留core和必要的组件等基础跑通再逐步加功能。3.2 基于BSP驱动的SPI读写封装Xilinx的BSP里SPI驱动接口是XSpi_*家族使用方式可以分为三步初始化、配置、传输。第一步初始化需要拿到XSpi实例指针和设备ID从xparameters.h里找到对应的参数宏。第二步配置主要有XSpi_SetOptions设置主模式、手动从机选择等。第三步就是传输XSpi_Transfer接受一个发送缓冲区注意这个接口是阻塞式的不适合大块数据。在我们的屏幕驱动里实际避免直接调用阻塞的XSpi_Transfer发送整个像素缓冲区而是配合中断或DMA方式把发送动作放到disp_flush回调里LVGL的刷新机制天然就有异步需求这点后面细讲。这里先搞定最基本的单字节读写验证屏幕ID寄存器能否正确读取这往往比直接刷颜色更能说明硬件链路是否正常。3.3 屏幕初始化序列的完整处理屏幕驱动IC上电后需要先送初始化命令块这部分的正确性直接决定屏幕能不能亮、颜色准不准。常规流程是拉高复位引脚、延时、拉低、再拉高等待内部上电稳定然后通过SPI写入一串初始化寄存器设置。初始化命令序列一般包含以下几个模块电源控制相关寄存器设置内部稳压器电压像素格式寄存告诉屏幕是RGB565还是RGB666显示方向设置比如横屏、竖屏、扫描方向显存访问控制决定行列扫描顺序gamma校正参数影响色彩表现我的习惯是把这个初始化序列做成const uint8_t数组每条命令都包含“命令字节”和“参数数据长度”写驱动时用一个统一的解析循环去送这样既清晰又容易维护。踩过一次坑是初始化时忘了关显示导致屏幕一直是白屏状态。很多屏幕IC默认显示关闭必须发送显示开命令比如ST7789的0x29才能实际显示内容这个命令一般放在初始化序列的最后漏掉它的话上面所有设置都白搭。4. LVGL移植核心从下载源码到点亮界面4.1 版本选择与源码裁剪LVGL目前主流的有v8.x和v9.x两个大版本。v9重新设计了底层架构对内存管理和刷新流程做了不少优化但API变化也大。v8.3是经过长时间验证的稳定版本网上资料最多教程最全几乎你能遇到的坑都有前人踩过。如果你想要快速出活建议先选v8.3如果项目周期充裕、希望一步到位可以考虑v9但要有一定的源码阅读和排错耐心。源码下载后需要重命名并修改lv_conf_template.h为lv_conf.h打开最关键的几个宏LV_COLOR_DEPTH设为16对应RGB565和屏幕色深匹配LV_MEM_CUSTOM设为0使用LVGL自带的内存分配器并配置LV_MEM_SIZE为实际内存池大小LV_TICK_CUSTOM设为1使用硬件时基源对应Zynq平台的Xtick或FreeRTOS的tick提示LVGL的内存池大小直接影响流畅度。图标的缓存、控件对象的创建销毁、绘制缓冲区的分配都依赖这块内存。我通常先给到64KB跑起来后再通过lv_mem_monitor观察实际占用情况一般32KB到64KB够用但如果你想同时开好几个复杂窗口可能要往上加。源码裁剪方面把不需要的组件从lv_conf.h关掉比如字体默认只保留LV_FONT_DEFAULT相关的一两种主题只保留基本的lv_theme_default其他demo代码全部不参与编译。这一步能大幅缩短编译时间避免一开始就陷入编译报错的泥潭。4.2 实现disp_flush回调LVGL和硬件之间的桥梁LVGL工作时会先把要绘制的内容渲染到一块内存缓冲区然后通过disp_flush回调函数把这块缓冲区交给底层屏幕驱动发送。所以这个回调的实现就是整个移植过程中最核心的代码。最简实现是阻塞式disp_flush拿到缓冲区指针和坐标信息后先设置屏幕的显示窗口set_window然后把数据通过SPI发送函数一块块传出去发送完调用lv_disp_flush_ready()通知LVGL可以继续渲染了。但这种阻塞写法在屏幕尺寸稍大时会有明显卡顿因为LVGL在flush完成前不会继续渲染下一帧整个绘制流程被SPI发送速度拖住。改进方向是双缓冲加异步刷新LVGL配置两个绘制缓冲区当一个缓冲区在SPI发送时另一个缓冲区可以继续渲染从软件流水线的角度把等待时间藏起来。在Zynq平台上更理想的方式是配合DMA或中断。LVGL的回调里只发起DMA传输不等它完成就返回等DMA完成中断触发时再调用lv_disp_flush_ready()。这样CPU几乎不用等待SPI传输可以把时间花在LVGL自身的绘制逻辑上。这个异步回调和阻塞回调的差异体感上非常明显。我用单缓冲阻塞式跑一个快速滑动的界面帧率明显偏低改成双缓冲DMA传输之后滑动流畅度有了质的提升CPU占用还降下来了。4.3 输入设备与心跳让系统真正“活”起来LVGL移植不只是显示还要处理交互输入。如果屏幕带触摸一般也走SPI接口比如XPT2046这类的触摸芯片。实现indev_read_cb回调把触摸坐标上报给LVGL即可。如果没有触摸也可以用物理按键配合LVGL的Encoder或按键分组功能实现焦点切换和点击操作工业面板场景很常见。按键的扫描放在一个周期任务或中断里把按下事件以lv_event_t形式发送给LVGL。心跳方面LVGL需要一个周期性时基源来驱动动画和超时处理。在Vitis裸机环境下可以用Xtick相关函数或者直接开一个定时器中断在FreeRTOS环境下直接使用xTaskGetTickCount作为LVGL的tick源配置LV_TICK_CUSTOM为1并提供lv_tick_get的实现即可。4.4 用FreeRTOS时的任务划分与优先级如果跑FreeRTOSLVGL任务的划分有讲究。主流做法是建三个任务lvgl_task负责调用lv_timer_handler()周期处理LVGL内部事件spi_send_task负责处理SPI发送队列touch_scan_task负责周期扫描触摸。lvgl_task的周期建议设为5ms即每秒调用200次lv_timer_handler这能让动画比较平滑。优先级方面LVGL任务的优先级要略高于触摸扫描任务但要低于SPI中断的优先级SPI发送队列由中断或DMA完成信号唤醒这样CPU不会有太长的忙等。任务间通信用队列LVGL的disp_flush回调把数据交给队列就返回SPI发送任务阻塞等待队列有数据就发。这样LVGL和SPI发送解耦合即使发送稍慢LVGL也能在缓冲区允许的范围内继续渲染体验上会比同步发送好很多。5. 调优与踩坑SPI屏幕跑LVGL的真实记录5.1 白屏问题的排查路径白屏是SPI屏幕上最典型的现象几乎每个人都会遇到。排查顺序建议从硬件到软件一步步来别一上来就怀疑LVGL。第一步确认屏幕供电和复位时序是否正常用示波器看复位引脚的波形第二步确认SPI总线是否有数据抓SCLK和MOSI看看发送初始化命令时有没有波形第三步确认CS片选是否正常很多屏幕要求CS在整个配置期间保持拉低。如果前三步都正常但还是白屏大概率是初始化命令漏了“显示开”指令或者像素格式设置和LVGL配置不一致。我曾经遇到过一次LVGL配的是RGB565屏幕寄存器设置成了RGB666格式结果颜色偏得离谱差点误判成数据线接错。5.2 画面撕裂与闪烁的根因和应对LVGL本身有防撕裂机制lv_disp_flush_ready调用时机如果不对就会出现画面上下块不一致的问题。常见原因是在异步DMA发送模式下LVGL已经准备刷新下一帧并写入了正在被DMA读取的缓冲区导致数据竞争。解决办法是不要直接修改正在传输的缓冲区要么等待DMA完成再复用该缓冲区要么加一层互斥保护。LVGL的双缓冲配置天然规避了这个问题只要保证两个缓冲区中正在被LVGL绘制的那个不会被DMA读取就不会撕裂。闪烁的问题则通常来自背光控制。如果LVGL刷新时背光也跟着闪很可能是因为你的背光控制放在了disp_flush里跟着高频开关。正确做法是背光只受显示开关和亮度调节控制和每帧刷新完全解耦。5.3 帧率瓶颈算清楚再优化前文算过40MHz SPI刷新整屏理论上约30ms实际加上命令开销和DMA配置时间整体在35ms到40ms也就大概25到28fps的极限。如果界面动画较多LVGL的渲染时间本身也会占用CPU最终体感帧率可能只有15fps上下。提高帧率的有效手段有几个方向缩小刷新区域LVGL只刷新脏矩形尽量让界面元素不触发全屏更新比如避免整屏的背景重绘降低色深或分辨率这是最直接但影响体验的方式非必要不推荐提高SPI时钟前提是信号质量能扛住在PCB布局合理的前提下很多屏幕支持到60MHz甚至更高开启DMA传输减少CPU等待让CPU在SPI发送期间还能继续做渲染计算实测下来在320x240分辨率、RGB565、SPI 50MHz、DMA双缓冲、FreeRTOS三级任务的配置下简单菜单和状态页面的交互非常流畅滑动列表时稍有迟滞但完全可用。5.4 几个容易被忽略的细节字节序问题Zynq的ARM核是大端还是小端会影响SPI发送时字节拼接的顺序。默认情况是little-endianRGB565的颜色值发送时要注意高低字节顺序否则颜色会红蓝互换。SPI模式选择ST7789虽然支持Mode 0和Mode 3但我实测某些批次屏幕在Mode 3下偶发异常最好固定用Mode 0。LVGL的内存碎片长时间运行后频繁创建销毁控件会导致内存碎片界面响应变慢。建议有时间的话开一下lv_mem_monitor观察一段时间内的峰值和碎片情况必要时调整内存池大小或改用LVGL内存池外的外部内存。DMA描述符的内存对齐Zynq的DMA要求缓冲区按32字节对齐LVGL绘制的缓冲区刚好不一定对齐这一步很多人会忽略。可以在创建缓冲区时手动加对齐属性避免DMA传输出错。6. 总结与扩展这趟移植练出了什么整个项目走下来最大的体悟是移植LVGL到SPI屏幕这件事本质上考验的不是图形库的使用能力而是对整个嵌入式系统数据通路的理解。从PL端的SPI控制器到PS端的BSP驱动再到LVGL的刷新协议每一层都需要对上下游有一定把握才能真正调出流畅的效果。再说一个很多资料里不会提的细节LVGL的LV_COLOR_DEPTH和屏幕驱动里的像素格式必须严格一致如果LVGL用16位色而屏幕设成18位色屏幕会显示偏色甚至花屏但代码逻辑完全正确找问题能找一整天。这套方案做完之后还可以做很多扩展。比如把PL端的AXI Quad SPI换成一个自己写的SPI控制器可以把时钟频率压得更高或者加一个DMA链式描述符实现多块连续显存的高效刷新。也可以在PS端跑Linux用主控节点方式驱动SPI设备配合LVGL的Linux版驱动那就是另一整套玩法了。无论是裸机还是Linux动作都差不多“吃透屏幕IC的手册搞清工具链的工程机制摸熟LVGL的刷新协议”。三件事都理顺了SPI屏幕在Zynq SoC上跑LVGL就是一道很常规的家常菜了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑