第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个缓冲区。一个在写,一个在读,还有一个备用。这样读写互不干扰,画面就不会撕裂。

我在项目中遇到过一个问题:只用单缓冲时,摄像头采集和显示器读取同时操作同一块内存,画面就会出现撕裂。后来改成双缓冲,问题就解决了。嗯,这里要注意,缓冲区的数量不是越多越好,多了浪费内存,少了又不够用。

VDMA工作原理框图 摄像头 AXI4-Stream VDMA 写通道 S2MM Stream→Memory VDMA 读通道 MM2S Memory→Stream DDR内存 帧缓冲0/1/2 显示器 AXI4-Stream 视频流 AXI4写 AXI4读 视频流 控制/状态 视频源/显示端 VDMA通道 存储端

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这块基本就通了。

可可精品资料库