☰
ZYNQ7020 PS与PL通过AXI-FULL HP接口高速读写DDR3
2026/9/29 3:13:13 网站建设 项目流程

搞 ZYNQ 的人迟早会碰到一个绕不开的问题:PL 端的逻辑跑得飞快,数据要往 DDR 里放,PS 端的 CPU 又要把这些数据取出来做处理,两边怎么把数据倒腾明白?我手上这块黑金 AX7020 用的是 Xilinx ZYNQ7020,PS 侧是双核 Cortex-A9 加 DDR 控制器,PL 侧是等效 Artix-7 规模的可编程逻辑。标题里说的 AXI-FULL DDR 的 PL 与 PS 交互,说白了就是让 PL 里的自定义逻辑通过满功能的 AXI 总线,直接读写挂在 PS 上的那颗 DDR3,而不是绕远路走 BRAM 或者 FIFO 中转。这套东西做通了,PL 采集的高速数据、图像帧、ADC 采样流,就能直接甩进大容量内存,PS 端再从容地做算法、跑协议栈、送显示。写这篇东西的起因是我自己调这块板子时被 HP 接口、Cache 一致性、突发长度这几个点反复折腾,网上资料要么太碎要么语焉不详,所以把整套流程和踩过的坑整理出来,适合刚上手 ZYNQ、想把 PS 和 PL 真正打通的人参考,也适合做过 AXI-Lite 但没碰过高速搬运的兄弟。

1. 先搞懂 ZYNQ7020 里 PS 和 PL 谁管 DDR

1.1 DDR 控制器长在 PS 里,PL 只能借道

这是整个交互逻辑的地基,必须先说清楚。ZYNQ7020 这颗芯片的 DDR 控制器(DDRC)是硬核,物理上就长在 PS 侧,和双核 Cortex-A9、片上存储器 OCM、各种外设控制器做在同一片硅里。PL 侧压根没有独立的 DDR 控制器,你在 PL 里写逻辑,想访问那 512MB 的 DDR3,唯一的路径就是通过 PS 暴露出来的 AXI 从机接口,进到 PS 内部的总线互联,再落到 DDR 控制器上。

黑金 AX7020 板载的 DDR3 是两片 16 位颗粒拼成 32 位总线,容量按批次有 512MB 和 1GB 两种,我手上这块是 512MB,地址空间覆盖 0x0000_0000 到 0x1FFF_FFFF。你要做的第一件事,就是确认自己板子的实际容量和起始地址,别想当然。为什么 DDR 地址从 0 开始?因为 Zynq 的地址映射里 DDR 就被分配在最低段,OCM 在顶部的 0xFFFF_0000,中间那一大片是各种外设寄存器。这个映射关系在 Vivado 的 Address Editor 里能看得一清二楚,建议一开始就去看一眼,脑子里有个地图。

理解了这一层,你就明白为什么 PL 访问 DDR 必须依赖 PS 的接口:数据不是搬进 PL 自己的存储,而是搬进 PS 的存储,PL 只是发起方。这一点直接决定了后面所有配置都要围绕"打通 PL 到 PS DDRC 的通道"来做。

1.2 GP、HP、ACP 三种 AXI 通道,选错一个带宽差十倍

ZYNQ7020 的 PS 对外暴露了好几组 AXI 接口,名字容易搞混,我按实际用途捋一遍。

GP 口(General Purpose):包括 M_AXI_GP0/GP1 和 S_AXI_GP0/GP1。M_AXI_GP 是 PS 作为主机去访问 PL 的从机,比如 PS 配置 PL 里的寄存器,这是最常用的。S_AXI_GP 是 PL 作为主机访问 PS 的从机,走这个口也能摸到 DDR,但数据宽度只有 32 位,而且要穿过 PS 的中央互联(Central Interconnect),带宽被卡得很死,实测单口撑死几百 MB/s,还抢 CPU 的总线。拿它做寄存器配置没问题,做大数据搬运就是自找麻烦。

HP 口(High Performance):S_AXI_HP0 到 HP3 一共四个,每个数据宽度 64 位,而且这几个口是直连 DDR 控制器和 OCM 的,专门为高带宽数据搬运设计。PL 侧做视频、ADC、网络包这类流量,一律走 HP。四个口可以并行工作,但底层的 DDR 物理带宽是共享的,这点后面算带宽时会细说。

ACP 口(Accelerator Coherency Port):这个口比较特殊,它连到 PS 的 L2 Cache 和 SCU 上,专门给需要和 CPU 保持缓存一致的加速器用。PL 通过 ACP 写数据,CPU 的 Cache 会自动更新,省掉手动刷 Cache 的步骤。但 ACP 的带宽和时序特性比较挑,一般做加速器直连才用,纯粹的大容量搬运还是 HP 更稳。

结论很直白:PL 要高速读写 DDR,就用 HP 口。GP 留给寄存器,ACP 留给需要一致性的加速器。我最初图省事拿 GP 口跑数据,结果带宽上不去还莫名拖慢了 CPU,换成 HP 之后整个世界都清爽了。

1.3 为什么必须上 AXI-FULL 而不是 AXI-Lite

AXI-Lite 和 AXI-FULL 的区别,用一个类比就懂:AXI-Lite 像是"一次只能寄一个包裹的快递",每次传输都要走一遍完整的地址握手,没有突发;AXI-FULL 则是"一次可以装一车货",支持突发(Burst)传输,给一个起始地址,后面连续 N 拍数据自动跟着地址递增走,握手开销被摊薄到极致。

做寄存器读写,AXI-Lite 简单省事。但搬到几百 KB、几 MB 的数据流,每一拍都握一次手,地址通道的握手会吃掉大量时钟周期,实际带宽可能连理论值的三分之一都到不了。AXI-FULL 的突发机制就是为了解决这个:一次 Burst 最多可以连续传 16 拍(Zynq-7000 的 HP 口是 AXI3 协议,突发长度上限 16),地址只发一次,数据通道连续流动。

这里有个细节容易被忽略:ZYNQ7020 的 HP 接口在协议版本上是 AXI3,不是 AXI4。AXI3 和 AXI4 在突发长度上不同,AXI3 最大 16 拍,AXI4 最大 256 拍。很多教程张口就说"AXI-FULL 支持 256 拍突发",那是 UltraScale+ 的规格。你在 7020 上写 AXI Master,突发长度字段只有 4 位,最大就是 16,写超了会出错。不过好消息是,Vivado 里的 AXI Interconnect 会自动做 AXI3/AXI4 的协议转换,如果你用现成的 AXI4 Master IP 连到 HP 口,互联会自动把长突发拆成 16 拍一段,你只要心里有数就行。

明白这三层逻辑之后,整条技术路线就清晰了:PL 里写一个 AXI-FULL(实际是 AXI3)协议的 Master,通过 S_AXI_HP 接口连到 PS 的 DDR 控制器,PS 端软件负责地址规划和 Cache 维护。

2. Vivado 工程搭建:把 HP 通道打开

2.1 ZYNQ7 PS 配置里几个必须动的地方

新建 Vivado 工程,器件选 xc7z020clg400-2(AX7020 用的就是这颗,速度等级 -2)。思路是 Block Design 里放一个 ZYNQ7 Processing System,先跑一次 Run Block Automation,让工具把 DDR 和 FIXED_IO 引出来。

接下来双击 ZYNQ7 PS 进配置,几个页面必须挨个过:

DDR Configuration:选 DDR3,Memory Part 选和板载颗粒匹配的型号。AX7020 常用的是 MT41K256M16 或对应批次颗粒,选不对会导致初始化失败、读写全错。如果你不确定型号,宁可选 generic 手动填参数,也别瞎选。DDR 时钟频率这里,板子一般跑 DDR3-1066(时钟 533MHz),保守起见先别超频,跑通再说。

Clock Configuration:把 FCLK_CLK0 使能,频率先设 100MHz,这是给 PL 逻辑用的参考时钟。后面 HP 接口的 ACLK 也接它。想跑更高带宽可以调到 150MHz 甚至 200MHz,但前提是时序能收敛。

PS-PL Configuration → AXI HP Interface:这是重点,把 S_AXI_HP0 勾上,Data Width 保持 64 位。勾上之后 Block Design 里的 PS 模块就会多出一个 S_AXI_HP0 接口。如果你打算做多路并行搬运,可以再开 HP1、HP2,但一开始别贪多,先把一路调通。

Interrupts:如果后面要 PL 中断通知 PS,在 PS-PL Configuration 里把 PL-PS Interrupt Ports 勾上 IRQ_F2P。轮询能解决的场景可以暂时不勾。

配置完点 Validate Design,没报错再往下走。

2.2 HP 接口与时钟的连接方式

HP 接口的 ACLK 时钟怎么接,是新手最容易懵的点。ZYNQ 的 HP 端口内部有异步桥接,理论上可以用 PL 侧的任意时钟驱动它的 ACLK,但为了性能,最好让 HP 的时钟域和 PL 逻辑的时钟域一致,避免跨时钟域带来的额外延迟。

我的做法是:把 ZYNQ7 PS 输出的 FCLK_CLK0(100MHz)直接接到 AXI Interconnect 的 ACLK 上,同时这个时钟也作为整个 PL 逻辑的主时钟。如果你有独立的 PL 时钟源,也可以接自己的,只要保证 AXI 主从双方时钟正确连接。Vivado 会在 Validate 时报时钟未连接的错,看到 "ACLK" 相关的红色提示,八成就是时钟没接上。

关于 FIFO 和寄存器切片:AXI Interconnect 内部会自动插入寄存器切片来改善时序,一般不用手动加。但如果你的频率调到 150MHz 以上时序不收敛,可以手动在 HP 接口前面加一级 AXI Register Slice,把长组合路径打断,时序会好很多。

2.3 地址分配与 AXI Interconnect 的取舍

Block Design 里,PL 的 AXI Master 和 PS 的 S_AXI_HP0 之间要放一个 AXI Interconnect(新版 Vivado 里叫 AXI SmartConnect 也行)。SmartConnect 对多主机多从机的场景更友好,资源占用也优化过,但它默认要求所有接口都是 AXI4,连到 HP 的 AXI3 口时它会自动转换,用起来没问题。传统 AXI Interconnect 更"老实",配置参数多,但对 AXI3 支持更直接。我个人的习惯是单主机单从机就用 Interconnect,多主机混用就上 SmartConnect。

接好之后点 Address Editor,你会看到 PS 的 DDR 段自动分配给了这个 HP 接口,地址就是 0x0000_0000 起,范围 512MB 或 1GB。这里必须核对一遍:确保 PL 侧能访问的 DDR 地址范围覆盖了你打算用的区域。有时候工具只分配了一小段,你得手动改 Range 或者把整个 DDR 段分过去。

分配好地址,生成 Output Products,Create HDL Wrapper,然后 Add Constraints 把引脚绑好(DDR 和固定 IO 一般 PS 配置里已经处理好,不用手动绑)。综合、实现、生成比特流,第一次可以跳过实现直接跑综合看有没有语法错误。

3. PL 侧 AXI-FULL Master 的写法

3.1 五个通道的握手流程梳理

要自己写 AXI Master,先把五个独立通道的关系理顺。AXI 是全双工、通道独立的协议,写操作和读操作各走各的:

写操作三个通道:AW(地址写)用来发写地址和控制信息;W(数据写)用来发实际数据、字节选通 WSTRB 和末拍标志 WLAST;B(写响应)是从机回给主机的,告诉你这次写成功还是失败(BRESP)。

读操作两个通道:AR(地址读)发读地址;R(数据读)从机回数据,配合 RLAST 标志末拍。

每个通道都用 VALID/READY 一对信号做握手,规则很简单:VALID 由发送方拉高表示数据有效,READY 由接收方拉高表示我准备好了,两者同时为高的那个时钟上升沿,数据被正式接收。发送方拉高 VALID 后,必须等接收方 READY 才能撤,不能中途反悔。

写操作的通道依赖关系要说清楚:AXI3 允许 AW 和 W 通道的先后顺序相对灵活,但我在实现时采用的是"先发完 AW 再发 W"的保守顺序,逻辑简单不易错。B 通道的响应必须等到 AW 和 W 都完成之后才会来。读操作没有响应通道,收到 RLAST 就代表这一笔读完成了。

3.2 写通道状态机实现

下面是我实际用的写状态机骨架,Verilog 写的,简化了信号名但逻辑完整。这个 Master 做的事是:收到触发信号后,往指定基地址连续写 N 拍数据,每拍 64 位。

localparam S_IDLE = 3'd0, S_AW = 3'd1, S_W = 3'd2, S_B = 3'd3, S_DONE = 3'd4; reg [2:0] wstate; reg [3:0] wcnt; // 突发计数,AXI3 最大 16 reg [63:0] wdata_reg; always @(posedge aclk or negedge aresetn) begin if (!aresetn) begin wstate <= S_IDLE; s_axi_awvalid <= 1'b0; s_axi_wvalid <= 1'b0; s_axi_wlast <= 1'b0; wcnt <= 4'd0; end else begin case (wstate) S_IDLE: begin if (start_write) begin s_axi_awaddr <= base_addr; s_axi_awlen <= 4'd15; // 16 拍突发(AXI3 用 4 位,值=拍数-1) s_axi_awsize <= 3'd3; // 每拍 8 字节 s_axi_awburst <= 2'b01; // INCR 递增模式 s_axi_awvalid <= 1'b1; wstate <= S_AW; end end S_AW: begin if (s_axi_awvalid && s_axi_awready) begin s_axi_awvalid <= 1'b0; s_axi_wvalid <= 1'b1; wcnt <= 4'd0; wstate <= S_W; end end S_W: begin if (s_axi_wvalid && s_axi_wready) begin if (wcnt == 4'd15) begin s_axi_wlast <= 1'b1; // 最后一拍 end if (s_axi_wlast && s_axi_wready) begin s_axi_wvalid <= 1'b0; s_axi_wlast <= 1'b0; wstate <= S_B; end else begin wcnt <= wcnt + 1'b1; end // 数据源在这里更新,实际项目从 FIFO 或采集逻辑取 wdata_reg <= wdata_reg + 64'h1; end end S_B: begin s_axi_bready <= 1'b1; if (s_axi_bvalid) begin s_axi_bready <= 1'b0; wstate <= S_DONE; end end S_DONE: wstate <= S_IDLE; endcase end end

这段逻辑里有几个点值得强调。AWLEN 字段写的是拍数减一,要传 16 拍就写 15,这个减一很容易忘。AWSIZE 决定每拍多少字节,3'd3 代表 8 字节,对应 64 位数据宽度,如果 AWADDR 没按 8 字节对齐,传输会报错或者行为异常。AWBURST 用 2'b01 表示递增模式,地址随突发自动往上走,这也是搬运连续数据最常用的模式。

3.3 读通道状态机与数据拼接

读通道相对简单,只有 AR 和 R 两个通道,但数据接收阶段的拼接和缓存要处理好,不然高速读的时候数据会丢。

localparam R_IDLE = 2'd0, R_ADDR = 2'd1, R_DATA = 2'd2; always @(posedge aclk or negedge aresetn) begin if (!aresetn) begin rstate <= R_IDLE; s_axi_arvalid <= 1'b0; s_axi_rready <= 1'b0; end else begin case (rstate) R_IDLE: begin if (start_read) begin s_axi_araddr <= base_addr; s_axi_arlen <= 4'd15; s_axi_arsize <= 3'd3; s_axi_arburst <= 2'b01; s_axi_arvalid <= 1'b1; rstate <= R_ADDR; end end R_ADDR: begin if (s_axi_arvalid && s_axi_arready) begin s_axi_arvalid <= 1'b0; s_axi_rready <= 1'b1; rstate <= R_DATA; end end R_DATA: begin if (s_axi_rvalid && s_axi_rready) begin // 实时把 rdata 送进下游 FIFO 或校验逻辑 if (s_axi_rlast) begin s_axi_rready <= 1'b0; rstate <= R_IDLE; end end end endcase end end

读数据阶段的关键是RREADY 要尽早拉高,让从机有数据就往外送,不要因为下游处理慢而卡住总线。如果下游逻辑来不及消费,就在 R 通道后面加一个异步 FIFO 或者同步 FIFO 做缓冲,RREADY 根据 FIFO 的满标志动态控制,这样既不丢数据也不浪费带宽。

3.4 突发长度和 4KB 边界这两个硬约束

AXI 协议里有两条硬性约束,违反任意一条,轻则数据错,重则总线挂死。

第一条是突发长度上限。Zynq-7000 的 HP 口是 AXI3,最多 16 拍。你如果想一次搬 1KB 数据(64 位 × 128 拍),就必须拆成 8 次 16 拍的突发来做。别指望一次发 128 拍,AXI Interconnect 虽然会自动拆分,但如果你自己写 Master 直接怼 128 拍给 HP 口,行为是未定义的。

第二条更隐蔽:任何一笔突发传输都不能跨越 4KB 地址边界。4KB 是 AXI 从机地址译码的粒度,一笔突发如果从 0x1000_0FF0 开始还要连传 16 拍(8 字节 × 16 = 128 字节),终点会跑到 0x1000_1070,跨过了 0x1000_1000 这个 4KB 边界,这在 AXI 里是违规的,很多从机会直接返回 SLVERR 或者干脆丢弃。

避免的办法是在地址计算阶段就做边界检查:每笔突发的起始地址和长度算一下,如果终点跨过了 4KB 对齐线,就提前把这一笔截断,下一笔从边界处重新开始。我写的一个小函数就是干这个的,按 4KB 边界把大块搬运拆成一串合法的突发序列。这个检查逻辑虽然啰嗦,但能避免一大类诡异的地址错乱问题,非常值得写。

4. PS 侧软件配套:地址规划与 Cache 一致性

4.1 共享内存的地址怎么划

PS 端软件跑在 DDR 里,程序代码、堆栈、全局变量都在占空间。PL 通过 HP 口写 DDR 时,如果地址和你程序的数据区重叠了,那就是灾难性的——CPU 的变量被莫名覆盖,程序跑飞,还特别难查。所以第一步是划出一块"谁都不碰"的共享区域。

我的做法是在链接脚本 lscript.ld 里专门留一段。假设程序本身不大,我用 0x1000_0000 到 0x1FFF_FFFF 这一大段给 PL 做数据缓冲区,程序和数据限制在低地址区。具体操作是在 lscript.ld 里加一个自定义段:

_shared_start = 0x10000000; _shared_end = 0x1FFFFFFF;

然后在 C 代码里直接用这个绝对地址读写。进阶一点的做法是在链接脚本里定义一个 section 并分配固定地址,再用__attribute__((section(".shared_mem")))把缓冲区放进去。好处是编译器知道这块内存的存在,不会和别的变量冲突,缺点是调试时得盯着 map 文件确认地址真的对上了。

还有个更省心的选择:用 OCM。Zynq 的 OCM 地址在 0xFFFF_0000,默认就是 non-cacheable 的,PS 和 PL 都能直接访问,做小数据量的握手标志位、状态同步特别合适,省掉了所有 Cache 维护的麻烦。但 OCM 只有 256KB,大块数据还是得放 DDR。

4.2 Cache 操作:Flush 和 Invalidate 别搞反

这块是 PS-PL 数据交互里最容易翻车的地方,我单独拎出来讲。Cortex-A9 有 L1 和 L2 Cache,CPU 读写内存时,数据可能只在 Cache 里,还没写回 DDR。这就导致两个方向的坑:

PS 写、PL 读:CPU 把数据写进 buffer,此时数据可能还躺在 Cache 里是脏的(dirty),DDR 里还是旧值。PL 通过 HP 口直接读 DDR,读到的是旧数据。解决办法是 PL 去读之前,PS 先调Xil_DCacheFlushRange(addr, len),把 Cache 里的脏数据强制写回 DDR。

PL 写、PS 读:PL 把新数据写进了 DDR,但 CPU 的 Cache 里可能还缓存着这块地址的旧副本。CPU 读的时候命中 Cache,拿到的是旧数据。解决办法是 PS 读之前调Xil_DCacheInvalidateRange(addr, len),让这块地址的 Cache 行失效,CPU 下次读会重新从 DDR 取。

这两个函数用反了,或者干脆忘了调,症状都是"数据对不上",而且时好时坏特别迷惑人。我踩过的坑是:单步调试时数据是对的,一全速跑就错,原因就是断点改变了 Cache 的时序。凡是 PS 和 PL 共享的内存区,每次跨边界访问前都要老老实实做 Cache 维护。

如果实在嫌麻烦,可以在 MMU 配置里把这块共享内存设成 non-cacheable(Device 或 Strongly Ordered 属性),一劳永逸,代价是 CPU 访问这块内存会慢一些,因为每次都真的去 DDR 读。数据量大、CPU 又不频繁访问的场景,这个取舍很划算。

4.3 中断还是轮询

PL 和 PS 怎么知道对方干完活了?两种方式。

轮询:PS 端死循环读某个标志位,PL 写完了就把标志位翻转。实现最简单,不需要配置中断,适合调试阶段。坏处是浪费 CPU,而且标志位本身也要考虑 Cache 和内存屏障问题。

中断:PL 通过 IRQ_F2P 拉高中断线,PS 收到中断进服务程序处理。效率高但配置繁琐:要在 PS 配置里使能 PL-PS 中断,在 PL 里生成中断信号,在 Vitis 里注册中断处理函数、使能 GIC。适合正式产品。

我调试阶段一律先用轮询把数据通路跑通,确认读写逻辑没问题,再换成中断。别一上来就搞中断,中断本身的 bug 和数据通路的 bug 混在一起,排查会崩溃。

顺带提一句流式数据的场景:如果你的 PL 侧是视频或者传感器采样这种连续流,通常先在 PL 里把 AXI-Stream 格式的数据转成 AXI-FULL 写进 DDR(这就是常说的写帧缓存),然后用中断通知 PS"这一帧写好了",PS 再去 DDR 取。这样 PL 和 PS 解耦,各干各的,效率最高。

5. 实机联调:从波形看到带宽

5.1 先用 AXI Traffic Generator 探底

自己写的 Master 第一次上板,别急着接真实数据源。先用 Xilinx 提供的 AXI Traffic Generator IP 快速探一下通路是否打通。这个 IP 可以自动产生指定模式的 AXI 读写流量,你只要配好地址范围、突发长度、读写比例,它就能自己跑出满负荷的流量。

具体配置:在 Block Design 里加一个 AXI Traffic Generator,把它设成 Master 模式,接口连到 AXI Interconnect,地址指向 DDR 段。配置成连续写模式,数据模式选递增或者固定 pattern。生成比特流上板后,用 Vitis 跑一个简单的程序,往 DDR 某个地址先写入已知数据,然后让 Traffic Generator 去读,看读回来的数据对不对;反过来,让 Traffic Generator 写,PS 去读验证。

这一步能验证的东西很多:HP 接口通不通、地址映射对不对、时钟有没有接好、互联配置有没有问题。如果 Traffic Generator 都跑不通,那你自己的 Master 更别想跑通,先解决底层通路。

5.2 ILA 抓握手信号定位卡死

自己写的 Master 上板后,最常见的问题是状态机卡在某个状态动不了。这时候 ILA(集成逻辑分析仪)就是救命稻草。在 Vivado 里给 AXI 的握手信号加 ILA 核,重点抓这几个:AWVALID、AWREADY、WVALID、WREADY、BVALID、ARVALID、ARREADY、RVALID、RREADY,再加上你自己的状态机状态寄存器。

抓波形的判断逻辑很直接:如果 AWVALID 拉高了但 AWREADY 一直低,说明从机没准备好接收,可能是地址不合法、接口没使能、或者时钟域不对。如果 AW 握手成功了但 W 一直不握手,检查 WSTRB 和 WLAST 有没有正确设置。如果卡在等 BVALID,说明写数据发出了但从机没回响应,往往是地址越界或者触发了 SLVERR。

我第一次调试时状态机卡在 S_W 死活不动,抓波形发现 WREADY 一直是低,折腾半天才想明白是 AWLEN 配错了——我写了 4'd15 想传 16 拍,但数据源只给了 8 拍就断了,从机等不到 WLAST 就一直不拉 READY。这个坑很典型:突发长度声明了多少拍,就必须实实在在地送够多少拍数据,配合正确的 WLAST。

5.3 带宽实测数据与优化

通路跑通之后就该关心性能了。先算理论值:HP 接口 64 位宽,ACLK 用 100MHz,单口理论带宽是 64bit × 100MHz = 6.4Gbps = 800MB/s。如果 ACLK 提到 150MHz,那就是 1.2GB/s。但注意,PS DDR3 的物理总带宽是有限的,32 位 @ 533MHz DDR 大概 4.26GB/s,四个 HP 口和其他主设备(CPU、DMA)都要分这块蛋糕。

实测下来,单 HP 口用 16 拍突发连续读写,效率通常能到理论值的 60% 到 80%。影响因素排序大概是:突发长度(越长效率越高,但受 16 拍限制)、读写比例(DDR 频繁读写切换有开销,连续读或连续写效率更高)、地址是否连续(连续地址比跳跃地址效率高得多)、以及 CPU 是否在抢总线。

我做的优化有几个:把连续搬运的突发尽量用满 16 拍;读写分开做,不要交替进行;批量操作时让 PS 端 CPU 先歇着,别在搬运时跑重活。调整之后同样的数据量搬运时间缩短了差不多三分之一。想要更高带宽就得上双 HP 口并行,但前提是 DDR 总带宽还没被吃满,不然加了也白加。

6. 踩坑记录与问题速查表

6.1 常见问题对照表

下面这张表是我和身边朋友在 ZYNQ PS-PL DDR 交互里遇到过的典型问题,按现象归类,方便对号入座。

现象可能原因排查方向
PL 读写 DDR 返回 SLVERR地址越界、HP 接口未使能、时钟未连接核对 Address Editor 地址范围,检查 PS 配置里 HP 是否勾选
读回数据全是 0 或全 FF握手不完整、WSTRB 未设置、从机未响应ILA 抓握手信号,确认每拍数据都被接收
数据整体错位一两个字节地址未按数据宽度对齐、WSTRB 与数据不匹配检查 AWADDR 是否 8 字节对齐(64 位传输)
PS 读到旧数据Cache 未 invalidate读前调 Xil_DCacheInvalidateRange
PL 读到旧数据Cache 未 flushPL 读前 PS 调 Xil_DCacheFlushRange
状态机卡死不动AWLEN 配错、WLAST 未在正确拍拉高核对突发拍数与 WLAST 时序
移植到高频率后时序不收敛HP 时钟太快、组合路径过长降频验证,加 AXI Register Slice
程序莫名跑飞PL 写的 DDR 区域和程序区重叠严格划分共享内存地址,别碰程序段
仿真时 DDR 无响应缺少 DDR 模型或未接 AXI VIP仿真用 BRAM 替代,或用 Micron DDR3 模型
固化到 Flash 后跑不起来FSBL 加载地址或 DDR 初始化问题确认应用是否必须放 DDR,必要时缩小放 OCM

6.2 几条血泪心得

先说仿真这件事。很多人问 Vivado 里怎么仿 DDR 交互,其实完整仿真整条 DDR 通路很麻烦,需要 Micron 的 DDR3 Verilog 模型,速度慢、配置繁琐。我的建议是:仿真阶段只验证你的 AXI Master 逻辑本身,用一个简单的 AXI Slave 模型或者 BRAM 控制器替代 DDR,把握手时序、突发拆分、状态跳转验证清楚就够了,真正的 DDR 时序留到上板用 ILA 看。上板抓波形比跑仿真直观一百倍。

再说固化的问题。有人问用 JTAG 固化 Flash 到底需不需要 DDR。答案是不强制:Zynq 的启动流程是 BootROM 先跑,加载 FSBL,FSBL 负责初始化 DDR 等外设,然后加载应用。如果你的应用不大,FSBL 和应用都可以加载到 OCM 里跑,完全不碰 DDR。但一旦应用超过 OCM 容量(256KB),就必须初始化并使用 DDR 来存放应用。所以要不要 DDR,取决于你的应用体积,不是固定的规矩。

PCB 层面的东西也顺带提一句。板子出厂时 DDR 走线都做好了等长和阻抗控制,这部分我们改不了也不用管。但如果你自己画板,DDR 地址线和数据线的等长、分组、参考平面这些就绕不开了,那又是另一门学问,远超这篇的范围。

最后是调试心态。PS-PL 交互出问题时,现象往往很"玄学"——单步对、全速错,或者跑一会儿才错。这种时候别急着怀疑玄学,老老实实回到三件事上查:Cache 有没有维护、地址有没有越界、握手有没有漏拍。我遇到过的九成问题,最后都能归到这三类里。把 ILA 波形和 Cache 操作日志对着看,问题基本无所遁形。

我个人在实际项目里的体会是,ZYNQ 的 PS-PL 数据交互看着复杂,但骨架很清晰:PL 写个规规矩矩的 AXI Master,PS 开好 HP 口和地址,两边用共享内存加 Cache 维护对上眼,剩下的就是带宽和时序的调优。真正花时间的从来不是写代码,而是第一次把信号接对、把地址算对、把 Cache 想明白。第一次跑通之后,后面加通道、提频率、改数据格式都是水到渠成。

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

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

立即咨询