搞 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 未 flush | PL 读前 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 想明白。第一次跑通之后,后面加通道、提频率、改数据格式都是水到渠成。