简介:面向Zynq嵌入式开发者的DDR数据传输完整工程包,解决PS端DDR控制器与PL可编程逻辑之间的数据交互问题。资源基于Vivado与SDK工具链,涵盖从DDR接口设计、AXI4-Stream/AXI4-Lite配置、HDL逻辑实现,到软件驱动与仿真验证的完整流程,适合学习FPGA/SoC开发的工程师与学生动手复现。包内共1027个文件,以v、vhdl、vh等硬件描述源码为主,配以c/h软件程序、xdc约束、tcl脚本、xci IP核配置、dcp网表、bit比特流和hdf硬件定义等,压缩包大小约37.97MB,目录结构完整可对照学习。目前已有848人学习浏览,资源中包含Vivado工程文件、SDK软件工程及仿真脚本,使用者可直接打开工程查看底层代码和数据通路设计,也可按步骤理解AXI总线在PS与PL通信中的作用。 有相当长一段时间,我拿到Zynq这类SoC时的第一反应是:这芯片里既有ARM又有FPGA,数据在两边挪来挪去应该很方便。真做了第一个带DDR吞吐需求的项目才发现,DDR颗粒只接在PS端,PL这半边连内存颗粒的引脚都摸不到。想把DDR里的一段数据交给PL的算法模块,不是拿根线连上就行的,得走专门的AXI接口通道。这篇就围绕“DDR的数据怎么传给PL”这件事,把架构关系、接口选型、Vivado实操和常见的坑彻底讲透,不管你是刚入门Zynq的小白,还是已经在写AXI的老鸟,应该都能找到有用的东西。
1. 先把架构理清:DDR在PS端,PL的访问通道到底怎么走
1.1 同一块芯片,为什么DDR偏偏挂在PS上
Zynq-7000和Zynq UltraScale+这类异构SoC,把ARM处理系统和FPGA逻辑做在同一个硅片上,但DDR控制器集成在PS的存储子系统里。DDR颗粒的物理引脚,比如DQ、DQS、地址线这些,由PS侧专用引脚引出,PL这边没有直连DDR颗粒的通道。很多人第一次看到原理图时会愣一下:明明是一个芯片,为什么DDR不设计成两边都能直接访问?
这个设计其实是系统工程上的理性取舍。DDR控制器的时序非常复杂,涉及PHY校准、刷新调度、ECC校验等一堆问题,由ARM生态里成熟稳定的方案统一管理,可靠性远高于让PL侧各自去实现一套。而且PS侧的软件、DMA、MMU都围绕DDR工作,把DDR挂在PS上能保证系统和CPU Cache的一致性,Linux、裸机都走同一条存储路径,调试也简单。代价就是PL要用DDR数据时,必须借道PS侧暴露出来的AXI接口,相当于绕一小段路。
很多人做第一个Zynq项目时,习惯性地把DDR理解成“整个芯片的公共内存”,觉得PL要访问天然就能访问。实际上,PL侧的AXI端口开了没、地址映射配了没、时钟频率对不对,任何一个环节没做对,DDR对PL就是不存在的。这篇讲的所有操作,本质上都是把这条“借道通路”打通。
1.2 三条可选通道:HP、ACP、GP,选错后面全白搭
Zynq PS侧给PL暴露了多组AXI接口,最常用来访问DDR的主要是三组,它们的方向、位宽和适用场景差别很大,我直接列个表说明:
| 接口 | 方向 | 数据位宽 | 主要特性 | 适合场景 |
|---|---|---|---|---|
| S_AXI_HP0~HP3 | PL作为主设备访问PS | 最高128bit | 高性能端口,内置FIFO缓冲,吞吐能力强 | 大数据量流式传输,比如PL读DDR图像数据 |
| S_AXI_ACP | PL作为主设备访问PS | 64bit | 加速器一致性端口,直连SCU,可感知CPU Cache | 需要与CPU Cache保持一致性的共享数据 |
| M_AXI_GP0/GP1 | PL作为主设备访问PS | 32bit | 通用接口,吞吐低,一般不到1GB/s | 寄存器配置、少量控制命令 |
选型建议非常直接:如果你要拿PL读DDR里的大块数据,优先选HP口,数据位宽直接拉到128bit,FIFO也能吸收突发带来的抖动。如果数据量很小,CPU和PL要频繁共享同一个变量,那ACP值得考虑,它能帮你省掉手动刷Cache的麻烦。GP口我基本不用来传数据,写个控制寄存器还凑合,拿它跑DDR数据流你会怀疑人生。
这里还有一个容易被忽略的点:HP口的底层协议实际是AXI3,不是说配置界面上写着AXI High Performance就自动支持AXI4的所有特性。AXI3的单次突发长度上限是16拍,而AXI4可以到256拍。虽然中间SmartConnect或Interconnect会做协议转换,但设计时不要把单次突发长度拉得太激进,否则跨时钟域和协议转换会引入额外开销,效率不一定更高。
2. 传输方式定生死:轮询、DMA还是用户逻辑自己读
2.1 三种搬运方式,我建议新手直接选DMA
通道选好了,接下来要考虑的是“谁发起读请求”。同样是DDR数据到PL,CPU搬运、DMA搬运、用户逻辑自己发AXI请求,这三种方式效率差距能有十倍。
CPU搬运最直接,ARM在代码里做memcpy,把DDR某段数据复制到另一块地址,PL再从对应的地址空间去读写。这种方式实现最简单,但ARM核的吞吐上限有限,而且CPU全程被占用,基本只适合传几百字节的控制信息。用CPU搬数据再喂给PL的算法模块,带宽大概率不够,还会把整个系统拖慢。
AXI DMA是业界最通用的方案,Xilinx提供了AXI DMA IP核,支持MM2S和S2MM两种方向。MM2S就是从DDR的某个地址读数据,转成AXI-Stream流输出到PL,PL侧只要接一个FIFO或者直接喂给算法模块就行。DMA内部用描述符链表管理一次或多次搬运,CPU只需要下命令和等完成中断,带宽高,逻辑也简洁。
用户逻辑自己写AXI Master,灵活度最高,PL侧想什么时候读就什么时候读,不需要等待DMA的软件调度。但代价也很明显,你得自己维护AXI写地址通道、写数据通道、写响应通道,或者读地址通道、读数据通道的状态机,还要处理乱序返回、超时、错误响应,调试成本明显更高。我的建议是:项目进度紧就乖乖用AXI DMA,不紧且你有充足时间调协议再自己写。
2.2 一笔带宽账:DDR3-1066的瓶颈其实在AXI
选型之前最好先算一笔账。常见的DDR3-1066,数据位宽64bit,理论带宽是1066MT/s乘8字节,约8.5GB/s。听起来很猛,但PL侧的HP口一般时钟跑到200MHz,数据位宽128bit,理论极限是3.2GB/s。这一下就把瓶颈暴露了:DDR本身很快,但AXI接口通道只有3.2GB/s的吞吐上限。
如果你用的是GP口,32bit位宽、150MHz时钟,理论带宽只有600MB/s,就算是简单控制流也够呛。所以为什么大家反复强调用HP口,不是没有道理的。但HP口的3.2GB/s也只是纸面数字,实际跑起来还有读写切换开销、AXI协议仲裁开销、地址不连续导致的等待周期。我实测用AXI DMA从DDR线性搬2MB数据到PL,稳定传输速率能到1.8GB/s左右已经算不错,优化地址对齐和突发长度之后可以到2.3GB/s上下。
这里顺便说一句,AXI的burst长度和DDR里的burst长度是两个层面的概念,很多人在论坛上混淆。AXI的burst指的是同一笔AXI事务里连续传输多少拍数据;DDR的burst指的是DDR颗粒内部一次激活连续读写多少个数据。两边都有burst,但对吞吐的影响机制完全不同。调PL侧速度时主要调AXI的burst长度,调DDR本身效率时就看DDR控制器的配置了。
3. Vivado实操:搭出PL读DDR的最小链路
3.1 配置PS:开HP口和选对DDR颗粒
第一步打开Vivado,新建Block Design,加入ZYNQ7 Processing System IP后,双击进入配置界面。在DDR Configuration里选择DDR型号,这一步必须照着开发板原理图和颗粒手册来,型号选错轻则初始化失败,重则内存训练不过,系统直接启动不起来。
接下来在PS-PL Configuration里展开S_AXI_HP0接口,勾选启用,并把数据宽度设为128bit。HP接口的时钟要单独确认,一般在100MHz到200MHz之间,这个频率直接决定前面算的3.2GB/s那个上限,建议按板卡支持的时序尽量跑高一点。
还有一个小细节:HP接口内部有FIFO,配置界面里通常会自动分配深度。默认值一般够用,但如果你的PL端DMA一次发起很大的突发,建议稍微调大FIFO深度,能够吸收总线切换时的抖动,减少背压。
3.2 地址分配:从0x00100000开始是有讲究的
Block Design里连完线之后,打开Address Editor,给PL侧的Master分配DDR地址空间。这里有个容易踩的坑:Zynq-7000的DDR默认基地址是从0x00100000开始的,前面有1MB的保留区,如果你手滑给Master分配0x00000000,等于让PL去访问保留地址,传输必然失败。
所以实际分配时,比如DMA的地址窗口起始地址就填0x00100000,大小根据你要搬运的数据量来定,16MB够用就填16MB,窗口开太大了反而容易和其他外设地址重叠。UltraScale+系列的内存映射是从0x00000000开始,规则又不一样,每次换芯片都要养成打开Address Editor确认地址的习惯,别拿老经验硬套。
3.3 接入AXI DMA:把DDR读请求变成数据流
在Block Design里再加一个AXI DMA IP,配置成MM2S模式,也就是Memory-Mapped to Stream,把DDR地址空间的读请求转成AXI-Stream输出。PL侧接一个AXI-Stream FIFO或者直接接到下游算法模块的输入。PS端只需要在程序里下发源地址和长度,DMA就会自动连续地从DDR读数据。
下面是SDK/Vitis里启动MM2S传输的示意代码,注意不同版本BSP生成的API名称可能有差异,以你工程里的头文件为准:
// 伪代码:PS端启动AXI DMA从DDR读数据到PL #include "xdma.h" XDma XDmaInst; u32 ddrSrcAddr = 0x00100000; // DDR中的源地址 u32 length = 4096; // 单次传输字节数 // 1. 初始化DMA驱动 XDma_Config *cfg = XDma_LookupConfig(XPAR_XDMA_0_DEVICE_ID); XDma_CfgInitialize(&XDmaInst, cfg, cfg->BaseAddress); // 2. CPU写入的数据先flush回DDR,否则PL可能读到旧值 Xil_DCacheFlushRange((UINTPTR)ddrSrcAddr, length); // 3. 启动MM2S通道,数据从DDR搬到PL侧的AXIS流 XDma_StartTransfer(&XDmaInst, XDMA_MM2S, ddrSrcAddr, length);重点说第二步:只要CPU往DDR里写过数据,这些数据可能还在Cache里,没真正落回DDR,PL直接读就会读到旧数据。Xil_DCacheFlushRange的作用就是把这段地址的Cache内容强制刷回DDR。没有这一句,你的DMA传输十有八九会读到一堆乱码,这个问题在裸机里尤其常见。
3.4 想自己写AXI Master也不难,记住这几个信号
不依赖AXI DMA,自己写一个AXI4 Master从DDR搬数据,核心是维护好几条通道的握手状态。以读操作为例,先用地址通道发ARVALID和ARADDR,目标从设备会回ARREADY;之后数据通道上RVALID和RDATA逐拍返回,主设备回RREADY,最后一拍还要注意RLAST信号标记传输结束。写操作则是AW通道发地址,W通道发数据和WLAST,最后从设备通过B通道返回写响应。
状态机可以简化为IDLE、ADDR、DATA、RESP几个状态,写时序时特别注意AXI协议里“burst不能跨越4KB边界”这一条。如果数据块跨了4KB边界,必须拆成两笔或更多笔事务。这也是很多自研Master性能上不去的重要原因,处理不好就频繁中断。我个人的习惯是先用ILA抓一次完整的AXI读事务,确认握手和last信号都正常,再往深处优化。
4. 调试实录:超时、旧数据和带宽缩水
4.1 读写超时:优先查时钟、复位和地址空间
PL侧Master发起DDR读请求后,如果长时间收不到响应,最常见的根因不是你的逻辑状态机写错了,而是时钟和复位根本没就绪。AXI总线的ACLK要真实存在,ARESETn要稳定释放,这两个条件不满足,任何握手都进行不下去。
排查顺序建议是:先用ILA挂AXI接口看是否有ARVALID被拉高但迟迟等不到RVALID,如果有这种pending请求一直悬挂,去查PS端是否启用了对应的S_AXI_HP端口,以及地址空间是否真的映射到了DDR区域。还有一种隐蔽问题是HP口的时钟没有打开,PS里虽然界面勾选了HP接口,但对应的参考时钟没使能,导致AXI总线时钟压根没有翻转。这种问题用眼睛看逻辑很难发现,一查时钟就露馅了。
4.2 读到旧数据:缓存一致性这道坎躲不过
PL读DDR读到旧数据,这基本是Zynq开发绕不开的经典坑。CPU写数据时,数据不一定立刻到DDR,可能躺在L1或L2 Cache里,DMA和PL直接读DDR拿到的就是旧值。反过来,PL写DDR后CPU立刻读,如果Cache里还缓存着旧值,CPU拿到的也是旧数据。
裸机开发里,CPU写完后PL读之前,调Xil_DCacheFlushRange;PL写完DDR后CPU读之前,调Xil_DCacheInvalidateRange。千万别嫌麻烦省掉这两步,我见过太多人浪费一整天在数据错位问题上,最后发现只是漏了一次flush。
如果是Linux环境,更推荐用DMA类型的内存,比如dma_alloc_coherent申请到的缓冲区天然是非缓存的,或者先做dma_map_single并指定DMA_TO_DEVICE方向。你要是用常规kmalloc内存,不做缓存维护,数据一致性就全靠运气了。如果项目对实时一致性要求高,可以考虑ACP口,PL访问时会通过SCU和CPU Cache交互,省去手动flush,但吞吐比HP口低,需要自己权衡。
4.3 带宽上不去:burst长度和4KB对齐
很多人跟我抱怨,明明用了HP口和AXI DMA,DDR读带宽却只有理论值的零头。这种情况先检查burst相关配置。AXI DMA会自己拆burst,但如果你用了自研Master,每笔burst只有4拍或者8拍,地址通道的仲裁开销占比会非常高,总线大部分时间在发地址而不是搬数据,吞吐自然上不去。尽量把burst长度拉满,在AXI3的约束下做到16拍,效率提升非常明显。
接着看地址对齐。源地址和长度如果不对齐到128bit,一笔事务会被拆成“首尾短、中间长”的多段,浪费大量周期。实际项目里,我遇到过把MM2S带宽从1.2GB/s优化到2.1GB/s,仅仅靠把源数据的存放地址从非对齐改成4KB对齐。还有一点,尽量不要在同一个DMA通道上频繁来回切换读和写,读写切换带来的总线空档比想象中更耗性能。如果有读有写的需求,分成两套DMA或者分时突发,效率会好很多。下面这个表是我整理的最典型的三个问题:
| 现象 | 常见根因 | 快速排查方法与建议 |
|---|---|---|
| 读写DDR超时 | HP时钟未使能、地址空间未映射 | 查PS时钟配置、Address Editor映射关系,用ILA看AXI握手是否有响应 |
| 读到旧数据 | CPU Cache脏数据未写回DDR | 读前flush,写后invalidate,或改用consistent/coherent内存 |
| 带宽只有理论值一半 | burst长度偏小、地址未对齐、读写切换频繁 | 拉长burst、按4KB对齐、避免单通道频繁读写交替 |
最后再分享一个习惯吧:做这类PS-PL数据交互,我从来不会一上来就测大规模DMA,而是先跑通“一个burst的读写”,用ILA确认AXI时序完全正确,再逐步加大数据量去测带宽。顺序反了,你会在缓存问题和时序问题同时出现时怀疑人生。项目里那些能跑到合理带宽的方案,通常也是能把每一条AXI信号讲清楚的方案。希望你们的数据通路一次跑通。
本文还有配套的精品资源,点击获取