本课程将对Xilinx提供的一款IP核——AXI VDMA(Video Direct Memory Access) 进行详细讲解,为后续的学习和开发做好准备。内容安排如下:首先分析为什么要使用VDMA、VDMA的作用;然后详细介绍VDMA的特点、寄存器作空间; 最后阐述如何使用VDMA,包括IP核的配置方法、代码编写流程等。
本章主要是理论学习,学习完本章,会对VDMA有全面的认识,有利于学习后续的图像生成、视频采集处理系统。由于VDMA主要用于视频流数据的存取,单独测试的意义不大,所以在接下来的章节会提供一些样例设计,进一步学习如何使用VDMA。
在讲解VDMA之前,先来探讨一下为什么要学习和使用VDMA,以明确学习目的。由于使用VDMA可以方便地实现双缓冲和多缓冲机制,所以本小节引入了帧缓存和缓冲机制的概念。另外,VDMA可以很好地契合Zynq内部架构,缩短开发周期。再加上VDMA本身能够高效地实现数据存取,所以在基于Zynq(也包括其他Xilinx FPGA)图像、视频处理系统中,VDMA可谓是必不可少的。
帧缓冲存储器(Frame Buffer):简称帧缓存或显存,它是屏幕所显示画面的一个直接映象,又称为位映射图(Bit Map)或光栅。帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。
在开发者看来,FrameBuffer 是一块显示缓存,往显示缓存中写入特定格式的数据就意味着向屏幕输出内容。所以说FrameBuffer就是一块画布,系统在画布上绘制好画面之后,就可以通知显示设备读取Frame Buffer进行显示了。
注意,笔者这里所说的Frame Buffer和Linux的Frame Buffer不是同一个概念,这里仅指显示缓存(画布)本身,并不是Linux下的一个设备。
最早解释多缓冲区如何工作的方式,是通过一个现实生活中的实例来解释的。在一个阳光明媚的日子,你想将水池里的水打满,而又找不到水管的时候,就只能用手边的木桶来灌满水池。水桶满了之后,关掉水龙头,将水提到水池旁边,倒进去,然后走回到水龙头。重复上述工作,如此往复直到将水池灌满。这就类似单缓冲工作过程,当你想将木桶里的水倒出的时候,你必须关掉水龙头。
现在假设你用两个木桶来做上面的工作。你会注满第一个木桶然后将第二个木桶换到水龙头下面,这样,在第二个水桶注满的时间内,你就可以将第一个木桶里面的水倒进水池里面,当你回来的时候,你只需要再将第一个木桶换下第二个注满水木桶,当第一个木桶开始注水的时候你就将第二个木桶里面的水倒进水池里面。重复这个过程直到水池被注满。很容易看得到用这种技术注满水池将会更快,同时也节省了很多等待木桶被注满的时间,而这段时间里你什么也做不了,而水龙头也就不用等待从木桶被注满到你回来的这段时间了。
当你雇佣另外一个人来搬运一个被注满的木桶时,这就有点类似于三个缓冲区的工作原理。如果将搬运木桶的的时间很长,你可以用更多的木桶,雇佣更多的人,这样水龙头就会一直开着注满木桶了。
在计算机图形学中,双缓冲是一种画图技术,使用这种技术可以使得画图没有(至少是减少)闪烁、撕裂等不良效果,并减少等待时间。
双缓冲机制的原理大概是:所有画图操作将它们画图的结果保存在一块系统内存区域中,这块区域通常被称作“后缓冲区(back buffer)”,当所有的绘图操作结束之后,将整块区域复制到显示内存中,这个复制操作通常要跟显示器的光栈束同步,以避免撕裂。双缓冲机制必须要求有比单缓冲更多的显示内存和CPU消耗时间,因为“后缓冲区”需要显示内存,而复制操作和等待同步需要CPU时间。
基于双缓冲机制可以实现页交换,页交换初始状态如下图所示:
如上图所示,此时由于处于初始状态,画图操作的结果都在后缓冲区中,而屏幕上显示的则是前缓冲区中的内容。此时画图操作尚未完成,画图操作完成之后,页转换操作开始执行,示意图如下图所示:
如上图所示,画图操作结束,下一个画图操作的结果保存对象指向前缓冲区,屏幕的显示对象指向后缓冲区,此时前缓冲区变成实际意义上的后缓冲区,后缓冲区变成实际意义上的前缓冲去,即实现“页交换”操作。
有时候也在页交换链中设置多个“后缓冲区”,这是就需要多缓冲区机制的支持。
在Zynq芯片内部,PS和PL是共享DDR控制器的。PS访问DDR十分简单,只要操作DDR映射的虚拟地址即可。对于PL而言,要接入DDR,必须通过AXI_HP端口。
Zynq共有四个AXI_HP通道,通道数据宽度可以配置为32位或64位,这些接口通过FIFO控制器连接PL到存储接口上,其中有两条连接到DDR存储控制器上,还有一条是连接到双端口的OCM上的,下图是AXI_HP访问DDR和OCM的连接图。
由上图可以看出,AXI_HP接口也是遵循AXI协议的,因此利用VDMA可以直接连接HP端口。除了使用VDMA,当然也可以自己开发出符合AXI协议的IP,但是综合考虑设计成本,没太有必要自己实现。此外,自己实现的IP功能也不见得比VDMA强大。
VDMA数据接口可以分为读、写通道,用户可以通过写通道将AXI-Stream类型的数据流写入DDR3,通过读通道可以从DDR3读取数据,并以AXI-Stream类型的格式输出。由此可知,VDMA本质上是一个数据搬运IP,为数据进、出DDR3提供了一种便捷的方案。
将数据存入DDR之后,CPU就可以进行一些处理(缩放、裁剪等),然后再送至显示设备,达到期望的应用目的。当然,也可能是简单地对捕获的视频进行解析,将数据存入帧缓存,以供显示。
VDMA可以控制多达32个帧存,并可以自由地进行帧存切换,所以就能够轻松地实现双缓冲和多缓冲操作。这也是一个很重要的特性,在后续进行系统设计的时候,通常是采用多缓冲的方式实现显示。
由以上分析可以发现,在基于Zynq的图像、视频处理系统中使用VDMA是十分有必要的。
AXI VDMA是Xilinx提供的软核IP,用于将AXI Stream格式的数据流转换为Memory Map格式或将Memory Map格式的数据转换为AXI Stream数据流,从而实现与DDR3进行通信。
许多视频类应用都需要帧缓存来处理帧率变化或者进行图像的缩放、裁剪等尺寸变换操作。AXI VDMA设计的初衷就是用于高效地实现AXI4-Stream视频流接口和AXI4接口之间的数据传输。
VDMA的关键特性&优势有以下几点:
Ø 使视频流能够高带宽直接接入内存
Ø 高效的二维DMA操作
Ø 独立的异步读写通道操作
Ø Gen-Lock帧存同步机制
Ø 最多支持32个帧存
Ø 支持视频格式动态切换
Ø 猝发长度和行缓存深度可调节
Ø 处理器可以控制IP的初始化、状态、中断和管理寄存器
Ø 基础AXI流数据位宽为8的整数倍,如8,16,24,32等,最大可达1024个位
AXI VDMA框图如下所示。
主要有以下几种接口类型:
Ø AXI-lite: PS通过该接口来配置VDMA
Ø AXI Memory Map write:映射到存储器写
Ø AXI Memory Map read:映射到存储器读
Ø AXI Stream Write(S2MM):AXI Stream视频流写入图像
Ø AXI Stream Read(MM2S):AXI Stream视频流读出图像
从框图中可以看出,VDMA主要由控制和状态寄存器、数据搬运模块、行缓冲这几部分构成。数据进出DDR要经过行缓冲进行缓存,然后由数据搬运模块写入或者读出数据。数据搬运模块具体如何工作,由相关寄存器负责控制。VDMA的工作状态可以通过读取状态寄存器进行获取。
各种总线都有自己的时钟信号,不用特别说明,需要指出的是,这些时钟是异步的,并不需要用同一个时钟。但在设计过程中,如无特别需求,可以使用相同的时钟,以降低设计难度。
同步复位信号axi_resetn,同步时钟为s_axi_lite_aclk,低电平有效(至少要保持16个时钟周期的低电平,才能够生效),有效时复位整个IP核。
l AXI4-Lite接口(S_AXI_LITE)
l AXI4读接口(M_AXI_MM2S)
l AXI4写接口(M_AXI_S2MM)
l AXI4-Stream主接口(M_AXI_MM2S)
l AXI4-Stream从接口(S_AXI_S2MM)
前缀S_、M_分别表示Slave和Master;后缀MM2S、S2MM说明数据流向是从memory map到stream还是从stream到memory map。具体每个接口所包含的信号,在基础篇第20章已有介绍,此处不再重复。
信号名称 |
方向 |
详细描述 |
mm2s_fsync |
Frame Sync |
MM2S帧同步输入。使能该信号后,VDMA操作开始于fsync每个下降沿。该信号至少要持续一个m_axis_mm2s_aclk时钟周期 |
s2mm_fsync |
Frame Sync |
S2MM帧同步输入。使能该信号后,VDMA操作开始于fsync每个下降沿。该信号至少要持续一个s_axis_s2mm_aclk时钟周期 |
在下一节将详细介绍这些信号的作用和应用场合。
信号名称 |
方向 |
详细描述 |
mm2s_frame_ptr_in(5:0) |
输入 |
输入的帧编号 |
mm2s_frame_ptr_out(5:0) |
输出 |
输出当前帧的编号 |
s2mm_frame_ptr_in(5:0) |
输入 |
输入的帧编号 |
s2mm_frame_ptr_out(5:0) |
输出 |
输出当前帧的编号 |
在讲述寄存器时,需要设定和显示(帧存)相关的参数,为了方便读者的理解,这里先介绍VDMA数据存放框架,如下图所示,黑色实线内的区域为实际存储画面的帧存。
图中H_STRIDE代表水平方向上的跨度,H_SIZE表示水平方向数据总量,V_SIZE表示竖直方向总共有多少行。
至于帧存内部数据如何组织,就取决于软件代码和硬件逻辑如何匹配了,通常来讲,数据存放格式为RGB+Alpha或者Alpha+RGB。
清晰地理解VDMA读写通道的工作时序,对以后的设计有很大的帮助,很多设计都是根据本小节所示的样例时序设计出来的。在下一章,读者就能够有所体会。
下图描述了读通道的时序,5行,每行16字节,跨度为32字节。
从图中可以看出:在收到mm2s_fsync信号后,VDMA在m_axi_mm2s_araddr的起始地址处发出m_axi_mm2s_arvalid信号。M_axi_mm2s_arvalid总共有效5次,分别获取一帧的5行数据。从MM读取的数据存储在行缓存里,当收到来自axi-stream端的m_axis_mm2s_tvalid信号后,将数据发送到axi-stream端。每一行的结束,axi-stream端会使m_axis_mm2s_tlast有效。
下图描述了写通道的时序,5行,每行16字节,跨度为32字节。
从图中可以看出:在收到s2mm_fsync信号后,VDMA发出s2mm_fsync_out和s_axis_s2mm_tready表明已经准备好接收来自axi-stream端的数据。读取到的数据存储在行缓存里,m_axi_s2mm_awvalid有效后,紧接着有效m_axi_s2mm_wvalid信号,同时将数据放至m_axi_s2mm_wdata。
VDMA的寄存器如下表所示。所有寄存器都被映射到非缓存内存空间。该内存空间必须按照AXI字(32位)进行对齐,换句话说,寄存器偏移地址至少间隔4个字节。
寄存器名称 |
偏移地址 |
详细描述 |
MM2S_VDMACR |
00h |
MM2S VDMA控制寄存器 |
MM2S_VDMASR |
04h |
MM2S VDMA状态寄存器 |
保留 |
08h~10h |
N/A |
MM2S_REG_INDEX |
14h |
MM2S寄存器索引 |
保留 |
18h~24h |
N/A |
PARK_PRT_REG |
28h |
MM2S和S2MM Park指针寄存器 |
VDMA_VERSION |
2Ch |
VDMA版本寄存器 |
S2MM_VDMACR |
30h |
S2MM VDMA控制寄存器 |
S2MM_VDMASR |
34h |
S2MM VDMA状态寄存器 |
保留 |
38h |
N/A |
S2MM_VDMA_IRQ_MASK |
3Ch |
S2MM错误中断掩码寄存器 |
保留 |
40h |
N/A |
S2MM_REG_INDEX |
44h |
S2MM寄存器索引 |
保留 |
48h~4Ch |
N/A |
MM2S_VSIZE |
50h |
MM2S垂直方向显示大小寄存器 |
MM2S_HSIZE |
54h |
MM2S水平方向显示大小寄存器 |
MM2S_FRMDLY_STRIDE |
58h |
MM2S帧延迟和跨度寄存器 |
MM2S_START_ADDRESS(1~16) |
5Ch~98h |
MM2S帧存起始地址(1~16) |
保留 |
9Ch |
N/A |
S2MM_VSIZE |
A0h |
S2MM垂直方向显示大小寄存器 |
S2MM_HSIZE |
A4h |
S2MM水平方向显示大小寄存器 |
S2MM_FRMDLY_STRIDE |
A8h |
S2MM帧延迟和跨度寄存器 |
S2MM_START_ADDRESS(1~16) |
ACh~E8h |
S2MM帧存起始地址(1~16) |
所有寄存器字节序都是小端格式,如下图所示。
各个寄存器的名称和大致作用从上表就可以看出,接下来,笔者会详细介绍重要寄存器的具体bit的作用。明白了每个bit的作用之后,自然就知道写入什么值能够达到自己的控制目的。
从上表可以看出,寄存器可以分为两组,分别对应MM2S通道和S2MM通道,两组寄存器的功能是相似的,区别仅在于偏移地址和所服务的对象。因此,在学习完MM2S通道的所有寄存器之后,只要大致浏览一下S2MM通道对应的寄存器的关键位即可(个别位不相同),在使用高级功能时,再仔细查阅VDMA用户手册。
顾名思义,该寄存器用于控制VDMA,具体可以实现复位、使能锁相同步、设定帧存切换模式、启动VDMA读写通道等操作。每一位作用如下图所示,低4位是最重要的,接下来会详细介绍。
位 |
名称 |
默认值 |
接入类型 |
描述 |
31~4 |
非常用位,请参考VDMA使用手册自学 |
|||
3 |
GenlockEn |
0h |
可读可写 |
使能锁相同步或者动态锁相同步模式。 0:关闭Genlock或动态Genlock同步 1:开启Genlock或动态Genlock同步 注:该位仅在通道被配置成锁相同步从接口或者动态锁相主、从接口时才起作用。配置成锁相同步主接口时,该位为保留位,值恒为0。 |
2 |
Reset |
0h |
可读可写 |
0:正常操作;1:复位MM2S通道 |
1 |
Circular_Park |
1h |
可读可写 |
指定帧存为循环模式还是停留模式 0:停留模式-显示用缓存页将停留在PARK_PTR_REG.RdFrmPntrRef指定的帧存; 1:循环模式-循环切换显示用缓存页 |
0 |
RS |
0h |
可读可写 |
运行/停止,控制VDMA通道的运行和停止。 开始任何VDMA操作前,该位必须置1. 0:停止;1:运行。 |
该寄存器用于获取VDMA工作状态。每一位作用如下图所示,低4位是最重要的,接下来会详细介绍。
位 |
名称 |
默认值 |
接入类型 |
描述 |
31~1 |
非常用位,请参考VDMA使用手册自学 |
|||
0 |
Halted |
1h |
只读 |
指示VDMA运行是否停止。 0:运行;1:停止。 |
该寄存器用于管理读、写通道的数据传输。
位 |
名称 |
默认值 |
接入类型 |
描述 |
31~29 |
保留 |
0h |
只读 |
|
28~24 |
WrFrmStore |
0h |
只读 |
用于存储写通道正在操作的帧的编号。指示S2MM通道正在操作的帧。 |
23~21 |
保留 |
0h |
只读 |
|
20~16 |
RdFrmStore |
0h |
只读 |
用于存储读通道正在操作的帧的编号。指示MM2S通道正在操作的帧。 |
15~13 |
保留 |
0h |
只读 |
|
12~8 |
WrFrmPtrRef |
0h |
可读可写 |
通过帧编号指定写通道操作的帧。当工作在停留模式,S2MM通道操作对象停留在WrFrmPtrRef指定的帧。 |
7~5 |
保留 |
0h |
只读 |
|
4~0 |
RdFrmPtrRef |
0h |
可读可写 |
通过帧编号指定读通道操作的帧。当工作在停留模式,MM2S通道操作对象停留在RdFrmPtrRef指定的帧。 |
学习了这个寄存器之后,就可以发现:当VDMA工作在Parked模式下,通过操作该寄存器,就能够实现帧缓存的切换,建立自己想要的缓存切换机制。
有最多32个寄存器用于存放帧存起始地址,其分别存在于两个寄存器bank上:bank0和bank1,每个bank上有16个寄存器。这两个bank上有相同的起始偏移地址(0x5C),选择这两个bank可以通过MM2S_REG_INDEX的值进行选择。假如想访问第1个寄存器,则给MM2S_REG_INDEX赋值为0,并设定偏移地址为0x5C;如果想访问第17个寄存器,需要将MM2S_REG_INDEX设为1,并设定初始偏移地址为0x5C。
该寄存器有两个作用,第一是bit24~bit28指定帧延迟,仅用于Genlock从模式,指定从接口比主接口至少要延迟多少个帧;第二是低16位指定水平方向的跨度,同样以字节为单位。所谓跨度是指每两行第一个像素之间间隔的数据个数,具体请参考22.3.2小节,VDMA帧存格式。
该寄存器的低16位用于指定每一行有多少字节的数据需要传输。例如显示分辨率为640*480,每个像素4个字节(RGB+Alpha),该值应该设定为640*4。
该寄存器有两个作用,第一是用低13位指定总共有多少行;第二是启动MM2S的传输。当MM2S_VDMACR.RS=1,对该寄存器的写操作会将所有设定参数传递给VDMA内部寄存器模块,用于VDMA控制。对某个通道进行配置时,必须在最后一步设置该寄存器。
顾名思义,该寄存器用于控制VDMA S2MM通道,具体可以实现复位、使能锁相同步、设定帧存切换模式、启动VDMA读写通道等操作。每一位作用如下图所示,低4位是最重要的,接下来会详细介绍。
位 |
名称 |
默认值 |
接入类型 |
描述 |
31~4 |
非常用位,请参考VDMA使用手册自学 |
|||
3 |
GenlockEn |
0h |
可读可写 |
使能锁相同步或者动态锁相同步模式。 0:关闭Genlock或动态Genlock同步 1:开启Genlock或动态Genlock同步 注:该位仅在通道被配置成锁相同步从接口或者动态锁相主、从接口时才起作用。配置成锁相同步主接口时,该位为保留位,值恒为0。 |
2 |
Reset |
0h |
可读可写 |
0:正常操作;1:复位S2MM通道 |
1 |
Circular_Park |
1h |
可读可写 |
指定帧存为循环模式还是停留模式 0:停留模式-显示用缓存页将停留在PARK_PTR_REG.RdFrmPntrRef指定的帧存; 1:循环模式-循环切换显示用缓存页 |
0 |
RS |
0h |
可读可写 |
运行/停止,控制VDMA通道的运行和停止。 开始任何VDMA操作前,该位必须置1. 0:停止;1:运行。 |
该寄存器用于获取S2MM工作状态。每一位作用如下图所示,低4位是最重要的,接下来会详细介绍。
位 |
名称 |
默认值 |
接入类型 |
描述 |
31~1 |
非常用位,请参考VDMA使用手册自学 |
|||
0 |
Halted |
1h |
只读 |
指示VDMA运行是否停止。 0:运行;1:停止。 |
有最多32个寄存器用于存放帧存起始地址,其分别存在于两个寄存器bank上:bank0和bank1,每个bank上有16个寄存器。这两个bank上有相同的起始偏移地址(0x5C),选择这两个bank可以通过S2MM_REG_INDEX的值进行选择。假如想访问第1个寄存器,则给S2MM_REG_INDEX赋值为0,并设定偏移地址为0x5C;如果想访问第17个寄存器,需要将MM2S_REG_INDEX设为1,并设定初始偏移地址为0x5C。
该寄存器有两个作用,第一是bit24~bit28指定帧延迟,仅用于Genlock从模式,指定从接口比主接口至少要延迟多少个帧;第二是低16位指定水平方向的跨度,同样以字节为单位。所谓跨度是指每两行第一个像素之间间隔的数据个数,具体请参考22.3.2小节,VDMA帧存格式。
该寄存器的低16位用于指定每一行有多少字节的数据需要传输。例如显示分辨率为640*480,每个像素4个字节(RGB+Alpha),该值应该设定为640*4。
该寄存器有两个作用,第一是用低13位指定总共有多少行;第二是启动S2MM的传输。当S2MM_VDMACR.RS=1,对该寄存器的写操作会将所有设定参数传递给VDMA内部寄存器模块,用于VDMA控制。对某个通道进行配置时,必须在最后一步设置该寄存器。
VDMA支持以下三种帧同步源:
Ø 基于AXI4-Stream的帧同步(使用tuser(0)信号)
n 读通道使用m_axis_mm2s_tuser(0)作为帧起始信号
n 写通道使用s_axis_s2mm_tuser(0)作为帧起始信号
Ø S2MM帧同步(s2mm_fsync)
Ø MM2S帧同步(mm2s_fsync)
Genlock,同步锁相,可以使一套或多套系统与同一同步源实现同步。能够使视频的刷新和外部视频源保持一致。当提供了一个适当的信号后,系统就会把它的显示刷新率和这个信号进行锁定 。
在许多视频应用中,输入端产生数据的速率往往不同于输出端数据速率,为了避免由速率不一致导致的潜在错误,帧缓冲的使用是很有必要的。帧缓冲机制开辟多个缓冲页,用于保存数据,输入和输出端分别操作不同的帧存,从而避免了冲突。
VDMA的锁相同步特性正是用于阻止读、写通道同时操作同一个帧存。VDMA的每个通道都可以选择自己的操作类型(同步锁相主/从或者动态同步锁相主/从),利用该特性,禁止主从接口同时访问同一缓存,从而保持同步。
VDMA支持四种模式的锁相同步,分别为:
Ø Genlock Master(锁相同步主端)
Ø Genlock Slave(锁相同步从端)
Ø Dynamic Genlock Master(动态锁相同步主端)
Ø Dynamic Genlock Slave(动态锁相同步从端)
读通道(MM2S):当配置为Genlock Master时,该通道不会跳过或者重复任一帧数据,并把当前帧的编号输出在mm2s_frame_ptr_out端口。通道不会检测mm2s_frame_ptr_in端口提供的帧编号。Genlock Slave通道应跟随Genlock Master通道变化,但有一定的延迟。延迟大小预定义在寄存器中(*frmdly_stride[28:24])。
写通道(S2MM):当配置为Genlock Master时,该通道不会跳过或者重复任一帧数据,并把当前帧的编号输出到s2mm_frame_ptr_out端口。通道不会检测s2mm_frame_ptr_in端口提供的帧编号。Genlock Slave通道应跟随Genlock Master通道变化,但有一定的延迟。延迟大小预定义在寄存器中(*frmdly_stride[28:24])。
读通道(MM2S):当配置为Genlock Slave时,该通道会通过跳过或者重复一些帧的方式,尝试与Genlock Master同步。通道会对mm2s_frame_ptr_in端口进行采样,获取Genlock Master的帧编号。为了实现状态反馈,通道会把当前帧的编号输出到mm2s_frame_ptr_out端口。
指定通道工作在Genlock Slave模式,必须进行如下操作。
Ø 将GenlockEn置1(MM2S_VDMACR[3]=1),使能主、从通道之间的Genlock同步。
Ø 将GenlockSrc置1(MM2S_VDMACR[7]=1),使能内部Genlock模式。如果在Vivado IDE中同时使能读、写通道,该位默认置位。当GenlockSRC=1时,VDMA默认支持内部同步锁相总线。这样一来就没有必要在外部对帧指针端口(*frame_ptr_out和*_frame_ptr_in)进行连接了。
Ø 根据主从通道的帧率,使用mm2s_frmdly_stride[28:24]设定合适的延迟时间。
写通道(S2MM):当配置为Genlock Slave时,该通道会通过跳过或者重复一些帧的方式,尝试与Genlock Master同步。通道会对s2mm_frame_ptr_in端口进行采样,获取Genlock Master的帧编号。为了实现状态反馈,通道会把当前帧的编号输出到s2mm_frame_ptr_out端口。
指定通道工作在Genlock Slave模式,必须进行如下操作。
Ø 将GenlockEn置1(S2MM_VDMACR[3]=1),使能主、从通道之间的Genlock同步。
Ø 将GenlockSrc置1(S2MM_VDMACR[7]=1),使能内部Genlock模式。如果在Vivado IDE中同时使能读、写通道,该位默认置位。当GenlockSRC=1时,VDMA默认支持内部同步锁相总线。这样一来就没有必要在外部对帧指针端口(*frame_ptr_out和*_frame_ptr_in)进行连接了。
Ø 根据主从通道的帧率,使用mm2s_frmdly_stride[28:24]设定合适的延迟时间。
动态Genlock Master与Genlock Master的区别在于,主通道会跳过从通道正在操作的帧。举例而言,对于三帧存而言,动态Genlock Master会按照0,1,2,0,1,2的顺序循环使用帧存,一旦检测到Master即将操作Slave正在操作的帧,就会跳过该帧继续循环。因此,如果Slave通道一直在操作帧存1,那么Master通道就会在帧0和帧2之间来回切换。
Dynamic Genlock Slave通道会操作Dynamic Genlock Master通道上一周期操作的帧。
下图描述了一种简单的Genlock操作时序。在这个示例中,S2MM通道是Genlock Master,MM2S通道是Genlock Slave,并且写通道帧率高于读通道帧率。
由于读通道帧率慢于写通道,所以读通道仅处理帧2和帧0,跳过帧1不做处理。
Xilinx集成开发环境升级到Vivado之后,VDMA的配置项比以前少了不少,一定程度上降低了使用难度。主要配置页面如下面两幅图所示。
具体配置项参见下表。
基本配置 |
高级配置 |
地址线宽度 |
是否使能异步模式(自动) |
帧存数量 |
写通道帧同步 |
是否使能读写通道 |
写通道GenLock模式选择 |
数据线宽度 |
写通道是否允许非对齐传输 |
触发长度 |
读通道帧同步 |
AXI-Stream流数据位宽 |
读通道GenLock模式选择 |
Line Buffer深度 |
读通道是否允许非对齐传输 |
关于地址线和数据线宽度,需要根据设计的实际情况配置。
Line buffer深度不能太小。
GenLock和帧同步前文已经讲解,根据需求自行配置即可。
以下步骤是最简单的VDMA控制初始化操作。
Ø 写VDMACR寄存器,将VDMACR.RS设为1,启动VDMA通道。
Ø 设定有效的帧缓存起始地址。
Ø 设定帧延迟(仅针对Genlock从模式)以及跨度到FRMDLY_STRIDE寄存器。
Ø 设定水平方向字节数到HSIZE寄存器。
Ø 设定竖直方向行数到VSIZE寄存器。启动通道的数据传输。
在VDMA运行过程中,可以动态的进行显示参数配置,但是需要注意的是,想要使参数生效,必须在设置的最后一步,对VSIZE寄存器进行写操作。
最后,给出一段通过VDMA对DDR读写传输的进行初始化的示例代码:
//VDMA configurateAXI VDMA0
/*****************从DDR读数据设置**********************/
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x0, 0x4); //reset
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x0, 0x8); //gen-lock
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x5C, 0x08000000);
// AXI4 Data Width为32位,是4个字节数
// 0x0A000000 0x0015F900
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x5C+4, 0x0A000000);
// 0x09000000 0x002BF200
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x5C+8, 0x09000000);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x54, 640);// 640
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x58, 0x01000280);
// 第0位: 运行 - 启动VDMA操作,在运行VDMA时,其状态寄存器中的停止位赋值为0 第一位:循环模式 -通过连续循环帧缓冲
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x0, 0x03);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x50, 480);//480
/*********** 写入DDR设置*************************/
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x30, 0x4); //reset
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x30, 0x8); //genlock
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0xAC, 0x08000000);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0xAC+4, 0x0A000000);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0xAC+8, 0x09000000);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0xA4, 640);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0xA8, 0x01000280);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0x30, 0x03);
XAxiVdma_WriteReg(XPAR_AXIVDMA_0_BASEADDR, 0xA0, 480);
可以看到VMDA的图像系统和前面介绍的DMA系统相比非常类似。实际上他们都是属于DMA系统,只是VDMA在配置完成后,可以无需依赖CPU可以独立运行,有点类似显卡的功能了。
本课程提供了二种方式启动VDMA,第一种是通过库函数版本,第二种是通过寄存器版本。寄存器版本主要是验证我们对VDMA的寄存器掌握情况。库函数具备更强的功能,和可维护性。
表6-6-1
#include "sys_intr.h" #include "xaxivdma.h" #include "xaxivdma_i.h" #define VTC_BASEADDR XPAR_MIZ702_VTG_VGA_0_BASEADDR #define DDR_BASEADDR 0x00000000 //#define UART_BASEADDR 0xe0001000 #define VDMA_BASEADDR XPAR_AXI_VDMA_0_BASEADDR #define H_STRIDE 640 #define H_ACTIVE 640 #define V_ACTIVE 480 #define pi 3.14159265358 #define COUNTS_PER_SECOND (XPAR_CPU_CORTEXA9_CORE_CLOCK_FREQ_HZ)/64 #define VIDEO_LENGTH (H_STRIDE*V_ACTIVE) #define VIDEO_BASEADDR0 DDR_BASEADDR + 0x2000000 #define VIDEO_BASEADDR1 DDR_BASEADDR + 0x3000000 #define VIDEO_BASEADDR2 DDR_BASEADDR + 0x4000000 u32 *BufferPtr[3]; unsigned int srcBuffer = (XPAR_PS7_DDR_0_S_AXI_BASEADDR + 0x1000000); 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) { u32 Status; Miz702_EMIO_init(); ov7725_init_rgb(); XAxiVdma InstancePtr; xil_printf("Starting the first VDMA \n\r"); Status = run_triple_frame_buffer(&InstancePtr, 0, 640, 480, srcBuffer, 2, 0); if (Status != XST_SUCCESS) { xil_printf("Transfer of frames failed with error = %d\r\n",Status); return XST_FAILURE; } else { xil_printf("Transfer of frames started \r\n"); } print("TEST PASS\r\n"); //VDMA configurateAXI VDMA0 /****************往DDR写数据设置**********************/ /*Xil_Out32((VDMA_BASEADDR + 0x030), 0x00000003);// enable circular mode Xil_Out32((VDMA_BASEADDR + 0x0AC), VIDEO_BASEADDR0); // start address Xil_Out32((VDMA_BASEADDR + 0x0B0), VIDEO_BASEADDR1); // start address Xil_Out32((VDMA_BASEADDR + 0x0B4), VIDEO_BASEADDR2); // start address Xil_Out32((VDMA_BASEADDR + 0x0A8), (H_STRIDE*4)); // h offset (640 * 4) bytes Xil_Out32((VDMA_BASEADDR + 0x0A4), (H_ACTIVE*4)); // h size (640 * 4) bytes Xil_Out32((VDMA_BASEADDR + 0x0A0), V_ACTIVE);*/ // v size (480) /*****************从DDR读数据设置**********************/ /*Xil_Out32((VDMA_BASEADDR + 0x000), 0x00000003); // enable circular mode Xil_Out32((VDMA_BASEADDR + 0x05c), VIDEO_BASEADDR0); // start address Xil_Out32((VDMA_BASEADDR + 0x060), VIDEO_BASEADDR1); // start address Xil_Out32((VDMA_BASEADDR + 0x064), VIDEO_BASEADDR2); // start address Xil_Out32((VDMA_BASEADDR + 0x058), (H_STRIDE*4)); // h offset (640 * 4) bytes Xil_Out32((VDMA_BASEADDR + 0x054), (H_ACTIVE*4)); // h size (640 * 4) bytes Xil_Out32((VDMA_BASEADDR + 0x050), V_ACTIVE); // v size (480) */ while (1) ; return XST_SUCCESS; } |
XAxiVdma_LookupConfig函数是XILINX 库函数的标准调用方式,可以获取到硬件的默认配置参数。默认的配置参数保存在 参数表XAxiVdma_ConfigTable 中。
表6-6-2-1 XAxiVdma_LookupConfig
/*****************************************************************************/ /** * Look up the hardware configuration for a device instance * * @param DeviceId is the unique device ID of the device to lookup for * * @return * The configuration structure for the device. If the device ID is not found, * a NULL pointer is returned. * ******************************************************************************/ XAxiVdma_Config *XAxiVdma_LookupConfig(u16 DeviceId) { extern XAxiVdma_Config XAxiVdma_ConfigTable[]; XAxiVdma_Config *CfgPtr = NULL; int i; for (i = 0; i < XPAR_XAXIVDMA_NUM_INSTANCES; i++) { if (XAxiVdma_ConfigTable[i].DeviceId == DeviceId) { CfgPtr = &XAxiVdma_ConfigTable[i]; break; } } return CfgPtr; } |
表6-6-2-2 XAxiVdma_ConfigTable参数表
XAxiVdma_Config XAxiVdma_ConfigTable[] = { { XPAR_AXI_VDMA_0_DEVICE_ID, XPAR_AXI_VDMA_0_BASEADDR, XPAR_AXI_VDMA_0_NUM_FSTORES, XPAR_AXI_VDMA_0_INCLUDE_MM2S, XPAR_AXI_VDMA_0_INCLUDE_MM2S_DRE, XPAR_AXI_VDMA_0_M_AXI_MM2S_DATA_WIDTH, XPAR_AXI_VDMA_0_INCLUDE_S2MM, XPAR_AXI_VDMA_0_INCLUDE_S2MM_DRE, XPAR_AXI_VDMA_0_M_AXI_S2MM_DATA_WIDTH, XPAR_AXI_VDMA_0_INCLUDE_SG, XPAR_AXI_VDMA_0_ENABLE_VIDPRMTR_READS, XPAR_AXI_VDMA_0_USE_FSYNC, XPAR_AXI_VDMA_0_FLUSH_ON_FSYNC, XPAR_AXI_VDMA_0_MM2S_LINEBUFFER_DEPTH, XPAR_AXI_VDMA_0_S2MM_LINEBUFFER_DEPTH, XPAR_AXI_VDMA_0_MM2S_GENLOCK_MODE, XPAR_AXI_VDMA_0_S2MM_GENLOCK_MODE, XPAR_AXI_VDMA_0_INCLUDE_INTERNAL_GENLOCK, XPAR_AXI_VDMA_0_S2MM_SOF_ENABLE, XPAR_AXI_VDMA_0_M_AXIS_MM2S_TDATA_WIDTH, XPAR_AXI_VDMA_0_S_AXIS_S2MM_TDATA_WIDTH, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_1, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_5, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_6, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_7, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_9, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_13, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_14, XPAR_AXI_VDMA_0_ENABLE_DEBUG_INFO_15, XPAR_AXI_VDMA_0_ENABLE_DEBUG_ALL, XPAR_AXI_VDMA_0_ADDR_WIDTH } }; |
WriteSetup VDMA写通道设置,主要设置分辨率,延迟参数,开启CircularBuf 模式,使能Gen-Lock。更底层的分析读者可以顺藤摸瓜下去。
表6-6-2-3 WriteSetup
/*****************************************************************************/ /** * * This function sets up the write channel * * @param dma_context is the context pointer to the VDMA engine.. * * @return XST_SUCCESS if the setup is successful, XST_FAILURE otherwise. * * @note None. * ******************************************************************************/ static int WriteSetup(vdma_handle *vdma_context) { int Index; u32 Addr; int Status; vdma_context->WriteCfg.VertSizeInput = vdma_context->vsize; vdma_context->WriteCfg.HoriSizeInput = vdma_context->hsize; vdma_context->WriteCfg.Stride = vdma_context->hsize; vdma_context->WriteCfg.FrameDelay = 0; /* This example does not test frame delay */ vdma_context->WriteCfg.EnableCircularBuf = 1; vdma_context->WriteCfg.EnableSync = 1; /* Gen-Lock */ vdma_context->WriteCfg.PointNum = 0; vdma_context->WriteCfg.EnableFrameCounter = 0; /* Endless transfers */ vdma_context->WriteCfg.FixedFrameStoreAddr = 0; /* We are not doing parking */ /* Configure the VDMA is per fixed configuration, This configuration * is being used by majority of customers. Expert users can play around * with this if they have different configurations */ Status = XAxiVdma_DmaConfig(vdma_context->InstancePtr, XAXIVDMA_WRITE, &vdma_context->WriteCfg); if (Status != XST_SUCCESS) { xil_printf( "Write channel config failed %d\r\n", Status); return Status; } /* Initialize buffer addresses * * Use physical addresses */ Addr = vdma_context->buffer_address; /* If Debug mode is enabled write frame is shifted 3 Frames * store ahead to compare read and write frames */ #if DEBUG_MODE Addr = Addr + vdma_context->InstancePtr->MaxNumFrames * \ (vdma_context->WriteCfg.Stride * vdma_context->vsize); #endif for(Index = 0; Index < vdma_context->InstancePtr->MaxNumFrames; Index++) { vdma_context->WriteCfg.FrameStoreStartAddr[Index] = Addr; #if DEBUG_MODE xil_printf("Write Buffer %d address: 0x%x \r\n",Index,Addr); #endif Addr += (vdma_context->hsize * vdma_context->vsize); } /* Set the buffer addresses for transfer in the DMA engine */ Status = XAxiVdma_DmaSetBufferAddr(vdma_context->InstancePtr, XAXIVDMA_WRITE, vdma_context->WriteCfg.FrameStoreStartAddr); if (Status != XST_SUCCESS) { xil_printf("Write channel set buffer address failed %d\r\n", Status); return XST_FAILURE; } /* Clear data buffer */ #if DEBUG_MODE memset((void *)vdma_context->buffer_address, 0, vdma_context->ReadCfg.Stride * vdma_context->ReadCfg.VertSizeInput * vdma_context->InstancePtr->MaxNumFrames); #endif return XST_SUCCESS; } |
ReadSetup VDMA读通道设置,主要设置分辨率,这里的延迟参数1,否则图像会有卡顿,开启CircularBuf 模式,使能Gen-Lock。更底层的分析读者可以顺藤摸瓜下去。
表6-6-2-4 ReadSetup
/*****************************************************************************/ /** * * This function sets up the read channel * * @param vdma_context is the context pointer to the VDMA engine. * * @return XST_SUCCESS if the setup is successful, XST_FAILURE otherwise. * * @note None. * ******************************************************************************/ static int ReadSetup(vdma_handle *vdma_context) { int Index; u32 Addr; int Status; vdma_context->ReadCfg.VertSizeInput = vdma_context->vsize; vdma_context->ReadCfg.HoriSizeInput = vdma_context->hsize; vdma_context->ReadCfg.Stride = vdma_context->hsize; vdma_context->ReadCfg.FrameDelay = 0; /* This example does not test frame delay */ vdma_context->ReadCfg.EnableCircularBuf = 1; vdma_context->ReadCfg.EnableSync = 1; /* Gen-Lock */ vdma_context->ReadCfg.PointNum = 0; vdma_context->ReadCfg.EnableFrameCounter = 0; /* Endless transfers */ vdma_context->ReadCfg.FixedFrameStoreAddr = 0; /* We are not doing parking */ /* Configure the VDMA is per fixed configuration, This configuration is being used by majority * of customer. Expert users can play around with this if they have different configurations */ Status = XAxiVdma_DmaConfig(vdma_context->InstancePtr, XAXIVDMA_READ, &vdma_context->ReadCfg); if (Status != XST_SUCCESS) { xil_printf("Read channel config failed %d\r\n", Status); return XST_FAILURE; } /* Initialize buffer addresses * * These addresses are physical addresses */ Addr = vdma_context->buffer_address; for(Index = 0; Index < vdma_context->InstancePtr->MaxNumFrames; Index++) { vdma_context->ReadCfg.FrameStoreStartAddr[Index] = Addr; /* Initializing the buffer in case of Debug mode */ #if DEBUG_MODE { u32 i; u8 *src; u32 total_pixel = vdma_context->ReadCfg.Stride * vdma_context->vsize; src = (unsigned char *)Addr; xil_printf("Read Buffer %d address: 0x%x \r\n",Index,Addr); for(i=0;i { src[i] = i & 0xFF; } } #endif Addr += vdma_context->hsize * vdma_context->vsize; } /* Set the buffer addresses for transfer in the DMA engine * The buffer addresses are physical addresses */ Status = XAxiVdma_DmaSetBufferAddr(vdma_context->InstancePtr, XAXIVDMA_READ, vdma_context->ReadCfg.FrameStoreStartAddr); if (Status != XST_SUCCESS) { xil_printf( "Read channel set buffer address failed %d\r\n", Status); return XST_FAILURE; } return XST_SUCCESS; } |
StartTransfer 启动VDMA读写通道
表6-6-2-5 StartTransfer
/*****************************************************************************/ /** * * This function starts the DMA transfers. Since the DMA engine is operating * in circular buffer mode, video frames will be transferred continuously. * * @param InstancePtr points to the DMA engine instance * * @return * - XST_SUCCESS if both read and write start successfully * - XST_FAILURE if one or both directions cannot be started * * @note None. * ******************************************************************************/ static int StartTransfer(XAxiVdma *InstancePtr) { int Status; /* Start the write channel of VDMA */ Status = XAxiVdma_DmaStart(InstancePtr, XAXIVDMA_WRITE); if (Status != XST_SUCCESS) { xil_printf("Start Write transfer failed %d\r\n", Status); return XST_FAILURE; } /* Start the Read channel of VDMA */ Status = XAxiVdma_DmaStart(InstancePtr, XAXIVDMA_READ); if (Status != XST_SUCCESS) { xil_printf("Start read transfer failed %d\r\n", Status); return XST_FAILURE; } return XST_SUCCESS; } |