基于Lemon ZYNQ的PS实验十九 用VDMA模块来缓存图像并在HDMI上显示(一)PS端彩条纹的显示

本文介绍如何在ZYNQ上增加VDMA模块,来作为HDMI 图像的缓存并最终在HDMI 上显示

备注 本节难度有点点大,可能会花个把小时,甚至一到两天时间才能完成,请留意每一个细节

  • 此章节内容适用于Lemon ZYNQ主板,如是其他板子请看对应板子目录
  • 本文在 vivado2018.3版本上演示

另外 为了方便之后教程的演示,这里在设置的过程中也同时打开了SD卡的功能(后续教程会跟进SD卡的数据读取显示功能,这里仅作准备工作)

一、背景

之前FPGA的HDMI彩色条纹显示的demo是由FPGA部分的逻辑实时生成的,所以数据流是根据RGB的时序直接给到了HDMI的IP模块,整个显示过程未开辟任何缓存,适合比较简单应用的场合。 但是实际的项目中,我们需要对摄像头传递过来的图像进行实时的处理,但图像的处理速度或者摄像头传递图像的速度和FPGA输出显示的速率不同步,所以需要对处理好的帧进行缓存,又或者当我们在PS上跑LINUX需要带高分辨率的图形界面时,或想用PS控制PL端HDMI显示的内容时,这种情况下,需要我们在系统上开辟一个缓存区域以用于图像的暂存用。

FPGA开辟缓存的方式有很多种,1)可以用FPGA芯片内部的资源开辟BLOCKRAM(优点是实现方便,缺点是FPGA内部的资源有限仅仅可以缓存小分辨率图像) 2)可以是FPGA芯片外部连接的 SDRAM, SRAM,纯FPGA连接的DDR,或者ZYNQ的PL部分连接的DDR 3)如果是ZYNQ平台,也可以是PS端的DDR芯片

我们Smart Zynq板子上硬件电路上有一块256M 16bit (即512MB)的DDR芯片,连接在主芯片的PS部分,也就是上述第三种情况。

PL访问PS上DDR缓存的方式有很多种,其中有一种专门为图像传输而开发的方式是VDMA, 这也就是我们需要用到VDMA的原因

VDMA 用于 AXI Stream 格式的数据 和 Memory Map 格式相互转换, 也就是说 VDMA 提供从 AXI4 域到 AXI4-Stream 域的视频读/写传输功能

芯片内部架构:因为板子上的DDR是直接物理连接到ZYNQ的PS部分上的,所以PS对DDR的访问控制只需要映射DDR的地址,然后对DDR的映射地址进行读写即可,但是如果ZYNQ的PL(FPGA)部分需要访问DDR,则必须要通过AXI_HP端口。

二、实验内容:

接下来用实际工程演示,PS部分通过软件对DDR进行彩色条纹的写入,并使用VDMA模块,让DDR内存中的彩色条纹通过HDMI显示出来

三、工程创建

工程创建的过程可以参考试验一中的内容,这里不再详细描述了。基于Lemon ZYNQ的PS实验一 GPIO之用EMIO方式点亮LED(完整图文)(芯片型号选XC7Z020CLG400-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,数据位宽选择16bit 最后点击“OK”,如下图所示。

5)因为Lemon ZYNQ主板的PS时钟是50M的晶振输入的,所以这里需要把默认的PS输入时钟33.33M改成50M

6)在ZYNQ中设置时钟功能:

找到 设置项目中的 Clock Configuration 选项, 在PL Fabric Clocks 设置自己需要的时钟频率,这里一共有4种频率可以设置 类似于我们的PLL功能。这里FCLK_CLK0我们保留默认的50M时钟,然后增加FCLK_CLK1 150M(用于AXI4 HP高速接口用)

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

8)为了 方便后续 例程的展示,这里增加一个SD的功能(为接下来 TF图片直接在 HDMI 屏上显示做准备工作,本章节用不到)

又因为我们的Lemon ZYNQ 的BANK501 的BANK电压是1.8V,而我们的TF卡资源是接在BANK501上的,所以这里我们需要手动在配置页面中对BANK501的电压进行设置(更改为1.8V)

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

虽说本次工程只用到了 VDMA的 读功能,但是为了方便今后的工程拓展,所以这里仍然保留写的部分功能,并且将帧缓存从默认的3(本例程只是静态图的显示 ,如果是动态的图片显示,需要多帧缓存,画面才不会有撕裂感), 因为我们显示的内容是RGB888,所以 将系统默认的stream Data Width 从32位改成24位。 Line Buffer Depth默认512(只要AXI总线的速率大于显示的数据流的速率,缓存可以改更小),Read Burst Size 修改成64

在系统里增加video out 模块,这个模块的作用是将AXI4-Stream 的数据流转换成标准的RGB888视频格式,可以和我们最后的 HDMI模块RGB2DVI的输入接口相对应

参数设置除了clock mode 修改成独立,其余都保留默认值

Clock Mode代表aclk与vid_io_out_clk两个时钟是否同源。因为HDMI 的显示和aclk的输入这里的时钟域并不同(HDMI显示的像素时钟和ACLK的数据流时钟不同),所以这里选择independent。让两个时钟相互独立。

Timing Mode:因为后面我们要用video timing controller 模块来提供视频时序,所以这里选择slave模式

10)增加Video Timing Controller 模块

video Timing Controller是视频时序控制器,支持AXI-Lite接口协议(可通过该接口在PS端动态调整分辨率,如果是固定分辨率则不需要该接口,这里演示我们不需要动态调整)

在block design图形编辑器里搜索并添加 video timing controller模块,并双击打开配置页

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

在第二页我们设置分辨率,这里我们设置800X600测试用,选择好分辨率,其他行场及消隐信号系统会自动为我们设置好(符合VGA和HDMI的格式规范)

11)增加时钟模块

clocking wizard 可以将输入的时钟倍频和分频到我们需要的频率, 因为HDMI 800X600 需要两个时钟5X 像素时钟和,单倍像素时钟, 根据时序,这两个时钟分别是40M 和200M,所以这里时钟模块我们设置成输出40和200M,输入部分改成50M输入(FCLK0 是50M)

并将复位信号reset 关闭(不复位 该模块一直工作),并使能 locked功能(locked 后面给后级模块使能用)

五、导入并添加digilent 的RGB2DVI模块

想要驱动HDMI部分并输出图像,我们还需要用到 digilent官方设计的一个IP核 RGB2DVI,这个IP核的官方下载地址如下:https://github.com/Digilent/vivado-library,这里为了方便使用,我也把这个IP核下下来放在本站供大家参考(IP版权归digilent所有)vivado-library-master

1) 解压缩这个vivado-library-master压缩包,并将文件夹复制到我们创建的工程目录下(如果放在其他目录,下次我们的工程更改路径,这个IP就会失效)

2)为了节省空间,我们将vivado-library-master的IP路径下除了RGB2DVI之前的其他功能IP都删除了(如果你硬盘空间多,这步可以不做)

3)在Vivado 中点击Setting

4)在弹出的设置窗口,如下图,展开IP选项,选中Packager,在点击里面的加号,选中我们刚刚复制的vivado-library-master目录(这里已提前将目录复制到工程目录下),点击ok

这里需要注意一个事情 RGB2DVI 官方手册上有明确说, 对应的24bit 线序是 RBG的 ,而不是 RGB,而VDMA 的模块输出是RGB的,所以 实验的过程中还需要对 RGB2DVI进行简单的 修改。详见第六部分

六、添加并修改RGB2DVI模块

1)添加刚刚第五步导入的RGB2DVI模块

在block design 增加 RGB2DVI模块(搜索RGB2DVI,点击检索出来的结果就好,需要先完成第四步的导入操作才会显示此IP)

双击我们刚刚生成的RGB2DVI模块进行设置,将Reset active high 的勾取消,也同样取消Generate SerialClk internally from pixel clock。(这里我们外部手工提供串行时钟)

2)因为RGB2DVI的接口是RBG,而我们的 Video out模块输出默认是RGB,所以这里我们需要对RGB2DVI模块进行点微小的修改,右键我们的 RGB2DVI模块,点Edit in IP Packager 对IP进行修改 。(不修改的话,之后显示的颜色蓝色和绿色会反过来)

3)弹出来的窗口点OK 默认就好

4)之后系统会弹出一个新的Vivado 界面,这个新的vivado的程序就是 RGB2DVI的源代码了,我们打开顶层rgb2dvi,拉到最下方,可以看到 23-16位是R, 15-8位是B, 7-0位是G了。甚至在程序里我们都能看到digilent 官方的备注: 因为某种原因,vid_data被封装成RBG

5) 我们直接修改右侧vid_pData()括号中的数字即可,将7 downto 0 和15 downto 8 调换位置。

pDataOut(2) <= vid_pData(23 downto 16); -- red is channel 2
pDataOut(1) <= vid_pData(15 downto 8); -- green is channel 1
pDataOut(0) <= vid_pData(7 downto 0); -- blue is channel 0

6) 之后按绿色箭头,对修改后的程序进行保存和编译。

7) 在Package IP 的界面下 ,点File Groups 点选 Merge Changes from File Groups Wizard

8)之后对程序重新封包,并在弹出的窗口中点YES, 之后这个新的Vivado 界面就会自动被关闭。

9)这里回到我们原先的Vivado界面,可以看到系统提示我们需要更新Rgb2dvi IP,这里我们点Report IP Status

10) 在弹出的界面点Upgrade Selected即可(如果弹出是否编译的界面,点是等待一会就好),至此,我们的Rgb2dvi IP已经修改完成,当前接口顺序已经从RBG修改成RGB了。


七、在Block Design 中对之前添加的模块进行连接

这时我们已经得到了所需要的各个模块(还有些复位模块后面系统会自动添加 先忽略),接下来的工作就是连接各个模块

1) 首先像下图(黄色部分)这样完成 图像数据流部分的连接

2) 将rgb2dvi 的pclk 时钟也与clock wizard 进行连接(pclk 和5pclk 分别对应40m 和200m)

将VTC(Video Timing Controller)模块的clk与像素时钟连接,因为这个clk和像素时钟需要高度同步,同理Axi4-Stream to video_out模块的clk也应该和像素时钟连接,并将rgb2dvi 的pix时钟也与这个信号连接

3) 将AXI4 LITE低速通信的时钟,和clock wizard的输入都连接到FCLK_CLK0的50M时钟上

4)将数据流部分时钟与FCLK_CLK1连接如下图所示

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

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

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

之后系统将自动帮我们连接好剩下的信号线以及添加需要的模块

7) 连接各个模块的使能和复位功能

将VTC ,Video out模块,RGB to DVI三个模块的 en 和ce,和rst_n等 使能复位信号都接到clk模块的locked信号上(作使能和控制复位用)

由于video out模块是高电平复位,而其他模块的复位和使能都是低电平,所以这里还要增加一个反相器来进行匹配,搜索并增加一个VECTOR模块

将逻辑类型改成非门not,size 改成1位

如下图所示 将反相器连接在 locked和 vid_io_out_reset信号中

8) 将HDMI 输出的IO 拉到外部 make external

最终的连接图如下(看似很复杂,其实连线的时候 只需要分清楚各个模块是从高速接口拉出 还是低速的接口,传输的是指令AXI LITE 还是数据流 AXI4 STREAM 还是 像素PIX,熟悉后再连就不会有问题了) 其中很多模块都是自动生成的 图片可能会看不清楚,可以在示例工程里查看(备注 因为我的工程是修改过的,所以 实际你们按流程走下来 rst_ps7_0_100M 可能是 rst_ps7_0_150M,这个只是名字上的区别,都可以用)

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

八、添加约束并对工程进行编译和综合

1) 新增约束文件 用约束文件方式添加管脚定义

set_property PACKAGE_PIN J18 [get_ports {TMDS_0_data_p[2]}]
set_property PACKAGE_PIN K19 [get_ports {TMDS_0_data_p[1]}]
set_property PACKAGE_PIN K17 [get_ports {TMDS_0_data_p[0]}]
set_property PACKAGE_PIN L16 [get_ports TMDS_0_clk_p]

2) 点击绿色箭头RUN 对代码进行编译

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

九、SDK部分配置

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

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

系统将自动打开SDK开发环境

3)创建一个新的空工程,可以取名叫HDMI_VDMA_TEST

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

5)接下来我们需要导入系统自带的demo 程序(因为我们需要里面的一个vdma_api.c 文件)

双击mss文件(展开BSP ,并双击system.mss文件)

6)导入VDMA的例程序,系统会自动打开vdma 的示例工程

7)我们需要里面的一个api 库,按下图所示,拷贝vdma_api.c 文件

8)之后粘贴到我们工程的src路径下

9)(之后 原先创建打开的示例工程就可以删除了,不删除碍眼 因为系统会提示错误,也可以不删无关痛痒)

接下来在我们创建的main.c中 复制以下代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "xil_types.h"
#include "xil_cache.h"
#include "xparameters.h"
#include "xaxivdma.h"
#include "xaxivdma_i.h"


#define VDMA_ID          XPAR_AXIVDMA_0_DEVICE_ID

unsigned int const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR + 0x1000000);

XAxiVdma     vdma;

#define WIDTH	800
#define DEPTH	600

int run_triple_frame_buffer(XAxiVdma* InstancePtr, int DeviceId, int hsize,
		int vsize, int buf_base_addr, int number_frame_count,
		int enable_frm_cnt_intr);

int main(void)
{
	int x, y;
	u8* vdma_buffer;
	vdma_buffer = (u8*) frame_buffer_addr;

	run_triple_frame_buffer(&vdma, VDMA_ID, WIDTH, DEPTH,frame_buffer_addr,0,0);

	for(y=0;y<DEPTH;y++){
		for(x=0;x<WIDTH;x++)
			if (x>=0&&x<(WIDTH/4)*1) {
				*(vdma_buffer+y*WIDTH*3+3*x+0)=0xff;
				*(vdma_buffer+y*WIDTH*3+3*x+1)=0xff;
				*(vdma_buffer+y*WIDTH*3+3*x+2)=0xff;
			}
			else if (x>=(WIDTH/4)*1 && x<(WIDTH/4)*2) {
				*(vdma_buffer+y*WIDTH*3+3*x+0)=0x00;
				*(vdma_buffer+y*WIDTH*3+3*x+1)=0x00;
				*(vdma_buffer+y*WIDTH*3+3*x+2)=0xff;

			}
			else if (x>=(WIDTH/4)*2 && x<(WIDTH/4)*3) {
				*(vdma_buffer+y*WIDTH*3+3*x+0)=0x00;
				*(vdma_buffer+y*WIDTH*3+3*x+1)=0xff;
				*(vdma_buffer+y*WIDTH*3+3*x+2)=0x00;
			}
			else {
				*(vdma_buffer+y*WIDTH*3+3*x+0)=0xff;
				*(vdma_buffer+y*WIDTH*3+3*x+1)=0x00;
				*(vdma_buffer+y*WIDTH*3+3*x+2)=0x00;
			}
	}

	Xil_DCacheFlush();

	while(1);
    return 0;
}

其中run_triple_frame_buffer是初始化VDMA的代码(初始化的参数包括了VDMA实例、VDMA的ID、宽度、高度、vdma起始地址),剩下的代码是向vdma_buffer(对应DDR的缓存变量) 中写入彩色条纹数据,而这个vdma_buffer就是对应VDMA的区域,也就是缓存图像的区域。

十、下载到板子上进行验证

选中工程中的硬件平台,并点击右键→Program FPGA,在弹出的对话框中选择默认,点击“program”,完成FPGA PL部分的Program工作

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

用HDMI 数据线连接主板和 HDMI显示器,正常情况可以看到HDMI显示器上显示着WRGB彩色条纹,证明我们的VDMA 已经工作正常了

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

之后点 APPLY 然后 再选择Run As→1 Launch on Hardware(System Debugger)就可

十一、调整分辨率 (大家自行尝试不再提供原始工程)

上文介绍的是800×600的设置,如果我们要调整输出的分辨率只需要修改3个地方即可(这里仅以1080P做介绍,其他分辨率大家自行尝试)

1)将 clk_out1 修改成1080p的 148.5 ,而clk_out2修改成5倍的clk_out1 即742.5即可。

实际我们输出的Actual 的时钟值742.188M和理论设置值742.5可能会有点区别,但是因为底层都是用的一套倍频分频逻辑,所以clk_out1和clk_out2仍然会保证 5倍分频关系。如下图所示:

2)VTC的 参数修改: 为了把hdmi显示的分辨率修改成1080P,我们这里还需要修改Video Timing Controller 的参数,如下图所示,双击模块,然后将Video Mode 修改成1080p即可

3) 在SDK的main 函数中,将工程的800X600分辨率修改成1920X1080

至此所有的地方都修改完成 ,正常debug 看结果就好,没问题的话,hdmi屏幕会显示和上文一样的彩色条纹,只是分辨率变成了1080P的了。 如果大家1080P的分辨率显示出现问题,也可能是屏幕兼容性的问题,大家可以再尝试下720P分辨率

写在后面:

网络上有部分教程导入了第三方 dynamic clock generator 模块来产生动态时钟,大家有兴趣的自行尝试

另外这里仅使用了库函数的方式来初始化VDMA, 其实还可以通过Xil_Out32的方式直接通过寄存器来配置VDMA, 将在VDMA 第二节 TF卡显示的工程上进行演示

下面是本次实验的完整工程:

备注:因为Smart ZYNQ SP/ SP2/ SL 已经更新了V1.1 V1.2 V1.3三个版本,在最新的V1.3 版本中 HDMI的CLK引脚从 N22修改成N19了, 为了兼容各个版本,所以DEMO中对RGB2DVI 模块进行了改造,可以同时输出两个CLK引脚,这样一套程序就可以兼容所有版本的主板了

  • 本文的完整工程下载:19_VDMA_HDMI_TEST
  • VIVADO的版本:2018.3
  • 工程创建目录:E:\Lemon_ZYNQ\SDK\19_VDMA_HDMI_TEST

发表回复

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