简介:本资源是一套面向Zynq-7000 SoC初学者与进阶开发者的FPGA与ARM协同开发实战课程材料,聚焦PL端直接通过AXI总线读写PS端DDR这一关键交互场景,解决传统DMA方案协议复杂、灵活性不足的痛点,适用于实时数据搬运、嵌入式加速与软硬协同调试等典型应用。压缩包含1284个文件,总计47.84MB,涵盖308个C/C++源码(.c/.h)、74个Verilog模块(.v)、42个Makefile构建脚本、28个VHDL文件、13个约束文件(.xdc)及大量SDK工程文件(.elf/.bit/.hdf/.xpr)、调试脚本(.tcl/.bat)和综合日志(.log/.rpt),结构完整,支持从硬件设计、SDK配置到上板验证的全流程复现。已有1998人学习下载,资源内含可直接运行的BD工程(hdmi_out.bd)、Xilinx官方库(libxil.a)、多版本编译脚本(runme.bat)及综合完成标记文件,便于快速定位关键路径、理解AXI4地址映射机制与Vivado联合调试方法。 做ZYNQ开发的人迟早会遇到这样一件事:FPGA侧(PL)要跨过PS端去读写DDR3/DDR4里的数据。比如摄像头采集进来一帧图像,如果先放到PS端DDR里做缓存,再让ARM核跑算法处理,就会涉及PL读写PS DDR这个核心环节。这个功能在很多项目里都是绕不开的坎:视频采集、软件无线电、高速数据采集、AXI DMA搬运、Linux与FPGA共享内存,全都要靠它。
这篇内容我打算从工程角度完整走一遍PL读写PS端DDR的实现路径。要搞清楚的几个关键点:为什么PL要去读写PS的DDR而不是自己挂一块存储,AXI_HP口和AXI_GP口到底怎么选,Block Design里怎么连线、地址怎么分配,PL端的AXI4读写状态机怎么写,最后怎么验证和排查问题。不管你是刚接触ZYNQ的入门者,还是已经在做视频/通信项目的开发者,这篇文章都值得收藏照着抄。
1. 项目概述:为什么PL要读写PS端的DDR
1.1 PL和PS的“共享地盘”到底指什么
ZYNQ芯片内部有两个世界:PS(Processing System)是ARM处理器那一侧,PL(Programmable Logic)是FPGA逻辑那一侧。PS侧集成了DDR控制器,所以DDR颗粒是挂在PS上的,PL侧本身没有独立的DDR控制器,也没有办法直接“拉根线”访问DDR颗粒。PL要想读写DDR数据,必须通过PS侧暴露出来的AXI互联接口,也就是S_AXI_HP、S_AXI_ACP这些从机接口发请求进去,由PS内部的DDR控制器帮PL完成对DDR颗粒的最终访问。
换句话说,PL读写PS端DDR,本质上是PL作为AXI总线的Master,PS的HP口作为Slave,两者通过AXI协议完成数据交换。这个机制是ZYNQ架构里最常用、也是最高效的数据通路之一。
1.2 哪些场景必须用这条路
最典型的是图像视频链路。比如MIPI摄像头通过Sensor接口进来,数据先进入PL侧逻辑,然后要送到PS端DDR做一帧缓存。等到ARM侧需要做图像处理,或者编码器要读数据的时候,再通过某种方式把帧数据拿出来。
另一个常见场景是软硬件协同处理。PS端跑Linux系统,PL端跑高速数据采集逻辑,采过来的数据要送给Linux里的应用程序做分析。PL先把数据写到DDR里的共享缓冲区,应用层再从缓冲区读取,两边通过DDR做“共享内存”,比PL直接往PS寄存器里塞数据的效率高好几个数量级。
还有软件无线电、通信基带处理这类场景,IQ数据吞吐量大、实时性要求高,PS端CPU根本来不及逐包转发,只能靠PL直接把数据流转到DDR。再比如异构AMP架构里,Linux核和裸机核共享同一片DDR内存,也需要先把数据通路打通。
1.3 为什么优先选AXI_HP而不是AXI_GP
ZYNQ的PS侧对外开放了几个AXI接口,用途完全不同。AXI_GP是通用目的接口,32位数据宽度,时钟频率通常100MHz,带宽比较有限,而且它要经过CPU互联矩阵,适合用来做寄存器级别的访问,比如PS配置PL里的控制寄存器。AXI_HP是高带宽接口,64位数据位宽,直连DDR控制器,有独立的读写通道,用于大数据量搬运,正是PL读写DDR的专用通道。
我见过不少新手第一次做PL访问DDR时,随手把AXI Master接到了AXI_GP口上,结果跑起来带宽只有一两百MB/s,还经常有其他硬件资源抢总线的问题。换成AXI_HP口之后,带宽翻了数倍。记住这个结论:PL需要批量读写DDR的时候,优先选S_AXI_HP0到S_AXI_HP3这4个口,只有在访问少量控制寄存器时才用AXI_GP。
2. 硬件搭建与关键配置
2.1 Block Design里怎么接线
在Vivado里做ZYNQ工程时,Block Design中的连线并不复杂。核心是添加ZYNQ7 Processing System IP,然后在它的PS-PL Configuration页面里打开S AXI HP0接口(或者根据需求打开HP0到HP3中的任意几个)。这里有个容易绕晕的点:HP口在PS端是Slave接口,但在Block Design中它呈现给PL侧的是一个可以供PL作为Master连接的总线口。
之后用AXI SmartConnect把PL侧的Master总线和PS侧的S_AXI_HP0连起来。SmartConnect负责时钟域转换、位宽转换、协议转换,是ZYNQ里连接AXI主从设备的标配IP。如果PL侧原来就是AXI4接口,建议直接在SmartConnect里设置保持AXI4协议,不要降级成AXI4-Lite,否则不支持突发传输,DDR读写效率会大打折扣。
时钟和复位也要接好。PL侧访问DDR的AXI时钟一般取PS输出的FCLK_CLK0或FCLK_CLK1,频率通常配置为100MHz到200MHz之间。复位信号建议经过Proc Sys Reset IP同步后使用,不要直接用PS_RST引脚上的电平去复位PL逻辑,因为不同时钟域需要做异步复位同步释放处理。
2.2 地址映射是谁来决定的
地址映射是整个过程中最容易出错也最需要先做对的一步。PL通过AXI总线发起地址请求时,这个地址是PL视角的地址,PS端收到请求后,会根据Address Editor中的映射关系把它路由到DDR地址空间。
在Vivado的Address Editor里,你需要给PL的AXI Master分配一个地址段。比如PS的DDR范围是0x00100000到0x3FFFFFFF,你可以在Address Editor里给PL Master分配0x10000000到0x1FFFFFFF这段,那么PL发起0x10000000地址的读请求,最终访问的就是物理DDR的0x10000000地址。这个地址空间分配好之后,PS端ARM核在写软件时也要用同一个物理地址。
实际操作中我建议把DDR空间分成几个大块,比如两路视频帧缓存各占一块,共享数据缓冲区占一块,每块固定基地址,这样调试时在SDK里用内存查看器或xsdb命令检查数据非常直观。
2.3 带宽预算到底够不够
做设计之前最好先算一笔带宽账,避免方案做到一半发现性能根本不够用。
假设DDR3颗粒是1066MT/s、物理位宽64bit,DDR的理论带宽约8.5GB/s,但实际可用带宽受到刷新开销、bank切换、读写切换等影响,通常按60%到70%估算,也就是5到6GB/s。PL侧AXI总线如果跑150MHz、数据位宽64bit,理论带宽是150M乘以8字节等于1.2GB/s。如果AXI数据位宽提升到128bit,理论带宽就能到2.4GB/s。
以1080p60的RGB888图像为例,一帧数据约1920乘1080乘3字节约6.2MB,60帧就是372MB/s,PL只需要持续写入DDR,1.2GB/s的AXI带宽是足够的。但如果要做两路4K视频,数据量就接近1.5GB/s,这个时候就必须考虑提高AXI时钟频率、扩展128bit位宽,或者用多HP口分摊带宽。
3. AXI总线与读写逻辑设计
3.1 AXI4接口信号怎么拆
PL侧读写DDR,写的是一段RTL逻辑,核心是AXI4 Full Master接口。AXI4 Full支持突发传输,想高效访问DDR必须用它。接口信号分成5个通道:写地址通道AW、写数据通道W、写响应通道B、读地址通道AR、读数据通道R。
每个通道都是独立的握手关系,用VALID和READY两个信号来同步。发送方拉高VALID表示数据或地址已经准备好,接收方拉高READY表示可以接收,只有两者同时拉高且时钟上升沿到来时,才算完成一次传输。这就像两个人交接箱子,一个人说“我递过来了”,另一个人说“我接得住”,两边都确认的那一刻箱子才真正到对方手上。
3.2 写DDR的完整事务过程
写一个完整的AXI4写事务,步骤是:先在AW通道发送起始地址,并携带突发长度信息AWLEN,表示这次要连续写多少个数据。然后在W通道逐拍发送数据,每一拍对应一个数据,最后一个数据时要把WLAST拉高,告诉从设备这是本次突发的最后一笔。从设备接收完所有数据后,会在B通道返回一个写响应,表示写入完成。
一个新手容易犯的错误是等W通道全部发完了才发AW地址,或者反过来等AW握手完了才发W数据。AXI协议允许AW通道和W通道同时进行,不需要等一个完成再开始另一个。为了减少延时,建议在同一拍同时拉高AWVALID和WVALID,这样数据流和地址流可以并行进入从设备,整体时序更紧凑。
写状态机最简单的方式是五态:IDLE、ADDR、DATA、WAIT_BRESP、完成。有些设计为了简化,甚至不用单独设置ADDR和DATA状态,直接在IDLE里拉高AWVALID和WVALID同时进入DATA状态,然后在DATA状态等待AWREADY、WREADY和BVALID,每拍记录进度。不过从可读性和调试便利性上考虑,我还是习惯把状态拆清楚。
3.3 读DDR的完整事务过程
读事务更直接:先在AR通道发送读地址和突发长度,从设备收到后会把数据通过R通道一拍拍返回。读方向没有独立的响应通道,R通道本身既传数据也传传输状态,最后一拍数据上RLAST为高,同时RRESP表示读取是否成功。
读状态机在发出AR地址后,进入等待数据状态。每一拍拉高RREADY,表示当前能接收数据。当RVALID和RREADY同时为高时,取走数据并累加数据计数,当RLAST为高且计数等于突发长度时,本次读事务结束。
这里有个细节:RVALID和RLAST是同时有效的,所以判断最后一笔数据不要只看RLAST,还要确认这一拍确实完成了握手。有的同学只判断RLAST为高就直接结束状态机,但那一拍其实可能还没被从设备认为是有效传输,会导致丢最后一个数据。
3.4 数据总线位宽、字节序和对齐问题
AXI总线是靠字节通道来区分数据的。64bit位宽下,数据总线有8个字节lane,地址最低3位对应字节选择。你从PL侧发0x1122334455667788这个64bit数据到地址0x10000000,它占据的是0x10000000到0x10000007这8个字节地址。如果PS端软件按0x10000000地址读一个uint64_t变量,拿到的恰好就是这个值。
但如果PL先把一个32bit的0x11223344写到地址0x10000002,PS端按32bit读这个地址,就可能因为字节偏移得到错位值。这种情况不是硬件问题,而是字节序理解不一致。建议两边约定好数据结构,尽量用同样位宽、同样对齐方式访问同一片缓冲区,或者统一在软件和逻辑里做字节交换处理。
关于对齐,AXI4有一个硬性规则:一个突发不能跨越4KB地址边界。也就是说,如果起始地址的低12位加上这次突发覆盖的字节数超过了4096,必须把这次传输拆成两笔。对绝大多数DDR访问来说,只要把缓冲区起始地址按4KB对齐,突发长度控制在128或256之内,基本上不会触发跨边界问题。
4. 验证方法与问题排查
4.1 最实用的回环测试方案
在工程搭建完成、RTL写完之后,不要急着接真实数据流,先做一个简单的回环测试。最常用也最容易定位问题的方式:让PS端往指定DDR地址写一批已知数据,然后让PL端从同一地址读出来做比对,或者反向操作。
我在实际项目里习惯这样做:先在SDK里用Xil_DCacheFlush()刷新Cache,再用memcpy或直接写指针往地址0x10000000写入256个64bit的递增数据,然后触发PL开始读。PL侧RTL里设计一个简单的比对器,读一个数据就和期望值比较一次,读到不一致时把错误标志寄存器和错误地址寄存器记录下来。PS端通过AXI_GP访问这些状态寄存器,就能判断PL读DDR是否正常。
反过来测试PL写DDR也一样:PL用固定递增序列写地址0x20000000开始的256个数据,写完之后PS端读回来检查。这个测试如果通过,说明从AXI握手到DDR控制器转发这一整条链路没有问题。
4.2 ILA抓波形时重点看什么
如果回环测试失败,用ILA(集成逻辑分析仪)抓内部信号是定位问题的最直接手段。把AXI接口上的awvalid、awready、wvalid、wready、wlast、bvalid、bready,以及读方向的arvalid、arready、rvalid、rready、rlast这些核心信号全部挂到ILA上。
触发条件建议先设置成awvalid上升沿或者idle状态跳转到第一个状态,这样能看到一笔写事务从发起地址到收到B响应的完整过程。重点观察:AWREADY和WREADY有没有正常拉高,如果某个READY一直不拉高,说明从设备根本没准备好,多半是SmartConnect配置问题或者时钟复位没对齐。还有BVALID是否及时返回,如果状态机一直在等待B响应,说明写响应通道出了问题。
抓波形时注意采样深度配足,特别是在连续突发模式下,如果深度太浅可能只抓到前几拍,看不出问题。建议深度至少设成16384。
4.3 现场排查经验速查表
有些问题在调试中反复出现,我把它们整理成一张表,方便你遇到现象时直接对照排查:
| 现象 | 排查方向 | 常见解决方法 |
|---|---|---|
| DDR初始化失败,启动或下载程序卡住 | PS端DDR参数配置、实际颗粒型号、焊接 | 检查Vivado里DDR型号/速度等级是否与实际颗粒一致,查焊接和电源 |
| PL读写事务一直卡在等待READY | AXI时钟/复位、SmartConnect映射 | 确认FCLK_CLK0频率,检查Processor System Reset的输出,查Address Editor映射 |
| 读回来的数据全0或全F | 地址没映射到DDR、DDR未初始化 | 先用SDK的mrd命令验证DDR可读写,再看Address Editor |
| 数据错位、高字节跑到底字节 | 位宽或字节序不一致 | 统一访问宽度,必要时做字节交换处理 |
| 偶发丢数 | 没等B响应就发下一笔、FIFO溢出 | 严格按AXI握手顺序,加深FIFO并留空满余量 |
| 能跑但带宽远低于预期 | 突发长度太短、没有用outstanding | 把突发长度调到64以上,支持多笔未完成事务 |
4.4 Cache一致性问题是最隐蔽的坑
有一个坑是新手最容易踩但最难发现:Cache一致性。PS端CPU在写DDR缓冲区的时候,数据先被写进CPU的Cache里,可能还没有真正落到DDR物理存储介质上。这时候PL直接通过AXI读DDR物理地址,读到的可能是旧数据,而不是CPU刚写入的新内容。
反过来也一样,PL写完DDR后,PS端CPU如果Cache里缓存了这块地址的旧数据,CPU读到的可能是旧的Cache内容。
解决办法是:PS端写入后、PL读取前,调用Xil_DCacheFlush()把Cache内容刷到DDR。PL写完DDR后、PS读取前,调用Xil_DCacheInvalidate()把Cache里的旧数据作废。在Linux环境下,如果使用DMA缓冲区,应该用dma_alloc_coherent分配一致性内存,或者通过设备树配置DMA的内存属性,避免Cache问题。
5. 性能优化与扩展玩法
5.1 带宽上不去的三个核心原因
调试中如果发现实测带宽远低于理论值,优先检查三件事。
第一是突发长度设置。AXI4的突发长度可以到256,但有的初学者写RTL时习惯把所有事务都做成单笔突发,比如一次只写8个数据。每发起一笔突发都要额外占用地址通道时间,吞吐率自然上不去。把突发长度拉长到64、128或者256,带宽提升会非常明显。
第二是没有利用多笔未完成事务机制。AXI协议允许Master在还没有收到第一笔B响应或最后一笔R数据时,就发起第二笔地址请求,这叫outstanding。如果一笔一笔串行等回复,总线空闲时间太多。我的做法是在读写FIFO里预存多个请求,让状态机连续发送多笔地址和数据,实测串行写只有500MB/s左右,把outstanding深度做到8之后能到1.1GB/s。
第三是AXI接口时钟和数据位宽没有优化。如果PL侧AXI总线只跑了100MHz、64bit,峰值就是800MB/s,扣掉协议开销实际能用的更少。把时钟提到150MHz甚至200MHz,或者把位宽扩展到128bit,是直接有效的办法。当然要评估PS侧DDR控制器的整体带宽上限,不要盲目堆参数。
5.2 AXI_HP口的ID信号怎么处理
S_AXI_HP接口的ID信号在ZYNQ里值得注意。多个AXI Master同时访问同一个HP口时,ID用于区分不同事务。使用SmartConnect连接时,它会自动分配和转发ID,一般情况下你不需要额外处理。但如果设计中PL侧有多个Master,比如一个VDMA、一个自己写的AXI Master、一个HLS模块,它们同时访问DDR,ID位宽分配不合理会导致事务乱序。
最简单的规避办法是给每个Master通过独立的SmartConnect连接到不同的HP口,HP0、HP1、HP2各接一个。这样天然隔离了ID域,不会相互干扰。如果必须多个Master共享一个HP口,就要仔细检查SmartConnect里ID宽度配置,确保足够容纳所有并发事务。
5.3 可以继续扩展的方向
这个功能打通之后,能扩展的方向很多。
第一个方向是接入Xilinx官方VDMA IP。VDMA内部就是封装好的AXI Master,专门用于把PL侧的视频流数据写到DDR,再从DDR读出来播放。它比自己写AXI Master更加成熟,适合视频项目快速落地。但自己写过一遍AXI Master后,再看VDMA的寄存器配置会通透很多。
第二个方向是在Linux下用/dev/mem直接访问共享缓冲区。PL写完DDR后,Linux应用层可以通过mmap映射0x10000000这个物理地址,直接读取数据。这套方案适合快速验证,但不适合对性能有极致要求的场景,因为普通进程的页面缓存和内存管理会带来额外开销。
第三个方向是AMP异构架构。Linux跑在CPU0,裸机程序跑在CPU1,两边的共享内存放在DDR固定地址,PL侧也可以参与搬运数据,实现真正的三端数据交互。常见做法是CPU1跑实时任务,CPU0跑管理任务,PL做数据预处理,三者通过DDR缓冲区交换数据。
第四个方向是HLS封装。如果你不想手写完整AXI时序,可以把核心逻辑用HLS写成C函数,顶层接口定义成m_axi类型,HLS自动生成AXI Master逻辑。Vivado HLS生成的代码虽然效率略逊于精心调优的RTL,但开发速度快很多,适合算法验证阶段。
5.4 一个关于OCM的组合技巧
如果你的ZYNQ板上没有DDR,只有一个OCM(片上内存),也可以通过AXI接口让PL访问OCM。OCM在PS端的地址大概在0xFFFC0000附近,容量通常只有256KB。PL可以通过AXI_GP口访问OCM,虽然带宽和容量都不大,但适合存放一些启动代码、标志位或者小规模共享数据。这在“无DDR的ZYNQ使用OCM加载”这类场景中很常用。
我做过一个项目,板子为了成本没有外挂DDR,PS端程序运行在OCM里,PL端通过AXI访问OCM里的一块共享数据区,用来给PS端传递采集状态和参数。虽然OCM不方便做缓存数据,但做控制信息交互是完全可行的。
6. 几个值得反复琢磨的细节
最后说几个我在实际调试中摸出来的经验。
Debug的时候我一般按“物理层、链路层、交互层”三层来排。物理层先确认DDR能正常初始化,PS不跑任何程序只做一条mrd指令,发现地址能读回数据就说明DDR在物理上是好的。链路层看AXI握手信号,用ILA确认AW、W、B、AR、R每个通道都正确完成握手。交互层再看数据内容,优先用递增序列做比对,因为递增数据能很快定位到哪一拍、哪个字节出了问题。
还有一点,AXI总线的调试先上小数据量,比如一次突发16个数据,先把全流程调通,再逐步加大突发长度和outstanding深度。不要一上来就配256深度、128bit位宽,一旦出问题,很难判断到底是哪个参数导致的。
另外,从PS侧用SDK访问DDR时,不要忘记在读取PL写入的数据前执行Xil_DCacheInvalidate(),在写入数据给PL读取前执行Xil_DCacheFlush()。这个操作看似简单,但很多人都会在联调阶段被它折腾一整天,我见过最典型的场景就是PL侧明明已经写了数据,PS侧读出来却全是旧值,最后发现只是Cache没有失效。
PL读写PS端DDR不是一个多高深的技术,但它串起了AXI协议、DDR控制器、地址映射、数据一致性和性能调优好几个关键点。把这套流程走通一遍,ZYNQ里最核心的数据通路就算掌握了大半。
本文还有配套的精品资源,点击获取