Zynq中DDR数据传给PL端:AXI DMA与Cache一致性详解

发布时间:2026/9/1 17:08:24
Zynq中DDR数据传给PL端:AXI DMA与Cache一致性详解 简介在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端的DDRCPU在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能直接“摸”到DDRPL能摸到的是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数据块典型场景就是前面说的硬件加速。特点是一次搬完源地址和目标地址都明确配合SGScatter 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。第二个是使用ACPAccelerator Coherency Port加速器一致性端口PL端DMA通过这个口访问内存时硬件会自动去监听CPU的Cache如果数据在Cache里就直接拿保证拿到的是最新值。第三个是把内存映射配置为non-cacheableCPU每次读写都绕过Cache直接访问DDR简单粗暴但性能损失比较明显。我实际项目里最常用的是第一种配合ACP口做双保险。第一次做的时候踩过坑DMA回环测试前几次数据正常后面突然全乱排查了半天才发现是CPU往缓冲区写数据之后没有刷CacheDMA读到了旧的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 EngineSG模式这个模式下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 FIFOS2MM口则是反过来接收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通道写回DDRCPU去读对应地址读出来是旧数据。原因就是CPU的Cache中还缓存着这个地址的旧内容DDR里的数据已经更新了但CPU读Cache时直接命中了。解决办法是在CPU读取前调用缓存无效化操作让CPU下次访问时从DDR重新加载数据。我早期踩这个坑时没意识到写回和无效化是两个不同方向的操作导致遇到“写进DDR的数据PL看不到”和“PL写回DDR的数据CPU看不到”两个问题时手忙脚乱。理解了Cache读写两套方向之后一切就顺理成章了。最后再分享一个实操习惯第一次调通链路时先不要传真实业务数据而是在DDR里放一段递增序列比如0x00, 0x01, 0x02...传到PL后核对每个字节。递增序列比真实数据更容易定位偏移、错位、字节序问题。我已经把这个流程固化成了自己所有DMA相关项目的验证第一步推荐你也试试。本文还有配套的精品资源点击获取