简介本资源是一套面向ZYNQ-7000 SoC初学者与进阶开发者的FPGA系统级交互实践课程资料聚焦PL端直接通过AXI总线读写PS端DDR这一关键能力解决传统DMA方案协议复杂、灵活性差、调试门槛高等痛点适用于图像处理、实时数据采集等需高带宽低延迟PL-PS协同的嵌入式FPGA开发场景。压缩包含1284个文件总计47.84MB涵盖308个C/C源码.c/.h、74个Verilog模块.v、42个Makefile构建脚本、28个VHDL文件、13个约束文件.xdc及大量Vivado工程文件.xpr/.bd/.dcp、SDK可执行镜像.elf/.bit和调试辅助脚本.tcl/.bat结构完整支持从硬件设计、软件驱动到联合调试全流程复现。已有1998人学习下载配套内容包含AXI4协议详解、Vivado ILA在线抓波实操、DDR地址映射配置说明及多层级调试日志.log/.rpt/.ltx便于读者深入理解总线机制并快速定位交互异常。 做ZYNQ开发的人迟早会碰到一个需求让PLFPGA逻辑直接读写PS端的DDR数据。这句话听起来很常规真动手时才会发现坑点一个接一个——从接口选型到地址映射从DDR training到缓存一致性任何一个环节出问题调试起来都挺费时间。这篇文章我按自己做过的验证工程来写从架构思路到具体代码再到我实际踩过的坑尽量把整个链路讲透。文章适合刚接触ZYNQ的工程师也适合已经在用PL搬数据但偶尔被“数据读回来不对”这种问题卡住的朋友。我默认你用的是Zynq-7000系列Vivado版本不同影响不大思路通用。内容主要围绕PL通过AXI接口访问PS端DDR这件事展开涉及DDR控制器、HP接口、AXI突发读写、裸机下的缓存一致性这些核心点。1. 为什么要让PL直接访问PS端DDR——架构思路与典型场景1.1 PS和PL各自管的“一亩三分地”先理清楚ZYNQ内部的分工。PS是ARM处理器系统里面带DDR控制器、UART、SPI、以太网、SDIO这些常用外设PL是可编程逻辑有LUT、FF、BRAM、DSP Slice能干并行处理和高速数据通路。问题在于DDR控制器只挂在PS里PL本身没有直接连接DDR颗粒的物理通道。PL想要访问外部DDR唯一的路径就是通过PS暴露出来的AXI接口穿过PS内部总线再走到DDR控制器。很多人一开始不理解这个架构总以为PL和DDR之间有一条“专线”。实际上PL访问DDR要经过好几道关卡PL发出的AXI请求先到PS的互联矩阵经过仲裁和地址映射再交给DDRC访问DDR颗粒。这个链路不算短但带宽其实足够用尤其是走HP口时64位数据总线加AXI突发吞吐可以做得很高。那为什么不让PL直接挂DDR颗粒成本和技术实现都不划算。DDR颗粒的物理接口对时序要求极高training、刷新、ZQ校准这些事情让PS里的硬核DDRC来做最稳妥PL里放软核DDR控制器不仅浪费资源时序收敛也麻烦。所以ZYNQ的推荐架构很明确DDR归PS管PL通过标准AXI接口来“借用”DDR。1.2 什么场景必须用PL读写DDR有些场景PL不碰DDR根本做不下去。我列举几个最常见的第一个是视频图像处理。CMOS sensor的数据从LVDS或MIPI进来PL做ISP或简单的格式转换一帧1080p图像裸数据大概3MB左右一秒钟30帧就是90MB/s这还只是没加额外处理开销的估算。PL内部BRAM通常只有几百KB到几MB根本不够当帧缓存必须把帧数据丢到DDR里等PS端做显示、编码或者进一步分析。第二个是高速数据采集记录。ADC在PL侧采样数据持续流入采样率一高数据量就失控了。比如250MSPS、16位的ADC原始数据率就到了500MB/s即便做了降采样数据还是要先有个大的存储缓冲区。此时PL把数据和同步头打包后用AXI突发写到DDR的环形缓冲区PS端软件再批量读取处理这是很典型的DDR记录仪架构。第三个是AMP异构运行。Zynq上跑Linux加裸机或者两个独立系统共享数据时DDR共享内存几乎是必选项。ARM0跑LinuxARM1跑裸机两边各自不能直接访问对方内存空间但通过DDR上一块约定好的shared memoryPL或任一CPU都可以往里面放数据然后通过中断通知对方。PL在这个模型里可以充当数据搬运工或者加速计算单元。1.3 三种访问路径的选型HP、ACP还是GPPS端给PL提供了几种AXI接口对应的访问路径不一样性能和应用场景差别很大。直接说结论大批量数据搬运优先选S_AXI_HP需要和CPU缓存保持一致可以考虑S_AXI_ACPGP口只适合传控制信号和一些小数据量访问。接口数据位宽特点适合场景S_AXI_GP32位无FIFO缓冲延迟高带宽低寄存器级读写、少量控制交互S_AXI_HP64位可配32位带读/写FIFO直通DDRC带宽高PL与DDR之间大量数据搬运S_AXI_ACP64位走一致性路径可维护CPU Cache需要和CPU侧缓存数据保持一致性的应用HP口是我用的最多的。它内部带了独立的读FIFO和写FIFOPL侧只要把AXI事务发出来数据会先进入FIFO缓冲然后由PS端DDRC调度写入DRAM。这种缓冲设计让PL侧的时序压力小了很多即使PL侧AXI主设备偶尔有气泡也不会直接影响DDR效率。ACP口听起来很香理论上可以直接保持Cache一致性省掉CPU侧的flush和invalidate操作。实际用下来有两类问题一是ACP走的一致性路径带宽受限于L2 Cache总线不一定比HP高二是一致性维护涉及硬件缓存一致性协议行为调试起来比较费劲。我的实践是如果真需要一致性就在软件层用Cache操作解决把接口留给HP口简单直接。2. 硬件底子看懂DDR控制器、AXI接口与地址映射2.1 PS端DDR控制器和DDR training到底在做什么PS里的DDR控制器简称DDRC它在ZYNQ启动过程中承担一个重要任务DDR training。上电复位后BootROM会运行一段初始化代码按FSBL阶段提前配置好的DDR参数去初始化DDR颗粒这包括设置模式寄存器、校准DQ与DQS的相位关系、调整片内终结ODT、执行ZQ校准等。如果DDR training没过启动会卡在初始化阶段表现就是串口没输出或者FSBL日志停在DDR init。为什么DIY板卡经常在这里挂掉因为Vivado里选择的DDR颗粒模型必须和板级实际颗粒一致包括DDR3还是DDR3L、位宽是16位还是32位、总容量多大、工作频率多少。参数不匹配训练时序就跑不对。另外要注意DDR的引脚约束在Zynq-7000上是固定的不需要写XDC管脚约束但硬件上DDR颗粒必须连接到PS专用DDR引脚区域。PCB布线走线等长这些事不是软件能绕过去的。遇到DDR training失败先查硬件就对了。2.2 AXI总线的实用概念握手、突发、数据位宽PL要通过AXI接口访问DDRAXI协议的一些基础概念必须得懂否则写出来的Master模块连模拟器那关都过不了。AXI4是五通道结构写地址AW、写数据W、写响应B、读地址AR、读数据R。每个通道都有VALID和READY信号二者的握手规则是当VALID为高且READY为高时传输发生。所以状态机里最常写的就是“拉高AWVALID等AWREADY拉高然后一起撤下”这个握手动作。突发传输是AXI的关键特性。一次突发包含多拍数据传输AWLEN定义拍数减1AWSIZE定义每拍字节数AWBURST定义地址递增模式。举个例子AWLEN15表示16拍AWSIZE3表示8字节64位那么一次突发总共传输128字节地址按8字节递增。AXI3协议规定的最大突发长度是16拍这在Zynq-7000的HP口上完全适配。数据位宽也直接影响地址对齐的写法。如果AXI数据总线是64位那么地址的低3位通常是0因为每拍访问8字节一次16拍的突发要传128字节地址要按128字节对齐否则容易触发AXI协议上的一些边界行为。我建议在实际代码里做地址对齐判断简单做法就是强制把起始地址的低7位清零虽然会浪费少量空间但逻辑简单可靠。2.3 地址映射从PL看DDR长什么样我第一次做PL读写DDR时犯了一个典型错误自以为DDR的基地址是0x00000000往这个地址写一堆数据后来发现写到了OCM。Zynq-7000的地址映射里OCM从0x00000000开始DDR则从0x00100000也就是1MB之后才开始。DDR的地址范围上限取决于板载容量和PS配置比如1GB DDR3时可访问范围是0x00100000到0x3FFFFFFF。但注意这个范围是PS视角的完整地址空间并非每个地址都能正常访问超出实际DDR容量的部分会访问异常。从PL侧看过去S_AXI_HP口默认就把地址映射到了PS的DDR空间。实际开发时Vivado的Address Editor会展示PL侧主接口与PS端地址空间的映射关系。你在自定义AXI Master里发出的地址就是PS视角下的物理地址这个和裸机程序里ARM访问DDR的地址是同一套。如果PS侧C代码里操作的是0x20000000PL侧Master也应该发0x20000000两边才能对上。有一个容易忽略的点Vivado默认情况下PL侧AXI Master发出的高地址部分会被地址译码器裁掉。比如HP口物理地址范围是0x00000000到0x7FFFFFFF但实际可用的DDR区只占中间一段访问DDR范围之外的区域可能导致AXI传输无响应卡死在等待READY上。所以我建议地址范围以DDR实际容量为准别往边界上撞。3. 从零搭建一个PL读写DDR的验证工程3.1 在Vivado中创建Block Design并配置HP接口先说一下工程总体结构。我习惯在纯PL侧构建一个简单的AXI Master测试模块通过它向PS端DDR写入特定模式的数据写完以后PS端校验反过来也可以由PS写入数据PL读回并比对。整个过程用AXI GPIO做触发和状态查询不需要额外串口调试。第一步Vivado里新建工程创建Block Design加入Zynq 7000 PS IP勾选DDR配置选择与实际板卡匹配的DDR颗粒型号把UART打开备用。运行Block Automation后PS的外设会自动连接好此时还需要手动开启PS-PL接口中的S_AXI_HP0接口数据位宽选择64位。这里有个细节HP口的数据位宽在Zynq-7000上是可配置成32位或64位的但为了带宽直接选64位后面写Verilog时也按64位来设计。第二步加入AXI GPIO它的用途是接收PL侧测试模块返回的中断或标志同时给PL侧发送启动信号。配置为两个通道一个输出通道用于发start一个输入通道用于读done。Zynq-7000的AXI GPIO是挂在普通GP口上读写速度不快但做触发和查询足够。第三步把自定义AXI Master模块的AXI接口连到S_AXI_HP0上时钟接FCLK_CLK0复位接外设复位。打开Address Editor给自定义AXI Master分配地址空间。这一步最容易踩坑默认如果分配到了0x00000000之类的地方DDR访问就会出问题我一般直接分配到一个明确的DDR地址比如0x20000000。3.2 自写AXI Master状态机读、写、校验一体我习惯用一个简单的AXI Master状态机完成DDR读写验证。这个模块不追求高性能重点是协议正确、结构清晰方便后续扩展成真正的数据搬运逻辑。核心状态包括IDLE、写地址、写数据、写响应、读地址、读数据、DONE这几个状态。模块参数可以这样定义module axi_master_tester #( parameter AXI_ADDR_WIDTH 32, parameter AXI_DATA_WIDTH 64, parameter LAST_ADDR 32h2010_0000 // 结束地址 )( input wire ACLK, input wire ARESETN, input wire start, output reg done, // 写地址通道 output reg [AXI_ADDR_WIDTH-1:0] AWADDR, output reg [2:0] AWLEN, output reg [1:0] AWSIZE, output reg [1:0] AWBURST, output reg AWVALID, input wire AWREADY, // 写数据通道 output reg [AXI_DATA_WIDTH-1:0] WDATA, output reg [AXI_DATA_WIDTH/8-1:0] WSTRB, output reg WLAST, output reg WVALID, input wire WREADY, // 写响应通道 input wire [1:0] BRESP, input wire BVALID, output reg BREADY, // 读地址通道 output reg [AXI_ADDR_WIDTH-1:0] ARADDR, output reg [2:0] ARLEN, output reg [1:0] ARSIZE, output reg [1:0] ARBURST, output reg ARVALID, input wire ARREADY, // 读数据通道 input wire [AXI_DATA_WIDTH-1:0] RDATA, input wire [1:0] RRESP, input wire RLAST, input wire RVALID, output reg RREADY );状态机里写事务的逻辑可以概括为IDLE状态下检测到start信号准备写地址发送AWADDR和AWLEN等待AWREADY握手成功后进入写数据状态逐拍发送WDATA最后一拍置WLAST全部发完后等待写响应BVALID确认BRESP为OKAY后跳转下一步。写数据模式下我常用递增计数生成伪随机数据低32位等于当前地址值加上一个固定key高32位做反转或者加密组合。这样PS端校验时能清楚看出数据是否错位、地址是否偏移。读事务类似先发ARADDR和ARLEN等待ARREADY握手成功后进入读数据状态逐拍读取RVALID和RLAST把数据收下来。为了验证PL侧本地会做一个数据的实时比如果比对不通过就拉高error标志否则读完指定长度后拉高done。3.3 PS侧C程序启动PL并做最终校验PS侧程序我用Vitis的裸机BSP。整体思路很简单准备一段DDR缓存区然后把控制GPIO拉高让PL开始发起写DDR操作等done之后再读取DDR内容并比对。关键点是缓存一致性。ARM Cortex-A9带L1/L2 CacheCPU读DDR时读到的可能是Cache里的旧数据而不是PL刚写到物理DDR的内容。所以在PL写完之后CPU读之前必须先调用Xil_DCacheInvalidateRange反过来如果是CPU先写数据PL再去读则写完后要调用Xil_DCacheFlushRange把Cache内容刷到DDR。核心代码大致这样#include xil_io.h #include xil_cache.h #include xparameters.h #define GPIO_CTRL XPAR_AXI_GPIO_0_BASEADDR #define DDR_TEST_BASE 0x20000000 #define DDR_TEST_SIZE (16 * 1024) // 16KB static unsigned int compute_expected(unsigned int addr_off) { return addr_off 0x5A5A0000; } int main(void) { volatile unsigned int *buf (volatile unsigned int *)DDR_TEST_BASE; unsigned int offset, errors 0; // 1. 启动PL侧写事务 Xil_Out32(GPIO_CTRL, 0x01); // 2. 等待PL完成 while ((Xil_In32(GPIO_CTRL 4) 0x01) 0); // 3. 读之前Invalidate Cache Xil_DCacheInvalidateRange((UINTPTR)DDR_TEST_BASE, DDR_TEST_SIZE); // 4. 校验数据 for (offset 0; offset DDR_TEST_SIZE / 4; offset) { if (buf[offset] ! compute_expected(offset * 4)) { errors; } } if (errors 0) xil_printf(DDR write verify PASS\n); else xil_printf(DDR write verify FAIL, errors%d\n, errors); return 0; }如果是验证PL读DDR流程反过来CPU先写一段数据Flush Cache然后给PL发读启动信号PL把DDR数据读回来并内部比对最后通过GPIO返回结果。这两种模式合起来就是一个完整的双向DDR验证工程。3.4 工程跑通后的检查清单整个工程跑通之后我每次换板子或换DDR配置时都会走一遍这个检查流程确认DDR颗粒型号、位宽、容量在Vivado里配置正确DDRC频率和DDR颗粒额定频率一致。确认S_AXI_HP0接口已使能数据位宽和PL侧Master总线位宽一致。确认Address Editor里PL主接口的地址分配落在实际DDR范围内不落在OCM或保留地址区。确认PL侧复位信号有效AXI总线的时钟频率不要超过PS端允许的最大频率。确认PS侧访问DDR时缓存操作正确写后Flush读前Invalidate。4. 踩坑实录从DDR training到数据错位4.1 DDR training失败时的排查顺序DDR training失败是ZYNQ开发里最让人头大的问题之一因为很多时候日志信息极其有限甚至串口只打印几行就停了。我遇到过的现象有几种FSBL日志停在DDR init步骤、完全没有串口输出、连接JTAG时调试器读不到DDR空间。处理顺序我建议按“硬件检查→配置检查→降频验证→独立测试”的步骤来。先量一下DDR供电VCCDDR、VCCIO_MIO0这些电源是否到位。Zynq-7000的DDR供电要求挺严格电压偏差过大直接导致training失败。接着核对DDR颗粒的ID信息比如美光的MT41K256M16Vivado里有没有选成另一家型号位宽是否匹配DDR3和DDR3L混用也会出问题。然后试着降频比如DDR3-1066降到DDR3-800如果降频后能启动说明是时序预算太紧大概率是PCB走线等长或者负载电容的问题。还有一种情况是板卡只有DDR3颗粒的一部分bit连到PS比如实际只接了16位但配置里选了32位training必然失败。这个可以通过对照原理图确认。4.2 读了全0、全F或数据错位怎么判断数据全0和全F是最常见的两种异常原因完全不同。全0通常是根本没写进去或者写到了一个访问异常的区域。我的排查思路是先确认地址是否落在DDR有效范围再确认AXI Master的状态机有没有卡在握手等待最后确认写数据通道有没有被拉低。可以在PL逻辑里加个计数器每完成一次写响应就加一PS端通过GPIO读出来看看是否在增加。如果计数器一直为0说明写事务压根没完成查AW握手和B响应。全F则更像总线浮空或者接口没连接好。比如AXI接口没有接到S_AXI_HP0上地址译码不到对应设备这时总线上读到的是默认值。出现全F先查Block Design连线再看Address Editor里是否给PL主接口分配了地址以及PL侧的READY信号是否始终拉低。数据错位是比较有意思的问题。66位总线传输时如果PL按32位地址模式生成数据PS端按32位读取后发现每个数的字节序不对常见于大小端不一致。另外如果AXI数据总线上有两个Master在写同一段DDR区域也会出现交错覆盖。建议动手之前先确认端到端的数据通路配置PL是64位总线PS访问时用64位还是32位类型读出来才对得上。4.3 缓存一致性问题flush与invalidate的时机缓存一致性是我在给团队做培训时反复强调的点。很多人写完PL读写DDR的代码功能上总觉得“就差最后一步”最后一步几乎都是Cache操作没做。ARM Cortex-A9的L1、L2 Cache会缓存DDR内容。PL通过HP口写DDR时数据直接写到DDR物理内存不会通知CPU的Cache失效CPU随后读这个地址时如果Cache里还留着旧数据读到的自然是旧值。这不是DDR写失败是Cache没有失效。反过来CPU先写DDR写完后数据还滞留在Cache里PL立刻去读DDR物理内存读到的是旧值因为CPU的写入还没有刷到DDR。这两类问题非常隐蔽因为单看任何一侧的寄存器访问都是正常的但跨边界时就是对不上。裸机开发下的处理方式就是调用Xil_DCacheFlushRange和Xil_DCacheInvalidateRange。要注意三个时机CPU写数据之前保证之前的脏Cache数据已经刷回DDR或者直接操作non-cacheable区域CPU写数据之后启动PL读之前必须FlushPL写完数据CPU去读之前必须Invalidate。如果测试是在一个大循环里反复做读写验证每次循环都要重新执行对应的Cache操作不能只在开头做一次。4.4 没有DDR的板子怎么玩OCM加载与DDR4降速有些开发板为了节省成本没接DDR颗粒这种板子跑FSBL时要么专门跳过DDR init要么干脆不跑FSBL直接使用OCMOn-Chip Memory。OCM是PS内部256KB的SRAM从0x00000000开始不需要DDRC训练上电就能用。用OCM来做PL与PS共享内存是可行的但要注意容量和速度。256KB装不了多少数据适合用来传递小批量的控制命令、状态参数、握手标志。PL侧访问OCM也走AXI接口地址直接指向0x00000000附近的区域即可不需要额外初始化。但OCM部分地址空间也可能有Cache参与所以读写之后同样要留意缓存一致性问题。如果你用的是Zynq UltraScale板卡DDR4颗粒频率太高导致training不稳定Vivado里可以通过降低DDRC输入时钟频率或调整DDR4时序参数来实现降速。具体在Zynq UltraScale的PS配置界面里修改DDR配置页的Memory Clock值从2400降到2133甚至1866同时保证PL侧时钟域不变。这里容易出错的点是DDRC的时钟源是从PS PLL出来的降频时要确认CLK和DDR时钟的比例关系否则训练出来频率和实际不符也一样跑不稳。关于OCM加载还有一个实用教程式的小技巧如果DDR训练不过又想快速验证PL逻辑可以把FSBL的DDR初始化这一步关闭或者改成跳过再通过OCM引导方式把程序跑起来。不过这只适合调试产品阶段DDR不可用基本等于板子设计有问题还是要回到硬件侧解决。整体做下来PL读写PS端DDR这套流程并不复杂但每个环节都可能埋着坑。我做这个验证工程时最大的体会是先把协议层搞明白再动手写代码比一边调一边猜要高效得多。尤其是AXI握手和缓存一致性这两个知识点搞透了ZYNQ开发里很多“玄学问题”其实都有明确的答案。如果你也是第一次做PL搬DDR数据的工程建议先从最小化的读写验证起步跑通了再加DMA、加中断、加缓存管理这样每一步的错都会好查很多。本文还有配套的精品资源点击获取