第三章 VDMA原理与配置:什么是VDMA、VDMA的寄存器配置、帧缓存机制详解

各位同学,咱们今天聊聊VDMA。这东西在Zynq图像加速里,可以说是绕不开的核心外设。我刚开始接触Zynq的时候,也对着文档看了半天,总觉得它就是个高级版的DMA。后来做项目踩了坑,才真正理解它到底牛在哪。

3.1 什么是VDMA?说白了就是个“视频搬运工”

VDMA,全称Video Direct Memory Access。你想想看,普通的DMA只能搬数据,但视频数据有个特点——它是连续的帧,每一帧还有固定的行、列、像素格式。VDMA就是专门为这种场景设计的。

我个人习惯把VDMA理解成“带帧感知能力的DMA”。它不光能搬数据,还能自动识别帧的开始和结束,自动切换缓存地址。嗯,这里要注意,它和普通DMA最大的区别就是:VDMA知道什么时候是一帧的结束,什么时候是下一帧的开始。

核心要点:VDMA = DMA + 帧同步逻辑 + 地址自动生成器

我在项目中遇到过这样一个场景:摄像头采集的数据要实时显示到HDMI上。如果用普通DMA,你得自己写逻辑去判断帧同步信号,然后手动更新地址。用VDMA呢?配置好寄存器,它自己就搞定了。省心不少。

3.2 VDMA的寄存器配置——别被那么多寄存器吓到

VDMA的寄存器其实不少,但常用的就那么几个。我刚开始看文档时也觉得头大,后来总结了一下,其实就三类:控制类、地址类、状态类。

咱们先看一个最简配置流程:

// 1. 复位VDMA
VDMA->S2MM_CONTROL_REG = 0x00000004;  // 复位S2MM通道
VDMA->MM2S_CONTROL_REG = 0x00000004;  // 复位MM2S通道

// 2. 等待复位完成
while(VDMA->S2MM_CONTROL_REG & 0x00000004);
while(VDMA->MM2S_CONTROL_REG & 0x00000004);

// 3. 配置帧缓存地址(以三缓冲为例)
VDMA->S2MM_FRAME_BUFFER1 = 0x10000000;  // 帧缓存1地址
VDMA->S2MM_FRAME_BUFFER2 = 0x10200000;  // 帧缓存2地址
VDMA->S2MM_FRAME_BUFFER3 = 0x10400000;  // 帧缓存3地址

// 4. 配置水平尺寸和跨距
VDMA->S2MM_HSIZE = 1920;               // 一行像素数
VDMA->S2MM_FRMDLY_STRIDE = (1920*3) << 16; // 跨距(字节)

// 5. 启动VDMA
VDMA->S2MM_CONTROL_REG = 0x00000003;   // 使能S2MM
VDMA->MM2S_CONTROL_REG = 0x00000003;   // 使能MM2S

你看,核心配置就这么几行。我建议你重点关注这几个寄存器:

寄存器名称 偏移地址 作用
S2MM_CONTROL_REG 0x00 控制S2MM通道的启动、停止、复位
S2MM_FRAME_BUFFER1 0xAC 帧缓存1的起始地址
S2MM_HSIZE 0xA4 一行数据的字节数
S2MM_FRMDLY_STRIDE 0xA8 帧延迟和行跨距
S2MM_VSIZE 0xA0 一帧的行数

个人经验:配置跨距(Stride)时,记得要按字节对齐。我曾经因为跨距没对齐,导致图像显示出来是斜的。查了半天才发现是跨距设置成了奇数,改成偶数就好了。

3.3 帧缓存机制详解——三缓冲到底好在哪?

帧缓存,说白了就是DDR里划出来的一块区域,用来存图像数据。VDMA支持单缓冲、双缓冲、三缓冲。我一般推荐用三缓冲,为什么呢?

你想想看,如果只用单缓冲:

  • 摄像头往缓存里写数据的同时,显示器从缓存里读数据
  • 读写冲突,画面就会出现撕裂(tearing)

双缓冲能解决撕裂问题:

  • 一个缓存写,一个缓存读
  • 但问题是,如果写入速度比读取快,读的那一帧还没显示完,写的已经覆盖了

三缓冲就优雅多了:

  • 一个缓存正在被写入(摄像头采集)
  • 一个缓存正在被读取(显示器显示)
  • 一个缓存作为备用,随时切换

我画了个图,帮你理解三缓冲的工作流程:

VDMA三缓冲工作机制 帧缓存 1 地址: 0x10000000 ← 正在写入(S2MM) 帧缓存 2 地址: 0x10200000 → 正在读取(MM2S) 帧缓存 3 地址: 0x10400000 待命中 摄像头数据流 显示器数据流 → → 写入缓存 读取缓存 待命缓存

你看这个图,三缓冲的核心思想就是:读写分离,互不干扰。VDMA内部有一个指针,自动在三个缓存之间循环切换。当一帧写入完成后,VDMA会自动把“写入指针”指向下一个空闲缓存,同时把“读取指针”指向刚刚写完的那个缓存。

避坑指南:我曾经在配置三缓冲时,忘了把三个缓存的地址设置成不同的。结果三个指针都指向同一个地址,画面直接花屏。查了两天才发现是地址写重复了。所以配置完一定要检查:三个地址是否不同?地址之间是否有足够的间隔?

3.4 帧同步与中断——别让CPU闲着

VDMA支持多种中断,我最常用的是帧完成中断。每次一帧数据传输完毕,VDMA就会触发一个中断。CPU收到中断后,可以做些轻量级的处理,比如统计帧率、更新显示参数等。

配置中断的代码也很简单:

// 使能帧完成中断
VDMA->S2MM_CONTROL_REG |= 0x00001000;  // 使能S2MM帧完成中断
VDMA->MM2S_CONTROL_REG |= 0x00001000;  // 使能MM2S帧完成中断

// 在中断服务函数中
void VDMA_ISR() {
    u32 status = VDMA->S2MM_STATUS_REG;
    if(status & 0x00001000) {  // 帧完成中断
        // 清除中断标志
        VDMA->S2MM_STATUS_REG = 0x00001000;
        // 处理帧完成事件
        frame_count++;
    }
}

嗯,这里要注意,中断服务函数里不要做太多事情。我见过有人在中断里做图像处理,结果导致中断响应不及时,丢帧严重。中断里只做标志位更新,真正的处理放到主循环里去做。

3.5 实战中的几个坑

最后分享几个我实际项目中踩过的坑:

  1. 地址对齐问题:VDMA要求帧缓存地址按64字节对齐。如果不对齐,数据传输会出错。我习惯用 memalign(64, size) 来分配内存。
  2. 缓存大小计算:一帧数据的大小 = 行数 × 跨距。跨距不一定等于宽度×像素字节数,因为可能有对齐要求。我建议跨距按16字节对齐。
  3. 帧率匹配:如果摄像头帧率是60fps,显示器也是60fps,三缓冲没问题。但如果摄像头是30fps,显示器是60fps,VDMA会重复显示同一帧。这时候需要做帧率转换,或者用双缓冲加帧复制。
  4. 复位顺序:配置VDMA前一定要先复位,等复位完成后再配置寄存器。我见过有人跳过复位直接配置,结果VDMA工作不正常。

我的习惯:每次配置完VDMA,我都会读一下状态寄存器,确认配置是否生效。比如读S2MM_STATUS_REG,看看bit0(VDMA是否使能)是不是1。这个小习惯帮我避免了很多低级错误。

好了,VDMA的原理和配置就讲到这里。记住一句话:VDMA就是个聪明的搬运工,你告诉它搬什么、搬到哪里、搬多少,它自己就能搞定。下一节咱们聊聊怎么用VDMA搭建一个完整的图像采集显示链路。

可可精品资料库