第四章:VDMA驱动开发——AXI-Stream与AXI4-MM转换、寄存器配置与中断处理
好,咱们进入VDMA驱动开发的正题。这一章,我打算把VDMA的核心机制掰开揉碎了讲清楚。说白了,VDMA就是视频数据流的“交通枢纽”——一边是AXI-Stream这种流式接口,一边是AXI4-MM这种内存映射接口。你想想看,摄像头采集的数据是源源不断的流,而DDR内存是按地址存取的,这两者怎么对接?VDMA就是干这个活的。
4.1 AXI-Stream与AXI4-MM的转换机制
先说说这两个接口的本质区别。AXI-Stream,没有地址概念,数据像水管里的水一样流过去。AXI4-MM呢,你得告诉它“把数据写到0x1000地址”,它才干活。VDMA内部其实就是一个状态机,负责把流式数据打包成带地址的突发传输。
我个人习惯把VDMA的工作模式分成两种:
- 写通道(S2MM):从AXI-Stream接收数据,写到DDR里。说白了就是“采集模式”。
- 读通道(MM2S):从DDR读数据,转换成AXI-Stream发出去。这就是“显示模式”。
我在项目中遇到过一个问题:摄像头输出的数据是24位RGB,但VDMA的AXI-Stream数据位宽是64位。这时候就需要做位宽转换。VDMA内部有FIFO做缓冲,但你要注意数据对齐——我曾经因为没配好STRIDE参数,导致图像显示出来是斜的,排查了半天才发现是行对齐出了问题。
核心要点:VDMA的转换效率取决于突发长度(Burst Length)。建议设置为16或32,太短了带宽上不去,太长了容易占总线太久。
4.2 VDMA寄存器配置详解
VDMA的寄存器说多不多,说少不少。我一般只关注几个关键的,其他的用默认值就行。咱们直接看代码:
// 基础配置
#define VDMA_BASE_ADDR 0x43000000
// 寄存器偏移
#define VDMA_S2MM_CR 0x00 // 控制寄存器
#define VDMA_S2MM_SR 0x04 // 状态寄存器
#define VDMA_S2MM_VSIZE 0x50 // 垂直尺寸
#define VDMA_S2MM_HSIZE 0x54 // 水平尺寸
#define VDMA_S2MM_FRMDLY 0x58 // 帧延迟
#define VDMA_S2MM_START_ADDR1 0xAC // 帧缓冲地址1
#define VDMA_S2MM_START_ADDR2 0xB0 // 帧缓冲地址2
#define VDMA_S2MM_START_ADDR3 0xB4 // 帧缓冲地址3
// 配置函数
void vdma_config(uint32_t width, uint32_t height, uint32_t stride) {
// 1. 复位VDMA
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_CR, 0x04);
usleep(1000);
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_CR, 0x00);
// 2. 配置帧尺寸
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_VSIZE, height);
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_HSIZE, width * 4); // 32位像素
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_FRMDLY, 0);
// 3. 设置帧缓冲地址(三缓冲)
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_START_ADDR1, DDR_BASE_ADDR);
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_START_ADDR2, DDR_BASE_ADDR + stride * height);
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_START_ADDR3, DDR_BASE_ADDR + stride * height * 2);
// 4. 启动VDMA
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_CR, 0x01); // 使能S2MM通道
}
我的经验:配置完寄存器后,一定要读一下状态寄存器(SR)确认VDMA是否进入运行状态。我见过有人配完直接就跑,结果VDMA根本没启动,数据全丢了。
这里有个坑要注意:HSIZE的单位是字节,不是像素。如果你的图像是1920x1080,每个像素32位(4字节),那HSIZE要填1920*4=7680。我曾经在这个地方栽过跟头,图像显示出来全是乱的。
4.3 中断处理机制
VDMA的中断处理,说白了就是让CPU知道“一帧数据已经准备好了”。我建议使用三缓冲机制,配合中断,这样CPU和VDMA可以流水线工作,效率最高。
中断配置分三步走:
- 使能VDMA中断:在控制寄存器里设置相应的中断使能位
- 配置中断控制器:在Zynq的GIC里注册VDMA中断
- 编写中断服务函数:处理帧完成事件
// 中断初始化
void vdma_intr_init() {
// 使能VDMA中断
uint32_t cr = Xil_In32(VDMA_BASE_ADDR + VDMA_S2MM_CR);
cr |= 0x1000; // 使能S2MM帧完成中断
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_CR, cr);
// 注册中断(以Xilinx SDK为例)
XScuGic_Config *gic_cfg = XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID);
XScuGic_CfgInitialize(&gic, gic_cfg, gic_cfg->CpuBaseAddress);
XScuGic_Connect(&gic, XPAR_FABRIC_VDMA_0_S2MM_INTROUT_VEC_ID,
(Xil_InterruptHandler)vdma_isr, NULL);
XScuGic_Enable(&gic, XPAR_FABRIC_VDMA_0_S2MM_INTROUT_VEC_ID);
}
// 中断服务函数
void vdma_isr(void *arg) {
uint32_t sr = Xil_In32(VDMA_BASE_ADDR + VDMA_S2MM_SR);
if (sr & 0x1000) { // 帧完成中断
// 清除中断标志
Xil_Out32(VDMA_BASE_ADDR + VDMA_S2MM_SR, 0x1000);
// 切换当前帧缓冲
current_frame = (current_frame + 1) % 3;
// 通知应用层处理图像
frame_ready = 1;
}
}
注意:中断服务函数里不要做耗时操作!我见过有人直接在ISR里做图像处理,结果导致中断嵌套,系统直接崩溃。正确的做法是:ISR里只设置标志位,主循环里再处理数据。
4.4 实战中的避坑指南
嗯,这里我要多说几句。VDMA驱动开发,坑真的不少。我总结几个常见的:
- 帧同步问题:如果VDMA和摄像头时钟不同步,会出现帧错位。解决办法是使用VDMA的帧同步信号(F Sync)来对齐。
- 缓存一致性问题:Zynq的CPU有L1/L2 Cache,VDMA写DDR的数据可能还在Cache里没刷出去。记得用
Xil_DCacheFlushRange()刷缓存。 - 中断丢失:如果中断处理太慢,VDMA可能已经产生了下一帧中断,导致上一帧中断被覆盖。我建议用环形缓冲区来缓存中断事件。
我曾经在一个项目中,VDMA采集的图像每隔几帧就会出现撕裂。排查了三天,最后发现是帧缓冲地址没对齐到64字节边界。嗯,从那以后我配置地址时都会用memalign(64, size)来分配内存。
4.5 VDMA工作流程图
下面这张图是我自己画的VDMA工作流程,你看一眼就能明白整个数据流向:
从这张图你能看到,VDMA处于整个数据流的中心位置。左边是摄像头过来的流式数据,右边是DDR的内存空间,上面是CPU通过AXI-Lite配置寄存器,下面是中断通知CPU“数据准备好了”。整个流程环环相扣,任何一个环节出问题,图像就出不来。
调试小技巧:如果VDMA不工作,先检查状态寄存器的VDMA_S2MM_SR的bit 12(帧完成标志)。如果这个位一直没置1,说明VDMA根本没收到数据。这时候回头查摄像头那边的时钟和数据使能信号。
好了,VDMA驱动开发的核心内容就这些。说白了就是三件事:配寄存器、处理中断、管好帧缓冲。你把这些搞明白了,图像采集这块基本就通了。