基于本站 ZYNQ 主板的 LVGL(V8.3.10版本)的手把手移植教程(VDMA+DMA方式)

本文介绍如何在本站 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版本上演示

关于本次移植的色深选用16bit(RGB565)的说明

备注:为了最大程度地发挥 LVGL 的性能,本文将 LVGL 的色深设置为 16bit(即 RGB565)。这一设置与之前 VDMA 工程使用的 24bit RGB888 色深略有不同。如果大家的工程是基于 VDMA 工程进行修改的,请特别注意调整这部分设置。

LVGL 只支持 1bit8bit16bit32bit 的色深,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的工程有重叠)

本文将分硬件和软件两个部分来介绍:


硬件部分

一、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的,可以看以下列表)

  • 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时钟)

  • 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

此图片的 alt 属性为空;文件名为 image-5.png

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 ) 打开 lv_port_indev.h 的文件的条件编译指令 #if 0 修改成 #if 1, 并把include “lvgl/lvgl.h”修改成 include “../../lvgl.h”

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

d )在lv_port_indev.c加入触摸屏部分的头文件 include “../../../gt911/gt911.h”

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工程

“基于本站 ZYNQ 主板的 LVGL(V8.3.10版本)的手把手移植教程(VDMA+DMA方式)”的19个回复

  1. 请问,移植工程的话,Ps侧是不是只需改分辨率和触屏i2c地址就行了。Pl侧如果是1024*600的LCD,时序输出那块该咋做更改呢(翻了LCD,还是不很清楚)

    1. PL测 修改 VTC 的分辨率(如果不是标准分辨率,那就需要根据屏幕手册里的时序去设置VTC), PS测需要修改 DISP 的屏幕分辨率, VDMA的两个缓存空间大小, LVGL的两个BUF区缓存大小, 以及DMA部分一次性搬点的length(这里你的1024 X600 会超出DMA一次性可搬运数据的最大长度, 所以你可能要一次性开辟两个DMA通道来搬运数据), 你提到触摸屏I2C部分内容:如果你的触摸屏上是GT911驱动芯片,那这里就不需要修改

        1. 我用的是野火的5寸800×480电容屏,触摸主控是GT917S,代码可以兼容,IIC速度可以到400kHz。VTC里的参数是根据屏幕手册里的Timing Table(一个表格)改的,不改的话屏幕只能显示半边的。

          1. 移植效率好高, 我的 timing table 是按照我的屏幕datasheet 里的常规参数填的,里面的HFP HS VS 之类的好像只有几个像素点, 这样25M下帧率也可以做到60HZ, 不过不一定适用于所有屏幕, 你的野火的屏幕table 方便留在评论区么

          2. 野火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
            其他参数和教程里的一样

          3. 哦哦 ,你这个是VTC 库里标准VGA的 800X480 的timing吧, 但是这个参数下60hz需要的33.333MH,而一般5寸屏之类的屏幕厂家给的手册最大DCLK 都是27M 左右, 33M 有点超范围了,所以LCD屏幕一般还是建议根据手册的 Timing table 来配置,根据timing table 肯定是没问题的,我的LCD是根据下表典型值TYP.(60HZ 下25M DCLK)来配的
            不过不管是什么参数,只要能用就好
            LCD timing table

        2. 我的代码中ID读出来 实际并没有用到,这个用作DEBUG的, 实际Init返回的值并没有用到
          很多触摸IC都是代码兼容的,你可以对对看

  2. 作者你好,其实我的移植还没完全成功。我是基于Vitis2024.1平台开发的,仅处理Cmake的问题(lvgl库的链接)就折磨了我两天,才让代码编译成功。
    现在有个问题想问一下:
    1、我的xparameters.h文件里只有DMA0的基地址0xf8003000,没有DMA1相关的宏定义。
    2、如果用DMA0的基地址去运行程序的话,会卡死在初始化中。用debug观察发现是因为XDmaPs_LookupConfig这个函数返回了0,导致程序进入无限循环,说明我DMA的初始化失败了。
    3、为什么没有DMA1呢,还是说DMA1需要在Vivado里面开启?我看了Vitis的板级支持包(BSP)里面的DMA示例程序也是没看明白。

    1. 问题解决了,屏幕现在可以显示lvgl界面了。程序卡死的原因是:我把DMA的设备ID和基地址ADDR搞混了,通过查阅资料我发现这个DMA的设备ID因该是 0x0 而不是基地址0xf8003000,很可惜Vitis2024.1里面的xparameter.h里面不会自动生成XPAR_XDMAPS_1_DEVICE_ID这个宏定义,让我花了好多精力去找问题在哪,这边再次建议初学者不要像我一样使用Vitis2024去学习ZYNQ,真的会遇到很多莫名其妙的问题,老老实实用SDK就很好。

      1. 感谢经验分享,我选择VIVADO2018.3是因为 xilinx官网之前很多有用的老资料都是基于2018.3来写的,再加上2018.3是最后一个用sdk的版本,以及2018.3占用空间比较小,所以网站上的资料都是基于2018.3整的

  3. 哈喽,贴主你好呀,我按照您的教程成功移植了LVGL例程画面,但是美中不足的是屏幕的触屏似乎用不了,我用的是GT911的芯片,像是IIC地址那些我看了您上面的评论应该是不相干的。请问接下来我应该从哪些方面下手调试?

  4. Up,你好。我想问下VDMA 数据位宽设置跟RGB565 ARGB8888设置有关吗?我自己判断出来应该是没有关系的。因为我看别人VDMA设置的24位宽,但lv_conf.h设置的是ARGB8888,也能输出。这是我移植过程中的问题

    1. 你可以设置VDMA 为24BIT , 然后LV_CONF.H 也设置成ARGB8888
      但是你的数据里面会有无用的数据,你就不能直接用DMA来搬点了, 只能自己些for循环去逐个搬点, 逐个搬点 你想怎么改数据都可以,但是效率就低了

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注