第3章:VDMA IP核配置与使用
大家好,我是你们的FPGA讲师。今天我们来聊聊VDMA——Video Direct Memory Access。这玩意儿在图像采集与显示系统里,可以说是核心中的核心。
我刚开始接触Zynq的时候,总觉得VDMA就是个高级版的DMA。后来踩了不少坑才明白,它远不止这么简单。说白了,VDMA就是专门为视频流设计的DMA控制器,它能把摄像头采集的数据搬到DDR里,也能把DDR里的数据送给显示器。
VDMA工作原理
VDMA的工作模式,我习惯把它分成三个环节:
- 读通道:从DDR里把图像数据读出来,转成AXI4-Stream流
- 写通道:把AXI4-Stream流的数据写进DDR
- 控制逻辑:管理帧缓冲、同步信号、中断这些杂事
你想想看,如果没有VDMA,CPU得亲自去搬数据,那画面卡顿得没法看。VDMA的好处就是——它自己就能搞定数据搬运,CPU只管发命令就行。
核心要点:VDMA支持多帧缓冲(Frame Buffer),一般用2~3个缓冲区。一个在写,一个在读,还有一个备用。这样读写互不干扰,画面就不会撕裂。
我在项目中遇到过一个问题:只用单缓冲时,摄像头采集和显示器读取同时操作同一块内存,画面就会出现撕裂。后来改成双缓冲,问题就解决了。嗯,这里要注意,缓冲区的数量不是越多越好,多了浪费内存,少了又不够用。
AXI4-Stream协议详解
VDMA和摄像头、显示器之间通信,用的就是AXI4-Stream协议。这个协议说白了就是——只有数据通道,没有地址通道。数据像流水一样,源源不断地流过去。
AXI4-Stream的关键信号就这几个:
| 信号名 | 方向 | 说明 |
|---|---|---|
| TVALID | 主→从 | 主设备告诉从设备:数据准备好了 |
| TREADY | 从→主 | 从设备告诉主设备:我准备好接收了 |
| TDATA | 主→从 | 数据总线,宽度可配(8/16/32/64位等) |
| TLAST | 主→从 | 一帧或一行的最后一个数据 |
| TUSER | 主→从 | 用户自定义信号,常用作帧同步 |
| TKEEP | 主→从 | 指示哪些字节有效(用于非对齐传输) |
我的经验:调试AXI4-Stream时,我最常犯的错误就是TVALID和TREADY的握手时序。记住一个原则——数据只在TVALID和TREADY同时为高时传输。我曾经花了一整天查一个图像错位的问题,最后发现是TUSER信号没对齐。
为什么会这样?因为摄像头输出的TUSER通常标记帧起始,如果它和TDATA的时序没配合好,VDMA就会把数据写到错误的位置。嗯,这里要注意,TUSER信号最好在TDATA有效的前一个时钟周期就拉高。
VDMA寄存器配置
VDMA的寄存器配置,我习惯把它分成三类:
- 控制寄存器:启动、停止、复位等操作
- 描述符寄存器:设置帧缓冲地址、传输大小等
- 状态寄存器:查询传输状态、中断标志等
下面是我常用的寄存器配置示例:
// 写通道配置(S2MM)
// 1. 设置帧缓冲地址
VDMA_S2MM_START_ADDR0 = 0x10000000; // 帧缓冲0
VDMA_S2MM_START_ADDR1 = 0x11000000; // 帧缓冲1
VDMA_S2MM_START_ADDR2 = 0x12000000; // 帧缓冲2
// 2. 设置水平尺寸(以字节为单位)
VDMA_S2MM_HSIZE = 1920 * 3; // 1920像素,每像素3字节(RGB888)
// 3. 设置帧尺寸(以字节为单位)
VDMA_S2MM_FRMDELAY = 0; // 帧延迟
VDMA_S2MM_FRMWIDTH = 1920; // 宽度(像素)
VDMA_S2MM_FRMHEIGHT = 1080; // 高度(行数)
// 4. 设置行间距(跨距)
VDMA_S2MM_STRIDE = 1920 * 3; // 一般等于HSIZE
// 5. 启动传输
VDMA_S2MM_CR = 0x00000001; // 使能S2MM通道
注意:STRIDE(行间距)和HSIZE(水平尺寸)是两个容易混淆的概念。HSIZE是一行有效数据的字节数,STRIDE是两行起始地址之间的偏移。如果图像数据在内存中是连续存放的,两者相等。但如果图像有对齐要求(比如64字节对齐),STRIDE可能大于HSIZE。
读写通道设置
VDMA的读写通道是独立的,可以分别配置。我一般先配写通道,再配读通道。为什么?因为写通道是数据入口,先确保数据能正确写入DDR,再考虑读出来显示。
读通道的配置和写通道类似,只是方向相反:
// 读通道配置(MM2S)
// 1. 设置帧缓冲地址(和写通道可以相同,也可以不同)
VDMA_MM2S_START_ADDR0 = 0x10000000;
VDMA_MM2S_START_ADDR1 = 0x11000000;
VDMA_MM2S_START_ADDR2 = 0x12000000;
// 2. 设置尺寸参数
VDMA_MM2S_HSIZE = 1920 * 3;
VDMA_MM2S_FRMWIDTH = 1920;
VDMA_MM2S_FRMHEIGHT = 1080;
VDMA_MM2S_STRIDE = 1920 * 3;
// 3. 启动传输
VDMA_MM2S_CR = 0x00000001;
这里有个关键点——读写通道的帧缓冲地址可以指向同一块内存,也可以指向不同区域。如果做的是简单的采集显示,指向同一块内存就行。但如果要做图像处理(比如缩放、旋转),最好分开,避免数据冲突。
我曾经在一个项目中,读写通道用了同一个帧缓冲地址,结果图像处理模块还没处理完,VDMA读通道就把原始数据读走了。画面一会儿正常一会儿花屏,查了好久才发现是这个问题。
避坑指南:如果你用双缓冲,记得在切换帧缓冲时,先检查VDMA是否正在访问当前缓冲区。可以通过状态寄存器查询,或者用中断来同步。我习惯的做法是——写通道写完一帧后产生中断,然后在中断服务程序里切换读通道的帧缓冲地址。
好了,VDMA的配置和使用就讲到这里。记住几个关键点:AXI4-Stream的握手时序、帧缓冲的管理、读写通道的独立配置。这些搞明白了,VDMA这块基本就通了。
可可精品资料库