Zynq AXI DMA实战:5分钟搞懂S_AXIS_S2MM和M_AXIS_MM2S的配置流程
Zynq AXI DMA实战:从零到一的流接口配置与避坑指南
如果你刚开始接触Zynq平台上的高速数据传输,面对AXI DMA IP核里那些以S_AXIS和M_AXIS开头的接口,是不是感觉有点眼花缭乱?别担心,这种感觉我太熟悉了。几年前我第一次在Vivado里拖拽AXI DMA IP时,光是搞清楚哪个接口该连到哪里,就花了整整一个下午,还因为一个时钟域的配置错误导致系统根本跑不起来。这篇文章,就是我想分享给你的那份“避坑地图”。我们不谈那些晦涩难懂的理论协议细节,就聚焦在Vivado这个真实的工程环境里,手把手带你完成S_AXIS_S2MM和M_AXIS_MM2S这两个核心流接口的配置,让你在5分钟内建立起清晰的配置脉络,并把常见的“坑”提前标出来。
我们的目标很明确:为一个典型的图像处理或高速数据采集场景搭建通路。比如,FPGA逻辑端(PL)产生或处理了一帧图像数据,需要通过DMA快速、无误地送入Zynq处理器端(PS)的DDR内存中供软件分析;或者反过来,软件在内存中准备好了一组控制参数,需要DMA实时地发送给PL端的逻辑模块。这个过程的核心枢纽,就是AXI DMA。下面,我们就进入实战环节。
1. 工程创建与IP核基础配置
在打开Vivado开始操作之前,我建议你先在纸上画个简单的框图。不需要多复杂,就画出PS、AXI Interconnect、AXI DMA,以及你PL侧的数据产生模块(比如一个自定义的AXI-Stream IP)和消耗模块。这个习惯能帮你理清数据流向,避免在Vivado里连成一团乱麻。
启动Vivado,创建一个新的RTL工程,器件选择你的Zynq型号(例如xc7z020clg400-1)。然后,通过“Create Block Design”进入图形化设计界面。这是我们的主战场。
第一步,放置核心IP。
- 在Diagram窗口中,点击“+”号添加IP。
- 搜索并添加
ZYNQ7 Processing System。双击它进行配置,根据你的硬件板卡,在“PS-PL Configuration”中使能至少一个HP(高性能)或ACP端口,作为DMA访问DDR的通道。我通常优先使用HP0端口。 - 再次添加IP,搜索并添加
AXI Direct Memory Access。这就是我们今天的主角。
第二步,关键参数设置。双击刚添加的AXI DMA IP核,会打开它的配置窗口。这里有几个地方需要特别注意:
- Enable Scatter Gather:对于初学者,我强烈建议先取消勾选。Scatter Gather功能强大,但配置更复杂,初期我们只做简单的块传输(Block Transfer)。关掉它,界面会清爽很多。
- Width of Buffer Length Register:这个值决定了单次传输你最多能设置多大的字节数。默认23位足够了,意味着单次传输最大约8MB(2^23字节)。如果你的数据包很大,可以适当调大。
- Stream Data Width:这是最容易出错的地方之一!这里配置的是
M_AXIS_MM2S和S_AXIS_S2MM这两个流接口的数据位宽。它必须与你PL侧发送/接收数据模块的位宽严格一致。常见的有32位、64位、128位等。假设你的自定义逻辑每次产生32位数据,这里就选32。
注意:此处的位宽配置会直接影响后续
AXI4-Stream Data FIFO的深度选择,如果位宽不匹配,数据根本无法正确传输。
配置完成后,点击OK。你会看到Block Design中出现了DMA IP,它自动展开了一堆接口。别慌,我们接下来就梳理它们。
2. 接口功能梳理与正确连接策略
现在,IP核上挂着一堆以M_AXI, S_AXI, M_AXIS, S_AXIS开头的接口。我们来快速对焦今天的主角——流接口,并理解它们在整个系统中的位置。
S_AXIS_S2MM:这是DMA的从流接口,用于接收数据。数据流向是:你的PL侧数据源(Master)→ 此接口 → DMA → DDR内存。所以,它需要连接到一个能产生AXI-Stream信号的源上。M_AXIS_MM2S:这是DMA的主流接口,用于发送数据。数据流向是:DDR内存 → DMA → 此接口 → 你的PL侧数据接收端(Slave)。所以,它需要连接到一个能接收AXI-Stream信号的目的端。
另外两个重要的内存映射接口是通道:
M_AXI_MM2S和M_AXI_S2MM:分别是DMA为了进行MM2S和S2MM传输时,主动去读写DDR内存的“手”。它们必须连接到Zynq PS的DDR控制器上,通常通过一个AXI Interconnect互联器。
理解了角色,开始连接。我推荐按以下顺序操作,逻辑更清晰:
连接DMA到内存系统:
- 添加一个
AXI InterconnectIP。 - 将DMA的
M_AXI_MM2S和M_AXI_S2MM连接到AXI Interconnect的S00_AXI和S01_AXI从端口。 - 将
AXI Interconnect的M00_AXI主端口连接到ZYNQ7IP 的S_AXI_HP0(或你使能的其他HP/ACP端口)。 - 将
AXI Interconnect的ACLK和ARESETN连接到系统时钟和复位网络。
- 添加一个
连接DMA控制接口到PS:
- DMA的
S_AXI_LITE接口是用于PS端的软件配置DMA寄存器(如设置源/目的地址、传输长度、启动传输)的。将它连接到另一个AXI Interconnect(或复用之前的,但建议为控制路径单独用一个),最终连到Zynq PS的M_AXI_GP0(通用端口)上。控制路径和数据路径分开是常见的好实践。
- DMA的
连接流接口到PL逻辑(这是核心):
- 假设你有一个自定义的、能产生AXI-Stream数据的模块(名为
data_source),其主流接口为M_AXIS。那么,将data_source.M_AXIS连接到AXI_DMA.S_AXIS_S2MM。 - 假设你有一个自定义的、能接收AXI-Stream数据的模块(名为
data_sink),其从流接口为S_AXIS。那么,将AXI_DMA.M_AXIS_MM2S连接到data_sink.S_AXIS。 - 关键一步:流接口的
TVALID、TREADY、TDATA信号会自动连接,但请务必手动连接TLAST信号!TLAST标志一个数据包的结束,对于DMA判断传输完成至关重要。确保你的数据源模块能在数据包末尾正确拉高TLAST。
- 假设你有一个自定义的、能产生AXI-Stream数据的模块(名为
时钟与复位连接:
- DMA IP通常有多个时钟输入:
s_axi_lite_aclk(控制接口时钟,一般用较慢的时钟,如100MHz),m_axi_mm2s_aclk和m_axi_s2mm_aclk(内存接口时钟,建议与HP端口时钟同源,如150MHz),m_axis_mm2s_aclk和s_axis_s2mm_aclk(流接口时钟,必须与你的PL侧数据模块时钟同源)。 - 一个黄金法则:流接口的时钟域(
m_axis_mm2s_aclk/s_axis_s2mm_aclk)独立于内存接口时钟域。它们之间通过DMA内部的异步FIFO进行跨时钟域处理。这意味着你可以用不同的时钟频率来驱动数据生产和消费,只要两边都满足时序。
- DMA IP通常有多个时钟输入:
连接好的简化框图示意如下:
[Zynq PS] <--AXI HP--> [AXI Interconnect] <--M_AXI_*--> [AXI DMA] | | S_AXIS_S2MM M_AXIS_MM2S | | [PL Data Source] [PL Data Sink]3. 流接口时钟、复位与数据宽度的深度配置
连接好线只是第一步,让流水线真正流动起来,还需要正确的“水压”(时钟)和“管道规格”(位宽/深度)。这部分配置不当,系统要么死锁,要么数据出错。
时钟与复位域隔离如前所述,DMA的流接口时钟(*_aclk)和内存接口时钟(m_axi_*_aclk)是独立的。在Vivado中,你需要创建两个时钟信号:
clk_mem:例如150MHz,供给ZYNQ7的FCLK_CLK0、AXI Interconnect的ACLK、以及DMA的m_axi_mm2s_aclk和m_axi_s2mm_aclk。clk_stream:例如100MHz,供给DMA的m_axis_mm2s_aclk、s_axis_s2mm_aclk,以及你的data_source和data_sink模块。
复位信号*_aresetn也需要对应各自的时钟域。使用Processor System ResetIP 可以方便地生成同步于不同时钟的复位信号。
数据位宽与FIFO深度在DMA的配置页面,Stream Data Width我们之前设定了。但还有一个隐含的配置是AXI4-Stream Data FIFO的深度。这个FIFO用于缓冲流接口和内存接口之间的数据,缓解速率不匹配。
- 深度配置在DMA IP的“Advanced”标签页或直接体现在
mm2s/s2mm的参数中。 - 如何设置深度?一个实用的经验公式是:深度 ≥ (内存访问延迟 * 流数据速率) / 数据位宽。例如,如果内存延迟可能达到100个
clk_mem周期,clk_stream为100MHz,数据位宽为32位(4字节),那么深度至少需要 (100 * 100e6 * 4) / (100e6 * 4) = 100。实际上,为了安全,我通常会设置得更大,比如1024。 - 深度设置过小,在内存访问出现延迟时,FIFO容易满或空,导致传输效率下降甚至卡死。设置过大,则会消耗更多的FPGA块RAM资源。
TDATA与TKEEP、TLAST
TDATA的宽度就是之前设置的Stream Data Width。你需要确保你PL模块的TDATA宽度与此一致。TKEEP信号用于指示TDATA中哪些字节是有效的,在数据位宽不是字节的整数倍,或传输非对齐数据时使用。对于常见的32/64/128位全有效传输,TKEEP可以连接为全1。- 再次强调
TLAST:它必须由数据源模块在一个数据包的最后一个有效数据周期拉高。DMA依靠这个信号来确认一个S2MM传输描述的完成,或者结束一个MM2S传输的数据包。忘记连接或错误生成TLAST是导致DMA传输无法完成的最常见原因之一。
4. 典型配置错误排查与调试技巧
即使按照步骤连接,第一次成功启动DMA的概率可能也只有一半。下面是我总结的几个“高发故障点”及其排查手段。
故障一:系统在Vivado中Validate Design时报错或生成比特流失败。
- 可能原因1:接口协议不匹配。例如,试图将AXI-Lite接口连接到AXI-Stream接口。Vivado的连线有时看起来“接上了”,但协议检查会失败。解决:仔细检查每个端口的协议类型(在IP文档或右键菜单中查看),确保Master连Slave,Stream连Stream,Memory-Map连Memory-Map。
- 可能原因2:时钟或复位未连接。DMA的每个时钟和复位端口都必须有明确的驱动源。解决:在Diagram中,确保没有“浮空”的时钟或复位线。使用“Run Connection Automation”功能有时能自动补全,但手动检查更可靠。
故障二:比特流加载后,软件无法检测到DMA或配置寄存器失败。
- 可能原因1:地址映射错误。DMA的
S_AXI_LITE接口必须被映射到PS端软件可访问的地址空间。解决:在Vivado中Address Editor标签页,检查DMA的S_AXI_LITE是否被分配了地址,并且该地址范围在PS的地址映射内(通常从0x4000_0000开始)。在SDK或Vitis中,你的驱动代码使用的基地址必须与此一致。 - 可能原因2:AXI Interconnect配置问题。如果控制路径的Interconnect没有正确配置,PS的访问无法到达DMA。解决:检查控制路径Interconnect的时钟、复位,以及其从端口(连接DMA Lite)和主端口(连接PS GP)的连线。
故障三:DMA传输启动后,数据无法正常搬运,或传输无法完成。
- 可能原因1:
TLAST信号问题。对于S2MM,DMA必须收到一个TLAST才会认为一次描述符传输完成,并可能产生中断。对于MM2S,DMA会在发送完指定长度数据后,在最后一个数据上输出TLAST。调试:在ILA(集成逻辑分析仪)中抓取S_AXIS_S2MM_TLAST或M_AXIS_MM2S_TLAST信号。观察它是否在预期的数据周期拉高,且仅拉高一个周期。 - 可能原因2:流接口握手(TVALID/TREADY)死锁。如果数据源一直不发出
TVALID,或者数据接收端一直不给出TREADY,传输就会停滞。调试:用ILA同时抓取TVALID和TREADY。一个健康的传输,在数据有效时,TVALID和TREADY应该同时为高。如果长期只有一方为高,另一方为低,就需要检查对应的PL模块状态。 - 可能原因3:内存地址或长度设置错误。软件配置DMA描述符时,目的地址(S2MM)或源地址(MM2S)必须是有效的、可访问的DDR物理地址,且长度不能超过缓冲区限制或实际内存范围。解决:在软件端使用
printf或调试器,打印并确认你配置给DMA的地址和长度值。确保内存区域已被正确分配(例如,在裸机程序中,地址可能是静态分配的数组;在Linux驱动中,可能是dma_alloc_coherent分配的)。 - 可能原因4:缓存一致性问题。在运行操作系统的场景下,CPU缓存的存在可能导致PS写入的数据DMA看不到,或者DMA写入的数据PS读不到最新的。解决:确保使用的内存是“一致性”的(如Linux下使用
DMA_ATTR分配),或者在适当位置执行缓存刷新(flush)和无效(invalidate)操作。
ILA调试实战建议在Vivado中插入ILA核来调试AXI-Stream总线是最直观的方法。我通常会监控以下信号组:
- S_AXIS_S2MM 组:
ACLK,TVALID,TREADY,TDATA,TLAST。观察数据流是否顺畅,TLAST是否出现。 - M_AXIS_MM2S 组:同上。
- DMA状态信号:有些DMA IP会提供
mm2s_prmry_reset_out_n或s2mm_prmry_reset_out_n等输出信号,指示内部状态机的复位状态,有助于判断初始化是否完成。
配置ILA触发条件时,可以设置为TLAST的上升沿,或者TVALID & TREADY的边沿,来捕获数据包的开始或结束时刻。
5. 进阶考量:性能优化与系统集成
当你成功完成了一次基础的数据搬运后,可能会开始追求更高的传输效率和更稳定的系统性能。这里有几个进阶的思考方向。
利用Scatter Gather提升效率基础模式(Simple Mode)下,DMA一次只能处理一个连续的内存块。Scatter Gather模式允许DMA从一个链表(描述符链表)中读取多个不连续的内存块描述,然后自动依次传输,传输完成后还可以产生中断通知CPU。这对于处理分散的数据缓冲区(如网络数据包)非常有用。启用Scatter Gather后,你需要:
- 在IP配置中勾选
Enable Scatter Gather。 - 软件端需要构建描述符链表,每个描述符包含数据地址、长度、控制信息以及下一个描述符的地址。
- 将链表头地址写入DMA的相应寄存器。DMA会自动遍历链表完成所有传输。
数据位宽与突发长度的权衡
- 流数据位宽:增加位宽(如从32位到64位、128位)可以在同一时钟周期内传输更多数据,直接提升峰值带宽。但这要求你的PL侧数据生产/消费模块也能处理更宽的数据,并且可能增加布线难度。
- 内存突发长度:DMA通过
M_AXI_*接口访问DDR时,会发起突发传输。在IP配置或软件配置中,可以调整突发长度(Burst Length)。更长的突发有利于提高DDR的访问效率,但需要DDR控制器和互联的支持。通常设置为256或512是一个不错的起点。
系统资源与时序收敛一个包含高速DMA(如工作在150MHz以上)的设计,对时序要求很高。在实现(Implementation)阶段,你可能会遇到时序违例。
- 策略1:流水线。在
M_AXI_*或S_AXIS_*/M_AXIS_*路径上插入寄存器(使用Register SliceIP),可以打散长路径,改善时序,但会引入一个时钟周期的延迟。 - 策略2:优化布局。使用
Pblock对DMA IP及其相关的Interconnect、时钟资源进行区域约束,让它们布局得更紧密。 - 策略3:降低时钟频率。如果时序无法收敛,适当降低
clk_mem或clk_stream的频率是最直接的方法,但会牺牲性能。
与处理器系统的协同最后,别忘了DMA是为减轻CPU负担而生的。在软件层面,你需要:
- 正确的驱动或库:使用Xilinx提供的
XDma库(裸机)或DMA Engine框架(Linux)来操作DMA,比自己直接读写寄存器更可靠。 - 高效的中断处理:配置DMA在传输完成或出错时产生中断。在中断服务程序(ISR)中,进行必要的状态清除、缓冲区切换或任务通知,避免轮询等待,释放CPU资源。
- 缓冲区管理:采用双缓冲或多缓冲机制。当DMA正在向缓冲区A写入数据时,CPU可以处理之前已经写满的缓冲区B。这能实现流水线操作,最大化系统吞吐量。
配置AXI DMA的流接口,就像搭建一条连接PL和PS的高速公路。理解每个接口的角色(S_AXIS是入口,M_AXIS是出口),配好交通信号(时钟、复位),设置好车道规格(位宽、深度),再处理好第一个收费站(TLAST),这条路就能跑起来了。我自己的经验是,第一次配置时,尽量简化设计:关掉Scatter Gather,用固定的数据模式测试,集中精力打通一条方向(比如先只做S2MM)。等一个方向调通了,理解了数据流和信号交互的节奏,再增加MM2S或者复杂功能,会顺利得多。遇到问题多抓ILA信号,那些波形图比任何文档都更能告诉你系统内部正在发生什么。
