ARTICLE DETAIL

资讯详情

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

XDMA从原理到实战:FPGA PCIe高速数据传输的完整指南

XDMA从原理到实战:FPGA PCIe高速数据传输的完整指南 做FPGA的兄弟应该都有体会凡是涉及高速数据传输的项目迟早都要跟PCIe打交道。而Xilinx官方提供的XDMADMA/Bridge Subsystem for PCI ExpressIP核基本是Zynq和UltraScale系列上做PCIe通信绕不开的选择。我前前后后在三四个项目里用过这个IP从最开始的完全摸不着头脑到后来能熟练定位各种枚举失败、DMA卡死的问题中间踩了不少坑。这篇笔记就是把XDMA从原理到实操的要点梳理一遍给准备入坑或者正在被XDMA折磨的朋友一些参考。先说清楚XDMA能干什么它本质上是一个运行在FPGA逻辑里的PCIe端点控制器一头接PS/PL的AXI总线一头接主机端的PCIe链路。有了它主机CPU就能通过DMA方式直接读写FPGA侧的DDR或者寄存器而不需要CPU逐字搬运吞吐量可以跑到PCIe带宽的90%以上。这篇文章适合刚接触PCIe的FPGA工程师、做高速数据采集的嵌入式开发者以及那些想搞清楚XDMA内部描述符机制、中断流程、驱动匹配原理的人。我用的环境主要是Vivado 2020.2到2022.1板卡覆盖了VCU118、ZU9EG和自家设计的PCIe x8板卡主机侧是Ubuntu 18.04/20.04驱动用的是Xilinx官方提供的xdma驱动源码自己编译。下面的内容都是基于这些环境实测得来的经验有些细节可能跟你手里的版本有出入但核心机制是通用的。1. XDMA到底是什么架构拆解与设计思路1.1 XDMA在整个PCIe通信链路中的位置要理解XDMA首先得有一个全局视角。整个PCIe通信链路是这样组织的主机CPU通过Root ComplexRC发出TLP事务层报文经过PCIe链路到达FPGA内部的EndpointEP也就是XDMA IP核。XDMA收到报文后有的报文是用来配置BAR空间的有的是用来读写数据的。XDMA内部有一个AXI Master接口和一个AXI Slave接口前者用来发起对FPGA内部地址空间的读写比如DDR后者用来响应主机的配置请求。这个架构里最关键的一点是DMA引擎在FPGA侧而不是在CPU侧。CPU只需要准备好描述符告诉FPGA引擎“你把这块数据搬到哪里”然后FPGA自己就去搬了搬完通过中断通知CPU。这就把主机的负担降到最低数据搬运全部在PCIe链路上完成。实际上XDMA也支持Host-to-CardH2C和Card-to-HostC2H两个方向的搬运这是两个完全独立的数据通路各自有独立的描述符环形队列。1.2 DMA模式与Bridge模式两种工作方式的取舍XDMA这个名字容易让人以为它只能做DMA其实它还支持一个Bridge模式也就是不带DMA引擎纯粹把AXI总线桥接到PCIe总线上。这个模式说白了就是让主机通过BAR空间直接访问FPGA侧挂在AXI总线上的外设访问延迟高一些但不需要驱动里维护描述符适合控制类寄存器读写。我个人的经验是小数据量控制通信用Bridge模式或者AXI-Lite接口就够了大数据量搬运必须用DMA模式。如果你在项目里发现吞吐量上不去先检查是不是走了Bridge路径。DMA模式的优势在于描述符由硬件自动获取数据路径上可以做流水线化和多队列并发而且XDMA内部还有写/读数据缓存能有效吸收PCIe突发传输带来的延迟抖动。具体到IP配置界面Mode选项有两种Advanced高级和Basic基础。Advanced模式下你可以在一个IP里同时例化AXI4 Memory Map接口和AXI4-Stream接口的DMA而且可以配置多个DMA通道。Basic模式只能选一种接口类型适合快速搭建验证环境。初次上手建议用Basic模式把通路跑通再去折腾Advanced。1.3 AXI接口类型选择AXI4-MM还是AXI4-Stream这是配置XDMA时第一个要做的决策。AXI4-MM接口适合连接DDR、BRAM这类有地址的存储资源数据通路是带地址的读写。AXI4-Stream接口没有地址概念就是连续的数据流适合接ADC采集、以太网MAC这类天然就是流式数据的模块。有一个常见误区是有人觉得AXI4-Stream更快其实不对。Stream接口因为少了地址译码和响应握手的开销小包传输时延迟确实低一点但大数据块传输时AXI4-MM只要地址连续硬件会自己合并成突发传输带宽一点不差。我实测在UltraScale上AXI4-MM读写DDR跑PCIe Gen3 x8吞吐量能到6500MB/s左右AXI4-Stream也就7000MB/s出头差距可以忽略。真正影响选择的因素是你FPGA侧的数据通路形态。如果数据是整块DDR缓存就用AXI4-MM省去把数据拆包的麻烦如果是流式的就用AXI4-Stream配合AXI-Datamover之类的辅助IP能做流水线处理。项目的实际需求决定了接口选型别为了追求一个“更快”的说法盲目选Stream。2. 工程配置与Block Design搭建实操一步步来2.1 Block Design里如何搭建最小可用系统XDMA在Vivado里是以IP核形式存在的但强烈建议在Block Design里搭建整个系统不要用纯HDL例化。原因是XDMA除了数据通路本身还牵扯到PCIe的复位逻辑、时钟管理、AXI互联这些在Block Design里可视化配置会更直观Debug时也能看连线。最小系统的搭建流程大致如下先把XDMA IP拖进Block Design然后添加一个Clocking Wizard供给100MHz参考时钟再挂一个AXI BRAM Controller加BRAM作为DMA搬运的目标先别急着接DDR等通路验证了再接。注意必须有一个AXI SmartConnect或AXI Interconnect把XDMA的AXI Master接口连到各个从设备上否则地址映射没法做。地址映射这里有个细节需要特别留意如果你配置的是AXI4-MM接口那么在Address Editor里要给XDMA的AXI Master接口分配从设备地址段这个地址段必须跟驱动里BAR空间无关它只是FPGA内部地址映射。比如我习惯把BRAM放在0x0000_0000开始的位置大小64KB驱动侧DMA时填写的地址就是0x0000_0000加上偏移量。2.2 关键参数配置误区与推荐取值XDMA配置界面里参数非常多真正影响系统行为的有这么几个PCIe链路速率Gen3还是Gen4、lane宽度x1到x16、DMA接口宽度64位还是128位、描述符通道数量。很多人为了追求性能直接选Gen4 x16结果板卡布线不过关根本link不上反而不如Gen3 x8稳。我踩过最大的坑是DMA接口宽度配置。XDMA IP的AXI接口数据位宽默认是64位但如果你在FPGA里接了128位宽的DDR控制器就需要把DMA接口位宽改成128位否则AXI互联带宽会卡在64位性能直接腰斩。修改后注意AXI总线的tick节拍会变化对应的M_AXI时钟频率也需要调高确保总带宽匹配。另外要提醒的是PCIe配置空间里的Device ID、Vendor ID、Class Code这些值会被驱动用来匹配设备。默认的Vendor ID是Xilinx分配的0x10EEDevice ID默认是0x9038或0x903F之类。如果你要做驱动开发建议在IP里改成自己的值避免跟别人的设备混淆。Class Code要设为0x0580Generic DMA Controller这样系统才能正确识别为DMA设备否则驱动可能匹配不上。2.3 时钟与复位最容易忽视却最致命的部分XDMA的工作时钟分两部分PCIe链路时钟由外部100MHz差分参考时钟提供而AXI用户逻辑时钟则由IP内部的User Clock输出提供。在Block Design里这个User Clock必须正确连接到你的逻辑或者互联上不然DATA通路读写会直接卡死或者超时。复位方面XDMA提供两个复位输出axi_aresetn和user_lnk_up。前者是AXI接口的复位为你用户逻辑提供后者表示PCIe链路已经link up。很多人的错误在于直接拿复位信号去复位自己的逻辑却不考虑上电时序。正确做法是用户逻辑的复位至少延时一段时间确保AXI接口稳定必要的话加一个复位同步和展宽逻辑。实际调试时可以用ILA抓user_lnk_up信号确认它在PCIe链路建立后拉高。如果这个信号一直没有拉高那问题大概率在物理层比如参考时钟不稳、lane反转、PCIe金手指接触不良等跟用户逻辑无关。先确认物理层链路是好的再去找DMA通路的问题。3. 驱动与软件侧主机端如何配合XDMA工作3.1 PCIe枚举过程与BAR空间分配机制主机上电或系统复位后BIOS/OS会枚举PCIe总线上的所有设备每个Endpoint都会被分配一个Bus/Device/Function编号同时BAR空间也会被映射到宿主机的内存物理地址。XDMA IP里配置的BAR空间大小决定了主机能看到多大的寄存器窗口和DMA访问窗口。XDMA的BAR0默认会暴露配置寄存器、中断控制、状态寄存器等BAR1通常作为用户逻辑的桥接窗口还有BAR2用来映射DMA控制寄存器。在Linux下用lspci -vv可以看到每个BAR的分配情况。如果BAR空间分配到了系统预留地址段驱动里使用remap函数时要注意物理地址转虚拟地址的过程我遇到过AR地址被分配在32位不可缓存区域导致性能骤降的情况后来通过在BIOS里开启Above 4G Decoding解决。枚举过程中还有一个小坑XDMA的复位状态默认是D0状态但如果系统做了ASPML1低功耗管理可能进入D3状态这时候驱动访问会失败。解决办法是在驱动加载时显式唤醒设备或者在内核参数里禁用PCIe的ASPM电源管理。3.2 描述符环形队列与Scatter-Gather机制详解XDMA的精髓在于描述符Descriptor机制。主机在内存里维护一个环形队列队列中每个描述符描述了一段数据的方向、大小和地址。FPGA侧的DMA引擎会持续从队列头部取描述符执行完成后把描述符状态写回然后通过中断通知主机可以回收和补充描述符。理解这个机制最重要的是明白地址翻译描述符里的地址是主机物理地址XDMA作为PCIe设备发起内存读获取描述符、内存写写回状态这个过程实际是DMA引擎向RC发起Memory Read/Write TLP。也就是说FPGA侧能直接读到的是主机物理内存不是FPGA内部地址。这跟AXI Master接口对FPGA内部DDR的访问是两套独立地址空间。Scatter-Gather的好处不言而喻主机侧不需要为DMA传输分配连续的物理内存可以提交多个物理上不连续的缓冲区给一次逻辑传输。XDMA每个描述符都能指定独立的长度和地址硬件会把它们串成一个SG列表顺序执行。驱动里用dma_alloc_coherent或者__get_free_pages分配内核缓冲区时天然就是物理分散的XDMA的SG机制正好匹配。3.3 Xilinx官方驱动框架选型与编译建议目前Xilinx官方维护的XDMA驱动有两种路径旧版内核自带的xilinx_dma驱动和在GitHub上维护的dma_ip_drivers项目里的xdma驱动。后者功能更全支持多队列、事件中断、用户态mmap推荐使用。编译驱动时要确认内核头文件与你当前Linux内核版本一致否则insmod会报版本不匹配。在Ubuntu上可以直接apt install linux-headers-$(uname -r)然后make。源码里会有libxdma的静态库这个库是内核模块的核心如果只改了DMA相关的宏没重新编译libxdma会导致驱动行为和预期不符。驱动加载后的设备节点一般是/dev/xdma0_control、/dev/xdma0_h2c_0、/dev/xdma0_c2h_0等。控制节点用来操作寄存器、中断测试数据节点用来做DMA读写。Xilinx在源码的tests目录下提供了一个run_test脚本可以快速验证读写正确性再配合dma_from_device和dma_to_device这两个命令行工具基本能完成大部分功能验证。4. 真正的调试战场常见问题与性能实测技巧4.1 板卡枚举失败从物理层到逻辑层的排查路径如果你插上PCIe板卡后系统里找不到设备按这个顺序排查基本都能定位先用lspci确认有没有设备ID然后看lspci -vv里的Link Status确认是不是LnkSta: Speed 8GT/s, Width x8如果Link Status没有或者很低那就是物理层问题重点查100MHz参考时钟有没有起振、PCIe金手指有没有插好、差分线是不是有断点物理层正常但设备不见重点查FPGA侧的复位时序和配置引脚以及PCIe的PERST#信号。一个特别的坑是PCIe参考时钟的PLL锁定问题。有的板卡使用可编程时钟发生器默认输出频率可能是100MHz或者125MHz如果型号配置不对XDMA无论如何都不能link up。我遇到过一次就是时钟芯片的配置电阻被顺手换了导致输出变成了125MHz整整排查了一天才发现。4.2 DMA传输卡死超时机制与环形队列水位异常DMA传输卡死是最常见的故障。碰到这种情况我建议先用XDMA自带的状态寄存器看环形队列的head和tail指针或者直接读描述符的状态位。如果tail指针一直不动说明FPGA没有去取新的描述符如果head指针已经推进但状态位没有更新说明FPGA执行异常。还有一些卡死是中断丢消息导致的主机一直等一个永远不会来的完成中断这时IO会一直挂起。排查时要确认中断使能寄存器有没有正确设置以及中断在驱动里有没有被正确清除。Xilinx的XDMA驱动默认使用MSI中断如果主板不支持MSI或者中断号被占用也会导致中断不触发可以尝试改用legacy中断测试。调试手段上Vivado里的ILA和VIO是利器。把XDMA的axi_aresetn、user_lnk_up以及AXI总线的AWREADY、WREADY、BVALID等信号拉出来看能快速确认是阻塞在握手还是数据没有返回。我通常在第一次上板验证工程里就预先留好这些探针不然后期在时序收敛都费劲的设计里再加ILA排查效率会低很多。4.3 从带宽测试到瓶颈定位三轮实测数据分享最后分享一组我实际测过的数据供参考。测试环境是VCU118UltraScale VU9PPCIe Gen3 x8XDMA配置为AXI4-MM 128位接口主机侧是双路至强。最开始我用官方默认配置测试读带宽只有3200MB/s写带宽4000MB/s明显偏低。第一轮优化把AXI接口位宽改成128位并调整SmartConnect的时钟到250MHz读带宽提升到4500MB/s。第二轮优化在驱动里增大DMA缓冲区到8MB减少SG列表的空洞带宽提升到5500MB/s。第三轮优化确保描述符队列和缓冲区在内存中按2MB对齐带宽稳定在6200MB/s左右写方向达到6600MB/s。这三轮优化的经验值很直观硬件位宽和时钟是地基驱动缓冲区大小是对齐的关键而2MB大页对齐能避免TLB未命中带来的性能损耗。如果你的带宽还上不去建议用perf工具看是不是有大量cache miss或者看mpstat确认CPU占用率通常非对齐缓冲区会导致CPU频繁处理缺页中断占用率居高不下。5. 一些实践经验总结5.1 从零开发时的学习路径建议如果你是第一次接触XDMA我建议不要上来就去啃完整的驱动源码和协议手册先跑通一个最小的Demo。具体路径是先在Vivado里搭建Block Design用XDMA BRAM组成最小系统导出硬件后在Linux下用官方驱动里的run_test脚本验证读写然后换成AXI4-MM直连DDR核对吞吐量最后再去看XDMA的寄存器手册和描述符格式编写自己的驱动代码。这样每一步都建立在可验证的结果上不容易陷入“看了大量文档却不知道哪里出错”的困境。5.2 项目设计中预留调试接口的重要性使用XDMA的项目通常意味着数据传输带宽较大调试难度也成倍增加。我的建议是在硬件设计阶段就预留好ILA探针、空闲的GPIO用于触发逻辑分析仪以及一个串口用于打印FPGA侧的状态信息。这些“不起眼”的设计到了现场联调的阶段能救命。我遇到过在客户现场出现偶发DMA超时UART打印的状态字帮助锁定了是外部DDR带宽不足导致的拥塞而不是XDMA本身的问题。5.3 XDMA后续扩展与其他IP搭配思路XDMA很少在系统中单独使用通常它会搭配AXI-Datamover、AXI-Stream Switch或者AXI-SmartConnect一起构成完整的数据通路。如果你需要多通道数据采集可以考虑在XDMA的AXI4-Stream接口上挂多个AXI-Stream Switch实现多通道分时复用如果是存储型应用可以在AXI4-MM接口后挂一个AXI Interconnect把数据分发到多块DDR或BRAM。总之XDMA是数据通路的核心但不是全部确定好数据流的方向和带宽需求再决定怎么搭配其他AXI生态的IP能省掉很多改版的痛苦。我用XDMA做过几款板卡印象最深的一点是它“看起来简单、用起来复杂”——IP配置几个下拉框就能生成一个看似完整的系统但真到调吞吐量、查中断、验证长时间稳定性时各种硬件细节和驱动交互问题才会浮出水面。希望这篇笔记能帮你少走点弯路尤其是那些我当初花了几个通宵才定位到的坑你能一次就绕过去。
返回列表