Zynq中DDR数据传给PL端:AXI DMA与Cache一致性详解
2026/9/1 17:07:54 网站建设 项目流程

简介:在Zynq SoC开发中,将PS端DDR中的数据高效传输至PL端,是软硬件协同设计的常见课题,DDR控制器位于PS部分,而PL端负责数据的灵活处理与再加工。这份资源提供了一套围绕该主题的完整Vivado与SDK工程资料,面向FPGA开发者和嵌入式工程师,涵盖Vivado工程、硬件平台定义、仿真工程与SDK软件项目,可帮助理解AXI4-Lite和AXI4-Stream两种常用接口的区别与应用。压缩包共1027个文件,约37.97MB,包含C/C++软件源码、Verilog/VHDL硬件描述代码、XDC约束、Tcl脚本、IP核配置,以及bit流、HDF、ELF等产物文件,既能阅读工程细节,也可直接烧录验证。目录将Vivado工程、SDK工程、仿真与日志分开组织,便于按需定位;目前已有848人学习,适合需要对照完整项目学习Zynq数据搬运、AXI总线通信或PL逻辑处理的开发者。通过梳理源码与配置,可较快掌握DDR数据读取、PL端接收处理以及软硬件联调的关键思路,减少从零搭建环境的弯路。 做Zynq开发的人,早晚都会遇到这么一个问题:PS端已经把一批数据写进了DDR内存,现在想让PL端的逻辑来处理这批数据,数据该怎么从DDR搬过去?“如何将DDR的数据传给PL端”听起来像句大白话,背后其实牵扯到Zynq的AXI总线结构、DDR地址映射、DMA引擎配置、Cache一致性处理等一系列问题。

我当年第一次做这个功能时,照着别人的代码抄了一个DMA方案,结果数据死活不对,折腾了一整天才发现是Cache没刷。今天这篇就把整条路线从头到尾捋清楚:先讲有哪些方案可以选,再讲关键原理,然后走一遍完整的实操流程,最后把高频踩坑点整理成速查表。无论你是做图像加速、视频编解码,还是纯数据搬运,只要涉及“CPU侧数据在DDR里、PL侧逻辑要读它”,这篇文章都适用。

1. 需求从哪来:DDR与PL之间的“数据搬运工”

1.1 典型场景回顾

先说说我在实际项目中遇到的几个典型场景。第一个是图像处理:PL端的摄像头接口采集视频帧,通过DMA写入PS端的DDR,CPU在Linux下跑算法处理,处理完的结果又需要通过VDMA送回PL端去做显示覆盖或者编码。这里的数据流向就是DDR到PL,而且是连续多帧的流式数据。

第二个是硬件加速:CPU生成一大块运算数据,放在DDR里,PL端做FFT或者卷积运算。这种场景下数据是一次性搬过去的,PL端拿到数据以后开始计算,算完再通过另一个通道把结果写回DDR。

第三个是协议处理,比如PS端组一个UDP数据包内容,放在DDR缓冲区里,PL端的以太网MAC从DDR读取数据并发送出去。

这些场景有一个共同点:DDR控制器在PS端内部,PL逻辑并没有直接连接DDR颗粒的引脚(除非板子上PL侧单独接了DDR接口),所以“传给PL”本质上不是物理接线的问题,而是通过AXI总线,把DDR地址空间中的数据搬运到PL逻辑能够访问的接口上。这就是我要说的第一件事:别想着PL能直接“摸”到DDR,PL能摸到的是AXI总线上映射出来的DDR地址区间。

1.2 拆解成三个子问题

如果把“DDR数据传给PL”拆开看,其实就是三个问题要解决。

第一个问题是地址:DDR里的数据在系统地址空间中长什么样?PL端逻辑怎么知道去哪个地址读?第二个问题是通路:通过哪条AXI通路把数据送过去?是CPU参与搬运,还是硬件DMA自动搬运?第三个问题是接口形态:数据送到PL端之后,以什么形式呈现?是一个寄存器里的值、一段AXI-Stream流,还是写入PL侧的BRAM?

把这三个问题想清楚,整条链路就通了。大多数方案的本质区别,只是对这三个问题的回答不同而已。

2. 方案选型:到底用哪条路传数据

2.1 AXI DMA:最通用的大块数据搬运

AXI DMA是Xilinx提供的一个IP核,核心作用是把内存映射格式(Memory-Mapped)的数据转换成AXI-Stream流式数据,或者反过来。它内部有一个硬核的搬移引擎,CPU只需要配置好源地址、目的地址和传输长度,然后启动传输,剩下的读写操作全部由DMA自己完成。

适用于一次性传输几百字节到几MB数据块,典型场景就是前面说的硬件加速。特点是一次搬完,源地址和目标地址都明确,配合SG(Scatter Gather)模式还能把分散的内存片段拼起来传输,灵活性很好。

选它的理由很简单:通用。无论数据是什么格式、什么用途,DMA都能搬。实际项目中我大多数时候用的就是AXI DMA。

2.2 AXI VDMA:视频帧专属搬运工

AXI VDMA是专门为视频帧设计的DMA,内部有帧缓冲管理逻辑,从DDR搬运一帧图像到PL端,可以配置分辨率、像素格式、帧缓冲地址,支持乒乓操作和多帧缓存。它和AXI DMA最大的区别在于“帧意识”:DMA只认字节,VDMA认帧。

如果你的数据恰好是一帧一帧的视频图像,用VDMA会省很多事。硬件上可以自动处理行同步、场同步,软件上只需要配置帧缓冲首地址就行。如果硬要用普通DMA搬视频帧,也不是不行,但你要自己做帧同步和缓冲管理,很容易踩坑。

2.3 直接通过PL侧AXI Master访问DDR

还有一条路是绕开DMA,直接在PL端写一个AXI Master逻辑,通过PS的HP口(High Performance port,高性能从端口)主动发起AXI读请求去读DDR。这种方案灵活度最高,数据到了PL端你想怎么处理就怎么处理,不需要经过Stream接口转换。

代价是实现复杂度很高。你要自己处理AXI读事务的状态机,考虑burst长度、outstanding请求数量、数据位宽转换、还有响应回退。我一般只在做一个简单的、固定地址的寄存器读取时才用这种方式,大块数据搬运绝不碰它。

三种方案对比下来,结论很明确:

方案适用场景实现复杂度带宽表现
AXI DMA通用大块数据、一次性搬移
AXI VDMA视频帧、多帧连续传输
PL侧AXI Master自定义按地址访问、小数据量

3. 核心概念:地址空间、AXI协议和Cache一致性

3.1 DDR地址空间到底怎么映射

Zynq-7000的PS端DDR物理地址通常从0x00100000开始(具体可以去Vivado的Address Editor里看),UltraScale+系列则是0x00000000开头,不同芯片有差异。最关键的是,这个地址区间在系统里是全局可见的:CPU能访问,PL端通过AXI接口也能访问。Xilinx把这种设计叫做统一地址空间,DDR、外设寄存器、PL侧IP都被映射到同一个地址空间里,靠地址来区分访问对象。

实际操作中会遇到一个问题:在Linux下CPU用的是虚拟地址,需要转换成物理地址才能喂给DMA。裸机或者Vitis环境下相对省心,直接用函数就能拿到物理地址,但也要注意XPAR宏里面的基地址是对应哪个实例的。我见过不少初学者把Linux虚拟地址直接填进DMA长度寄存器,结果DMA直接总线错误,卡死整个系统。

3.2 AXI协议里和你相关的部分

AXI协议很庞大,但理解核心的几个通道就够了:读地址通道(AR)、读数据通道(R)、写地址通道(AW)、写数据通道(W)、写响应通道(B)。DMA本质就是不断在这个总线上发起读事务和写事务的组合。

读事务流程是这样的:Master在AR通道上发出目标地址和突发长度,Slave收到后把数据一路通过R通道返回。这个过程中Master不需要管数据在DDR里怎么存储,那是DDR控制器的事情。PL端通过HP口访问DDR时,实际上就是作为AXI Master去发读请求。带宽取决于总线位宽和时钟频率,Zynq的HP口通常是64位接口,跑在DDR相同的时钟下,理论带宽可以达到几个GB/s。

3.3 Cache一致性:最容易忽略的坑

这块我要重点讲,因为几乎所有从DDR读数据异常的问题都出在这里。

CPU向DDR地址写入数据时,并不会直接写进DDR,而是先写进CPU的Cache里,标记为dirty,等某个时机再回写(writeback)到DDR。如果CPU刚写完数据,马上让PL端的DMA从DDR对应地址读数据,那PL读到的是DDR里的旧数据,因为CPU最新的数据还在Cache里没回写。

解决办法有三个。第一个是在写完后主动调用Cache刷新指令,把Cache里的脏数据回写到DDR,比如Xilinx提供的Xil_DCacheFlushRange。第二个是使用ACP(Accelerator Coherency Port,加速器一致性端口),PL端DMA通过这个口访问内存时,硬件会自动去监听CPU的Cache,如果数据在Cache里就直接拿,保证拿到的是最新值。第三个是把内存映射配置为non-cacheable,CPU每次读写都绕过Cache直接访问DDR,简单粗暴但性能损失比较明显。

我实际项目里最常用的是第一种,配合ACP口做双保险。第一次做的时候踩过坑:DMA回环测试前几次数据正常,后面突然全乱,排查了半天才发现是CPU往缓冲区写数据之后没有刷Cache,DMA读到了旧的DDR内容。

4. 实操:用AXI DMA把DDR里的数据送到PL端

4.1 硬件设计:Block Design里如何连线

下面以Vivado Block Design为例,走一遍AXI DMA的搭建流程。我假设你已经创建好了一个Zynq PS核(Zynq-7000或UltraScale+都适用)。

在Block Design里添加AXI DMA IP核,双击打开配置界面。推荐勾选Enable Scatter Gather Engine(SG模式),这个模式下DMA通过描述符链传输数据,可以支持不连续的物理地址,灵活性高很多,而且对寄存器配置要求相比Simple模式更简洁。如果不勾选,就是Simple模式,每次只能传输一段连续的地址空间,写完一次要重新配置才能再传一次。

连线方式很关键。AXI DMA的M_AXI_MM2S和M_AXI_S2MM这两个口,是DMA作为AXI Master去访问DDR的通道,需要连到PS端的S_AXI_HP口(不同型号可能叫S_AXI_HP0或S_AXI_HP0_FPD)。AXI DMA的S_AXI_LITE口是配置寄存器接口,CPU通过它来控制DMA,连接PS端的M_AXI_GP口。

然后看AXI DMA的AXI-Stream接口。MM2S口是DMA读取DDR数据后输出的数据流,接PL端的自定义IP或AXI-Stream FIFO;S2MM口则是反过来,接收PL端数据并写入DDR。我们的场景是DDR传给PL,所以重点是用MM2S通道。

PS端的HP口和GP口都要选中使能,并设置合适的数据位宽。HP口建议保持64位或128位,别为了省资源降到32位,带宽会直接砍半。接着点击地址映射(Address Editor),让Vivado自动分配地址,确认DDR和DMA寄存器映射关系正确。特别注意DDR的基地址不要和别的模块冲突。

4.2 PL端接收逻辑示例

PL端可以简单做一个AXI-Stream转AXI-Stream信号监测的IP,或者直接接一个AXI-Stream Data FIFO,把数据序存在FIFO里,后面再接用户逻辑。为了验证方便,我更推荐先接一个AXI-Stream FIFO,把数据存进去,再用一个计数器读出值来判断数据是否正确。

如果是自己写RTL接收端,核心接口信号就是TVALID、TREADY、TDATA、TLAST。DMA发出数据后,TVALID拉高,用户逻辑在TREADY为高时采样TDATA即可。时序上要记住:当TVALID和TREADY同时为高时,有效数据在TDATA上,时钟沿采样;TLAST表示一个包传输结束,可以用来触发数据处理或中断。

4.3 驱动代码:PS端如何配置和启动

在Vitis/SDK里写驱动,用Xilinx官方提供的XAxiDma驱动最省事。核心代码如下:

#include "xaxidma.h" #include "xil_cache.h" #define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID #define BUFFER_BASE XPAR_DDR_0_S_AXI_BASEADDR + 0x10000000 #define BUFFER_SIZE 4096 static XAxiDma dma_inst; int dma_init(void) { XAxiDma_Config *cfg; cfg = XAxiDma_LookupConfig(DMA_DEV_ID); if (!cfg) return -1; return XAxiDma_CfgInitialize(&dma_inst, cfg); } int dma_send_data(u32 phys_addr, u32 len) { u32 reg; // 刷新Cache,确保DMA能读到CPU写入的最新数据 Xil_DCacheFlushRange((u64)phys_addr, len); // 等待DMA空闲 while (XAxiDma_Busy(&dma_inst, XAXIDMA_DMA_TO_DEVICE)); // 关闭DMA并复位,清掉上一次的状态 XAxiDma_Reset(&dma_inst); while (!XAxiDma_ResetIsDone(&dma_inst)); // 启动MM2S通道传输 int status = XAxiDma_SimpleTransfer( &dma_inst, (u32)phys_addr, len, XAXIDMA_DMA_TO_DEVICE); if (status != XST_SUCCESS) return -1; // 等待传输完成 while (XAxiDma_Busy(&dma_inst, XAXIDMA_DMA_TO_DEVICE)); return 0; }

这里的BUFFER_BASE是DDR中你分配的物理地址。比如在Linux下通过/dev/mem或者CMA分配一块物理连续内存,拿到物理地址后传给这个函数;裸机环境下直接用分配好的缓冲区地址就行。

缓冲区地址必须对齐到4KB边界,这是AXI DMA的要求,不对齐会导致传输异常或地址错误。传输长度也要是总线字节宽度的整数倍,比如64位总线下建议长度按8字节对齐,不然最后几个字节可能会丢失。

4.4 整个流程串起来

完整跑一遍流程是这样:CPU往缓冲区写数据,调用Cache刷新确保数据回写DDR,配置DMA源地址为缓冲区物理地址,传输长度,然后启动MM2S通道。DMA通过HP口从DDR读取数据,转换成AXI-Stream流发出。PL端逻辑在Stream接口上接收数据,完成处理。

这个流程有一个容易忽略的点:传输完成之后,如果PL端还要把结果写回DDR给CPU读取,那么CPU读之前要做一次数据失效操作,把Cache里可能存在的旧数据作废,避免读到缓存的旧内容。很多人的问题出在这一步,只注意了发送前刷Cache,忘记了接收后作废Cache。

5. 常见问题与排查技巧实录

5.1 高频问题速查表

现象可能原因解决办法
收到的数据全是0DMA没有真正启动;源地址填错查看DMA寄存器、确认启动序列、检查地址映射
收到的数据是旧数据CPU写完数据没刷Cache在启动DMA前调用Xil_DCacheFlushRange
第一次传输成功,后续失败Simple模式DMA状态未复位每次传输前复位DMA、等待ResetIsDone
数据错位或字节顺序异常AXI-Stream位宽不匹配、字节序不对核对总线位宽、确认小端序数据处理逻辑
中断一直触发但没数据中断号配置错、SG描述符没建立好检查中断映射、描述符链表是否正确初始化
传输性能远低于预期burst长度太小、DMA跨多个DRAM page增大传输长度、使用SG聚合、确认内存连续
系统访问DDR崩溃物理地址计算错误、访问越界确认物理地址有效范围、不要用Linux虚拟地址

5.2 调试思路:先加ILA看波形

遇到数据异常,我习惯先在PL端的AXI-Stream接口上加一个ILA(集成逻辑分析仪),把TVALID、TREADY、TDATA这些信号抓下来看。判断方法很简单:如果ILA上根本看不到数据,说明问题在DMA到PL这段,要么DMA没启动,要么地址映射错误;如果能看到数据但内容不对,把抓到的数据和DMA发送端的数据对比,就能快速定位到方向。

这个方法帮我解决过不少疑难杂症。有一次调试一个多通道传输,现象是数据偶尔错一个字节,其余都正常。用ILA抓数据后对比发现,错字节总是出现在DMA刚启动的时刻,最后确认是时钟域没有做同步,复位释放时数据通路还没稳定。这类问题不看波形,纯靠猜代码能猜一整天。

5.3 性能排查:为什么带宽上不去

如果数据能通但带宽很低,重点看几个方向。第一是AXI突发长度,DMA配置里默认的burst长度如果设置太小,比如只有4个beats,那么每次访问DDR都要重新建立地址,效率很低。第二是DMA传输的缓冲是否跨了DDR的多个page,如果跨了,需要开启SG模式让DMA自动处理。第三是PL端接收逻辑的TREADY信号是否长时间拉低,这个造成反压,DMA会主动暂停等待,数据吞吐自然下降。

我实际测过一个项目,PL端接收逻辑在每次接收前都有占用时钟周期的额外操作,导致TREADY有效时间只有30%,最终实测带宽只有理论值的四分之一。把接收逻辑改成流水线处理之后,TREADY几乎一直拉高,带宽才打上去。

5.4 一个隐性问题:数据接收后CPU读不到正确结果

这种情况和发送端的问题是对称的。PL端把处理结果通过S2MM通道写回DDR,CPU去读对应地址,读出来是旧数据。原因就是CPU的Cache中还缓存着这个地址的旧内容,DDR里的数据已经更新了,但CPU读Cache时直接命中了。解决办法是在CPU读取前调用缓存无效化操作,让CPU下次访问时从DDR重新加载数据。

我早期踩这个坑时没意识到写回和无效化是两个不同方向的操作,导致遇到“写进DDR的数据PL看不到”和“PL写回DDR的数据CPU看不到”两个问题时手忙脚乱。理解了Cache读写两套方向之后,一切就顺理成章了。

最后再分享一个实操习惯:第一次调通链路时,先不要传真实业务数据,而是在DDR里放一段递增序列(比如0x00, 0x01, 0x02...),传到PL后核对每个字节。递增序列比真实数据更容易定位偏移、错位、字节序问题。我已经把这个流程固化成了自己所有DMA相关项目的验证第一步,推荐你也试试。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询