ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Zynq FPGA视频传输中VDMA从硬件配置到Linux驱动完整实战解析

Zynq FPGA视频传输中VDMA从硬件配置到Linux驱动完整实战解析 简介本资源是面向嵌入式Linux驱动开发工程师与FPGA视频系统开发者的技术实践包聚焦Xilinx VDMAVideo Direct Memory Access在Linux环境下的驱动实现与应用配置解决视频流高速传输、低延迟控制及硬件协同优化等核心问题。压缩包共3个文件2个C源码 1个说明文档总大小仅17KB轻量但内容扎实其中xilinx_vdma.c实现设备注册、寄存器初始化、传输控制与中断处理等完整驱动逻辑ast_mode.c封装针对视频编码/显示等场景的预设工作模式简化用户层配置xilinx_vdma.txt提供硬件接口说明、寄存器映射、调用示例及常见问题解析构成“代码配置文档”三位一体的学习闭环。目前已有268人学习下载适合具备Linux字符设备驱动基础、正开展Zynq/Vitis嵌入式视频项目开发的中高级工程师快速上手VDMA底层控制与定制化适配。 做视频传输或者图像采集的FPGA工程师尤其是用Zynq或MPSoC平台的人应该都见过xilinx_vdma.rar这种名字的压缩包。里面往往是一个Vivado工程、一堆IP配置和Linux下的驱动测试代码但解压之后对着里面的工程很多人反而更懵了VDMA IP核到底该怎么配设备树怎么改Linux下那个xilinx_dma驱动跟硬件是怎么对应上的用户态程序又该怎么写才能把一帧图像从DDR里搬出来这篇就是把VDMA这套东西从硬件到Linux软件打通。不只讲IP核怎么配还会把设备树节点、驱动匹配过程、用户态DMA映射这些平时容易卡住的点串起来按真实项目里“从拿到工程到跑通视频通路”的顺序走一遍。适合已经有Vivado基础、正在做Zynq/MPSoC视频项目但对Linux DMA子系统还不太熟的工程师。资料上能找到的寄存器手册会点到为止重点放在代码和实践中真正决定“跑不跑得通”的地方。1. VDMA IP核到底在解决什么问题先别着急打开Vivado得先搞清楚VDMA在整个视频链路里的位置。很多刚从纯FPGA逻辑转到Zynq Linux开发的工程师最容易栽的第一个跟头就是把VDMA当成一个普通DMA控制器来用结果发现完全不是那么回事。1.1 DMA和VDMA的本质差异普通DMA比如AXI DMA做的是“搬一次数据”你给我源地址、目的地址、长度我就搬一次搬完了告诉你。这种模式适合网络包、存储块这类一次性的数据搬运。但视频流不一样。摄像头或者HDMI RX进来的是连续的像素流一秒钟60帧每帧1920x1080x3字节帧与帧之间不能断。如果用普通DMA你得每帧都重新配置一次DMA描述符CPU开销大不说帧间隔稍微抖动一下画面就撕裂了。这里就要引入VDMAVideo Direct Memory Access的核心设计思路了——它本质上是一个“自动循环搬运器”。你在IP核里配置好帧缓冲基地址、行宽、帧高、像素格式它自己就能一帧接一帧地往DDR里写S2MM方向Stream to Memory-Mapped或者从DDR里不停往外读MM2S方向Memory-Mapped to Stream直到你让它停。这就像自来水管道和水桶的区别普通DMA是你喊一次“接一桶”VDMA是管道直接接好了水自己一直流。1.2 帧缓冲机制为什么需要至少两个bufferVDMA能连续工作的关键在帧缓冲Frame Buffer机制。IP核内部有一个寄存器组指向DDR里的若干块内存区域——通俗讲就是“画板”。它写满第一块画板自动跳到第二块第二块写满跳第三块如果配置了三块那就循环回第一块。这里有个关键问题如果只配一块缓冲会发生什么画面会闪烁、撕裂。因为软件在读取当前帧数据的同时硬件可能正在往同一块内存里写入新帧读写打架了。实际项目里通常配三块缓冲也就是常说的三缓冲机制。一帧在写入一帧在显示一帧留给应用程序处理。这三块区域在内存里的地址分配需要在VDMA IP配置阶段就规划好后续软件侧通过寄存器来切换当前活跃的buffer。这也是VDMA和普通DMA在工程实现上一个非常重要的区别软件要把“缓冲区的生命周期管理”和“硬件的循环写入”结合起来设计。1.3 VDMA的存储映射与AXI接口VDMA工作在两种接口之间一边是AXI4 Memory-Mapped接口接DDR控制器另一边是AXI4-Stream接口接视频流源端或目的端。所以一个VDMA IP核实际上有多个AXI接口在配置界面里能看到S_AXI_LITE控制寄存器接口、M_AXI_S2MM写DDR通道、M_AXI_MM2S读DDR通道以及S_AXIS_S2MM接收视频流、M_AXIS_MM2S输出视频流。这几个接口方向容易搞混我见过不少人在连线的时候把S2MM和MM2S接反了。一个简单的记忆方法S2MM是“Stream to Memory-Mapped”也就是接收外部视频流写入DDRMM2S是“Memory-Mapped to Stream”从DDR读数据输出给下游设备。视频采集用S2MM视频显示用MM2S。如果你的应用既要采集又要显示那需要例化两个VDMA或者用带两个通道的版本。2. Vivado工程里的VDMA配置与地址规划拿到xilinx_vdma.rar这种资源包第一步不是看代码而是看Vivado工程里的Block Design是怎么把VDMA接起来的。这个工程文件往往是验证过的环境照抄配置比自己从头摸索要快得多但你必须知道每个配置项为何这样设才能在换分辨率、换像素格式时不翻车。2.1 逐项拆解VDMA IP配置界面打开VDMA IP配置界面看起来选项很多但核心的只有几个而其中最容易出问题的就是地址位宽和突发长度。第一项是Address Width这个必须和DDR控制器的地址位宽一致。Zynq-7000系列通常是32位或40位MPSoC系列常见是40位或44位。如果这个设置不对轻则内存访问异常重则整个系统挂死而且这种问题极难排查因为编译和下载都不会报错运行时才暴露。第二个关键项是Burst Size。VDMA支持2、4、8、16、32、64等突发长度数值越大DDR访问效率越高但也会占用更多的内部存储资源。我通常选择16这是一个性能和资源消耗的平衡点。如果总线时钟频率较低而DDR带宽充裕可以考虑32。这里有个经验判断标准先看你的行宽是多少行宽除以突发大小得到每行需要的突发次数如果这个次数不是整数说明行配置不太合理容易产生效率浪费。第三个关键项是Stream Data Width。这个必须和你的视频源或目的端数据位宽一致常见的有32位和64位。比如RGB888的1920x108060Hz像素时钟148.5MHz32位接口就够了但如果是4K分辨率总线吞吐压力大可能需要64位。这个参数直接决定了行缓冲Line Buffer的大小也影响带宽计算。其他参数比如Frame Buffers数量建议至少设为3、Write/Read Burst、FIFO深度等在资源允许的情况下可以适当调大但作用没有上面几项决定性。2.2 与外设的连接方式和注意事项在Block Design里VDMA通常接在图像传感器或HDMI接收器的下游。视频源信号一般先经过Sensor Interface或Video Timing ControllerVTC做时序处理然后以AXI4-Stream形式进入VDMA。这里有个常见错误直接拿并行像素总线去接VDMA的Stream接口这是不行的。VDMA的AxI4-Stream接口要求信号打包成TVALID/TREADY/TDATA/TKEEP等标准握手信号并行像素接口需要先通过AXI4-Stream DataWidth Converter或者自定义逻辑完成转换。另外VDMA的S_AXI_LITE控制接口要接到PS端的GP或HP端口。怎么接决定了Linux驱动里访问寄存器时要映射的地址范围。比如Zynq-7000的S_AXI_HP0口如果VDMA挂在这个接口后面它的基地址就出现在0x40000000之后的某个位置。这个地址必须记好后面设备树里要写对应地址一个字节都不能差。2.3 中断连接的必要性分析VDMA的中断输出是垂直消隐期信号帧完成中断这在Linux驱动里非常关键。连中断的时候要清楚它走的是PL到PS的哪个中断通道。在Zynq上一般是接IRQ_F2P或通过AXI Interrupt Controller串联在MPSoC上是接GIC的某个SPI中断。我这里要特别强调很多早期调试中中断连不连都能跑因为VDMA有轮询模式。但当你要做实时视频处理的性能评估或者应用程序要“等到一帧完成后立刻处理”中断就是必需品。轮询模式的问题是CPU空转等待浪费在DDR访问时延上中断模式则是在帧边界唤醒CPU效费比完全不同。所以从一开始就把中断连好省得后面改版。3. Linux内核中VDMA驱动的匹配与设备树设计硬件工程做完烧好bitstream启动Linux后VDMA并不会自动出现在/dev下。Linux下的Xilinx DMA驱动使用的是内核的dmaengine子系统你需要把硬件配置用设备树描述清楚让驱动和硬件能对上。这一段是xilinx_vdma.rar里最容易被忽略但又最关键的内容。3.1 设备树节点写法详解在系统设备树里你的VDMA节点通常长这样vdma_s2mm: dma43000000 { compatible xlnx,axi-dma-7.1; reg 0x0 0x43000000 0x0 0x10000; #dma-cells 1; dma-channels 1; dma-channel43000000 { compatible xlnx,axi-dma-s2mm-channel; interrupts 0 29 4; xlnx,datawidth 0x20; }; };注意这里用的是xlnx,axi-dma-7.1而不是xlnx,vdma-xxx这是很多刚接触的人困惑的地方——Xilinx的VDMA和AXI DMA在内核驱动里共用xlnx,axi-dma这个compatible字符串因为VDMA本质上是在DMA基础上扩展了Video功能驱动框架是同一套。所以检测内核模块时会发现xilinx_dma驱动同时绑定了axi-dma和axi-vdma设备。reg属性里的0x43000000必须和Vivado里的地址分配完全一致。interrupts属性里的三个字段分别为中断控制器类型0代表SPI、中断号、触发类型4代表高电平敏感。这个中断号是相对于GIC而言的需要加上32的偏移比如Vivado里显示IRQ_F2P[13:0]的中断号在设备树里要写成实际GIC中的编号。这个如果不一致驱动会加载失败但/proc/interrupts里又会显示一个未注册的中断极易误导排查方向。3.2 xilinx_dma驱动源码结构与内部流程Linux内核的drivers/dma/xilinx/xilinx_dma.c是实现VDMA控制的核心。这个驱动的代码结构还算清晰大致分这几层外层是对dmaengine框架的接口封装实现device_alloc_chan_resources、device_prep_dma_cyclic、device_issue_pending等标准回调。这些回调会让你的应用程序不需要关心硬件寄存器细节一切走抽象接口。中间层是硬件寄存器操作位比如xilinx_dma_start_transfer这个函数负责初始化描述符、写VDMA寄存器组、启动通道。这里比较重要的是desc描述符结构的分配和回收机制每次提交DMA请求时驱动都会为这次传输分配一个描述符传输完成后在中断回调中回收。如果描述符耗尽请求会阻塞这在长时间持续的视频流场景里是个隐患好在VDMA的循环模式下描述符通常是提前配好循环使用的。最底层是中断处理驱动在中断上下文里读ISR寄存器判断是S2MM还是MM2S通道产生的完成中断然后调用回调通知上层。3.3 在应用层通过dmaengine API请求通道理解了驱动结构后App端就简单了。你要做的第一件事是拿到DMA通道的句柄struct dma_chan *chan; dma_cap_mask_t mask; dma_cap_zero(mask); dma_cap_set(DMA_CYCLIC, mask); chan dma_request_channel(mask, NULL, NULL); if (IS_ERR(chan)) { pr_err(Failed to request DMA channel\n); return PTR_ERR(chan); }这里提个细节DMA_CYCLIC这个capability是VDMA循环传输模式必需的。如果你申请的是普通的内存到内存DMA用DMA_MEMCPY就行。很多直接把别的DMA例程搬过来的人就是卡在这里——申请不到通道因为VDMA在dmaengine里注册的是循环传输能力。申请到通道后就是准备描述符struct dma_async_tx_descriptor *desc; desc dmaengine_prep_dma_cyclic(chan, dma_addr, buf_len, period_len, direction, flags);dma_addr是你在应用程序里分配并映射得到的总线地址period_len对应一帧的大小buf_len是总缓冲长度。配置好后调用dmaengine_submit提交然后dma_async_issue_pending启动。4. 用户态对VDMA缓冲区的高效访问与缓存一致性硬件和驱动都打通了但应用程序看到的是一个DMA缓冲区的地址。怎么让Linux用户态程序能高效地读写这块内存同时处理好缓存一致性这是整个项目里最容易“出来花屏、进去丢帧”的环节。4.1 通过mmap把物理地址映射到用户空间DMA缓冲区的地址是物理地址用户态程序不能直接访问。常见方案是写一个简单的字符设备驱动在mmap回调里用remap_pfn_range或dma_mmap_coherent把缓冲区映射到用户地址空间。如果用dma_alloc_coherent分配的缓冲区在驱动里实现mmap非常简单static int dma_mmap(struct file *filp, struct vm_area_struct *vma) { return dma_mmap_coherent(dev, vma, buffer-virt_addr, buffer-dma_addr, buffer-size); }应用程序拿到映射后的虚拟地址就可以直接按像素格式解析和修改图像数据了。需要注意这个映射是uncached或write-through的读写的效率比普通内存低一些但避免了缓存一致性的坑。如果你的应用是纯采集或纯显示这个性能损失可以接受如果要做复杂的图像处理算法建议映射成cached再通过dma_sync_single_for_cpu和dma_sync_single_for_device手动维护一致性。4.2 缓存一致性花屏和丢帧的一大元凶这里展开说下缓存一致性的问题。在Zynq和MPSoC上DDR是支持Cache的CPU读的是L2 Cache而VDMA是直接读写DDR的。如果CPU先写了一个图像的buffer然后告诉VDMA去读VDMA读到的可能是DDR里的旧数据——因为CPU写的数据还在Cache里没刷到DDR。反过来VDMA往DDR写入了新的一帧CPU去读buffer时读到的可能是Cache里的旧数据。解决这个问题有两种路线第一种用dma_alloc_coherent分配内存它天然是非缓存映射的。这个方案简单但读写性能一般。第二种用dma_map_single做动态映射在启动DMA前用dma_map_single或者dma_sync_single_for_device做一次cache clean在DMA完成后用dma_sync_single_for_cpu做一次cache invalidate。实际项目中我见过很多工程师因为省掉了这步同步操作导致采集的图像看起来“卡顿”“模糊”甚至出现整帧重复。排查半天找不到原因最后发现是缓存里存的是上一帧的数据。这个坑的隐蔽性极大因为它只在特定条件下出现——比如CPU负载高、Cache缺失率变化时表现不同有时看着又正常。4.3 一个可直接参考的采集应用程序骨架这里给一个简陋但能跑的S2MM采集框架去掉错误处理后大概长这样int frame_size WIDTH * HEIGHT * 3; int buf_size frame_size * 3; dma_addr_t dma_handle; void *virt_addr dma_alloc_coherent(dev, buf_size, dma_handle, GFP_KERNEL); // 提交循环传输 struct dma_async_tx_descriptor *desc; desc dmaengine_prep_dma_cyclic(chan, dma_handle, buf_size, frame_size, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); desc-callback my_frame_done_callback; dmaengine_submit(desc); dma_async_issue_pending(chan); // 等待帧完成中断 wait_for_completion(frame_done); // 此时VDMA已经写完了一帧到buffer可以用dma_sync_single_for_cpu同步后读取 dma_sync_single_for_cpu(dev, dma_handle, frame_size, DMA_FROM_DEVICE); process_frame(virt_addr); dma_sync_single_for_device(dev, dma_handle, frame_size, DMA_FROM_DEVICE);关键点在回调函数里只做complete(frame_done)不要在中断上下文里做图像处理否则会阻塞其他中断并导致帧丢失。用户态拿到数据后需要快速处理尽量在一个帧周期内完成否则三缓冲也会被追尾覆盖。5. 实测中常见的坑从设备树到中断再到帧不同步就算你按前面的步骤搭好了环境跑起来之后多半还是会有问题。下面这些是我在多个VDMA项目里实测踩过的坑按出现频率排序逐个说下现象和解决办法。5.1 设备树地址或中断号不匹配这个最基础但最常被忽视。Vivado里地址分配可能是自动分配的如果你改了Block Design加了别的IPVDMA的地址可能偏移了而设备树没同步更新。结果就是驱动加载时访问寄存器超时报xilinx-dma 43000000.dma: Cannot verify DMA hardware这样的错。排查方法是启动后先看内核日志dmesg | grep dma如果看到Cannot verify先用devmem检查寄存器devmem 0x43000000 32VDMA_IP_VERSION寄存器的偏移是0x2C读出的值应该类似0x01010200代表版本号。如果读出0xDEADBEEF或全零说明地址不对或者IP核没上电。中断号不匹配的表现则更隐蔽驱动加载正常但一启动传输就卡死cat /proc/interrupts里看不到对应中断号的计数。前面说过的GIC偏移问题这里再强调一次Vivado的IRQ_F2P[13:0]映射到GIC后SPI号要加上32。比如Vivado里连接的是IRQ_F2P[13]设备树的interrupts应该写0 45 4133245。这个不加中断永远触发不了。我见过有人在interrupts里写了0 13 4然后折腾了整整一个周末。5.2 帧不同步导致画面滚动或撕裂帧不同步的表现是画面有规律地滚动。这通常不是因为VDMA配置错而是VTCVideo Timing Controller的时序参数跟实际视频源不一致。VTC里配置的水平同步、垂直同步、前后肩等参数必须和视频源完全一致否则会产生一帧里行数不对的情况。另一个容易忽略的是VDMA的FrameSync信号。如果视频源端没有给VDMA提供帧同步信号或者VDMA配置成GenLock模式但没正确接入同步源VDMA无法判断帧起始位置就会出现持续的画面滚动。解决方法在Vivado里把VDMA的S2MM_FrameSyncIn接上来自Sensor或VTC的VSYNC信号或者配置成Internal GenLock并设置好master通道。这里有个细节VDMA默认是Dynamic GenLock模式可能会跟踪外部信号自动调整如果外部信号抖动它会频繁重启传输。我的做法是优先选择S2MM_FrameSync作为硬同步源。5.3 带宽不足导致的丢帧1920x108060Hz的原始RGB888数据流带宽大约有298MB/s如果把三缓冲的开销、DDR刷新、CPU访问都算上对DDR带宽的压力已经不小了。如果Block Design里还有其他高带宽模块比如PCIe、以太网DMA带宽资源可能不够。丢帧的常见现象是/proc/interrupts里的中断计数不规律或者帧完成中断偶尔丢失。排查方法是查看VDMA的ISR寄存器看S2MM_IRQ_ERR位是否置位。如果置位说明传输过程中出现了FIFO溢出或超时。解决带宽问题有几个方向提高DDR时钟频率或调整DDR仲裁机制给VDMA更高优先级优化Burst Size和FIFO深度减少总线占用时间压缩像素格式比如从RGB888改成YUV422甚至NV12带宽直接减半用Buffer Length对齐优化避免跨页操作这里我特别想提醒的是第三点。很多项目一开始用RGB888做验证没问题但到了产品化阶段发现带宽成了瓶颈。如果一开始就计划好最终用YUV格式VDMA的配置可以稍微做调整省得后面重新改IP。5.4 中断回调里不要做重活最后说一个软性的坑。xilinx_dma驱动的中断回调是在硬中断上下文或tasklet里执行的。如果你在回调里加了耗时的打印、加锁、或者调用了不安全的函数轻则导致中断延迟重则触发内核schedule while atomic的报错。我之前有个项目为了调试方便在回调里加了一堆printk结果帧率直接掉了一半。后来把打印挪到用户态就正常了。调试时的一点经验回调里只做必要的状态更新或唤醒所有的日志输出、数据处理都放到用户态或内核线程里做。6. 调试手段和效率提升技巧很多VDMA的问题排查起来麻烦是因为它牵涉到FPGA硬件、Linux内核、应用程序三个层面。这里分享一套我常用的分层定位法能帮你快速缩小问题范围。6.1 从寄存器层面验证硬件工作状态软件和驱动都能加载但图像出不来时最先要看的是硬件层面是否真的在产生数据。用devmem直接操作寄存器最直接# 查看VDMA的版本寄存器 devmem 0x43000000 32 # 启动S2MM通道 devmem 0x43000030 32 0x00000001VDMA寄存器组里S2MM_DMACR偏移0x30的第0位是通道使能位S2MM_VSIZE偏移0x50和S2MM_HSIZE偏移0x54分别配置行数和行字节数。你可以手动配置一个简单的传输然后通过读S2MM_CURDESC偏移0x48确认地址是否更新。如果地址一直在变说明硬件在搬数据了。这个方法最妙的地方在于它绕过了Linux驱动的“黑盒”直接验证了FPGA侧的连接和VDMA配置是否正确。如果手动配置能跑通问题就在驱动或设备树如果手动配置都不行那就是IP配置或Block Design连线的锅跟Linux一点关系都没有。6.2 用逻辑分析仪内嵌抓取关键信号如果问题在FPGA侧靠devmem只能看到寄存器的值看不到具体的时序问题。这时候可以用Vivado里的ILAIntegrated Logic Analyzer插入到AXI4-Stream总线上抓取关键信号。在Block Design里右击VDMA的S_AXIS_S2MM接口选择“Insert ILA”设好采样深度即可。抓的时候要关注几个关键信号TVALID和TREADY是不是一直握手成功TDATA的数据是否符合预期TKEEP是否有无效字节。如果这些没问题说明输入正确问题在VDMA内部配置或DDR侧如果数据就不对那问题在更上游的传感器或时序转换逻辑。不过ILA的使用要克制它本身会占用不少逻辑资源和BRAM。调试阶段加一两个关键点位就够了跑通后及时删掉否则浪费资源不说还可能影响时序收敛。6.3 通过文件系统节点快速验证最后即便应用层还没写好也可以用Linux文件系统里的简单工具快速验证VDMA通路是否正常。比如在/sys或/debug下dmaengine的通道信息会暴露出来ls /sys/class/dma/ cat /sys/class/dma/dma0chan0/private或者查看内核配置里是否启用了CONFIG_DMA_ENGINE和CONFIG_XILINX_DMAzcat /proc/config.gz | grep XILINX_DMA如果你开启了CONFIG_DMA_API_DEBUG还能在跑应用时通过dmesg看到很多关于DMA API使用错误的提示比如DMA-API: device driver maps memory from process context这种对排查驱动代码的遗漏帮助很大。只是这个配置项本身有性能开销生产环境一定要关掉。6.4 实测效率对比和优化方向我实际测过一个Zynq-7020平台上的项目1920x108060的RGB888输入VDMA S2MM采集CPU跑Linux做简单的边缘检测后经MM2S输出。最早用轮询模式跑CPU占用接近90%而且会偶发丢帧。后来换成了中断驱动三缓冲CPU占用直接降到35%。这是因为轮询模式下CPU要不停检查DMA状态而中断驱动让CPU在帧间休眠只在帧边界被叫醒效率完全不同。再进一步优化把图像处理算法改成NEON优化或放到PL侧CPU占用还能再降。但这个优化的前提是你的DMA和缓冲区的设计已经从底层弄对了不然算法再好也吃不到帧数据。7. 一个绕不开的话题Xilinx官方例程真的靠谱吗网上流传的xilinx_vdma.rar也好官方例程也好代码质量参差不齐。有的是某个工程师的项目导出虽然能跑但工程结构混乱很多细节是写死的。拿来学习没问题直接用到产品上有一定风险。以我接触过的几份工程来看问题主要集中在几个地方一是设备树地址不通用和具体Vivado工程绑定二是驱动版本和内核版本不匹配比如内核5.4和4.19下xilinx_dma驱动的注册流程有差异三是缓冲区分配方式不同有的工程是用/dev/mem直接物理地址映射有的用UIO有的用DMA-BUF代码看着像但机制完全不同混用会出各种问题。我的做法是把这类资源包当作参考实现而不是解决方案。拿到手后先核对几个关键信息使用的Vivado版本、内核版本、硬件平台。然后照着它把工程在本地重新走一遍碰到跟实际平台不匹配的地方改掉之后再逐步移植到自己的板子上。这个过程本身就是一个很好的学习路径能让你真正理解VDMA的每个细节。本文还有配套的精品资源点击获取
返回列表