ARTICLE DETAIL

资讯详情

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

Zynq AXI DMA实战:S2MM/MM2S接口时序与调试经验

Zynq AXI DMA实战:S2MM/MM2S接口时序与调试经验 去年调一块Zynq-7020的数据采集板我在AXI DMA这个IP核上卡了整整两个星期。PL侧ADC采到的波形数据明明已经送到了S_AXIS_S2MM接口ILA抓波形时tvalid和tready也在正常握手可DDR里读回来的数据要么全是0要么隔一段才出现一块有效数据折腾到差点怀疑硬件焊错了。最后还是老老实实把S_AXIS_S2MM和M_AXIS_MM2S的完整数据路径梳理了一遍才发现问题出在我对接口方向的底层理解上——S2MM和MM2S看起来就是两个流接口实际上一个管进一个管出背后还牵扯着一整条描述符链、缓存一致性还有PS端寄存器的配合。这篇东西不是照着UG021念手册而是把我在Zynq上使用AXI DMA IP核做高速数据搬运的实战经验完整摊开重点围绕S_AXIS_S2MM和M_AXIS_MM2S这两个接口的信号时序、上下游对接方式、PS端配置流程和调试踩坑。适合已经会建Vivado工程、但对AXI DMA还不算熟的开发者看完应该能少走我之前那一大圈弯路。1. 从数据流向看清AXI DMA的两张脸1.1 为什么PS-PL之间的高速数据搬运绕不开DMAZynq的PS和PL之间要交换数据常规思路无非是CPU直接读写PL侧的寄存器或BRAM这条路走AXI_GP接口优点是实现简单缺点是带宽很低我记得GP口实际也就几百MB/s的峰值而且CPU会一直被占用搬运大数据时毫无体验可言。第二种做法是让PL逻辑直接通过AXI_HP口去读写DDR这个速度快得多但代价是PL侧要自己写一套完整的AXI Master逻辑包括读请求、写请求、地址管理、B响应处理工作量不比做一个小CPU核少。AXI DMA这个IP核解决的就是这个尴尬问题。它把从DDR读数据和向DDR写数据这两件事做成了标准的AXI4 Master接口PL侧只需要关心两个AXI-Stream接口——一个往里面送数据一个从里面拿数据。你做ADC采集时采样逻辑把数据拼好往S_AXIS_S2MM一送DMA自己负责把这些数据以Burst方式高效写进DDR要做波形回放时DMA从DDR把数据读出来通过M_AXIS_MM2S吐给你的DAC或显示逻辑。CPU只需要配置一下寄存器剩下的大流量搬运全交给DMA硬件完成。当年我第一次看这个IP的时候最懵的就是接口名太像了。这里先给一个我后来总结的口诀看名字先看后半段S2MM就是Stream to Memory-Mapped方向是从流变成内存MM2S就是Memory-Mapped to Stream方向是从内存变成流。1.2 S2MM与MM2S一个入一个出方向别再搞反很多初学者栽在主从这个字眼上。其实从AXI DMA这个IP的角度去看每个方向都有两条通路MM2S方向DMA通过M_AXI_MM2S这个AXI4 Master接口主动去读DDR内存读回来的数据从M_AXIS_MM2S这个流主接口吐给下游PL逻辑。也就是说M_AXI_MM2S是面向内存的读通道M_AXIS_MM2S是面向下游逻辑的输出流通道。S2MM方向上游PL逻辑把数据从S_AXIS_S2MM这个流从接口送进来DMA通过M_AXI_S2MM这个AXI4 Master接口主动把数据写入DDR内存。S_AXIS_S2MM是输入流M_AXI_S2MM是面向内存的写通道。SG引擎还有一个M_AXI_SG主接口用来读取和回写描述符链这个在开启Scatter Gather功能时会用到。也就是说从DMA角度看S_AXIS_S2MM和M_AXIS_MM2S都不算主接口数据方向完全相反。我用一张简单的对照表帮自己理清也分享给你接口名方向从IP核看传输动作数据端点S_AXIS_S2MM从接口输入流接收PL上游数据DMA写入DDR外部PL逻辑 - S_AXIS_S2MM - M_AXI_S2MM - DDRM_AXIS_MM2S主接口输出流DMA从DDR读出数据吐出给PL下游DDR - M_AXI_MM2S - M_AXIS_MM2S - 外部PL逻辑M_AXI_SG主接口读取/回写SG描述符DDR描述符区 - DMA内部SG引擎只要记住S2MM是入内存、MM2S是出内存后面分析时序和配寄存器时思路会清晰很多。2. S_AXIS_S2MM接口的时序与上游对接2.1 信号清单与握手规则S_AXIS_S2MM接口最常见的信号有这些s_axis_s2mm_aclk流接口时钟所有信号都基于它采样s_axis_s2mm_tdata数据总线位宽由IP配置里的Stream Data Width决定可以是32位、64位、128位甚至更宽s_axis_s2mm_tkeep字节使能标记tdata中哪些字节是有效数据s_axis_s2mm_tvalid有效标志由上游发起s_axis_s2mm_treadyDMA准备好接收由DMA返回s_axis_s2mm_tlast包的最后一拍标志s_axis_s2mm_tuser用户自定义信号可用于帧起始、调试打标等AXI-Stream的核心握手规则很简单tvalid和tready同时为高时tdata上的一拍数据才被采样传输。也就是说只要上游把tvalid拉高数据就必须稳在tdata上直到一拍tready1的上升沿真正交易完成tvalid才能撤掉。有一个很容易被忽视的约束tvalid信号不能依赖tready即上游不能在看到tready之后才拉tvalid而是只要数据准备好就拉tvalid然后耐心等tready。这一点和很多串行接口的思路都不一样我刚从串口思维转过来时吃过亏。2.2 上游是ADC/图像传感器怎么接做高速数据采集时最常见的上游就是ADC采集逻辑。假设你有一颗100MSPS、16bit的ADC输出经过两个通道拼接成了32bit数据采样时钟作为同步时钟。最简单的方式就是把采样有效标志直接拉成tvalid把32bit拼接数据放到tdata上时钟用采样时钟域。如果ADC是连续采样tvalid可以长期为高这样DMA就会源源不断地吸收数据中间不会出现气泡。需要注意一个实际问题如果ADC逻辑和DMA不在同一个时钟域通常会在PL里加一个异步FIFO做跨时钟域缓冲。FIFO的读侧就是S_AXIS_S2MM接口逻辑读侧几乎空时tvalid拉低读侧非空且DMA的tready为高时一拍出一笔数据。这样写出来的代码量很小约30行Verilog就能搞定但能省掉后面大量的时序问题。如果你接的是图像传感器或视频源一般建议按照AXI4-Stream Video协议来接。该协议把tuser用作帧同步信号SOF把tlast用作行结束EOL这对DMA本身不是强制要求但如果你后面接了Xilinx的视频IPVDMA、Video Timing Controller等这些IP之间就靠这套约定通信。2.3 TLAST到底要不要给这是S2MM方向最容易被问懵的问题我的数据源根本没有包的概念TLAST能不能不接答案是可以但要分情况。在SG模式下DMA遇到两种情况会结束当前缓冲区传输一是收到了TLAST二是写入的数据量已经达到当前描述符里设置的Buffer Length。如果你的数据源是连续流且每个描述符的长度是固定的那完全可以不接TLASTDMA会在填满一个描述符缓冲后自动跳到下一个描述符继续工作。但如果你做的是包式数据传输比如以太网帧或者突发传感器数据包的长度和描述符长度不一致就一定要在包的最后一拍把TLAST拉高否则DMA会一直傻等后续数据直到缓冲填满才收工。这样一来包的末尾混进了一大堆无效数据而且下一包数据的起始位置也全乱了。如果你要在PL里手动给数据流加TLAST可以用一个字节计数器。记录当前包还剩多少字节当剩余量为0且当前拍tready有效时把tlast拉高一拍即可。这里有一个关键点TLAST必须是和数据本身同一拍有效不能早也不能晚。3. M_AXIS_MM2S接口的时序与下游对接3.1 信号清单与一次读取的完整流程M_AXIS_MM2S的信号和S2MM方向完全类似只是方向相反m_axis_mm2s_tdataDMA从DDR读出的数据m_axis_mm2s_tkeep字节使能m_axis_mm2s_tvalid数据有效由DMA发起m_axis_mm2s_tready下游逻辑准备好接收m_axis_mm2s_tlast缓冲区/包传输完成的最后一拍m_axis_mm2s_tuser用户自定义信号在SG模式下可由描述符中的TXSOF位置位控制一次MM2S传输的完整动作是CPU把输入缓冲区的源地址写到MM2S_SA寄存器把传输长度写到MM2S_LENGTH然后置位DMA控制寄存器的RS位并启动。DMA内部发起一系列AXI读突发从DDR读数据数据缓冲到内部FIFO之后按照流接口时钟从M_AXIS_MM2S输出。这里的握手下游必须主动配合当DMA把tvalid拉高时下游如果在忙必须把tready拉低DMA会自动暂停等待等tready拉高后继续发送。这个机制对慢速下游很友好不会丢数据。3.2 下游是DAC/显示屏怎么接DAC回放场景里下游DAC通常有固定的采样率数据必须按节拍灌入。一般做法是DMA输出的M_AXIS_MM2S先接一个AXI-Stream FIFOFIFO读侧再接一个采样率控制逻辑每次采样节拍到达时从FIFO取出一个样本送给DAC。DMA这边只要保证FIFO不读空就行FIFO水位一旦低于阈值可以反馈给CPU或者通过某种机制重新装载数据。显示屏场景一般会走Xilinx的视频通路VDMA是比AXI DMA更贴合的IP但如果你坚持用AXI DMA需要自己把MM2S流打包成视频时序。这时TUSER作为SOF、TLAST作为行同步的设计就很重要了。SG模式下的描述符TXSOF位可以控制DMA在某个描述符的第一个有效数据拍拉高TUSER正好用来标记帧起始。3.3 SOF/EOF在流接口上的体现Direct Register模式下DMA每完成一次LENGTH长度的传输都会在最后一拍自动拉高TLAST。这一点非常直观也适合用来验证链路。但到了SG模式TLAST出现的位置不再依赖长度自动收尾而是看描述符里是否置位了TXEOF。如果描述符Buffer Length对应的数据是一个包的结尾那就要在CONTROL字段里置TXEOF位这样DMA才会在该描述符的最后一拍把TLAST拉起来。TUSER的用法类似。MM2S方向把描述符CONTROL字段的TXSOF置位DMA在该描述符的第一拍把TUSER拉高S2MM方向接收到带TUSER的数据时DMA会把TUSER值连同数据状态写回描述符的STATUS/APP字段软件可以从内存中读到该信息。这套机制在做视频帧同步和打时间戳时非常好用。4. PS端寄存器与描述符链把DMA跑起来的关键4.1 直通模式和SG模式怎么选AXI DMA有两种工作模式这个在IP配置阶段就要决定Direct Register模式不使能SG只支持单次传输。CPU设置源地址/目的地址和长度DMA传完就停。优点是寄存器少、逻辑简单缺点是每次传输都要CPU重新配置一轮适合一次性波形搬移或调试起步。SG模式使能Scatter Gather Engine数据搬运由一组描述符链BD控制。CPU不需要反复写地址长度只要构建一条或多条BD链告诉DMA链首和链尾DMA会自动沿着链表逐条执行。非常适合连续采集、乒乓缓冲、循环输出等场景。我的建议是正式项目只要数据流是持续不断的优先SG模式如果只是临时验证IP或者做板卡自检先用Direct Register模式把链路跑通再切换到SG。4.2 描述符的布局与填写SG模式下描述符存放在DDR中DMA通过M_AXI_SG接口自己读取。一个典型描述符链由N个BD组成每个BD保存了下一跳BD的地址、数据缓冲区地址、控制信息、状态信息和APP字段。我平时习惯在裸机工程里用一个结构体来管理BDtypedef struct { uint32_t NXTDESC; // 下一描述符地址 [31:0] uint32_t NXTDESC_MSB; // 下一描述符地址 [63:32] uint32_t BUFFER_ADDRESS; // 数据缓冲地址 [31:0] uint32_t BUFFER_ADDRESS_MSB; // 数据缓冲地址 [63:32] uint32_t Reserved0; uint32_t CONTROL; // 控制字长度、SOF/EOF位 uint32_t STATUS; // 状态字DMA完成时回写 uint32_t APP0; // 应用字段可记录TUSER等信息 } Bd;填充BD链时要关注几个常见位。MM2S方向CONTROL字段里Buffer Length占低位TXSOF在Bit31TXEOF在Bit30S2MM方向的CONTROL字段主要填Buffer LengthSTATUS字段在DMA写完后会回写SOF/EOF以及其他状态。注意不同IP版本的具体位偏移可能略有差异以你手上Vivado生成的IP版本和UG021为准。描述符和对应的数据缓冲区地址都必须是对齐后的物理地址。另外描述符一定是DMA要主动去DDR里读的内存区域CPU侧更新描述符后必须做Cache Flush否则DMA读到的很可能是旧值。4.3 一次完整的启动流程以SG模式下MM2S方向为例启动流程如下在DDR中准备好N个BD每个BD的NXTDESC指向下一个BD最后一个BD的NXTDESC可以指向首BD形成循环也可以为空。每个BD的BUFFER_ADDRESS指向源数据缓冲CONTROL字段写入Buffer Length按需置位TXSOF/TXEOF。对BD链内存区域和源数据缓冲执行Xil_DCacheFlushRange。将第一个BD地址写入MM2S_CURDESC寄存器。将最后一个BD地址写入MM2S_TAILDESC寄存器这个写操作会触发SG引擎开始读取描述符。将MM2S_DMACR的RS位置1使能DMA通道。轮询或中断等待状态寄存器里的IOC_Irq位置1表示这一轮描述符执行完毕。S2MM方向的流程完全对称只是BUFFER_ADDRESS填的是DDR写入目的地址TAILDESC的写入同样触发DMA进入等待接收状态。启动后上游只要往S_AXIS_S2MM发送数据DMA就会按描述符把数据写入DDR。5. 缓存一致性Zynq上最容易翻车的环节5.1 PL DMA和CPU Cache的关系Zynq-7000的PS侧L1/L2 Cache在CPU和DDR之间做了一层缓存。CPU读DDR时可能命中Cache里的旧副本CPU写DDR时数据也可能先留在Cache里没有立刻写回DDR。神奇的是PL侧的AXI DMA访问DDR时完全不经过这层Cache它是直接面对DDR控制器的。这就产生了一个经典的不一致问题CPU先把一段波形数据写到内存里这段数据还在Cache中没落DDRDMA紧接着执行MM2S去读这块内存结果读出来的全是旧数据。反过来也一样DMA通过S2MM写入DDR的数据CPU再去读时命中的可能是Cache里的旧副本读出来一片乱码。这不是玄学几乎是Zynq上DMA开发必踩的坑。解决办法就两个函数写完后用Xil_DCacheFlushRange把数据从Cache刷进DDRDMA写完数据CPU要读之前用Xil_DCacheInvalidateRange把Cache中相应地址的副本作废让CPU重新去DDR读最新数据。5.2 Flush/Invalidate的时机我总结了一套规规矩矩的顺序照着做基本不会出问题MM2S方向DMA把DDR数据发给PLCPU写入源数据缓冲区。调用Xil_DCacheFlushRange刷源数据缓冲和BD链。启动DMA传输。等DMA传输完成中断后CPU如果要复读该缓冲区先Xil_DCacheInvalidateRange再读。S2MM方向PL数据由DMA写入DDR调用Xil_DCacheFlushRange刷BD链。启动DMA传输。等DMA传输完成中断后CPU读DDR数据前必须调用Xil_DCacheInvalidateRange。再访问缓冲区。很多人只Flush不Invalidate导致回读还是老数据。切记缓存一致性是两个方向都要处理不能只做一边。5.3 内存对齐与length的隐藏规则AXI DMA对地址和长度有一套隐含的对齐要求不问清楚很容易出诡异问题。地址对齐如果Memory Map Data Width是64位那源地址和目的地址必须8字节对齐如果总线是32位则4字节对齐。如果地址不对齐DMA传输可能报错或者行为异常。我在用SDK的malloc分配缓冲区时从来不敢直接用返回值而是先分配一个比需求大一些的区域再手动对齐到8字节边界。长度方面DMA的Buffer Length寄存器单位是字节但传输的数据总线一拍能传多个字节。建议传输长度尽量做成总线宽度的整数倍。以64位总线为例长度最好是8的倍数。如果不是整数倍最后一拍会依赖TKeep来标记有效字节DMA会处理好但下游逻辑和存储端必须理解TKeep语义否则可能出现错位。还有一点Buffer Length的位宽由IP配置里的Width of Buffer Length Register决定。默认14位时单次缓冲最大16383字节。如果你要单次传输超过这个数要么把IP参数调到26位要么拆成多个BD由SG链自动接力。6. 实测踩坑ILA抓时序与常见故障定位6.1 数据没进内存先从接口抓波形我调试AXI DMA时有一套固定的排查链路从接口向两端扩散第一步看S_AXIS_S2MM或M_AXIS_MM2S的握手。ILA里挂上tdata/tvalid/tready/tlast如果数据源有输出但tready始终为低说明DMA内部并没有真正进入可接收状态。常见原因有RS位没置1、CURDESC/TAILDESC没配对、SG引擎报错、或者时钟没起来。第二步看DMA到DDR的方向。如果S2MM方向流接口握手正常DMA肯定发起了写DDR的AXI请求这时候ILA或Vivado里观察M_AXI_S2MM通道的awvalid/awready/wvalid/wready如果看到请求一直在等应答往往是地址映射没配好DMA访问到了无效地址区域。第三步直接用XSDB命令读DDR数据验证。比如mrd 0x01000000 4如果读出来和你启动DMA前写入的初始值不同说明DMA确实写了数据如果完全没变就要回头查DMA到底有没有执行传输。6.2 常见失败场景与对策把几个我实际遇到过的典型故障整理成一张表方便对照排错现象直接原因排查/对策S2MM的tready一直为低DMA未启动或BD链配置错误检查RS位、CURDESC、TAILDESC读DMASR确认Halt/Idle状态MM2S的tvalid一直为低描述符长度填0或源地址无有效数据打印所有BD的地址和长度确认TAILDESC写入时间中断触发但数据不对忘了Invalidate Cache回读前加Xil_DCacheInvalidateRange描述符报SGDecErr/SGSlvErr描述符地址未对齐、指向不可访问区核对描述符物理地址排除地址越界数据错位、每段开头有垃圾长度不是总线字节倍数或TLast位置不对调整长度对齐检查上游包计数逻辑DMA传一次后再也不动TAILDESC或CURDESC没能正确更新SG模式下需要软件更新下一个里程碑BD并写TAILDESC唤醒有一个我特别想提醒的点SDK/XSDB里读内存看到的地址和DMA看到的地址必须一致。Zynq里DDR物理地址通常在0x00100000之后而如果你在SDK里用指针访问0x01000000要确保这个地址既在DMA的地址空间里也落在DDR的实际映射范围内。Vivado Address Editor里DMA的S2MM/MM2S地址映射要指到DDR所在区域。6.3 我的调试习惯先Direct Register跑通再上SG直接上SG模式调试一旦不工作你很难判断是BD链的问题、Tail Descriptor问题还是Cache问题。所以我的建议是第一次接触AXI DMA时先用Direct Register模式做一个最简单的自回环CPU写一段缓冲区MM2S把数据发出来绕一圈送进S2MMDMA写进另一段缓冲区CPU读回来对比。这条路走通说明DMA数据通路、时钟复位、PS到PL的中断路径都没问题再上SG模式去啃BD链。实际这么操作之后我后面做ADC采集项目时基本是半天就调通了S2MM通道剩下的时间全在优化DDR带宽和Cache刷新策略上。相比第一次硬啃SG描述符时的狼狈效率差距非常明显。最后再分享一个小习惯。调试AXI DMA时我会在Vivado的Block Design里把MM2S、S2MM两个通道的流接口各引出一组ILA探针同时把DMA的中断输出也接到PS端的IRQ_F2P。这样硬件上能直接看到握手和中断时序软件里也能通过GIC状态确认中断有没有到达PS。两头对照着看定位问题比只看一头快得多。等整个系统稳定了再把多余的ILA摘掉减小一点资源占用。这个思路从Zynq到Zynq UltraScale我一直在用省下来的时间绝对值得。
返回列表