本文介绍如何在本站 ZYNQ 主板上移植LVGL UI系统 (本文包含VIVADO硬件部分,与SDK软件部分内容)
- 此章节内容适用于下列主板
- Lemon ZYNQ 主板 (CLG400封装)(本文提供最终工程)
- Smart ZYNQ SP / SP2 / SL 版的板子(非标准版)(CLG484封装)(本文提供最终工程)
- Smart ZYNQ 标准版(停产) (CLG400封装7010/7020 )本文暂时只提供移植方法
- Tiny ZYNQ(停产) (CLG400封装7010) 本文暂时只提供移植方法
- 至于别家的ZYNQ主板,也可以根据本文的内容进行LVGL的移植
- 本文在 vivado2018.3版本上演示
本贴前后花了很大精力去写了,如需搬运请注明出处 “ hellofpga.com ”。(本贴也可能是全网第一份公开的LVGL 通过DMA搬运点到VDMA的教程,大家如果有遇到问题可在本贴下方进行留言)
关于本次移植的色深选用16bit(RGB565)的说明
备注:为了最大程度地发挥 LVGL 的性能,本文将 LVGL 的色深设置为 16bit(即 RGB565)。这一设置与之前 VDMA 工程使用的 24bit RGB888 色深略有不同。如果大家的工程是基于 VDMA 工程进行修改的,请特别注意调整这部分设置。
LVGL 只支持 1bit、8bit、16bit 和 32bit 的色深,24bit 色深不可直接使用。如果需要开启 RGB888 格式,必须使用 32bit ARGB 模式。然而,使用 32bit 色深会导致数据流处理和内存消耗相较 16bit 色深增加约两倍,这将影响演示的性能。因此,本次演示不使用 32bit 色深。如果您对显示颜色质量有较高要求,可以自行尝试更改为 32bit 模式。
(和之前VDMA章节不同的地方是 VDMA模块里的位宽设置,以及Video out 模块的位宽和格式设置,下文会提到)
关于本次移植LVGL版本是V8.3.10的说明
考虑到现在(2024年12月20日)最新版的 GUI GUIDER(V1.8.1GA) (桌面图形LVGL编辑器)支持的版本是8.3.10,所以本文中的移植将以LVGL 8.3.10版来展开介绍,详细的LVGL下载地址如下 https://github.com/lvgl/lvgl/releases
本次实验需要外接本站的5寸IPS 屏幕模块,有关本次实验的 RGB屏的原理图手册以及其他例程资料可以参看下面汇总贴:5寸IPS 800X480 RGB接口LCD屏幕模块 (理论上本次移植过程支持所有RGB888 的800X480屏幕,触摸IC是GT911)

LVGL简介
LVGL(轻量级图形库)是一款开源的图形用户界面(GUI)库,旨在为嵌入式设备提供高效、低资源消耗的图形显示解决方案。它广泛应用于低功耗、资源受限的嵌入式系统中,支持多种硬件平台,并提供丰富的图形界面组件和动画效果。
此外LVGL还有以下几个特点(翻译自LVGL官网)
它是开源的
LVGL 是完全开源的,这有几个显著的优势。首先,它让你可以完全掌控该库,因为你不仅可以查看、修改、编译和调试底层源代码,还可以完全获取它。一旦下载,它就属于你了。这种不依赖单一供应商的独立性具有巨大的价值。此外,开源还鼓励协作与知识共享,全球的开发者都在为软件的改进做出贡献,使其变得更加可靠并功能更加丰富,以解决现实生活中的各种问题。
它是免费的
LVGL 采用 MIT 许可证发布,允许用户自由使用、修改和分发软件,而无需承担复杂的限制或条件。这为开发者和企业提供了灵活性,可以将该软件集成到自己的项目中,甚至用于商业目的,同时仍需保留对原作者的署名。
关于LVGL的详细信息,可以访问LVGL的官网获得 https://lvgl.io/
本文将重点放在如何将LVGL 移植到本站的ZYNQ 主板上。 坐稳扶好,接下来将是本文的正题。
前言
备注: 为了方便之后的项目拓展,本文在VIVADO工程中除了开启 VDMA和IIC的功能外,还开启了SD卡 ,UART,以及 QSPI FLASH的功能(LVGL演示中用不到,但是今后的项目也许会用到)
关于VDMA点屏,以及触摸屏的部分,在之前的工程中有提到,可以参考(这里以Smart ZYNQ主板为例,本文在VIVADO的创建上会有部分和之前VDMA的工程有重叠)
- 基于Smart ZYNQ (SP/SP2/SL 版) 的PS实验二十一 5寸RGB屏实验:用VDMA模块来缓存图像并在LCD上显示(一)PS端彩条纹的显示
- 基于Smart ZYNQ (SP/SP2/SL 版) 的PS实验二十二 5寸RGB屏实验: 用VDMA模块来缓存图像并在LCD上显示(二)显示TF卡上的BMP格式图片
- 基于Smart ZYNQ (SP/SP2/SL 版) 的PS实验二十三 5寸RGB屏实验: 电容触摸部分DEMO
本文将分硬件和软件两个部分来介绍:
硬件部分
一、Vivado工程创建
工程创建的过程和以往创建方式是一样得,在设置芯片型号的界面,大家根据自己主板实际的芯片型号进行设置:
- Lemon ZYNQ : XC7Z020CLG400-1
- Smart ZYNQ (SP/SP2/SL) : XC7Z020CLG484-1 (这里可以选-2因为 板子实际贴的是-2)
- Smart ZYNQ 标准版(停产) :XC7Z020CLG400-1 / XC7Z010CLG400-1 (按7010/7020版来选择)
- Tiny ZYNQ (停产) :XC7Z010CLG400-1
二、Vivado 中的设置
1)IP INTEGRATOR→Create Block Design,在弹出的对话框中输入设计名,最后点击“OK”,如下图所示

2)在右侧的窗口里 ,点击加号,在选择框里搜索ZYNQ,并找到ZYNQ7 PROCESSING SYSTEM ,双击并打开

3) 软件自动生成了一个 zynq的block 如下图所示,接下来要做一些相应的设置,双击下图中的ZYNQ核

4)依次在弹窗里找到DDR Configuration→DDR Controller Configuration→DDR3,在Memory Part下拉菜单中根据自己板子上的DDR来选择相应的DDR3,本实验所用到型号:MT41K256M16RE-125(512MB),数据位宽选择16bit 最后点击“OK”,如下图所示。(如果您是Tiny 系列主板,这里的DDR需要修改成256MB的,可以看以下列表)
- Lemon ZYNQ 主板 : MT41K256M16RE-125(512MB)选16BIT
- Smart ZYNQ 全系列的板子: MT41K256M16RE-125(512MB)选16BIT
- Tiny ZYNQ(停产) :MT41K128M16JT 125(256MB)选16BIT

5)在ZYNQ中设置时钟功能:
a ) 找到 设置项目中的 Clock Configuration 选项, 在PL Fabric Clocks 设置自己需要的时钟频率,这里一共有4种频率可以设置 类似于我们的PLL功能。这里FCLK_CLK0我们设置成25M(用于LCD 25M DCLK用)FCLK_CLK1设置成50M时钟用于AXI_LITE基础通信,然后增加FCLK_CLK2 150M(用于AXI4 HP高速接口用)

b ) 修改PS的输入主时钟(Clock Configuration 界面中将 Input Frequency 由默认的33.333333 修改成主板对应的PS时钟)
- Lemon ZYNQ 主板 : 需要修改成50M (片外PS晶振是50M)
- Smart ZYNQ 全系列的板子: 保持默认33.3333 不用修改
- Tiny ZYNQ(停产) :保持默认33.3333
(ZYNQ主板默认的PS时钟是33.33M,但是有一些市面上比较火的主板,如Digilent 的PYNQ-Z1 ,TUI的PYNQ-Z2, 以及Digilent 的Arty Z7 等比较流行的ZYNQ主板所使用的PS时钟是50Mhz的)如果是其他型号的主板请按主板上的配置进行修改

6) 在PS-PL接口部分,增加 AXI HP 高速接口

7)为了方便后续 例程的拓展,这里增加SD的功能(本次工程用不到,仅为了方便后续调试用)

8)为了方便后续 例程的拓展,这里增加QSPI FLASH的功能(本次工程用不到,仅为了方便后续调试用)使能QSPI的功能 如下图所示(当QSPI 时钟大于40MHZ的时候 就需要勾选Feedback clk)

9)为了方便后续 例程的拓展,这里增加UART的功能(本次工程用不到,仅为了方便后续调试用)这里各个主板会有区别:
- Lemon ZYNQ 主板 : 使能UART 0 并在IO选项里 选择MIO方式,下拉菜单里选择MIO14-15
- Smart ZYNQ SP / SP2 / SL 版的板子: 使能UART 0 并在IO选项里 选择EMIO方式
- Smart ZYNQ 标准版(停产):使能UART 0 并在IO选项里 选择MIO方式,下拉菜单里选择MIO50-51
- Tiny ZYNQ(停产) 使能UART 0 并在IO选项里 选择EMIO方式
下图是以EMIO 方式为示例:(MIO方式也在该页面修改,修改IO对应的选项即可)

10) 使能I2C 0 并选择EMIO (触摸屏是通过I2C进行控制的)

11) 在GPIO的地方添加3路EMIO GPIO (其中一路控制 LCD的背光,一路控制触摸屏的RST,一路控制触摸屏的 INT脚)

12) 修改PS部分BANK 1 I/O 的BANK电压(BANK 501)
这部分的设置会对SD卡,网络 ,USB部分功能的稳定性产生影响,所以需要将这部分电压按照主板实际的BANK电压先进行设置。(可以查询主板的原理图,或者询问主板的生产厂商)
- Lemon ZYNQ 主板 : 修改成LVCMOS 1.8V
- Smart ZYNQ 全系列主板: 保持默认LVCMOS 3.3V
- Tiny ZYNQ(停产): 保持默认LVCMOS 3.3V

之后点选OK 保存退出ZYNQ的配置页
13) 回到Block Design 界面,对IIC, EMIO GPIO进行 External 引出( 分别右键 GPIO_0 和 IIC_0 选中Make External 引出这两组信号接口)

14)对EMIO UART进行 External 引出 (如果上文中是MIO UART的就不需要进行此步骤)
- MIO UART的主板(不需要此步骤): Lemon ZYNQ , Smart ZYNQ 标准版(停产)
- EMIO UART的主板(需要此步骤) :Smart ZYNQ SP / SP2 / SL 版,Tiny ZYNQ主板
( 右键 UART_0选中Make External 引出UART信号接口 )

14)添加一个VDMA模块,搜索并添加VDMA模块,并双击打开这个模块的配置页

本次工程只用到了 VDMA的 读功能,所以这里勾除了Enable Write Channel功能,帧缓存保留默认的3(实际本例的LVGL只使用2帧缓存), 因为我们显示的内容是RGB565(16位色,之上文有提到),所以 stream Data Width 改为16 (这里和之前的VDMA部分章节不同)。 Line Buffer Depth默认512(只要AXI总线的速率大于显示的数据流的速率,缓存可以改更小),Read Burst Size 修改成64

在系统里增加video out 模块,这个模块的作用是将AXI4-Stream 的数据流转换成标准的视频格式输出,与我们的RGB LCD屏幕对应

clock mode改成independent ,另外Video Format 改成Mona/Sensor(因为没有符合的RGB565格式,只能用这个临时替代) input 和output Width 都修改成16位(这里和之前章节的VDMA实验工程不同)

Clock Mode代表aclk与vid_io_out_clk两个时钟是否同源。因为HDMI 的显示和aclk的输入这里的时钟域并不同(HDMI显示的像素时钟和ACLK的数据流时钟不同),所以这里选择independent。让两个时钟相互独立。
Timing Mode:因为后面我们要用video timing controller 模块来提供视频时序,所以这里选择slave模式
15)增加Video Timing Controller 模块
video Timing Controller是视频时序控制器,支持AXI-Lite接口协议(可通过该接口在PS端动态调整分辨率,如果是固定分辨率则不需要该接口,这里演示我们不需要动态调整)
在block design图形编辑器里搜索并添加 video timing controller模块,并双击打开配置页

因为我们不需要动态配置 分辨率,所以这里把LITE端口关闭,以及取消数据监视功能

在第二页我们设置分辨率,这里我们设置800X480测试用,因为我们的LCD屏幕的800×480分辨率不在预设分辨率之中,所以这里我们手动设置参数。 选择CUSTOM,其他的参数按下图所示填写即可(下列参数是我根据LCD屏幕手册推荐的参数修改得到,DCLK时钟25M情况下可以输出约每秒60帧)

我的LCD手册上800×480对应的时序表,如果是第三方的屏幕大家根据实际手中的屏幕修改(评论区中有人说野火的屏幕这个参数不适用,需要根据手册微调)

五、在Block Design 中对之前添加的模块进行连接
这时我们已经得到了所需要的各个模块(还有些复位模块后面系统会自动添加 先忽略),接下来的工作就是连接各个模块
1) 首先像下图(黄色部分)这样完成 图像数据流部分的连接

2)左键 video out 模块的vid_io_out 端口的“加号”,展开端口的内容,并选中vid_active_video,vid_data,vid_hsync,vid_vsync这几个信号,并右键make external 将信号引出

分别将这些信号重命名为lcd_de,lcd_data,lcd_hsync,lcd_vsync。 (选中信号,在左侧窗格的name 中修改)

3)引出我们的FCLK_CLK0信号作为lcd的lcd_clk信号: 右键ZYNQ模块的 FCLK_CLK0,并选中Make External 引出这个信号

并选中输出的port,将名字修改成lcd_clk

4) 将VTC(Video Timing Controller)模块的clk与25M的DCLK像素时钟(通过ZYNQ的FCLK_CLK0产生)链接,因为这个clk和像素时钟需要高度同步,同理Axi4-Stream to video_out模块的clk也应该和这个像素时钟连接。

5) 将AXI4 LITE低速通信的时钟连接到FCLK_CLK1的50M时钟上

6)将和数据流有关部分的时钟与FCLK_CLK2(150M)连接如下图所示

7) 增加video timing 模块的GEN_CLKEN连线如下图所示

8) 点 Run connection automation 来自动连接剩下的走线

在弹出的设置对话框里勾选内容,如下图所示

之后系统将自动帮我们连接好剩下的信号线以及添加需要的模块
9) 连接各个模块的使能和复位功能
a ) 新添加一个 Processor System Reset 模块(系统之前已经帮我们添加了两个时钟的复位模块(150M的和50M的),但是我们的VTC 和 Video out 等模块都是基于CLK_0也就是25M的,如果这时候把复位信号接到 150M或者50M的复位模块上,系统也能使用,但是会弹出警告以及timing 警告),所以这里才需要额外添加一个基于CLK_0的 复位模块。

这里将Processor System Reset的 slowest_sync_clk连接到 ZYNQ的 FCLK_CLK0上也就是我们的DCLK相关的25M上, ext_reset_in接到ZYNQ的复位输出上。

b )将VTC ,Video out模块的 rst_n复位信号接到我们刚刚添加的process system reset的peripheral_aresetn信号上(低电平复位)
由于video out模块的vid_io_out_reset是高电平复位,我们将这个信号接到刚刚添加的process system reset的peripheral_reset信号上(高电平复位)

c )我们添加一个constant模块来添加一个常量1,用于连接我们各个模块的ce以及aclken 使能信号

之后将这个constant 像下图一样,连接video out 的aclken信号,vid_io_out_ce信号,以及VTC的clken信号

最终的连接图如下(看似很复杂,其实连线的时候 只需要分清楚各个模块是从高速接口拉出 还是低速的接口,传输的是指令AXI LITE 还是数据流 AXI4 STREAM 还是 像素PIX,熟悉后再连就不会有问题了) 其中很多模块都是自动生成的 图片可能会看不清楚,可以在示例工程里查看

10 ) 保存工程,然后点击source→Design Source ,右键我们创建的BLOCK工程,点击create HDL wrapper,打包BLOCK文件并生成.v代码

软件自动为我们生成HDL文件

至此,Block Design 部分已经设置完成。
六、添加我们的FPGA部分顶层TOP_module代码,并例化ZYNQ模块
其实这里我们不增加顶层模块,直接编译综合也是可以,但是考虑到我们的工程后续可能会添加PL部分(即FPGA)的代码,所以这里我们还是增加了顶层模块。
1)在主界面点击左侧 Add Sources ,点击 复选框的Add or create design sources 选项 并点击NEXT

2)在出现的Add Sources 中 选择创建新文件 Create FILE 如下图所示,并在弹出的窗口中 选择类别为Verilog ,在FILE name中填写文件的名称,这里用TopModule代替,点击OK 并点击FINISH
3)在跳出的窗口中可以填写模块的输入输出信号,由于这部分工作在代码中可以完成,所以这里直接点OK 完成VERILOG 文件的创建。
4)双击打开刚才创建的Top_Module文件 ,这里我们可以添加我们的FPGA代码,这里我们暂时不在工程中添加任何其他功能,所以我们只需要对刚刚定义的Block design中创建的ZYNQ模块进行例化即可。
5) 在TopModule程序中例化ZYNQ模块
打开刚才的Top_Module.v,并添加下列代码,里面包括ZYNQ模块的例化部分。(备注 UART MIO 和 UART EMIO的 Top_Module是有点区别的,大家按实际的来选,两端都放上了)
//EMIO UART
//www.hellofpga.com
`timescale 1ns / 1ps
module Top_Module(
output [7:0] LCD_R,
output [7:0] LCD_G,
output [7:0] LCD_B,
output LCD_HS,
output LCD_VS,
output LCD_DE,
output LCD_CLK,
inout LCD_BL,
inout TOUCH_RESET,
inout TOUCH_INT,
inout TOUCH_SDA,
inout TOUCH_SCL,
input UART_RX,
output UART_TX
);
wire [15:0] LCD_DATA_RGB565;
ZYNQ_CORE_wrapper u1(
.lcd_clk(LCD_CLK),
.lcd_data(LCD_DATA_RGB565),
.lcd_de(LCD_DE),
.lcd_hsync(LCD_HS),
.lcd_vsync(LCD_VS),
.GPIO_0_0_tri_io({LCD_BL,TOUCH_RESET,TOUCH_INT}),
.IIC_0_0_scl_io(TOUCH_SCL),
.IIC_0_0_sda_io(TOUCH_SDA),
.UART_0_0_rxd(UART_RX),
.UART_0_0_txd(UART_TX)
);
assign LCD_R={LCD_DATA_RGB565[15:11], 3'b000};
assign LCD_G={LCD_DATA_RGB565[10:5], 2'b00};
assign LCD_B={LCD_DATA_RGB565[4 :0], 3'b000};
endmodule
//MIO UART
//www.hellofpga.com
`timescale 1ns / 1ps
module Top_Module(
output [7:0] LCD_R,
output [7:0] LCD_G,
output [7:0] LCD_B,
output LCD_HS,
output LCD_VS,
output LCD_DE,
output LCD_CLK,
inout LCD_BL,
inout TOUCH_RESET,
inout TOUCH_INT,
inout TOUCH_SDA,
inout TOUCH_SCL
);
wire [15:0] LCD_DATA_RGB565;
ZYNQ_CORE_wrapper u1(
.lcd_clk(LCD_CLK),
.lcd_data(LCD_DATA_RGB565),
.lcd_de(LCD_DE),
.lcd_hsync(LCD_HS),
.lcd_vsync(LCD_VS),
.GPIO_0_0_tri_io({LCD_BL,TOUCH_RESET,TOUCH_INT}),
.IIC_0_0_scl_io(TOUCH_SCL),
.IIC_0_0_sda_io(TOUCH_SDA)
);
assign LCD_R={LCD_DATA_RGB565[15:11], 3'b000};
assign LCD_G={LCD_DATA_RGB565[10:5], 2'b00};
assign LCD_B={LCD_DATA_RGB565[4 :0], 3'b000};
endmodule
肯定有人会问 ZYNQ_CORE 里明明有那么多信号,为啥我都留空了只保留了LCD控制相关信号,其实这里我也偷懒了,因为本章中ZYNQ只额外添加了LCD部分的接口,而剩下诸如DDR FIX_IO这种信号线因为本身是硬件连接的,所以即使程序例化的时候留空,这些信号线仍然是硬件同外部连接的。当然你也可以将所有的信号添加全。(备注 如果有其他的EMIO 或者AXI 等涉及到和FPGA通讯或者映射的信号,则这里例化的时候必须添加信号上去)
这样我们就在Top_Module.v中增加了ZYNQ部分了。 (这里容易产生误区,ZYNQ模块是硬件存在的,并不是实例化凭空出现的,只是通过例化这种方式将PS和顶层模块PL两部分结合在一起了)
七、添加管脚约束
1) 新增约束文件 用约束文件方式添加管脚定义 (因为涉及到多个主板,大家根据自己的主板型号选择) 这里暂时先放Smart ZYNQ(SP/SP2/SL)引脚约束,其他主板可自行修改内容(如果是MIO UART的主板,请把UART相关引脚部分删除)
Smart ZYNQ(SP/SP2/SL)引脚约束(非Smart ZYNQ 标准版):
Smart ZYNQ (SP/SP2/SL) Pin Constraints (Non-Smart ZYNQ Standard Version)
set_property PACKAGE_PIN U22 [get_ports LCD_VS]
set_property PACKAGE_PIN T22 [get_ports LCD_DE]
set_property PACKAGE_PIN W22 [get_ports LCD_CLK]
set_property PACKAGE_PIN V22 [get_ports LCD_HS]
set_property PACKAGE_PIN Y16 [get_ports LCD_BL]
set_property PACKAGE_PIN Y20 [get_ports {LCD_B[7]}]
set_property PACKAGE_PIN Y21 [get_ports {LCD_B[6]}]
set_property PACKAGE_PIN AA22 [get_ports {LCD_B[5]}]
set_property PACKAGE_PIN AB22 [get_ports {LCD_B[4]}]
set_property PACKAGE_PIN AA21 [get_ports {LCD_B[3]}]
set_property PACKAGE_PIN AB21 [get_ports {LCD_B[2]}]
set_property PACKAGE_PIN AB20 [get_ports {LCD_B[1]}]
set_property PACKAGE_PIN AB19 [get_ports {LCD_B[0]}]
set_property PACKAGE_PIN Y19 [get_ports {LCD_G[7]}]
set_property PACKAGE_PIN AA19 [get_ports {LCD_G[6]}]
set_property PACKAGE_PIN AA16 [get_ports {LCD_G[5]}]
set_property PACKAGE_PIN AB16 [get_ports {LCD_G[4]}]
set_property PACKAGE_PIN AA18 [get_ports {LCD_G[3]}]
set_property PACKAGE_PIN Y18 [get_ports {LCD_G[2]}]
set_property PACKAGE_PIN AB15 [get_ports {LCD_G[1]}]
set_property PACKAGE_PIN AB14 [get_ports {LCD_G[0]}]
set_property PACKAGE_PIN AA13 [get_ports {LCD_R[7]}]
set_property PACKAGE_PIN Y13 [get_ports {LCD_R[6]}]
set_property PACKAGE_PIN W13 [get_ports {LCD_R[5]}]
set_property PACKAGE_PIN V13 [get_ports {LCD_R[4]}]
set_property PACKAGE_PIN W17 [get_ports {LCD_R[3]}]
set_property PACKAGE_PIN W18 [get_ports {LCD_R[2]}]
set_property PACKAGE_PIN AB17 [get_ports {LCD_R[1]}]
set_property PACKAGE_PIN AA17 [get_ports {LCD_R[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_VS]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_HS]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_DE]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_CLK]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_BL]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports TOUCH_RESET]
set_property IOSTANDARD LVCMOS33 [get_ports TOUCH_SDA]
set_property IOSTANDARD LVCMOS33 [get_ports TOUCH_SCL]
set_property IOSTANDARD LVCMOS33 [get_ports TOUCH_INT]
set_property PACKAGE_PIN W16 [get_ports TOUCH_RESET]
set_property PACKAGE_PIN AA14 [get_ports TOUCH_SDA]
set_property PACKAGE_PIN Y14 [get_ports TOUCH_SCL]
set_property PACKAGE_PIN V14 [get_ports TOUCH_INT]
set_property IOSTANDARD LVCMOS33 [get_ports UART_RX]
set_property IOSTANDARD LVCMOS33 [get_ports UART_TX]
set_property PACKAGE_PIN M17 [get_ports UART_RX]
set_property PACKAGE_PIN L17 [get_ports UART_TX]
八、对工程进行编译和综合
1) 点击绿色箭头RUN 对代码进行编译

2) 生成bit文件 :按下Generate Bitstream 完成综合以及生成bit文件,等待弹出综合完成的窗口

3)File→Export→Export hardware…,在弹出的对话框中勾选“include bitstream”,点击“OK”确认,如下图所示。


到此,我们的ZYNQ 硬件工程就算创建完成了,接下来是软件移植部分
软件部分
一、下载LVGL的源代码
1)考虑到现在(2024年12月20日)最新版的 GUI GUIDER(V1.8.1GA) (桌面图形LVGL编辑器)支持的版本是8.3.10,所以本文中的移植将以8.3.10版来展开介绍,详细的LVGL下载地址如下 https://github.com/lvgl/lvgl/releases (其余版本的移植过程大同小异,另外后续GUI GUIDER也肯定会支持更新版本的 LVGL,所以下载前最好也去GUI GUIDER官网确认下支持的LVGL最新版本)
2)找到对应的8.3.10版本

3)点击要下载的压缩包即可 (选ZIP)

4)下载完成后对压缩包进行解压
备注:访问github 需要一点点魔法,这里不做介绍了,如果进不去github的,这里也可以在评论区里找8.3.10的下载地址。
二、打开SDK软件
1)File→Export→Export hardware…,在弹出的对话框中勾选“include bitstream”,点击“OK”确认,如下图所示。(Export 这一步之前已经做过)


2)File→Lauch SDK,在弹出的对话框中,保存默认,点击“OK”,如下图所示。

系统将自动打开SDK开发环境
3)创建一个新的空工程,可以取名叫LVGL_DEMO

4)右键工程 的SRC目录,然后新建一个SOURCE FILE,并取名main.c 并点finish 确认

这里的main.c 我们暂时保留 ,后续再做修改。
三、添加我们的触摸屏部分代码
1 )在我们工程DEMO的 src路径下新建一个文件夹 并命名为(gt911)

2 ) 在刚才创建的gt911目录下添加 gt911.c 和gt911.h内容分别如下

gt911.c: (包含触摸屏初始化,以及触摸屏寄存器的读写等操作,这几加个物理水印:内容是本站hellofpga.com 编写)
//www.hellofpga.com
#include "xgpiops.h"
#include "xiicps.h"
#include "sleep.h"
#define RESET_PIN 55
#define INT_PIN 54
#define Slave_Address (0xBA>>1)
static XIicPs i2c;
XGpioPs Gpio;
int gt911_read_reg(uint16_t addr, uint8_t *buf, int len) {
uint8_t send_buf[2];
int status;
// Set the high and low bytes of the register address
send_buf[0] = (uint8_t)(addr >> 8);
send_buf[1] = (uint8_t)(addr & 0xFF);
// Send the read command
status = XIicPs_MasterSendPolled(&i2c, send_buf, 2, Slave_Address);
if (status != XST_SUCCESS) {
return status;
}
// Wait for the I2C bus to be idle
while (XIicPs_BusIsBusy(&i2c));
// Receive data from the I2C bus
status = XIicPs_MasterRecvPolled(&i2c, buf, len, Slave_Address);
if (status != XST_SUCCESS) {
return status;
}
// Wait for the I2C bus to be idle again
while (XIicPs_BusIsBusy(&i2c));
return XST_SUCCESS;
}
int gt911_write_reg(uint16_t addr, uint8_t *buf, int len) {
uint8_t send_buf[256] = {0};
int status;
// Set the high and low bytes of the register address
send_buf[0] = (uint8_t)(addr >> 8);
send_buf[1] = (uint8_t)(addr & 0xFF);
// Copy the data from the buffer into send_buf starting at index 2
memcpy(&send_buf[2], buf, len);
// Send the write command
status = XIicPs_MasterSendPolled(&i2c, send_buf, len + 2, Slave_Address);
if (status != XST_SUCCESS) {
return status;
}
// Wait for the I2C bus to be idle
while (XIicPs_BusIsBusy(&i2c));
return XST_SUCCESS;
}
int gt911_init(void)
{
XIicPs_Config *i2cConfig;
uint8_t buffer[5] = {0};
i2cConfig = XIicPs_LookupConfig(XPAR_XIICPS_0_DEVICE_ID);
XIicPs_CfgInitialize(&i2c, i2cConfig, i2cConfig->BaseAddress);
XIicPs_SetSClk(&i2c, 200000);
XGpioPs_SetDirectionPin(&Gpio, RESET_PIN, 1);
XGpioPs_SetDirectionPin(&Gpio, INT_PIN, 1);
XGpioPs_SetOutputEnablePin(&Gpio, RESET_PIN, 1);
XGpioPs_SetOutputEnablePin(&Gpio, INT_PIN, 1);
//SET 0XBA/0XBB
XGpioPs_WritePin(&Gpio, RESET_PIN, 0);
XGpioPs_WritePin(&Gpio, INT_PIN, 0);
usleep(200); //200us
XGpioPs_WritePin(&Gpio, RESET_PIN, 1);
usleep(55000); //55ms
XGpioPs_SetDirectionPin(&Gpio, INT_PIN, 0);//input_floating
//READ ID
gt911_read_reg(0x8140, buffer, 4);
buffer[3]='\0';
//xil_printf("CTP ID:%s\r\n", buffer);
if(strcmp((char*)buffer,"911")==0){
return 0;
}
return 1;
}
// Define a global structure to store touch point data
typedef struct {
int16_t x;
int16_t y;
} TouchPoint;
// Global variable to store touch point data
static TouchPoint touchPoint = {0, 0};
int read_touch(void) {
uint8_t buf[7] = {0};
// Read touch register data
gt911_read_reg(0x814E, buf, 6);
// Clear touch status
buf[6] = 0x0;
gt911_write_reg(0x814E, &buf[6], 1);
// Check if a touch event is detected
if ((buf[0] & 0x80) && (buf[0] & 0x0F)) {
// Extract and store coordinates
touchPoint.x = (buf[3] << 8) | buf[2]; // X coordinate
touchPoint.y = (buf[5] << 8) | buf[4]; // Y coordinate
return 1; // Return 1 if touch is valid
}
return 0; // Return 0 if no touch is detected
}
void get_touch_coordinates(int16_t *x, int16_t *y) {
// Return the stored coordinates directly
*x = touchPoint.x;
*y = touchPoint.y;
}
//www.hellofpga.com
gt911.h
//www.hellofpga.com
#ifndef __GT911_H
#define __GT911_H
int gt911_init();
int read_touch(void);
void get_touch_coordinates(int16_t *x, int16_t *y);
#endif
之后点保存
四、导入我们的LVGL源码到我们的工程中
1 ) 解压我们刚刚下载的压缩包,得到一个lvgl-8.3.10文件夹

2 )修改这个文件夹名称,将lvgl-8.3.10 改名为 lvgl

3 ) 复制我们的lvgl文件夹

2) 回到我们的SDK界面 ,在我们工程(LVGL_DEMO)的src文件夹 右键选中 Paste 将刚才赋值的LVGL源码粘贴到我们的目标工程src路径下

此时SDK路径会显示红色的警告,这里我们暂时先忽略这些红色的警告
四、修改LVGL源码
1 )修改配置文件 lv_conf(改名前 lv_conf_template)
a ) 修改lv_conf_template.h文件名为 lv_conf.h (可以不修改,但是修改后更专业一些)

b )将lv_conf.h文件拖出拖出lvgl文件夹,放到工程的src目录下

c ) 打开lv_conf.h 将文件的条件编译指令 #if 0 修改成 #if 1

d )修改堆栈大小 这里我们修改成10M(大家根据实际项目进行修改)
#define LV_MEM_SIZE (10 * 1024U * 1024U) /*[bytes]*/

e )如果要查看帧率 可以打开(关于帧率,代码默认限制在33帧,也可以通过修改LV_DISP_DEF_REFR_PERIOD来解锁最高帧率,但是实测,解锁之后部分空闲时间帧率会增高,但是动态的场景帧率反而会因为CPU的使用量变高而降低,所以保留默认帧率33就挺好)
define LV_USE_PERF_MONITOR 1
f )这里我们使能demo LV_USE_DEMO_WIDGETS (这里以WIDGETS DEMO 作为LVGL的演示用)
#define LV_USE_DEMO_WIDGETS 1
2) 修改lv_port_disp文件 (改名前 lv_port_disp_template
)(在lvgl 源码的 examps/poring 目录下)
a)修改lv_port_disp_template
文件名 (在lvgl 源码的 examps/poring 目录下)
- 将
lv_port_disp_template.c
文件名改成lv_port_disp.c
- 将
lv_port_disp_template.h
改成lv_port_disp.h

b) 打开lv_port_disp.h
将文件的条件编译指令 #if 0 修改成 #if 1

c ) 打开 lv_port_disp.c 将
include “lv_port_disp_template.h” 修改成 include “lv_port_disp.h”

d ) 同样的也将 lv_port_disp.c
的文件的条件编译指令 #if 0 修改成 #if 1

e )在lv_port_disp.c文件中inculde VDMA 以及DMA相关的头文件
#include "xparameters.h"
#include "xaxivdma.h"
#include "xaxivdma_i.h"
#include "xdmaps.h"
#include "xscugic.h"
并添加 VDMA_BASEADDR的定义
#define VDMA_BASEADDR XPAR_AXI_VDMA_0_BASEADDR
f ) 修改屏幕分辨率,将MY_DISP_HOR_RES 的值改成800 将MY_DISP_VER_RES的值改成480(这里也可以考虑把warning 警告的那行删除,不然SDK会始终弹出警告)


g ) 修改 lv_port_disp_init 函数
注释掉前两个多行缓存的方案, 本次使用整帧缓存的方案来进行图像的缓存(buffer x 2帧)

并将LV_VER_RES_MAX 修改成MY_DISP_HOR_RES

将disp_drv.draw_buf 的缓存地址定义为 &draw_buf_dsc_3;
disp_drv.draw_buf = &draw_buf_dsc_3;

取消 disp_drv.full_refresh = 1; 这句代码前面的注释(这句话是用在第三种扫描模式下,也就是我们用的双缓存全局扫描模式,不使能这句话则图像只会对修改的部分进行刷新,使能后是对全图进行刷新)

h ) 再在我们打开的lv_port_disp.c中增加下列 DMA,以及VDMA相关的初始化及控制命令。(可以加在刚才我们添加的define VDMA_BASEADDR XPAR_AXI_VDMA_0_BASEADDR 这句代码之后)
#define DMA_DEVICE_ID XPAR_XDMAPS_1_DEVICE_ID
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define DMA_DONE_INTR_0 XPAR_XDMAPS_0_DONE_INTR_0
#define DMA_LENGTH MY_DISP_HOR_RES * (MY_DISP_VER_RES) /2
static int Frame_Buffer1[DMA_LENGTH] __attribute__ ((aligned (32)));
static int Frame_Buffer2[DMA_LENGTH] __attribute__ ((aligned (32)));
static uint8_t frame_flag=0;
void DmaDoneHandler(unsigned int Channel, XDmaPs_Cmd *DmaCmd, void *CallbackRef)
{
if(frame_flag==0) Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000000);
else Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000001);
frame_flag=!frame_flag;
}
XDmaPs DmaInstance;
extern XScuGic GicInstance;
void SetupInterruptSystem(XScuGic *GicPtr, XDmaPs *DmaPtr)
{
XScuGic_Config *GicConfig;
Xil_ExceptionInit();
GicConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
XScuGic_CfgInitialize(GicPtr, GicConfig, GicConfig->CpuBaseAddress);
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, GicPtr);
XScuGic_Connect(GicPtr, DMA_DONE_INTR_0, (Xil_InterruptHandler)XDmaPs_DoneISR_0, (void *)DmaPtr);
XScuGic_Enable(GicPtr, DMA_DONE_INTR_0);
Xil_ExceptionEnable();
}
XDmaPs_Config *DmaCfg;
XDmaPs *DmaInst = &DmaInstance;
XDmaPs_Cmd DmaCmd;
void XDmaPs_init(XScuGic *GicPtr, u16 DeviceId)
{
DmaCfg = XDmaPs_LookupConfig(DeviceId);
XDmaPs_CfgInitialize(DmaInst, DmaCfg, DmaCfg->BaseAddress);
SetupInterruptSystem(GicPtr, DmaInst);
XDmaPs_SetDoneHandler(DmaInst, 0, DmaDoneHandler, NULL);//CHANNEL 0
memset(&DmaCmd, 0, sizeof(XDmaPs_Cmd));
DmaCmd.ChanCtrl.SrcBurstSize = 4;
DmaCmd.ChanCtrl.SrcBurstLen = 4;
DmaCmd.ChanCtrl.SrcInc = 1;
DmaCmd.ChanCtrl.DstBurstSize = 4;
DmaCmd.ChanCtrl.DstBurstLen = 4;
DmaCmd.ChanCtrl.DstInc = 1;
DmaCmd.BD.SrcAddr = (u32) NULL;
DmaCmd.BD.DstAddr = (u32) NULL;
DmaCmd.BD.Length = DMA_LENGTH * sizeof(int);
}
void Vdma_init()
{
Xil_Out32((VDMA_BASEADDR + 0x000), 0x00000001);
Xil_Out32((VDMA_BASEADDR + 0x05c), (uint32_t)Frame_Buffer1);
Xil_Out32((VDMA_BASEADDR + 0x060), (uint32_t)Frame_Buffer2);
Xil_Out32((VDMA_BASEADDR + 0x058), (MY_DISP_HOR_RES * 2));
Xil_Out32((VDMA_BASEADDR + 0x054), (MY_DISP_HOR_RES * 2));
Xil_Out32((VDMA_BASEADDR + 0x050), MY_DISP_VER_RES);
Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000000);
}
这段命令中,我们开辟了两段缓存(Frame_Buffer1,Frame_Buffer2)用来作为VDMA的帧缓存用,我们也添加了DMA相关的初始化命令,以及DMA的中断响应函数(在中断响应函数中进行了帧切换的指令)
i )修改disp_init 显示初始化函数,在这个里面我们调用VDMA和DMA的初始化函数
static void disp_init(void)
{
Vdma_init();
XDmaPs_init(&GicInstance,DMA_DEVICE_ID);
}
j )修改 void disp_flush(),在这里我们根据frame_flag的值来设定当前DMA 的Dst目标地址对应的是哪个VDMA帧缓存区域,并设定DMA 的SrcAddr地址,最后用XDmaPs_Start 开启DMA操作
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
if (disp_flush_enabled) {
if(frame_flag==0)DmaCmd.BD.DstAddr = (u32) Frame_Buffer1;
else DmaCmd.BD.DstAddr = (u32) Frame_Buffer2;
DmaCmd.BD.SrcAddr = (u32) color_p;
XDmaPs_Start(DmaInst, 0, &DmaCmd, 0);
}
lv_disp_flush_ready(disp_drv);
}
这里我们用的一个全局变量frame_flag是来作为双缓冲交替的标记位(防止LVGL 写入 和VDMA 读取在同一个缓冲区导致图像撕裂),之后我们删除原先的点对点转换的代码, 增加了我们DMA搬运数据的代码,这种方式是效率最高的,相当于LVGL直接将计算后的图形用DMA写入到VDMA的缓存分区中,最终在DMA中断函数中实现VDMA显示帧的交替)
k ) 打开lv_port_disp.h把include “lvgl/lvgl.h” 修改成 include “../../lvgl.h”

3 )修改lv_port_fs文件(改名前 lv_port_fs_template)(在lvgl 源码的 examps/poring 目录下)
a)修改lv_port_fs_template
文件名 (在lvgl 源码的 examps/poring 目录下)
- 将
lv_port_fs_template.c
文件名改成lv_port_fs.c
- 将
lv_port_fs_template.h
改成lv_port_fs
.h
b ) 打开 lv_port_fs
.c 将include “lv_port_fs_template.h”修改成include “lv_port_fs.h”

c )因为本工程没有用到文件系统,所以预编译的 #if 0 不变动
4 )修改lv_port_indev文件(改名前 lv_port_indev_template) (在lvgl 源码的 examps/poring 目录下)
lv_port_indev负责lvgl输入的外设部分
a)修改lv_port_indev_template
文件名 (在lvgl 源码的 examps/poring 目录下)
- 将
lv_port_indev_template.c
文件名改成lv_port_indev.c
- 将
lv_port_indev_template.h
改成lv_port_indev
.h
b ) 打开
h 的文件的条件编译指令 #if 0 修改成 #if 1, 并把include “lvgl/lvgl.h”修改成 include “../../lvgl.h”lv_port_indev
.

c ) 打开
c 的文件的条件编译指令 #if 0 修改成 #if 1lv_port_indev
.
d )在
c加入触摸屏部分的头文件 include “../../../gt911/gt911.h” lv_port_indev
.
e )修改touchpad_init 函数,在里面添加我们触摸屏的gt911_init()函数
static void touchpad_init(void)
{
gt911_init();
}
f )修改我们touchpad_is_pressed 与touchpad_get_xy函数,在里面加入我们触摸屏的函数接口
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
return read_touch();
}
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
{
get_touch_coordinates(x, y);
}
g )再进入void lv_port_indev_init(void) 函数, 这里我们暂时只用到了触摸屏的内容,其他内容诸如Mouse,Keypad等你可以按照需求注释掉,以节省资源。(当然不注释问题也不大,这里我们只注释,但是不删除源码,以方便后续我们对工程增加对应的内容)

五 、编写main.c 主程序
1) 打开我们前文中创建的main.c 文件
2) 主程序 主要包括下面内容 ,下面是main.c的主要移植要点, 下面是整个程序的简单框架,其中包括GPIO初始化(用来点亮背光),1ms定时器的初始化,以及1ms定时器的中断响应函数。
a ) 在主程序中,我们调用 gpio、以及timer的初始化, 也调用lv_init();lv_port_disp_init();lv_port_indev_init();lv_demo_widgets(); 对LVGL ,LVGL的显示,LVGL的输入,以及LVGL的DEMO进行初始化
b) 在主循环中我们加入lv_task_handler(); LVGL 的主循环函数,负责运行图形界面的更新和事件处理,是确保 GUI 正常工作的核心部分。
c) 在1ms 中断函数中,我们增加 lv_tick_inc(1);时间基准函数,用于向图形库的时间系统增加时间,这也是lvgl工作的心跳。
void Gpio_Init(void) {
// Configure GPIO for LCD backlight control.
}
void SetupInterruptSystem(XScuGic *GicInstancePtr, XScuTimer *TimerInstancePtr, u16 TimerIntrId) {
// Set up the interrupt system for the timer.
}
static void TimerIntrHandler(void *CallBackRef) {
lv_tick_inc(1);
// Handle timer interrupts.
}
void Timer_Init() {
// Initialize and start the timer.
}
int main(void) {
// Main function to initialize hardware and run the GUI.
Gpio_Init();
Timer_Init();
lv_init();
lv_port_disp_init();
lv_port_indev_init();
lv_demo_widgets();
while (1) {
lv_task_handler();
}
return 0;
}
以下是main.c的完整内容
/*
* main.c
*
* Created on: 2024年12月25日
* Author: Administrator
*/
//www.hellofpga.com
#include "xparameters.h"
#include "xgpiops.h"
#include "xstatus.h"
#include "xplatform_info.h"
#include "xscutimer.h"
#include "Xscugic.h"
#include "lvgl/lvgl.h"
#include "lvgl/examples/porting/lv_port_disp.h"
#include "lvgl/examples/porting/lv_port_indev.h"
#include "lvgl/demos/lv_demos.h"
#include <sleep.h>
#define LCD_BLK 56
#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID
XGpioPs Gpio;
void Gpio_Init(void){
XGpioPs_Config *ConfigPtr;
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
XGpioPs_CfgInitialize(&Gpio, ConfigPtr,ConfigPtr->BaseAddr);
XGpioPs_SetDirectionPin(&Gpio, LCD_BLK, 1);
XGpioPs_SetOutputEnablePin(&Gpio, LCD_BLK, 1);
XGpioPs_WritePin(&Gpio, LCD_BLK, 1);
}
#define TIMER_DEVICE_ID XPAR_XSCUTIMER_0_DEVICE_ID
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define TIMER_IRPT_INTR XPAR_SCUTIMER_INTR
#define TIMER_LOAD_VALUE 0x514c7 //666*1000*1/2 666mhz
XScuGic GicInstance; //GIC
static XScuTimer Timer;//timer
static void SetupInterruptSystem(XScuGic *GicInstancePtr,
XScuTimer *TimerInstancePtr, u16 TimerIntrId);
static void TimerIntrHandler(void *CallBackRef);
void SetupInterruptSystem(XScuGic *GicInstancePtr,XScuTimer *TimerInstancePtr, u16 TimerIntrId){
XScuGic_Config *IntcConfig; //GIC config
Xil_ExceptionInit();
//initialise the GIC
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
XScuGic_CfgInitialize(GicInstancePtr, IntcConfig,IntcConfig->CpuBaseAddress);
//connect to the hardware
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
GicInstancePtr);
//set up the timer interrupt
XScuGic_Connect(GicInstancePtr, TimerIntrId,
(Xil_ExceptionHandler)TimerIntrHandler,
(void *)TimerInstancePtr);
//enable the interrupt for the Timer at GIC
XScuGic_Enable(GicInstancePtr, TimerIntrId);
//enable interrupt on the timer
XScuTimer_EnableInterrupt(TimerInstancePtr);
// Enable interrupts in the Processor.
Xil_ExceptionEnableMask(XIL_EXCEPTION_IRQ);
}
static void TimerIntrHandler(void *CallBackRef){
XScuTimer *TimerInstancePtr = (XScuTimer *) CallBackRef;
lv_tick_inc(1);
XScuTimer_ClearInterruptStatus(TimerInstancePtr);
}
//www.hellofpga.com
void Timer_Init(){
XScuTimer_Config *TMRConfigPtr;
TMRConfigPtr = XScuTimer_LookupConfig(TIMER_DEVICE_ID);
XScuTimer_CfgInitialize(&Timer, TMRConfigPtr,TMRConfigPtr->BaseAddr);
XScuTimer_SelfTest(&Timer);
XScuTimer_LoadTimer(&Timer, TIMER_LOAD_VALUE);
XScuTimer_EnableAutoReload(&Timer);
XScuTimer_Start(&Timer);
SetupInterruptSystem(&GicInstance,&Timer,TIMER_IRPT_INTR);
}
int main(void)
{
Gpio_Init();
Timer_Init();
lv_init();
lv_port_disp_init();
lv_port_indev_init();
lv_demo_widgets();
while(1){
lv_task_handler();
}
return 0;
}
//www.hellofpga.com
至此,我们的移植算是完成了,之后进行我们板子的程序下载和验证。
六、下载到板子上进行验证
选中工程中的硬件平台,并点击右键→Program FPGA,在弹出的对话框中选择默认,点击“program”,完成FPGA PL部分的Program工作

2)选中我们的工程 展开绿色箭头(RUN)右边的图标,选择Run As→1 Launch on Hardware(System Debugger)

正常情况可以看到LCD上显示着LVGL的lv_demo_widgets 这个demo了,如下图所示

备注 :为了方便调试可以按照下列描述进行勾选,这样每次debug的时候就自动重新对PL部分进行配置了(强烈推荐把每个工程都这样设置)


之后点 APPLY 然后 再选择Run As→1 Launch on Hardware(System Debugger)就可
写在后头:
在很以前的时候曾经在STM32H7系列上移植过LVGL, 当时直接LTDC建立了双缓存,之后将双缓存的地址直接交给LVGL进行UI的交替绘制,再在每一页绘制完disp_flush函数中手动指定更改LTDC的地址,实现双BUFFER的无缝切换。
但是在写本次ZYNQ移植LVGL的过程中,我也尝试了类似的操作,给VDMA初始化了两个缓存地址,并将两个地址都交由LVGL进行UI的交替绘制,并在disp_flush函数中()通过修改VDMA的28H地址的后5位来指定切换对应的帧,实现BUFFER的无缝切换, 可是经过反复尝试发现,VDMA在手动指定帧后,它并不会立刻切换到目标帧,而是会继续扫描完之前的帧后,才会切到下一帧,而此过程中上一帧的内容已经被LVGL修改, 但是又由于绘制的过程中DDR和cache中的数据是不一致的,导致最终显示的结果出现大量横条纹(绘制的过程中关闭cache可以解决问题,不过因为vdma切换滞后的原因其实同样会导致图像撕裂,而关闭cache的同时也会导致效率大大降低图像出现严重卡顿),无奈最后才尝试在中间增加DMA这个搬运环节。
网上的demo或者教程大部分是复制像素点的, 但是这个效率太低了 。用了我们的DMA+VDMA的方式处理效率将大大增加。
另外DMA也有长度限制,这个是在DMA手册上看不到的,当DMA_LENGTH超过263167之后(不含263167,这个数据是我查问题时单步试出来的), 会导致DMA执行函数中的LoopCount1大于256,导致DMA底层进入二层嵌套循环,造成DMA底层拒绝工作的情况(会报错DMA operation cannot fit in a 2-level loop for channel 0, please reduce the DMA length or increase the burst size or length)。 但是因为DMA每个点对应的是32位的,而我们的LVGL以及VDMA的点是16位的,所以DMA_LENGTH 实际只需要整个分辨率像素点的一半就可以了。
备注,关于VDMA切换,这里直接用了Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000000);或者Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000001);来进行切换,实际VDMA 28h这个寄存器还包含很多内容,因为我们整个硬件构架只用了VDMA读取功能,所以这里可以偷懒使用这种方法来进行帧的切换,详细内容可以看VDMA的官方手册(如果有VDMA写的部分,这里就不能直接这么操作,而是需要先将当前28h的寄存器内容读出,再把5-0位修改成目标帧号,再将修改后的数据写入回去)。
本贴前后花了很大精力去写了,如需搬运请注明出处 “ hellofpga.com ”。(本贴也可能是全网第一份公开的LVGL 通过DMA搬运点到VDMA的教程,大家如果有遇到问题可在本贴下方进行留言)
创作不易,如果你也喜欢这篇文章 ,可以在本贴评论区中为我留言,也欢迎大家在下方讨论。
本文的完整工程下载:
本文提供Smart ZYNQ SP/SP2/SL主板的LVGL工程和 Lemon ZYNQ主板的LVGL工程(如果是其他ZYNQ主板或者第三方的主板,推荐下载Smart ZYNQ SP/SP2/SL主板的LVGL工程进行移植,对应PS时钟是常用的33.33M)
- 本文的完整工程下载:
- VIVADO的版本:2018.3
- Smart ZYNQ SP/SP2/SL主板:(其他主板推荐下载这个版本进行移植)
- 工程:ZYNQ_LVGL_DEMO_01 (其他ZYNQ主板推荐下载)
- 工程目录:E:\Smart_ZYNQ_SP_SL\LVGL\ZYNQ_LVGL_DEMO_01
- Lemon ZYNQ 主板:
- 工程:ZYNQ_LVGL_DEMO_01 (注意主板的PS时钟对应50M)
- 工程目录:E:\Lemon_ZYNQ\LVGL\ZYNQ_LVGL_DEMO_01
- 其他ZYNQ主板,推荐下载Smart ZYNQ SP/SP2/SL主板的LVGL工程
请问,移植工程的话,Ps侧是不是只需改分辨率和触屏i2c地址就行了。Pl侧如果是1024*600的LCD,时序输出那块该咋做更改呢(翻了LCD,还是不很清楚)
PL测 修改 VTC 的分辨率(如果不是标准分辨率,那就需要根据屏幕手册里的时序去设置VTC), PS测需要修改 DISP 的屏幕分辨率, VDMA的两个缓存空间大小, LVGL的两个BUF区缓存大小, 以及DMA部分一次性搬点的length(这里你的1024 X600 会超出DMA一次性可搬运数据的最大长度, 所以你可能要一次性开辟两个DMA通道来搬运数据), 你提到触摸屏I2C部分内容:如果你的触摸屏上是GT911驱动芯片,那这里就不需要修改
是其他触屏芯片的话是否只改从机地址和读ID那段条件语句就行了
我用的是野火的5寸800×480电容屏,触摸主控是GT917S,代码可以兼容,IIC速度可以到400kHz。VTC里的参数是根据屏幕手册里的Timing Table(一个表格)改的,不改的话屏幕只能显示半边的。
移植效率好高, 我的 timing table 是按照我的屏幕datasheet 里的常规参数填的,里面的HFP HS VS 之类的好像只有几个像素点, 这样25M下帧率也可以做到60HZ, 不过不一定适用于所有屏幕, 你的野火的屏幕table 方便留在评论区么
野火5寸电容屏800X480
像素时钟典型值为33.333MHz(可以改FCLK_CLK0获得)
Video Timing Controller(VTC) IP核参数如下
1. Horizontal Settings
(1) Active Size : 800
(2) Frame Size : 1056
(3) Sync Start : 862
(4) Sync End : 866
2. Frame/Field 0 Vertical Settings
(1) Active Size : 480
(2) Frame Size : 525
(3) Sync Start :510
(4) Sync End : 516
其他参数和教程里的一样
哦哦 ,你这个是VTC 库里标准VGA的 800X480 的timing吧, 但是这个参数下60hz需要的33.333MH,而一般5寸屏之类的屏幕厂家给的手册最大DCLK 都是27M 左右, 33M 有点超范围了,所以LCD屏幕一般还是建议根据手册的 Timing table 来配置,根据timing table 肯定是没问题的,我的LCD是根据下表典型值TYP.(60HZ 下25M DCLK)来配的

不过不管是什么参数,只要能用就好
我的代码中ID读出来 实际并没有用到,这个用作DEBUG的, 实际Init返回的值并没有用到
很多触摸IC都是代码兼容的,你可以对对看
为啥我讲FCLK改成33.33后综合不过呀?V_TC 和 ZYNQcore 报错了。。
把报错贴上来看看
我的芯片是911,但ID读出来是911(int),换乘ASCII码转出来明显不对
我感觉你应该是已经读出来了吧 都911了 你再对对看
作者你好,其实我的移植还没完全成功。我是基于Vitis2024.1平台开发的,仅处理Cmake的问题(lvgl库的链接)就折磨了我两天,才让代码编译成功。
现在有个问题想问一下:
1、我的xparameters.h文件里只有DMA0的基地址0xf8003000,没有DMA1相关的宏定义。
2、如果用DMA0的基地址去运行程序的话,会卡死在初始化中。用debug观察发现是因为XDmaPs_LookupConfig这个函数返回了0,导致程序进入无限循环,说明我DMA的初始化失败了。
3、为什么没有DMA1呢,还是说DMA1需要在Vivado里面开启?我看了Vitis的板级支持包(BSP)里面的DMA示例程序也是没看明白。
问题解决了,屏幕现在可以显示lvgl界面了。程序卡死的原因是:我把DMA的设备ID和基地址ADDR搞混了,通过查阅资料我发现这个DMA的设备ID因该是 0x0 而不是基地址0xf8003000,很可惜Vitis2024.1里面的xparameter.h里面不会自动生成XPAR_XDMAPS_1_DEVICE_ID这个宏定义,让我花了好多精力去找问题在哪,这边再次建议初学者不要像我一样使用Vitis2024去学习ZYNQ,真的会遇到很多莫名其妙的问题,老老实实用SDK就很好。
感谢经验分享,我选择VIVADO2018.3是因为 xilinx官网之前很多有用的老资料都是基于2018.3来写的,再加上2018.3是最后一个用sdk的版本,以及2018.3占用空间比较小,所以网站上的资料都是基于2018.3整的
哈喽,贴主你好呀,我按照您的教程成功移植了LVGL例程画面,但是美中不足的是屏幕的触屏似乎用不了,我用的是GT911的芯片,像是IIC地址那些我看了您上面的评论应该是不相干的。请问接下来我应该从哪些方面下手调试?
检查下你屏幕的硬件电路上IIC 是否带上拉电阻吧,另外GT911的SLAVE ADDRESS 和 RST 和INT的上电时序也有关系
Up,你好。我想问下VDMA 数据位宽设置跟RGB565 ARGB8888设置有关吗?我自己判断出来应该是没有关系的。因为我看别人VDMA设置的24位宽,但lv_conf.h设置的是ARGB8888,也能输出。这是我移植过程中的问题
你可以设置VDMA 为24BIT , 然后LV_CONF.H 也设置成ARGB8888
但是你的数据里面会有无用的数据,你就不能直接用DMA来搬点了, 只能自己些for循环去逐个搬点, 逐个搬点 你想怎么改数据都可以,但是效率就低了