ARTICLE DETAIL

资讯详情

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

Zynq PL高效读写DDR的利器:AXI Datamover IP核应用解析

Zynq PL高效读写DDR的利器:AXI Datamover IP核应用解析 1. 项目概述与核心需求解析1.1 PL为什么需要读写DDR做Zynq的开发绕不开PS和PL协同工作。PS端跑Linux或裸机通过DDR控制器访问DDR内存这没什么好说的。但PL端要访问DDR情况就变了。很多人第一次遇到这个需求是这样的FPGA逻辑里采集了一批ADC数据或者要往某个加速模块塞一批系数数据量很大存在BRAM里不现实必须放到DDR里。这时候你有几个选择一是把数据通过AXI GP口传给PS让PS帮你写到内存里但GP口效率低而且占CPU二是自己写一个AXI Master逻辑直接怼到互联矩阵上但AXI协议、突发传输、对齐要求、响应处理这些自己撸一轮工作量不小坑也多。Datamover这个IP做的事情就是把第二种方案的工程量大幅简化。它是一个AXI数据搬运引擎内置读通道和写通道的状态机你只需要给它一个命令它就能按你的要求从DDR里读出一段数据转成连续的AXI-Stream流或者把AXI-Stream流收下来打包成AXI写突发写入DDR。整个过程不需要CPU干预搬完之后给你一个中断或者状态信号。从连接关系上看Datamover一边是AXI4主接口接到系统的互联矩阵或MIG另一边是AXI4-Stream接口接到你的PL逻辑。它还有一组精简的寄存器接口用来接收命令和上报状态。1.2 Datamover能解决的问题清单我在项目里实际用过之后总结Datamover适合处理的场景高速采集ADC数据连续流入实时落盘或缓存到DDR需要先把数据搬到内存。数据预处理DDR里的图像数据按块读出来送到PL端的图像处理IP处理完再写回DDR。小型DMA引擎做高速接口比如PCIe、GbE的时候用Datamover做数据面搬运把接口数据和内存数据解耦。多通道数据汇集多个低速流聚合到DDR或者一个高速流分拆到多个内存区域。它不适合做什么不适合做那些需要频繁跳转地址、访问粒度很小、或者需要复杂地址重映射的场景。Datamover适合的是“连续的大块数据搬移”高效区间是128字节以上的数据传输。曾经的方案走的是PS通过AXI-Lite配置寄存器、再通过AXI-GP口访问DDR当数据量大、搬移频繁之后CPU负荷高得离谱。换成Datamover之后PS只需下发命令剩下的搬移全部由Datamover完成CPU占用率直接掉了一大截。2. AXI协议核心细节与Datamover的关联2.1 AXI握手与背压逻辑Datamover本身是AXI协议的忠实执行者所以先得把AXI协议的关键点说透。AXI4协议的核心是五个独立通道读地址AR、读数据R、写地址AW、写数据W、写响应B。每个通道都有一套VALID/READY握手机制。握手规则就三条VALID不能等READYREADY可以等VALID只有VALID和READY同时拉高时该拍才有效源端一旦拉高VALID必须保持到握手成功。这里的细节决定了很多逻辑能不能跑通。比如你想实现一个能从外设反压的AXI从机那么你的READY信号就相当于水龙头的阀门阀门关小水流自然就慢了。Datamover内部就是靠这套握手来协调DDR读数据和Stream数据输出的节奏。DDR侧读数据什么时候回来是MIG决定的有延迟Stream侧什么时候能收取决于下游FIFO或用户逻辑。中间怎么平衡靠的就是AXI协议天然支持的背压能力。当你的下游处理速度慢Stream的READY拉低Datamover会停住对应的读通道也通过没有READY的机制停止接收新的读数据最后通过AXI协议逐级上溯的背压传导让MIG读通道暂时暂停。很多新手调不开数据链路问题就出在对背压的不理解。Datamover跑起来数据是流水线式的只要有一拍没握手成功后面的数据必须等。如果你在Stream端一个FIFO没写对ProgFull信号没用对或者根本没有对READY做正确的时序处理数据就会在某个节点堵死表现就是整条链路半天不出数。2.2 AXI突发与对齐问题AXI协议里有一个重要的概念突发Burst。Datamover读DDR的时候默认按照INCR类型、Burst Length为16的方式进行。这是DDR控制器效率最高的方式。但是AXI协议要求每个突发不能跨越4K地址边界这意味着如果你要读的地块横跨了4K边界Datamover会自动把它拆成几个小的突发来处理。对齐是另一个必修课。AXI要求写的起始地址必须与数据总线的宽度对齐。你的MIG如果配置成64bit位宽、DDR数据总线512bit那AXI侧的总线宽度是256bit或者128bit取决于你MIG的配置。Datamover要求它的MM2S和S2MM接口收到命令对齐到内部数据宽度的边界否则可能会有意想不到的行为。实际项目里常见错误是命令地址没有对齐导致读出来的数据字节序出错。这里我吃过亏地址不对齐的时候MIG返回的数据是正常顺序的但Datamover在内部会把数据拼成Stream流起始字节如果不在总线宽度边界上它就会把后面几个周期作为起始数据这就把整段数据搞歪了。解决办法要么是地址全部对齐到32字节要么在软件里做偏移补偿。2.3 valid/ready时序中的隐藏坑Datamover的Stream接口通常还会带有TLAST信号表示当前这个是最后一个数据。TLAST的位置由命令里的BTT字节传输数量决定。BTT如果不是总线位宽的整数倍TLAST就会出现在中间某一拍的中间字节处。这时候你必须正确解析TLAST和TKEEP才能准确识别最后一个有效字节。还有一种情况MM2S模式下BTT的长度比实际DDR里的有效数据还大Datamover依然会发起对应的读请求直到把BTT读完。如果你的DDR地址越界了可能读到未定义的数据也可能触发MIG的ECC错误。所以软件在下发命令之前务必确认地址和长度都在合法的DDR内存范围内。3. 实际配置与操作流程3.1 Datamover IP配置详解在Vivado里添加AXI Datamover IP打开配置界面你会看到几个关键选项第一是关于MM2S和S2MM两个通道的使能选择。如果你只需要单向搬移可以只勾一个通道节省LUT和FF资源。双向通道都使能的情况下两个通道是独立的可以同时跑也就是全双工。第二是地址宽度和数据宽度。地址宽度要和你的系统中Datamover所连AXI总线的一致通常是32位或64位。数据宽度建议和MIG的AXI数据端口位宽一致这样效率最高。比如MIG配置AXI数据端口为512bitDatamover的数据宽度就选512bit。如果选窄了比如128bit就相当于在Datamover内部做了一个数据宽度的转换会损失带宽。第三是Buffer Length也就是Max Burst Size。这个参数决定了Datamover发出最大的AXI突发长度内部FIFO的大小相关。默认16对应16个节拍如果数据宽度512bit那一突发就是1KB数据。这个值不是越大越好要根据你MIG的实现来定。第四是指令队列和状态FIFO的深度。命令队列深度代表Datamover可以缓存多少条未处理的命令。对于需要预先下发多条命令、减少CPU交互的场景这个值要适当加大。默认4条一般情况下够用但如果你希望一次性下发很多传输命令、并且还要支持乱序完成可以开到8或16代价是资源占用增加。第五是Stream数据宽度这是Datamover输出给用户逻辑的AXI-Stream端口位宽。理论上它可以和数据宽度相同也可以不同。多数情况两者一致省得做宽度转换。3.2 Datamover命令寄存器与错误上报Datamover用的是一组精简的寄存器映射正经路径主要通过两个寄存器进行命令发送指令寄存器0x00~0x08依次写入低32位地址、高32位地址如果地址宽度是64位、剩余字节数BTT以及控制位。控制位里比较关键的有EOF产生TLAST、Tag用户自定义标签在状态返回时带回来。写命令的时序是先写地址再写BTT控制。写地址和写BTT之间其实有冒险稳妥做法是最后写BTTBTT写入触发器发送命令。状态寄存器0x10附近表示当前通道是否空闲、命令队列是否满、错误码等。这里重点说的是错误状态。我在项目里踩过一个坑一条非对齐的读命令真的把状态寄存器里的错误位点亮之后整个通道暂停工作不管你怎么写命令都没反应了。这个状态必须通过写状态清除寄存器来恢复否则通道永远卡死。错误的另一个来源是命令长度和地址的对应关系。Datamover内部会计算最后一个突发是否会越过4KB边界如果命令本身跨边界它会自动拆分这个没问题。但如果跨了物理内存的边界比如你配置的DDR基地址只有512MB命令却访问了1GB的偏移Datamover自己不会检查直接走到了MIG控制器MIG那边返回错误标志Datamover把错误记为“中断错误”再去查具体是哪条命令出了问题就要配合上游软件的地址分配表来判断了。3.3 PS发送读写DDR完整时序一个典型的PS通过Datamover读写DDR的流程如下第一步PS侧配置好DDR内存空间确保待搬运数据在内存中有合法映射。如果是Linux系统需要分配连续物理内存。用CMA或者内核模块预先分配一块固定物理地址的空间然后再给到用户态使用。很多朋友直接在应用层malloc拿到的虚拟地址和物理地址完全没有对应关系送入Datamover之前必须转换成物理地址。第二步设置Datamover寄存器使能对应的通道把命令队列清空确保是干净状态。第三步对于写操作即DDR-PL方向MM2S命令包括源地址DDR侧、BTT长度、控制字。对应地PL侧准备好接收Stream数据FIFO要有足够的纵深。对于读操作即PL-DDR方向S2MM命令包括目标地址DDR侧、BTT长度。PL侧发起搬移时把准备好数据的FIFO接入Datamover的S2MM接口即可。第四步轮询状态寄存器或者等待中断确认命令完成。如果想等中断Datamover的中断输出引脚要连到PS的中断控制器。第五步对于Linux系统如果是用户态访问数据的话还要做一次缓存刷新和无效化操作否则DMA写完之后CPU端缓存还是旧的等你想读的时候实际拿到是脏缓存数据。这是做DMA驱动最容易忽略的一个点。3.4 通过Datamover实现PS和PL间的DDR数据交换再具体一点假设一个实际场景PS端在Linux系统上运行一个应用生成了一张256x256的灰度图像放在DDR地址0x20000000处。需要PL端对该图像做边缘检测处理好之后再放回DDR的另一块区域。整个流程这样走PS把源图像的物理地址和大小传递到PL寄存器。PL端解析参数启动MM2S通道Datamover从0x20000000连续读出数据输出到Stream接口。PL端边缘检测模块从Stream接口读数据数据流经过了FIFO做跨时钟域处理然后在检测模块内部做像素缓存和卷积处理。处理完之后结果写入S2MM通道的Stream接口Datamover把结果数据打包成AXI写事务写入目的地址。PS等待PL端状态寄存器里的完成标志然后取回结果。这套流程的关键点在于整个数据搬移过程中PS的CPU只参与了启动和完成判断中间的所有搬移都是Datamover自主完成。如果缓存涉及PS端访问记得在启动前调用DMA缓冲区的Cache Clean操作在完成后调用Cache Invalidate操作确保缓存一致性。4. 常见问题与排查技巧实录4.1 数据链路不工作时的排查顺序我在实际联调的时候遇到过不少问题最希奇的一次是DataMover怎么也不发送突发Stream接口上一点动静都没有。当时的第一反应是看寄存器是否写入正确可设了断点观察寄存器数值都对。后来沿着Datamover的复位和时钟网络查了一圈才发现MM2S通道被设计成s_axi_lite_aclk时钟域复位我配置的复位信号没有正常释放通道起不来。所以排查Datamover这类IP我总结了一个固定的顺序先看复位和时钟再看命令寄存器状态再测握手信号最后看数据内容。第一步检查复位。Datamover也有一个比较隐蔽的行为如果你把复位信号拉高无效状态的时间虽然足够长但没有满足Datamover对复位默认电平的要求可能会出现部分通道复位、部分不复位的非正常状态。最可靠的方法是在DDR控制器和互联矩阵都稳定之后再释放Datamover的复位。第二步检查时钟。认真确认一下Datamover的S_AXI_ACLK和S_AXI_LITE_ACLK是同一个还是不同频率。如果是不同频率你写命令时用的是LITE时钟域发出来的数据在AXI时钟域跨时钟域的逻辑如果没处理好可能出现命令丢失。第三步检查命令队列是否为空。如果Datamover处于busy状态并且命令队列里积压了数据这时再写命令可能不会生效。调试时先读状态寄存器确认idle后再发命令。第四步抓握手。在ILA里面把S_AXIS的TVALID、TREADY、TDATA、TLAST都拉出来仔细观察有没有数据在传输中途卡住。最常见的卡住情况是TVALID一直为高但TREADY长时间为低说明你的下游逻辑没有正确接收数据。4.2 背压导致吞吐量上不去的案例有一个印象很深的性能问题。MM2S读DDR的时候带宽始终上不去大概只有理论值的一半左右。抓了流水信号之后发现Datamover的Stream输出经常出现空闲周期没有持续不断的数据。看波形数据的TREADY有时候会拉低一低就是好几个周期。追溯下去是下游的FIFO几乎快满了FIFO的ProgFull拉高再把ready拉低了。而ProgFull的阈值设置得非常保守只有容错几个数据。FIFO一满上游通道全部暂停带宽损失非常严重。解决办法是两层的第一层增加FIFO深度ProgFull阈值设置在90%以上第二层让Datamover的Stream数据宽度和下游处理模块的数据宽度保持一致减少数据速率的失配。改完之后带宽达到了合理水平DDR读写效率明显上升。这个案例再次验证了AXI握手协议的价值。Valid/ready这套背压机制虽然让设计复杂了一点但它提供了强大的反压能力。如果不用这套机制而是直接靠FIFO接口配合那么当上游比下游快的时候要么丢弃数据要么硬件阻塞导致数据损坏。AXI-Stream的握手设计恰好能把这种速率失配的问题平滑地“消化”在握手周期里。4.3 字节序和数据字节错乱的例子另一个频繁出现的问题是数据搬移出来后字节序全乱了。比如源数据是0x11223344搬完变成0x44332211。这个在ARM的DMA和Xilinx的DMA引擎里都存在核心原因是ARM Cortex-A9或A53是小端架构而DDR控制器端也有自己的字节序定义。处理方式确认MIG配置里的 Byte Write Enable 相关策略或者直接通过硬件上的字节重排逻辑来解决。还有就是在Datamover命令里它其实不关心字节序只按地址连续传输所以字节序问题要由上下游来解决。我的建议如果PS和PL之间传的是二进制数据比如图像原始数据自己约定好字节顺序在PL端做一次Byte Swap就可以解决。如果传的是结构体需要同时考虑结构体对齐和字节序建议采用双方统一的打包格式比如约定小端序。4.4 Datamover与MIG联动时的性能坑当Datamover直接接到MIG控制器的AXI端口时有几个临界情况必须注意。首先DDR控制器的读写仲裁能力。如果你同时使能了MM2S和S2MM两个通道它们会同时发起读写请求DDR控制器里的仲裁器会强制给读写分配优先级。如果配置不当比如两个通道都很活跃时读写互相争抢吞吐量可能下降很多。其次DDR控制器的Burst Length与Datamover的Buffer Length是有关联的。MIG内部有Bank管理和预充电机制如果Datamover单次突发长度与MIG的最佳page命中长度不匹配会出现频繁的Bank切换效率降低。通常DDR3/DDR4的Page大小是8Kbit到16Kbit级而MIG的AXI端口最佳读写长度为4Kbit左右取决于位宽和数据速率你需要根据实际测量调优Datamover的突发长度配置。还有一个常见的硬件约束问题Datamover和MIG之间如果加了AXI Interconnect作为桥接仲裁和延迟会增加对于性能敏感的设计建议直连MIG端口或通过AXI SmartConnect做最小化桥接。4.5 调试Datamover的必备手段最后聊调试工具。首选ILA用于观察AXI和Stream接口上的关键信号。把MM2S/S2MM两个通道的TVALID、TREADY、TDATA、TLAST、以及命令接口的写地址、写BTT采样下来。这个可以在Design里综合后直接插入Mark Debug然后重新布局布线。对接口时序的情况这个方法足够使用。如果是跑Linux系统也可以用/dev/mem方式Map出Datamover的寄存器空间直接用读写工具来下发命令通过观察状态寄存器来判断是否进入工作状态。这种方式比嵌ILA更快能提前排除很多软件层面的问题。仿真阶段强烈建议用官方的AXI Verification IP进行功能验证。在Vivado里创建一个BD工程加入Datamover用AXI VIP作为Master模拟PS配上DDR3/DDR4仿真模型可以验证从命令下发到数据返回的整个链路。注意仿真时DDR初始化时间较长要给足时间DDR控制器里的校准例程完成之后才能开始正常访问。VIP也可以做错误注入故意使用非对齐地址或者错误长度测试Datamover的异常响应确定错误处理机制符合预期。5. Datamover的扩展应用思考5.1 性能调优方向Datamover不只是简单的搬数据工具调好它的性能是有方法论的。在实际设计里影响吞吐的因素有DDR控制器的效率、AXI互联矩阵的仲裁策略、命令队列深度、Stream端FIFO深度、突发长度、时钟频率匹配。DDR效率方面尽量确保每个请求的粒度和DDR的Page大小匹配突发长度适当加大频繁的小请求会严重影响效率。互联仲裁方面如果你有多个Master同时访问DDR给Datamover配置合理的优先级比如设置QoS属性让它能有更高的优先级。时钟匹配方面波形测量看到的Datamover跑在150MHz和300MHz吞吐潜力差距是一倍但如果你Stream端对接的处理模块只能跑100MHz那么Datamover再快也没用。这种时候合理方案是用异步FIFO做隔离让Datamover侧时钟和用户逻辑侧时钟分离各跑各的频率中间用FIFO缓冲来吸收速率差。5.2 与VDMA、DMA的选型对比Datamover不是万能的。如果你的数据是有固定帧结构并且需要在DDR里支持2D地址行和列搬移那么AXI VDMA更合适它内建了行同步和帧同步的机制如果只是单次大块数据传输Datamover够用了。如果需要处理链表或描述符机制Xilinx的AXI DMA带SG模式会更方便因为Datamover本身不支持描述符链表必须由PS软件维护命令队列。选型建议Datamover适合那些你完全清楚数据流向和地址布局的场景它简单直接、资源开销最小。如果你需要灵活的地址跳转或者多通道管理那就考虑自己的DMA引擎或者升级到带SG的DMA。5.3 从Datamover看AXI生态的设计哲学做完这个项目我对AXI生态的体会是它把“通信”和“搬移”抽象成了一组标准接口让不同IP之间做到即插即用。Datamover正是这种设计哲学下的一个典型代表——它把一个很底层的DDR访问需求抽象成了两个简单的命令MM2S和S2MM。你不需要关心DDR的Bank是怎么切换的、列地址怎么排布的、突发拆分的细节这些全部由Datamover和MIG的联动处理掉。这种抽象层次的意义在于它能让你专注于自己的算法和应用逻辑而不是把精力耗在总线协议细节上。对于中小规模的FPGA团队来说这种效率提升非常明显。如果说BRAM是供PL自己使用的“本地缓存”那么Datamover就是PL访问DDR这座大仓库的“传送带”它让数据在PL逻辑和DDR之间以流式的方式持续流动这是构建高性能数据处理系统的基础能力之一。6. 结合个人经验的收尾建议Datamover这类IP使用方法本身不复杂难点在于理解AXI协议的行为特征和DDR控制器的效率模型。折腾几次之后我有这么几条心得值得分享。第一条心里要有流水的概念。Datamover搬数据是一条流水线从DDR读出来到Stream输出任何一个节点的停顿都会影响整条线的效率。设计的时候把每一个环节的缓冲都做足特别是FIFO深度不要抠门。很多割裂的问题其实就是FIFO太浅导致的宁可多用一点BRAM/URAM也别在缓冲上省钱。第二条充分考虑复位和时钟域的初始化时间。DDR控制器在初始化期间不能访问Datamover此时如果收到命令就会出错。在系统级的复位和启动流程里必须在DDR初始化完成之后再释放Datamover的命令接口。第三条调试时多抓AXI的握手波形少猜。用ILA直接把TVALID/TREADY/TLAST抓出来看一眼大多数问题马上就能定位。如果波形上看到哪个信号一直不动就从那个信号对应的模块往前追。这个方法基本上一抓一个准。这篇文章是从一个实际项目里提炼出来的经验总结希望对正在用或者准备用Datamover的朋友有帮助。如果后续有机会我会继续写写关于Datamover和VDMA在实际视频处理管线中的配合使用那个领域里的坑也不少。
返回列表