☰
基于Zynq SoC与Vitis的SPI屏幕LVGL移植实践
2026/10/20 6:22:18 网站建设 项目流程

最近把一块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 0(CPOL=0, CPHA=0)和Mode 3(CPOL=1, CPHA=1),实际用下来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时的任务划分与优先级

如果跑FreeRTOS,LVGL任务的划分有讲究。主流做法是建三个任务: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-endian,RGB565的颜色值发送时要注意高低字节顺序,否则颜色会红蓝互换。
  • 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就是一道很常规的家常菜了。

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

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

立即咨询