ARTICLE DETAIL

资讯详情

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

Zynq AXI DMA深入解析:S2MM与MM2S接口原理与实战指南

Zynq AXI DMA深入解析:S2MM与MM2S接口原理与实战指南 最近一直在折腾Zynq平台上的高速数据搬运绕不开的就是Xilinx官方的AXI DMA IP核。这个IP核看起来就是个黑盒子接上AXI总线就能把数据从内存搬到外设、或者从外设搬回内存但真正上手的时候很多人在S_AXIS_S2MM和M_AXIS_MM2S这两个接口上踩坑要么数据对不上要么带宽上不去要么干脆死等中断。这篇文章我打算把这两个接口从原理到实战完整拆一遍包括IP配置、寄存器操作、描述符机制、中断处理、Cache一致性以及我实际调板时遇到的那些坑给正在搞Zynq数据通路的朋友一份能直接对着干活的参考。这篇内容适合刚入门Zynq、准备用AXI DMA做ADC采集、以太网收发、视频帧搬运、或者自定义Stream接口外设的开发者也适合已经调通但想搞清楚为什么这么配置、提高吞吐量的老手。我不讲那种官方文档里删减过的ABC尽量站在实际工程的角度把关键点捋清楚。1. 从架构角度理解AXI DMA的设计定位很多第一次接触AXI DMA的人都会有个疑问既然Zynq的PS端本身就有DMA控制器为什么还要在PL侧用AXI DMA这就要从Zynq的架构谈起。PS端的DMA也就是DMA-330虽然可以访问DDR和部分外设但它能直接服务的数据源大多是PS侧的总线外设无法直接对接PL逻辑里的高速数据流。如果摄像头传感器、高速ADC、或者自定义信号处理模块要把数据送到DDR中间必须经过PL而PL内部的数据往往是一帧一帧的连续流和AXI4内存映射总线的突发事务模型差别很大。AXI DMA的主要作用就是把“流式数据”和“内存数据”之间做桥接。它内部有两条独立的通道MM2SMemory-Mapped to Stream负责从内存读数据转成连续的Stream流送给下游S2MMStream to Memory-Mapped负责把上游进来的Stream流收下来写入内存。这两个通道互相独立可以同时工作各自有独立的描述符、独立的中断实际使用的时候可以全双工跑。你在IP配置界面看到的M_AXIS_MM2S就是MM2S通道输出数据的那根Stream总线S_AXIS_S2MM就是S2MM通道承接输入数据的那根Stream总线。搞清楚这两根线整个IP就懂了一半。剩下的AXI4-Lite接口用于CPU配置寄存器AXI4接口用于DMA真正访问DDR或者BRAM。1.1 内存映射接口与流式接口的分工AXI4和AXI4-Stream是两种个性完全不同的总线。AXI4有独立的地址通道、读数据通道、写数据通道任意一笔传输都要带上地址适合随机访问代价是协议开销大。AXI4-Stream只有一组数据通道没有地址通道数据连续不断地从主机流向从机适合实时流。AXI DMA就是这两个世界的翻译官。CPU或者PS端通过AXI4-Lite配置好要传多少字节、源地址和目标地址之后DMA就自行通过AXI4接口去DDR突发读写同时通过AXI4-Stream接口和PL侧外设“对喷”数据。这里有个容易混淆的点M_AXI_MM2S和M_AXI_S2MM这两个AXI4接口都是DMA作为主机去访问内存的而S_AXIS_S2MM是DMA作为从机接收PL侧数据的M_AXIS_MM2S是DMA作为主机向PL侧发送数据的。命名里的S和M对比的是Stream总线上的角色而不是内存总线上的角色。很多人在写地址映射或者连线时搞混就是因为没注意这个角色关系。1.2 S2MM和MM2S是两条独立且对称的通路把两条通道单独拎出来看它们的工作是镜像对称的。MM2S通道的流程是CPU配置读描述符和起始地址DMA从DDR连续读出数据拼成Stream流从M_AXIS_MM2S发出去直到一个Buffer描述符指向的字节数传完产生一个MM2S中断然后自动跳到下一个描述符或者停在那里等你重新配置。S2MM通道正好反过来上游PL逻辑把数据推进S_AXIS_S2MMDMA内部把这些数据累积到一定长度或突发边界后通过M_AXI_S2MM写回DDR。写完一个Buffer后产生S2MM中断。两条通道的寄存器组也是分开的DMACR、DMASR、CURDESC、TAILDESC、SA、DA这些都有独立的MM2S版本和S2MM版本。操作的时候各写各的互不干扰。设计上把两条通路拆开的好处是显而易见的一个全双工的高速数据通路里收和发可以孤立法并行带宽互不抢占而且可以单独设置不同的Buffer大小和传输长度灵活性高很多。2. Vivado中的IP配置与工程搭建AXI DMA的配置界面一眼看上去选项不多但每一项都值得仔细琢磨。配置不对后面调起来会非常痛苦。我一般在Vivado里新建IP后要做的第一件事就是把Data Width、Burst Size这几个核心参数先想清楚因为它们直接决定了DMA访问DDR的效率也决定了你的FIFO深度和数据流格式。2.1 关键参数选型及背后的原因先说Address Width。这个参数一般跟着AXI4接口的位宽走。在Zynq-7000上PS端配置的DDR地址空间通常在32位以内所以32位就够了。到了Zynq UltraScale或者有些需要访问64位地址空间的场合才需要把它设成64。注意改这个参数不只要改IP配置还要保证连接到DMA的AXI4互联网络和地址译码器都支持相应位宽。实际使用中我曾见过有人把Address Width设成64但PS端的地址没有正确映射到高地址区域导致DMA写出去的数据完全不知道飞到哪里去了。Data Width这里参数有两个值可选32位或者64位指的是DMA访问DDR那边的数据位宽。如果PL侧的Stream数据位宽是32位DDR那边选64位其实更划算因为同样的时钟频率下64位访问可以在一个周期内搬两倍的数据。但这个宽度和Stream总线的数据位宽没有直接映射关系Xilinx在DMA内部已经处理好了位宽转换。唯一要注意的是当DMA内部做了窄到宽的转换时一次突发传输内部可能需要多个周期来累积数据所以Stream侧进来的数据节拍如果是乱序的或者TLAST信号来得不对齐DMA的S2MM通道就可能卡住不写内存。Burst Size这个参数在AXI DMA里通常可以选4、8、16。它表示一次AXI突发传输包含多少拍beat。比如数据宽度是64位8字节Burst Size设为16那么一次突发就能传128字节。突发越大DDR控制器侧的效率越高因为访问DDR的bank切换开销被摊薄了。但突发也不能贪大超过DDR控制器的固定突发上限会被拆分而且过大突发会增加内部FIFO压力。Zynq上我一般设置为16或者8实测带宽和稳定性都比较均衡。2.2 连接DMA、PS与DDR的完整步骤在Vivado中搭建AXI DMA的工程常规步骤可以归纳为四步步骤一例化Zynq PS核开启需要的HPHigh Performance从接口。Zynq-7000的PS有多个HP口推荐把DMA的M_AXI_MM2S和M_AXI_S2MM都挂到HP口上因为HP口直连DDR端口控制器访问DDR的延迟和带宽都优于经AXI互联转接的普通接口。如果一个HP口不够用可以一个通道挂HP0、另一个通道挂HP1甚至两个通道分别挂不同HP口并绑定不同的DDR端口。步骤二例化AXI DMA核根据外设的Stream位宽选择Data Width。比如ADC出来的数据是32位的就把Stream位宽设置成32DMA侧的Data Width可以在32或64之间选。然后把M_AXI_MM2S和M_AXI_S2MM分别接到HP口上的AXI从接口上。注意一个HP口本身只有一个从接口如果你想两个通道都挂在同一个HP口上需要加一个AXI Interconnect做地址复用或者核对一下HP口的接口数量规划。步骤三连接时钟和复位。DMA的m_axi_sg_aclk是描述符访问用的时钟一般和DMA主数据传输时钟共用一个。如果外设Stream侧和DMA侧跑在不同频率那么外设的时钟要经过CDCClock Domain Crossing处理DMA的s_axi_lite_aclk通常使用较低频率的CPU总线时钟即可。步骤四生成地址位宽分配中断。在Zynq中DMA的中断信号接到PS的PL-PL中断输入上。如果只用S2MM一个方向可以只使能s2mm_introut如果收发都要就把两个中断都接进去并在中断控制器里分配IRQ ID。连线完成之后就是地址分配的问题。描述符的Buffer地址和描述符本身所在的内存都需要在PS侧的地址空间里规划好并且保证这段内存在Linux或者裸机环境下不会被其他操作踩踏。我见过有人简单地把DMA描述符放在0x1000000这种接近DDR起始的地址结果被一段早期启动代码直接清掉DMA一运行就当场崩溃。3. 寄存器编程与BD描述符机制AXI DMA能不能高效稳定地跑起来关键在于描述符。描述符是DMA搬运数据的最小任务单元每个描述符包含了数据所在内存的地址、字节数、下一个描述符的指针以及一些控制标志位。DMA通过读取这一个个描述符像链表一样把很多个不连续的数据块串起来顺序搬运这就解决了Stream流数据必须连续、而内存中缓冲区可能不连续的问题。3.1 两条通道的寄存器控制流程先看MM2S通道的寄存器控制。它的核心寄存器是MM2S_DMACR偏移0x00、MM2S_DMASR偏移0x04、MM2S_CURDESC偏移0x08、MM2S_TAILDESC偏移0x10、MM2S_SA偏移0x18。这个SA寄存器在启用了描述符机制后就不起作用了但在Direct Register模式下它就是源起始地址。默认情况下AXI DMA工作在Scatter Gather模式这时要用描述符。控制流程是这样的先把MM2S_DMACR里的RS位置1让通道运行然后把描述符所在内存的物理地址写入MM2S_CURDESC最后把需要执行的任务链最后一个描述符的地址写入MM2S_TAILDESC。写入TAILDESC相当于一个“触发器”DMA看到这个地址后就知道新的任务到了会自动从CURDESC开始读取描述符并执行。当DMA执行到CURDESC等于TAILDESC时就会停下来产生一个IOC中断。S2MM通道的控制流程类似区别是把数据写进DDR的地址用的是描述符里的Buffer Address而不是SA寄存器。你要把接收缓冲区的物理地址填到描述符里的Buffer Address字段然后同样的流程启动通道。这里的坑在于描述符中的地址必须是物理地址。如果系统跑Linux而且进程用的是虚拟地址就得做内存映射保证描述符和Buffer都是连续的物理内存。裸机环境下相对简单但要确认没有开MMU或者确认段映射没有把物理地址改变。3.2 描述符环怎么分配怎么填写实际工程中最常用的描述符组织方式是环形缓冲。每个描述符大小固定为0x40字节64字节有些字段是留给硬件的状态反馈用的。描述符数量根据需要的Buffer数量定。我常用的分配方式是直接在DDR中开辟一段专门的空间给描述符比如定义32个描述符每个64字节这样一段2KB的内存区域地址做16字节对齐或者至少8字节对齐。每个描述符的字段结构如下将以下代码块放到S2MM描述符中时0x00偏移处是NXDESC本描述符控制字0x04偏移处是NXDESCNext Descriptor地址0x08偏移处是Buffer Address0x0C到0x2C之间是保留字段和状态字段。对于透明传输不涉及地址转换通常只需要在初始化时填好前三个字段后续驱动或者中断处理时再读状态字段来确认传输完成、字节数等。描述符控制字NXDESC里比较关键的位有0x01表示Desired Buffer Address为有效地址0x02表示这个描述符是当前缓冲区的最后一个0x08表示IOC中断使能。如果在一串任务链里只想在最后一个Buffer传完时产生中断那么前面的描述符可以不置IOC位只在最后一个置上这样可以有效减少CPU中断频率。填写S2MM描述符时还有个容易搞错的地方S2MM侧的Buffer Address是指DMA要把Stream数据写入的内存地址。如果这个地址指向DDR那没问题如果指向BRAM或者某个AXI外设的地址就要确保那个外设支持AXI4写操作。别把Stream接口和内存映射接口搞混了。3.3 AXI数据搬运的Buffer清空与Cache处理在Zynq上做DMACache一致性是绕不开的大坑。PL侧的DMA是“普通人”它不知道CPU的Cache里存了什么CPU也不知道DMA会不会把自己刚写入Cache的数据改掉。如果不做处理最典型的症状就是DMA明明已经把数据搬到DDR里了CPU读出来却是旧的或者在发送方向上CPU写好了数据DMA读到的却是Cache里的旧值。裸机环境下最简单粗暴的方案是禁用数据Cache但代价是性能下降。更合理的做法是在DMA传输之前调用Xil_DCacheFlushRange把CPU写入发送Buffer的数据刷到DDR在DMA接收完成之后调用Xil_DCacheInvalidateRange让CPU重新从DDR加载数据。这两个函数是Xilinx SDK里的标准库函数编译时注意包含xil_cache.h。在Linux环境下内核驱动可以用DMA API的dma_map_single或dma_alloc_coherent来保证一致性但很多Zynq开发者在裸机上写代码所以这里再强调一次不要以为DMA传输完成了数据就一定在内存里等着你读Cache不处理好数据永远是“薛定谔的数据”。这个坑我踩过不止一次调试时最容易骗过人的是“第一次传输正常”因为Cache在初始化后是冷的第一次访问DMA Buffer时确实会从DDR读后面对同一段Buffer的读就又走了Cache老路。4. 中断、双缓冲与性能调优中断是AXI DMA和CPU交互的钥匙。很多人刚开始用DMA时图省事用轮询方式查看DMASR寄存器里的完成位这种做法在数据量小、速率低的时候还能忍受一旦数据速率上去CPU就会被死等拖垮。正确做法是让DMA在完成一个Buffer传输后产生中断CPU在中断服务程序中处理数据、重新提交描述符。这个过程看似简单但里面的门道不少。4.1 中断处理的关键优先级、中断号和标志位在Zynq的裸机环境下AXI DMA的中断通常接到GIC通用中断控制器。中断号在Vivado里可以通过Address Editor或者连接关系确认不同的设计里IRQ ID可能不同不能想当然。中断服务程序里首先要判断是MM2S的完成中断还是S2MM的完成中断因为两个通道的中断号是分开的。如果两个方向都用一个IRQ引脚上没有区分的话就要读DMASR来确认到底是哪个通道的事件。对于S2MM通道DMASR里的IOC_Irq位代表一个Buffer完成而且这个标志要在写1清0之前先读出来。如果你在中断里不去清除这个标志下一次DMA完成同一个Buffer时中断就不会再触发这是最常见的中断丢失原因之一。我习惯的写法是IRQ service里先读DMASR保存IOC状态再向DMASR写1来清除IOC位最后处理数据。写1清0这个习惯和很多外设不太一样新手很容易在中断里写0结果发现中断死活清不掉。另一个容易忽略的寄存器是DMACR里的IRQThreshold字段占用高16位它表示每完成多少个描述符产生一次中断。默认值是1也就是每个Buffer完成都产生中断。如果想降低中断频率可以设置成4或者16但代价是CPU处理数据的实时性下降。这个参数的设置要根据实际吞吐量和CPU负载来平衡我一般先把Threshold设为1确认通路没问题后再调大。4.2 双缓冲机制如何避免“卡顿”在连续采集场景中比如ADC不停地上送数据到S2MM通道如果只用一个Buffer就会出现一个很尴尬的局面当DMA正在往当前Buffer写入数据时CPU要想处理上一个Buffer的数据根本不可能因为有且只有一个Buffer在手里。解决的办法是双缓冲或者多缓冲。双缓冲的思路很简单准备两个Buffer当DMA在写Buffer A时CPU处理Buffer B等DMA写完A产生中断CPU再去处理A同时DMA自动切换写入B。前提是你的描述符环里至少有两个可用的描述符并且TAILDESC的更新时机在启动时就把两个描述符都填好让DMA连续执行两个Buffer。在实现双缓冲时需要注意S2MM的DMA只要写完当前描述符的Buffer就会立即跳到下一个描述符继续写不会等到CPU介入。所以CPU在中断里要做的只是“取走”已经完成的Buffer然后把这个Buffer重新提交给DMA。重新提交的操作就是把描述符状态清空然后更新TAILDESC。这里有个关键点更新TAILDESC时必须把CPU当前能安全让DMA使用的最后一个描述符地址写进去。大多数双缓冲实现中CPU处理完Buffer A后TAILDESC要指向描述符A同时确保描述符B还在环里且未被DMA再次启用。如果描述符环的组织不够精细比如更新TAILDESC时不小心覆盖了DMA正在处理的描述符轻则数据覆盖重则DMA进入异常状态。调试时可以通过观察DMASR里的Idle位和DMACR里的RS位来判断DMA是否处于忙状态但最好在设计上就避免这种风险。4.3 实测性能评估与带宽瓶颈分析关于性能我做过一个简单的基准测试PL侧产生连续的32位数据流通过S2MM通道写入DDRMM2S通道负责从DDR读回并回环。在使能Cache刷新、描述符环大小设置为16、突发长度设置为16、DMA数据宽度64位的情况下实测吞吐率大约能做到1.2GB/s级别但这需要DDR控制器和AXI互联网络不成为瓶颈。瓶颈往往出现在意想不到的地方。有一种情况是PL侧的Stream数据没有按照DMA期望的突发对齐产生TLAST信号导致DMA内部FIFO无法高效组合成满突发DDR访问效率急剧下降。还有一种情况是HP端口被CPU或者其他外设占满了DMA长时间等不到AXI总线授权吞吐率自然上不去。用Vivado里集成的ILA去抓M_AXI_MM2S的AW通道和W通道波形是最直接的定位方式。另外带宽测试结果和驱动里中断处理的效率高度相关。如果你在中断服务里做很多耗时的打印操作或者把Buffer地址转换、CRC校验都放在ISR里CPU的中断响应周期会被拉长导致DMA出现了空闲等待。一个更稳妥的设计是中断里只做一件事——告诉主循环哪个Buffer已经完成了真正的数据处理挪到主循环或者单独的任务里去。用双缓冲配合这种“中断通知后处理”模式系统吞吐量和实时性能都更稳定。5. 常见问题排查与工程经验从纯理论到跑通中间一定会遇到各种诡异问题。这一节我会把最常见的几个现象和排查思路整理出来这些基本上覆盖了我接触过的九成Zynq AXI DMA调试问题。5.1 Cache一致性问题怎么定位如果你发现DMA接收完成后CPU读到的数据和实际发送的不一致而且数据时对时错比如第一次对、第二次错那大概率是Cache一致性没有处理好。最简单的定位方法先在S2MM传输完成中断里加上Xil_DCacheInvalidateRange把接收Buffer从DDR里重新加载再看数据是否正常。如果加了就好了说明就是这么回事。更系统地排查可以分三个步骤第一确认你的Buffer地址没有超过DDR物理地址空间且映射没有经过MMU的别名变换。在裸机里如果开了MMU要保证Buffer段映射带Bufferable和Cacheable属性并且通过内核DMA API或者Xil_DCache函数做维护。第二确认你在发送之前执行了Xil_DCacheFlushRange而不是只调用一次之后就不再管了。因为同一个Buffer可能是复用的上一次传输结束后Buffer里的Cache行可能还是旧的如果不刷掉DMA可能搬走的是旧数据。第三确认你的描述符和Buffer没有放在带Cache的SRAM区域里。Zynq中的OCM有些区域是不带Cache的但通常没人把整个Buffer放OCM因为容量有限。如果你把描述符放到了OCMDMA访问和CPU访问之间的同步也需要额外小心。5.2 描述符地址对齐与错误状态位描述符的物理地址必须至少16字节对齐这一点在Xilinx官方文档里写得很清楚但实际工程里仍然有人因为malloc返回的地址不对齐而莫名其妙地出问题。裸机环境下如果用了标准库的malloc建议自己再包一层内存对齐分配函数。更保险的方式是定义静态数组然后强制转换成物理地址时注意地址对齐。如果DMA运行中突然停止DMASR寄存器里会出现错误位比如S2MM通道的Slave Error、Decode Error、Internal Error等。这些错误位一旦置位通道就自动停机了需要你先清错误、再重新启动通道。特别注意DMASR里错误位的清除方式同样是写1清0而且是写完错误位后要等待一个时钟周期再重新启动通道。我碰到过一次从头到尾都是Decode Error的情况后来查出是S2MM通道的Buffer地址指向了一个不存在的地址区域仔细核对地址后发现是地址计算时没有乘以Buffer大小Bus地址和字节偏移混在一起了。还有一个容易被忽视的错误来源是TLAST信号。Stream协议里的TLAST表示这是最后一拍数据S2MM通道在收到TLAST后会把当前DPT字节数和数据写入DDR。如果上游外设产生的TLAST位置不对或者TLAST的字节长和DMA寄存器设置的传输长度不匹配DMA可能一直等数据导致DMA看起来“死机”。遇到这种问题我建议先用ILA抓S_AXIS_S2MM的TREADY和TVALID信号确认数据流本身是连续的、TLAST和字节数是否符合预期再回头检查DMA配置。5.3 多通道同时使用时的冲突与优先级调整在一个比较复杂的PL工程里常常有多个DMA或者多个Stream外设同时工作。比如一个DMA负责从ADC采集数据另一个DMA负责把处理结果送给DAC两台DMA同时访问DDR时AXI互联网络会按固定优先级或者轮询方式仲裁。默认情况下优先级并不一定适合你的业务。Vivado的AXI Interconnect IP里可以配置每个从口的优先级也可以调节Request In/Out管道寄存器来平衡时序和带宽。如果发现一个通道的带宽被另一个通道严重挤压可以通过调整AXI Interconnect的优先级或者把不同DMA通道挂到不同HP口来隔离。实测中把MM2S和S2MM分别挂到不同HP口后双向带宽都能跑满但代价是多占用了HP资源。如果HP口不够用也可以把DMA描述符放在一个低延迟的OCM区域减少描述符访问对DDR总线的占用间接提升有效带宽。还有一点当PL侧的Stream数据频率很高时AXI DMA内部的FIFO会成为瓶颈。如果发现DMA长时间拉低TREADY导致上游数据溢出来就得考虑增大DMA内部FIFO的深度或者在IP配置里启用允许非对齐突发传输等高级选项。不过改这些参数之前最好先抓波形确认瓶颈在哪不要盲目加资源。6. 实战流程收尾与一点个人心得我在好几个项目里反复用AXI DMA之后最大的感受是这个IP核本身不复杂复杂的是它周围的一整套系统和工程习惯。只要把描述符机制吃透、Cache处理好、中断写规范Zynq的数据通路基本就稳住了一半。文章里提到的寄存器名称和流程建议你在实际调试时对照UG021文档一起看那本手册虽然厚但很多位定义在关键时刻比任何总结都管用。最后再分享一个小技巧第一次调通AXI DMA最好先在PL侧做一个数据回环比如把M_AXIS_MM2S直接接回S_AXIS_S2MM外设侧不做任何数据生成这样你就能单独验证DMA本身是否能可靠地完成内存到内存的搬运排除外设时序带来的干扰。我每次开新板子都会先跑一个这样的回环测试数据稳定了再接真实外设排查效率能高很多。
返回列表