本文介绍如何在ZYNQ上增加VDMA模块,来作为HDMI 图像的缓存并最终在HDMI 上显示
备注 本节难度有点点大,可能会花个把小时,甚至一到两天时间才能完成,请留意每一个细节
本节内容下的教程是基于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芯片
Tiny Zynq板子上硬件电路上有一块128M 16bit 的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显示出来
新建一个项目
1)打开Vivado 新建一个项目, 新建一个VIVADO 工程,打开软件 选中Create Project, 如下图所示
2)点击NEXT ,在出现的第二个对话框“Project name”中输入工程名;在“Project location”中选择保存路径;勾选“Create project subdirectory”(默认),最后点击“Next” 备注,所有的路径均不能出现中文名称
3)点击 RTL PROJECT 选项,点击NEXT
4) 第四步Add Sources 选项直接留空,NEXT
5)第五步Add Constraints 选项直接留空,NEXT
6)选择芯片型号 我们板子上用的芯片是XC7Z010 ,并在列表栏中选择对应的封装型号,完整型号是XC7Z010CLG400-1 如下所示,选中后点NEXT
7)确认所选信息 点击“Finish”,完成vivado的工程创建
之后 工程就新建好了, vivado 进入到开发界面
Block Design 部分创建
此处需要创建一个ZYNQ CORE ,并在ZYNQ CORE中设置DDR 以及UART等参数
1)创建一个BLOCK设计
2)搜索并添加ZYNQ7 Processing System,添加ZYNQ7 PROCESSING SYSTEM模块
3)软件自动生成了一个 zynq的block 如下图所示,接下来要做一些相应的设置,双击下图中的ZYNQ核
4)在zynq中设置DDR功能:
依次在弹窗里找到DDR Configuration→DDR Controller Configuration→DDR3,在Memory Part下拉菜单中根据自己板子上的DDR来选择相应的DDR3,本实验所用到型号:MT41K128M16JT 125,数据位宽选择16bit 最后点击“OK”,如下图所示。
4)在ZYNQ中设置时钟功能:
找到 设置项目中的 Clock Configuration 选项, 在PL Fabric Clocks 设置自己需要的时钟频率,这里一共有4种频率可以设置 类似于我们的PLL功能。这里我们设置50M时钟,然后增加FCLK_CLK1 150M(用于AXI4 HP高速接口用)
5) 在PS-PL接口部分,增加 AXI HP 高速接口
6)为了 方便后续 例程的展示,这里增加一个SD的功能(为接下来 TF图片直接在 HDMI 屏上显示做准备工作,本章节用不到)
6)添加一个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模块DVI_Transmitter的输入接口相对应
参数设置除了clock mode 修改成独立,其余都保留默认值
Clock Mode代表aclk与vid_io_out_clk两个时钟是否同源。因为HDMI 的显示和aclk的输入这里的时钟域并不同(HDMI显示的像素时钟和ACLK的数据流时钟不同),所以这里选择independent。让两个时钟相互独立。
Timing Mode:因为后面我们要用video timing controller 模块来提供视频时序,所以这里选择slave模式
7)添加HDMI 的 DVI 模块
想要驱动HDMI模块,我们还需要用到一个DVI_Transmitter模块(和RGB2DVI模块的功能几乎完全一致,使用方法也几乎一样,其实这里也可以用RGB2DVI替换)
这里为了方便使用,我也把这个IP核下下来放在本站供大家参考
下载下来后进行解压缩
接着在工程中导入刚刚下载下来的IP模块
点击Setting
在弹出的设置窗口,如下图,展开IP选项,选中Packager,在点击里面的加号增加目录,选中DVI_Transmitter的目录,点击ok
选择目录点select ,
之后点ok确认 apply 应用,然后OK推出就好
在block design 增加 DVI_Transmitter模块(搜索dvi,点击检索出来的结果就好)
8)增加Video Timing Controller 模块
video Timing Controller是视频时序控制器,支持AXI-Lite接口协议(可通过该接口在PS端动态调整分辨率,如果是固定分辨率则不需要该接口,这里演示我们不需要动态调整)
在block design图形编辑器里搜索并添加 video timing controller模块,并双击打开配置页
因为我们不需要动态配置 分辨率,所以这里把LITE端口关闭,以及取消数据监视功能
在第二页我们设置分辨率,这里我们设置800X600测试用,选择好分辨率,其他行场及消隐信号系统会自动为我们设置好(符合VGA和HDMI的格式规范)
9)增加时钟模块
clocking wizard 可以将输入的时钟倍频和分频到我们需要的频率, 因为HDMI 800X600 需要两个时钟5X 像素时钟和,单倍像素时钟, 根据时序,这两个时钟分别是40M 和200M,所以这里时钟模块我们设置成输出40和200M,输入部分改成50M输入(FCLK0 是50M)
并将复位信号reset 关闭(不复位 该模块一直工作),并使能 locked功能(locked 后面给后级模块使能用)
这时我们已经得到了所需要的各个模块(还有些复位模块后面系统会自动添加 先忽略),接下来的工作就是连接各个模块
首先像下图这样完成 图像数据流部分的连接
将DVI 的pix时钟也与clock wizard 进行连接(pclk 和5pclk 分别对应40m 和200m)
将AXI4 LITE低速通信的时钟,和clock wizard的输入都连接到FCLK_CLK0的50M时钟上
将timing模块的clk与像素时钟连接,因为这个clk和像素时钟需要高度同步,同理video_out模块的clk也应该和像素时钟连接,并将DVI 的pix时钟也与这个信号连接
同理,将数据流部分时钟与FCLK_CLK1连接如下图所示
增加video timing 模块的GEN_CLKEN连线如下图所示
点 Run connection automation 来自动连接剩下的走线
在弹出的设置对话框里勾选内容,如下图所示
之后系统将自动帮我们连接好剩下的信号线以及添加需要的模块
连接各个模块的使能和复位功能
将VTC 的 clken ,Video out模块,RGB to DVI的 en 和ce,和rst_n 使能都接到clk模块的locked信号上(作使能和控制复位用)
由于video out模块存在高电平复位,而其他模块的复位和使能都是低电平,所以这里还要增加一个反相器来进行匹配
搜索并增加一个VECTOR模块
将逻辑类型改成非门not,size 改成1位
如下图所示 将反相器连接在 locked和 vid_io_out_reset信号中
将HDMI 输出的IO 拉到外部 make external
最终的连接图如下(看似很复杂,其实连线的时候 只需要分清楚各个模块是从高速接口拉出 还是低速的接口,传输的是指令AXI LITE 还是数据流 AXI4 STREAM 还是 像素PIX,熟悉后再连就不会有问题了) 其中很多模块都是自动生成的 图片可能会看不清楚,可以在示例工程里查看
(备注 因为我的工程是修改过的,所以 实际你们按流程走下来 rst_ps7_0_100M 可能是 rst_ps7_0_150M,这个只是名字上的区别,都可以用)
保存工程,然后点击source→Design Source ,右键我们创建的BLOCK工程,点击create HDL wrapper,打包BLOCK文件并生成.v代码
点击绿色箭头RUN 对代码进行编译
过程会报警告,直接无视就可
点击RTL 中的SCHEMATIC , 并选择右边出现的 IO Ports 来增加HDMI的管脚定义
修改HDMI 对应的管脚定义,如下图所示 ,修改后保存(如弹出 窗口需要,则在窗口中输入约束文件名,然后保存) TMDS[2]对应 L19, TMDS[1]对应 M19 ,TMDS[0]对应 V20 ,CLK 对应 W18
按下Generate Bitstream 完成综合以及生成bit文件
4 SDK部分配置
1)File→Export→Export hardware…,在弹出的对话框中勾选“include bitstream”,点击“OK”确认,如下图所示。
2)File→Lauch SDK,在弹出的对话框中,保存默认,点击“OK”,如下图所示。
系统将自动打开SDK开发环境
. 创建一个新的空工程,可以取名叫HDMI_VDMA_TEST
b.右键工程 的SRC目录,然后新建一个SOURCE FILE,并取名main.c
接下来我们需要导入系统自带的demo 程序(因为我们需要里面的一个vdma_api.c 文件)
双击mss文件(展开BSP ,并双击system.mss文件)
导入VDMA的例程序,系统会自动打开vdma 的示例工程
我们需要里面的一个api 库,按下图所示,拷贝vdma_api.c 文件
之后粘贴到我们工程的src路径下
(之后 原先创建打开的示例工程就可以删除了,不删除碍眼 因为系统会提示错误,也可以不删无关痛痒)
接下来在我们创建的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的区域,也就是
6.下载到板子上进行验证
选中工程中的硬件平台,并点击右键→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)就可
以下是完整工程如下(仅供学习参考):
备注 :网络上有部分教程导入了第三方 dynamic clock generator 模块来产生动态时钟,我也做了些尝试,后来发现因为板子的HDMI 上的信号跨始终域了,导致使用第三方模块dynamic clock generator 的时候会布线报错。所以大家 尽量用clock wizard模块来产生时钟。
本文仅演示了800X600分辨率的,下一节会对VDMA部分进行补充,以适配720P 和1080P
另外这里仅使用了库函数的方式来初始化VDMA, 其实还可以通过Xil_Out32的方式直接通过寄存器来配置VDMA, 将在VDMA 第三节 TF卡显示的工程上进行演示