第三章 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内部有一个指针,自动在三个缓存之间循环切换。当一帧写入完成后,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 实战中的几个坑
最后分享几个我实际项目中踩过的坑:
- 地址对齐问题:VDMA要求帧缓存地址按64字节对齐。如果不对齐,数据传输会出错。我习惯用
memalign(64, size)来分配内存。 - 缓存大小计算:一帧数据的大小 = 行数 × 跨距。跨距不一定等于宽度×像素字节数,因为可能有对齐要求。我建议跨距按16字节对齐。
- 帧率匹配:如果摄像头帧率是60fps,显示器也是60fps,三缓冲没问题。但如果摄像头是30fps,显示器是60fps,VDMA会重复显示同一帧。这时候需要做帧率转换,或者用双缓冲加帧复制。
- 复位顺序:配置VDMA前一定要先复位,等复位完成后再配置寄存器。我见过有人跳过复位直接配置,结果VDMA工作不正常。
我的习惯:每次配置完VDMA,我都会读一下状态寄存器,确认配置是否生效。比如读S2MM_STATUS_REG,看看bit0(VDMA是否使能)是不是1。这个小习惯帮我避免了很多低级错误。
好了,VDMA的原理和配置就讲到这里。记住一句话:VDMA就是个聪明的搬运工,你告诉它搬什么、搬到哪里、搬多少,它自己就能搞定。下一节咱们聊聊怎么用VDMA搭建一个完整的图像采集显示链路。
可可精品资料库