ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ZYNQ7020中PL用AXI-FULL访问PS DDR实战

ZYNQ7020中PL用AXI-FULL访问PS DDR实战 手上这块黑金 AX7020 在抽屉里躺了大半年前阵子接了个活儿要处理一路速率不低的数据流先在前端做个流水处理再把结果整块整块地扔进内存让 CPU 慢慢算。一开始想偷懒让 PS 端用中断加 FIFO 一包一包搬测下来 CPU 占用直接顶到 90%数据稍微快一点就丢包。后来改成让 PL 端自己通过 AXI-FULL 直接把数据写进 DDRCPU 只在前后做两次 Cache 操作和一次「开始」信号占用立刻掉到个位数。这一套东西的核心就是把 ZYNQ7020 里 PS 侧的 DDR 控制器当成 PL 侧逻辑的一个「大号共享内存」来用。关键词里那几个 —— ZYNQ7020、AXI-FULL、DDR、PL、PS —— 其实就是这条链路的全部要素PL 写一套 AXI4-FULL 主机逻辑经过 SmartConnect 接到 PS 的 S_AXI_HP 口最终落到 DDR 颗粒上PS 端再用指针加 Cache 维护跟 PL 共享同一片物理内存。这篇内容适合三类人看刚上手 ZYNQ 想搞明白 PS/PL 到底怎么分工的初学者、被 AXI 握手时序折腾过的中级开发者、以及需要在 PL 和 PS 之间搬大数据量、又不想用 DMA IP 的实战派。我下面会把带宽怎么算、地址怎么划、状态机怎么写、Cache 怎么刷、出问题怎么查一条条摊开讲代码和参数都能直接抄。1. 搞清家底AX7020 上 PS、PL 与 DDR 到底是谁的1.1 DDR 颗粒挂在哪里决定了 PL 访问它的路径很多刚接触 ZYNQ 的朋友会下意识觉得「DDR 是 PL 的资源」因为在纯 FPGA 板子上DDR 确实挂在 FPGA 的 IO Bank 上要自己写 DDR3 控制器、自己做引脚分配和时序收敛。但 ZYNQ 完全是另一回事AX7020 上那两片 16bit 的 DDR3 颗粒物理走线是连到 PS 侧的专用 DDR 控制器引脚上的PL 侧根本没有直连 DDR 的引脚。这个事实带来的第一个结论是PL 想访问 DDR唯一的路径就是穿过 PS 的 AXI 互联和 DDR 控制器。PL 扮演的是一个「AXI 主机」PS 的 DDR 控制器扮演「AXI 从机」中间的路由工作由 PS 内部的互联矩阵完成。我们做的所有配置本质上都是在给这条路径开权限、配参数、划地址。第二个结论对硬件工程师更友好DDR 的地址线、数据线等长走线拓扑、阻抗控制这些 PCB 层面的活儿全都不用你操心。板厂在出板的时候已经按 DDR3-1066 的要求做完了。你只有在 PL 侧外挂独立的 DDR 颗粒也就是所谓的「PL 端 DDR」时才需要在 PCB 工具里做地址线等长、fly-by 拓扑、终端匹配这些事。AX7020 用不到所以看到别人讨论地址线等长的时候先确认一下人家说的是 PS DDR 还是 PL DDR别自己吓自己。第三个结论是资源归属DDR 的容量是 PS 的不是 PL 独有的。裸机程序跑在 PS 上它自己也要用 DDR 当堆栈、当代码段、当堆。你划给 PL 的那一段本质上是从 PS 的地址空间里割出来的一块地割多少、割在哪需要你自己规划清楚否则不是 PL 把 PS 的程序数据踩了就是 PS 的 malloc 把 PL 的缓冲区覆盖了。1.2 四种 AXI 接口选错了后面全是麻烦ZYNQ-7000 的 PS 侧对外暴露了好几类 AXI 接口我在论坛上见过太多人一上来就把自定义 IP 挂到 GP 口上然后抱怨带宽上不去。这里把它们的定位讲清楚后面选型就不会犹豫。接口类型数量数据位宽协议版本主要用途典型带宽量级M_AXI_GP232bitAXI3PS 主动访问 PL 的寄存器几十 MB/sS_AXI_GP232bitAXI3PL 主动访问 PS 的寄存器区几十 MB/sS_AXI_HP464bitAXI3支持部分 AXI4 特性PL 高速访问 DDR/OCM单口 1GB/s 级S_AXI_ACP164bitAXI3PL 与 CPU 做硬件缓存一致访问受 L2 带宽限制先说 GP 口。它是个 32bit 的窄口频率也不高通常跑 100MHz 上下。理论峰值算一下32bit 就是 4 字节100MHz 意味着每秒 1 亿次传输4×10^8 字节每秒也就是 400MB/s。但这是理论值实际上 GP 口要穿过 PS 内部的多层互联还要和 CPU、DMA、其他外设抢通道实测能跑到 50100MB/s 就算不错了。所以 GP 口的正确定位是传控制信号和状态寄存器比如你想给 PL 发一个 32bit 的起始地址、一个长度、一个 start 位用 GP 口再合适不过。但你要是想拿它搬图像、搬采样数据那就是给自己找罪受。再重点说 HP 口也就是我们这个项目的主角。S_AXI_HP0HP3 是 PS 专门为 PL 准备的高速数据通道64bit 位宽每个口内部还带一块 1KB 左右的数据 FIFO用来吸收 PL 侧和 DDR 侧的速度波动。这四个口在 PS 内部是并行接到 DDR 控制器的理论上四个口一起用可以接近吃满 DDR 带宽。而且 HP 口直连 DDR 控制器不经过 CPU 的 L1/L2 缓存——这一条既是优点也是坑优点是不会污染 CPU 的缓存坑是数据一致性要你自己管第 5 节会专门讲。最后是 ACP 口。这个口比较特殊它是从 PS 的 SCUSnoop Control Unit进来的意味着 PL 通过 ACP 访问内存时硬件会自动帮你做缓存一致性维护——CPU 缓存里的数据和你读到的数据天然一致不用手动 flush。听起来很美好但代价是 ACP 的流量要走 CPU 的 L2 缓存通道会和 CPU 自己抢带宽而且访问的数据会被塞进 L2对大数据流非常不友好。所以 ACP 的典型用法是小数据量、低延迟、对一致性要求高的场景比如 PL 侧要实时读取 CPU 刚更新的一张小的查找表。对我们这个「大块数据搬进 DDR」的需求答案很明确用 S_AXI_HP0。1.3 这套链路到底能解决什么实际问题抛开抽象的技术描述说说我这次为什么非要用它。第一类是数据采集与预处理。比如 PL 端接了 AD 或者图像传感器数据流是连续不断的如果每个采样点都通过中断丢给 CPU中断开销比数据处理本身还大。正确做法是 PL 内部先做流水线处理滤波、抽取、打包攒够一整块比如 1MB之后用 AXI-FULL 一次性突发写进 DDR 的环形缓冲区写完发一个中断告诉 CPU「第 N 块好了」CPU 再去处理。CPU 的负担从「每来一个点都要响应」变成「每块响应一次」差别是数量级的。第二类是计算加速。CPU 算一段矩阵或者做一次卷积太慢把数据丢给 PL 做结果是 PL 直接写回 DDR。这时候 PS 的角色就是「喂数据 收结果」中间的数据搬运完全由 PL 自己完成PS 甚至可以在 PL 干活的时候去睡大觉。第三类是大缓冲区的共享。有些算法需要一块几百 MB 的中间缓冲区PL 内部的 BRAM 加起来才几百 KB根本不够。这时候把 DDR 当 PL 的「外挂大内存」用是最经济的方案。2. 方案设计带宽账、地址账、时钟账先算清楚2.1 先把 DDR 侧的带宽天花板算出来动手之前先算账这是我这些年养成的习惯能省掉很多「跑起来发现慢得离谱」的返工。AX7020 板载的是两片 16bit 的 DDR3 颗粒并联成 32bit 的总位宽。核心频率 533MHzDDR 是双边沿传输所以等效数据率是 1066MT/s对应 DDR3-1066 这个规格。带宽计算很简单理论带宽 1066 × 10^6 次传输/秒 × 32bit / 8 4264 MB/s ≈ 4.2 GB/s这是理想流水线不停顿的情况。实际因为刷新、行激活、读写切换、Bank 冲突等等能拿到 70%80% 就算相当健康了也就是3.03.4 GB/s 左右。记住这个数字它是整条链路的总预算。PL、CPU、DMA、显示控制器……所有想访问 DDR 的东西都在这个池子里分。2.2 HP 口的带宽其实是被 PL 侧时钟卡住的很多人以为 DDR 有 4.2GB/sPL 就能吃到 4.2GB/s这是误解。真正卡脖子的是 PL 侧的 AXI 时钟。HP 口的数据位宽固定 64bit也就是每个时钟周期传 8 字节。这个 AXI 事务的时钟由谁来提供由你在 PS 配置里给 PL 的 FCLK 提供或者由 PL 侧自己产生再送回去HP 口支持独立时钟但一般图省事都用 FCLK。所以单口的理论上限就是PL 侧 AXI 时钟单次传输字节数理论峰值带宽占 DDR 总带宽比例100 MHz8800 MB/s约 19%150 MHz81200 MB/s约 28%200 MHz81600 MB/s约 38%这张表就是选型依据。如果你只用一个 HP 口、跑 150MHz那么你最多只能拿到 DDR 总带宽的不到三成剩下的都浪费了。想要更高要么提时钟但 200MHz 对 ZYNQ-7000 的 PL 时序已经很紧了尤其 -2 速度等级也不宽裕要么同时开多个 HP 口做并行。我自己的习惯是单个 HP 口、FCLK 给 150MHz先把逻辑跑稳实测能拿到 900MB/s 到 1.1GB/s 的持续写带宽就收工。真要更高的场景再考虑双口并行但那样地址要交错划分复杂度上一个台阶新手建议先别碰。2.3 地址怎么划给 PL 留一块「专属飞地」ZYNQ-7000 的 DDR 物理地址范围是 0x0000_0000 到 0x3FFF_FFFF对应 1GB。但注意低 1MB 区间是有特殊用途的OCM片上 256KB 内存会在低地址和高地址各做一份镜像中断向量表、FSBL 的启动代码都习惯放在那里。所以给 PL 用的缓冲区我一般从0x0010_0000 往上开始划避开最底下那一小块。我的习惯布局是这样的地址区间大小用途说明0x0000_0000 ~ 0x000F_FFFF1 MBOCM 镜像与系统保留别动0x0010_0000 ~ 0x0FFF_FFFF240 MBPS 程序段、堆栈、Linux 内核按实际系统调整0x1000_0000 ~ 0x1FFF_FFFF256 MBPL 写往 DDR 的数据区PL 侧输出缓冲0x2000_0000 ~ 0x2FFF_FFFF256 MBPL 从 DDR 读的源数据区PL 侧输入缓冲0x3000_0000 ~ 0x3FFF_FFFF256 MB预留 / 大页共享需要时再切这么划的好处是每块区域都有明确的归属Vivado 的 Address Editor 里给 S_AXI_HP0 分配地址窗口时直接填 0x1000_0000 到 0x1FFF_FFFF一眼就能看出 PL 能碰哪些地址。一旦 PL 逻辑里算错了地址比如想写 0x2000_0000 却写成了 0x0200_0000至少从地址段上能立刻判断出越界了。提示Address Editor 里分配的地址范围主要是给 Vivado 做地址冲突检查、给软件生成 xparameters.h 用的它并不构成硬件层面的访问过滤。ZYNQ-7000 的 HP 口没有地址防火墙只要地址落在 DDR 控制器的译码范围内PL 就能真的写进去。所以「别越界」这件事得靠你自己的逻辑来保证。2.4 AXI4 还是 AXI3版本转换这一步最容易出糊涂账这是我在带新人时反复强调的一点。你在 Vivado 里用向导生成的 AXI4-FULL 接口协议版本是AXI4它支持的最大突发长度是256 拍AWLEN 是 8bit取值 0255。但 ZYNQ-7000 的 S_AXI_HP 口协议版本是AXI3AXI3 的最大突发长度只有16 拍。那么问题来了你在 PL 侧写一个 256 拍的突发写中间经过 SmartConnect 的时候会发生什么答案是SmartConnect 会自动把它拆成 16 个 16 拍的传输。这是合法的、自动的行为不会报错。但它带来两个你需要知道的影响一是 PL 侧看到的「一次突发」和 DDR 侧实际发生的「十六次突发」不是一回事如果你在抓波形时发现地址跳变次数比预期多别慌这是正常的转换行为二是每次拆分会引入微小的额外开销突发被切得越碎握手协议开销的占比就越高有效带宽就越低。理解了这一层你就明白为什么突发长度要尽量拉满。一次 256 拍的写和 256 次单拍写前者的有效带宽可能是后者的三到五倍差的就是这些握手开销。注意别为了「保持 AXI4 的纯正血统」去想办法绕过 SmartConnect 直连 HP 口。HP 口是 AXI3 的你在 PL 侧生成 AXI4 主机直连Vivado 会在接口类型上给你报错或者自动插入转换逻辑反而更难排查。老老实实走 SmartConnect让它干这个脏活。3. Vivado 工程搭建从 Block Design 到一键出比特流3.1 建工程与器件型号第一步别选错打开 VivadoCreate Project一路 Next 到器件选择那一步。AX7020 上焊的是 ZYNQ-7020封装 CLG400速度等级看板子丝印常见的是 -2 也有 -1 的。我这次的工程选的是xc7z020clg400-2你自己板子上如果丝印是别的速度等级一定要对应改否则时序报告会失真跑得起来是运气跑不起来是正常。Vivado 版本方面ZYNQ-7000 是老器件了从 2015.x 到 2024.x 都还在支持列表里。但我不建议用太新的版本因为新版本把 SDK 换成了 Vitis很多老教程的界面完全对不上容易卡在找不到按钮这种低级问题上。我自己习惯用2018.3和2020.2这两个版本前者是黑金教程的配套版本后者是 SDK 和 Vitis 都能用的过渡版本。选一个装上别在版本上反复横跳。3.2 ZYNQ7 PS 的配置逐项过一遍工程建好之后Create Block Design然后 Add IP找到 ZYNQ7 Processing System 加进来。接下来双击它会弹出 Re-customize IP 的界面左边是一长串配置项很多人第一次看到会懵。我按重要性排个序你照着勾就行。第一步是 PS-PL Configuration。展开 AXI Non Secure Enablement再展开 64bit 下的 AXI HP Interface。这里你会看到 S_AXI_HP0 到 S_AXI_HP3 四个复选框勾上S_AXI_HP0。勾完之后下面会出现 HP0 的详细配置Data Width 保持64FIFO 深度我给满了也就是 1024 字节的读和写各一份。这里还有个 Enable Transaction QoS 的选项我们单口场景用不上先不勾。第二步是 Clock Configuration。展开 PL Fabric Clocks勾上 FCLK_CLK0频率填150MHz。这个时钟有三个用途给自定义 IP 的逻辑用、给 AXI 互联用、给 HP 口的 AXI 事务用。之所以选 150 而不是 100是因为 100MHz 的单口带宽只有 800MB/s有点浪费选 200MHz 的话对 -2 速度等级的布线压力偏大容易在实现阶段报时序违例。150MHz 是个比较舒服的平衡点。第三步是 DDR Configuration。这里需要填 DDR 颗粒的具体参数。最省事的做法是导入板卡预设——如果你装了黑金提供的板级支持文件可以直接在 Preset 里选对应的型号。如果没装就得手工填。我把关键的几项列出来参数项取值说明Memory TypeDDR3颗粒类型Speed1066 Mbps (533MHz)对应 DDR3-1066Memory PartMT41K256M16 或兼容单颗 4Gb16bit 位宽Data Width32 bit两片 16bit 并联Number of Banks83 根 Bank 地址线Row Address Width15 bit行地址Column Address Width10 bit列地址Page Size2048 Byte用于计算突发效率这些参数如果填错最典型的后果是内存容量识别不对比如你填成 8bit 位宽容量就少一半或者访问到某一段地址就出乱码。填完之后可以在 DDR Configuration 界面里看右边的 Summary容量对不对一眼就能核对。第四步是 Peripheral I/O Pins。至少勾上UART1MIO 48/49这是你后面打印调试信息的唯一通道绝对不能漏。如果要固化程序到 QSPI Flash把 QSPI 也勾上。要跑 Linux 的话 SD 卡也得勾。第五步是 Interrupts。展开 PL-PS Interrupt Ports勾上PL-PS Interrupt Ports 下的 IRQ_F2P。虽然我们这个纯轮询的测试用例用不上中断但留着不占资源后面扩展成「PL 写完一块通知 CPU」的时候直接接上就行省得重新配置一遍。配置完点 OK然后右键 Block Design 空白处Run Block Automation让工具把 DDR 和 FIXED_IO 这些外部接口引出来。3.3 DDR 参数的几个隐藏细节填错会很难查上面那张表是主参数但 PS 配置界面里还有一堆时序参数比如 tRCD、tRP、tRAS、CL 这些。这些参数一般跟着颗粒型号走导入预设基本不会错。但有两个地方我想单独提醒。一个是DDR 时钟与 AXI 时钟的比例关系。ZYNQ 的 DDR 控制器内部有个 4:1 或者 2:1 的模式选择影响的是 DDR 时钟和内部 AXI 时钟的比值。默认配置一般是可用的不要手贱去改改错了表现是能跑但带宽腰斩很难往这上面想。另一个是DDR 的刷新周期。高温环境下如果刷新率不够会出现偶发的位翻转。工业级应用里如果发现跑几个小时才出一次数据错除了查逻辑也要怀疑一下 DDR 的温飘问题。AX7020 是消费级定位长时间满负荷跑的时候摸一下 DDR 颗粒烫手的话就该考虑加散热片了。3.4 自定义 AXI4-FULL Master IP 的创建与封装这是整个流程里最需要手工的部分。Vivado 提供了一个向导能帮你把接口骨架和一大堆样板代码生成好非常省事。操作路径是菜单栏 Tools → Create and Package New IP选择Create a new AXI4 peripheral然后填 IP 的名字比如叫axi_ddr_master。关键的几步在接口定义那里Interface Type 选AXI4不要选 AXI4-Lite那个位宽只有 32bit没法定长突发Interface Mode 选Master默认是 Slave一定要改Data Width 填64Address Width 填32Number of Registers 填0我们不需要寄存器文件控制信号用独立的 AXI-Lite 口或者简单的 IO 端口更好点 Finish 之后IP 会以「编辑模式」打开一个临时工程里面有一个ip名_v1_0.v的顶层文件和一个ip名_v1_0_S00_AXI.v或者 M00_AXI的接口文件。后面第 4 节讲的状态机就写在接口文件里把原来那套简单的读写寄存器的逻辑替换掉。写完逻辑之后回到 Package IP 的界面在 File Groups 里确认综合源文件都被收进去了然后点 Review and Package → Re-Package IP。之后回到主工程的 Block DesignAdd IP 里就能搜到你自己这个 IP 了。提示每次修改 IP 源码后都要重新 Re-Package否则主工程用的还是旧版本的缓存文件。这是个非常隐蔽的坑我曾经改了半天代码发现波形没变化最后发现是 IP 没重新打包。3.5 SmartConnect、地址编辑器与时钟连接把自定义 IP 加进 Block Design 后再 Add IP 加一个AXI SmartConnect。双击它配置Number of Slave Interfaces 填1Number of Master Interfaces 填1。然后连线自定义 IP 的 M_AXI 输出口 → SmartConnect 的 S00_AXI 输入口SmartConnect 的 M00_AXI 输出口 → ZYNQ 的 S_AXI_HP0。时钟方面SmartConnect 的 aclk 和自定义 IP 的 aclk 都接 FCLK_CLK0aresetn 接 FCLK_RESET0_N。这点很重要AXI 互联和主机必须在同一个时钟域跨时钟域的事情交给 SmartConnect 内部的异步 FIFO 去做如果确实需要的话别自己乱接。连完线点一下工具栏的 Validate Design如果没报错就可以打开Address Editor了。Address Editor 里你会看到 S_AXI_HP0 这一行Range 那一列默认可能是 1GOffset 是 0x0000_0000。按我们第 2.3 节的规划把它改成Offset Address0x1000_0000Range256M改完之后 Vivado 会自动帮你算出高位地址。这一步的意义是让工具知道 PL 会访问 DDR 的哪一段后面生成 xparameters.h 和地址映射文档时都能对上。3.6 约束、综合实现与导出 XSA约束文件主要写三样东西FCLK 的时钟约束如果是走 FCLK 到 PL一般 PS 会自动传递但自己写一条 create_clock 更保险、复位信号的约束、以及你在 IP 里用到的其他 IO 引脚约束如果逻辑里有外部触发信号的话。综合Synthesis跑完之后看一眼 Report重点看有没有 latch 推断的警告有的话回去把状态机的 default 分支补全。实现Implementation之后看时序报告WNS最差负裕量只要大于 0 就过了小于 0 就得回去优化逻辑或者降频。我们这个设计逻辑不复杂150MHz 一般是能过的。最后一步是Export Hardware勾上 Include bitstream导出成 XSA 文件。这个文件同时包含了硬件描述和比特流后面 Vitis 建平台的时候要用。4. PL 侧 AXI4-FULL 主机逻辑怎么写才不出错4.1 五通道握手的三条铁律AXI4-FULL 有五个独立通道写地址AW、写数据W、写响应B、读地址AR、读数据R。每个通道都是独立的 VALID/READY 握手。新手最容易犯的错误几乎都能归到下面三条上。铁律一VALID 一旦拉高在对应的 READY 到来之前绝对不能撤销而且数据必须保持稳定。这条是协议硬性规定。我见过有人在状态机里写了「如果 READY 没来就回 IDLE 重新发」结果是 READY 来的时候数据已经变了从机收到的地址和数据对不上表现为写进去的数据莫名其妙。正确的写法是 VALID 拉高后就死等只在valid ready同时为真的那一个时钟沿撤销。铁律二数据拍数必须和 AWLEN 严格匹配WLAST 必须在最后一拍拉高。如果你设了 AWLEN 15也就是 16 拍那么 W 通道上必须不多不少发 16 拍数据第 16 拍 WLAST 拉高。少发一拍从机会一直等BVALID 永远不来你的状态机就挂死了。多发一拍从机收完 16 拍就认为传输结束多的那一拍会被当成下一次传输的数据地址就全乱了。这两个错误我都犯过现象分别是「卡死不返回」和「数据整体错位」。铁律三BID 必须和 AWID 匹配RID 必须和 ARID 匹配。单主机单通道的场景下ID 一般设成常量 0 就行从机返回的 BID 也会是 0。但如果你以后扩展到多主机共用一个 SmartConnectID 就是用来区分响应归属的必须检查。养成好习惯从简单场景开始就做 ID 校验。4.2 写通道状态机从 AWVALID 到 BVALID写操作的完整流程是拉高 AWVALID 送地址 → 从机拉高 AWREADY 接受 → 拉高 WVALID 送数据 → 逐拍完成直到 WLAST → 等从机拉高 BVALID 返回响应 → 检查 BRESP 是否 OKAY → 回 IDLE。下面是核心的状态机骨架用 Verilog 写我做了简化重点看握手逻辑localparam S_IDLE 3d0, S_AW 3d1, S_W 3d2, S_B 3d3; // 写地址通道VALID 拉高后死等 READY always (posedge aclk) begin if (!aresetn) begin awvalid 1b0; awaddr 32d0; awlen 8d0; end else if (state S_IDLE wr_start) begin awaddr wr_base_addr; awlen 8d255; // AXI4 最大 256 拍 awsize 3b011; // 每拍 8 字节 awburst 2b01; // INCR 递增模式 awcache 4b0011; awprot 3b000; awvalid 1b1; end else if (awvalid awready) begin awvalid 1b0; // 只在握手成功的那一拍撤销 end end // 写数据通道逐拍送出最后一拍拉 WLAST always (posedge aclk) begin if (!aresetn) begin wvalid 1b0; wlast 1b0; end else if (state S_W) begin wvalid 1b1; wdata fifo_dout; wstrb 8hFF; // 64bit 全字节有效 wlast (beat_cnt awlen); if (wvalid wready) begin beat_cnt beat_cnt 1b1; end end else begin wvalid 1b0; wlast 1b0; end end这个骨架里有几个点值得展开说。关于awcache的取值。HP 口直连 DDR 控制器不经过 CPU 缓存所以这个域实际影响的主要是互联内部的排序和缓冲行为。Xilinx 参考设计里 4b0011 是很常见的取值我也一直这么用实测稳定。至于更严格的缓存属性语义在 HP 口这个场景下意义不大不用钻牛角尖。关于awsize。3b011 是 2^3 8 字节正好匹配 64bit 的数据位宽。如果你把 awsize 写成 3b0104 字节那么每次突发传输的地址步进只有 4 字节相当于一半的带宽浪费掉了而且 WSTRB 也不能全 1。关于awlen。AXI4 允许 0255也就是 1256 拍。但受 4KB 边界限制下一小节详说实际能用的长度取决于起始地址的低位对齐情况。最保险的做法是让起始地址按突发总字节数对齐。4.3 读通道状态机与 RLAST 的处理读操作比写简单一点只有 AR 和 R 两个通道但要处理「从机数据没准备好」的反压。流程是拉高 ARVALID 送地址 → 等 ARREADY → 等待 RVALID 拉高 → 每个 RVALID/RREADY 同时为真的时钟沿收一拍数据 → 直到 RLAST 拉高表示最后拍 → 检查 RRESP → 回 IDLE。关键点在于RVALID 是由从机控制的你不知道它什么时候来。DDR 的读延迟CAS latency 加控制器流水线通常在几十个时钟周期量级所以从发地址到第一个 RVALID 之间有明显的等待期这是正常的不要在状态机里写超时太短的计数器报警。另一个关键点是RLAST 只在最后一拍拉高。你必须完整接收所有拍中途如果因为你的 RREADY 拉低而暂停从机会保持数据不变等你这没问题。但你不能「收到想要的数据就提前跑掉」否则从机会一直挂在那个事务上后续所有读操作都会被阻塞。还有个容易被忽略的细节读回来的数据顺序是严格按地址递增的INCR 模式下。如果你发了 256 拍的读你会收到 256 拍连续的数据第一拍对应起始地址最后一拍对应起始地址加上 255×8 字节。4.4 WSTRB、4KB 边界与突发长度的硬约束这一小节是实战中最容易踩坑的地方我单独拎出来。先说 WSTRB。它是个按字节的写使能64bit 数据对应 8 根 WSTRB 线。全 1 表示 8 个字节都写进去全 0 表示这一拍不写任何字节。什么时候会用到部分写比如你只想更新一个 u32但总线是 64bit那你只能让低 4 字节的 WSTRB 为 1、高 4 字节为 0。但在我们的批量搬运场景里数据永远是整块整块的WSTRB 应该恒定为 8hFF。如果你发现写进 DDR 的数据只有一半是对的先检查 WSTRB 是不是漏接了或者接错了。再说 4KB 边界这是硬性规定。AXI 协议明确规定一次突发传输不能跨越 4KB 地址边界。原因是 4KB 是很多从机内部的最小地址译码单位跨界的突发可能会导致从机解译出两个不同的目标。这条规则怎么落实简单的算术起始地址的低 12 位 突发总字节数 ≤ 4096举例来说你设 awlen 255256 拍每拍 8 字节总字节数就是 2048。那么起始地址的低 12 位必须 ≤ 4096 - 2048 2048也就是起始地址必须落在 0x0000x800 之间且按 8 字节对齐。如果起始地址低 12 位是 0xC00加上 2048 就超过 4096 了跨界。实际写代码的时候有两种处理方式。一种是强制对齐把缓冲区基址按 4KB 对齐分配然后每次突发长度固定为 256 拍这样每次传输都从 4KB 边界内部的 0x000 或者 0x800 开始绝对不会跨界。另一种是动态截断根据当前地址算出还能发多少拍取一个不超过 256 且不越界的安全值。我自己用的是第一种简单粗暴还不容易出错。缓冲区基址直接定义成0x1000_0000天然 4KB 对齐每次搬 2KB256 拍 × 8 字节搬完指针加 2KB下次又从一个 2KB 对齐的位置开始永远不会碰边界。最后说突发长度的收益。前面提过 256 拍会被 SmartConnect 拆成 16 个 16 拍那还有必要拉满吗有。因为拆分的开销是固定的每个子突发一次握手突发越长平摊到每个字节上的开销越小。我实测过 256 拍和 16 拍的对比同样搬 64MB 数据前者比后者快了将近一倍。这个差距在做实时处理的时候是致命的。4.5 位宽转换与跨时钟域处理这一小节讲讲可能踩到的两个「隐形坑」。位宽转换。我们这里 PL 接口是 64bitHP 口也是 64bit位宽一致不需要转换这是最省事的情况。但如果你的数据源是 32bit比如从某个 FIFO 出来而 AXI 接口是 64bit就得做打包把两个 32bit 拼成一个 64bit 再送出去。这里有个坑是字节序——两个 32bit 拼成 64bit 的时候哪个放低位哪个放高位要和 PS 端读数据时解析的顺序对上。我的习惯是低地址放低位也就是先把第一个 32bit 放在wdata[31:0]第二个放在wdata[63:32]和 ARM 的小端序一致。跨时钟域。如果你的数据源时钟和 AXI 时钟不是同一个比如 AD 采样时钟是 50MHzAXI 是 150MHz中间必须加异步 FIFO。这里我强烈建议直接用 Xilinx 的 FIFO Generator IP自己手写双口 RAM 加格雷码指针虽然也能做但深度和宽度的参数一多很容易出问题。用 IP 的话把独立时钟模式打开读写时钟各接各的剩下的交给工具。注意跨时钟域的时候WLAST 的生成时机一定要和数据的写入节奏对齐。如果异步 FIFO 里存了 2048 字节你却在 FIFO 只填了一半的时候就发了 AR 或者 AW那从机收到的数据量就对不上了。稳妥做法是数据先全存进 FIFO存满了再启动 AXI 传输传输过程中 FIFO 只做速率匹配不做数据缓存。5. PS 端软件配合裸机指针读写与 Cache 一致性5.1 裸机下的直接指针访问硬件比特流导出之后在 Vitis或者老版本的 SDK里新建一个 Platform 和一个空的 Application 工程选 hello world 模板然后开始改。裸机程序访问 DDR 里某段物理地址最简单的方式就是直接定义指针。因为裸机没有 MMU 的地址转换或者说 MMU 是恒等映射你写0x10000000就是访问物理地址0x10000000#include xil_printf.h #include xil_cache.h #include xparameters.h #include xtime_l.h #define SHARED_BASE 0x10000000U /* 与 Address Editor 一致 */ #define SHARED_SIZE (4U * 1024U * 1024U) /* 4MB 测试缓冲区 */ volatile u32 *pShared (volatile u32 *)SHARED_BASE; int main(void) { u32 i; xil_printf(PL-PS AXI-FULL DDR test start\r\n); /* 1. CPU 填数据 */ for (i 0; i SHARED_SIZE / 4; i) { pShared[i] i ^ 0xA5A5A5A5U; } /* 2. 刷 Cache把数据真正落到 DDR */ Xil_DCacheFlushRange((UINTPTR)SHARED_BASE, SHARED_SIZE); /* 3. 启动 PL通过 AXI-Lite 写一个 start 寄存器这里省略具体实现 */ /* Xil_Out32(BASEADDR 0x00, 0x1); */ /* 4. 等 PL 完成轮询状态寄存器或者等中断 */ /* while ((Xil_In32(BASEADDR 0x04) 0x1) 0); */ /* 5. PL 把结果写到另一块区域读之前失效 Cache */ Xil_DCacheInvalidateRange((UINTPTR)(SHARED_BASE 0x200000), SHARED_SIZE); /* 6. 校验 */ volatile u32 *pResult (volatile u32 *)(SHARED_BASE 0x200000); xil_printf(result[0] 0x%08X\r\n, pResult[0]); xil_printf(result[1] 0x%08X\r\n, pResult[1]); return 0; }这段代码里有几个点必须说清楚不然你会遇到「明明 PL 说写进去了CPU 读出来全是 0」这种鬼故事。5.2 必须搞懂的 Cache 一致性这是整个项目里最容易出问题、也最难排查的部分。前面反复说过HP 口直连 DDR 控制器不经过 CPU 的 L1/L2 缓存。这句话反过来读就是CPU 的缓存和 DDR 里的实际内容可能是不一致的。具体分两个方向说。方向一CPU 写 → PL 读。CPU 往缓冲区里写了 4MB 数据这些数据实际上可能还躺在 L1 或者 L2 缓存里根本没写回 DDR 颗粒。这时候 PL 通过 HP 口去读 DDR读到的是旧的、脏的数据。解决办法就是在启动 PL 之前执行一次Cache Flush把缓存里的脏行强制写回 DDR。Xil_DCacheFlushRange((UINTPTR)SHARED_BASE, SHARED_SIZE);方向二PL 写 → CPU 读。PL 通过 HP 口把结果写进了 DDR 颗粒但 CPU 的缓存里可能还留着这个地址段的旧副本。CPU 一读命中了缓存里的旧数据完全看不到 PL 写的新值。解决办法是在读取之前执行Cache Invalidate把对应的缓存行标记为无效强制 CPU 下次读的时候回 DDR 重新取。Xil_DCacheInvalidateRange((UINTPTR)SHARED_BASE RESULT_OFFSET, RESULT_SIZE);如果一块区域是双向使用的CPU 写完 PL 读PL 写完 CPU 再读那就要 flush invalidate 一起做。Xilinx 提供了Xil_DCacheFlushRange和Xil_DCacheInvalidateRange两个函数也有人用Xil_DCacheFlush()和Xil_DCacheInvalidate()做全局操作——全局操作简单但会清掉所有缓存对性能影响大数据量小的时候无所谓数据量大的时候还是用 Range 版本更划算。这里有个非常经典的坑Cache 操作的最小单位是 Cache Line而 Cortex-A9 的 Cache Line 是 32 字节。这意味着什么假设你有一个 4 字节的变量flag它和另一个变量data恰好在同一个 32 字节的 Cache Line 里。你对flag所在的 4 字节做了 invalidate硬件实际会把整个 32 字节的 Cache Line 都失效掉——如果data刚好在这个 Line 里而且是 CPU 刚写进去还没刷回 DDR 的脏数据那么这一失效data就永久丢失了DDR 里还是旧值。所以我的建议是所有跟 PL 共享的缓冲区都要单独划一块区域并且按 32 字节对齐长度是 32 字节的整数倍。不要把共享变量和普通变量混在一起放。这个习惯能帮你省掉大量莫名其妙的「数据偶尔丢失」问题。还有一个更省事的方案直接把共享区域设成 Non-Cacheable。裸机下可以用Xil_SetTlbAttributes修改 MMU 的页表属性把某一段地址标记成非缓存/* 把 SHARED_BASE 开始的区域设为 Normal, Non-cacheable */ Xil_SetTlbAttributes(SHARED_BASE, 0x14de2);这个属性值的含义是普通内存、非缓存、可共享用过之后 CPU 访问这一段的每一次读写都会直接打到 DDR不需要再手动 flush 和 invalidate 了。代价是性能下降——没有缓存加速CPU 每次读写都要等 DDR 的延迟大概是几十个时钟周期比缓存命中慢一两个数量级。所以在「CPU 只需要小块数据、PL 需要频繁读写」的场景下很合适在「CPU 要大量遍历」的场景下不划算。我自己一般是这么权衡的控制寄存器、状态标志、小块的命令缓冲区用 Non-Cacheable大块的数据缓冲区用 Cacheable 手动 flush/invalidate。前者省心后者快。5.3 用 ILA 串口做联合调试纯靠打印调试 AXI 逻辑效率极低。正确的姿势是ILA 抓波形 串口打日志联合定位。ILA 的挂法很简单Add IP 里搜 System ILA 或者 ILA挂到 AXI 相关的信号上。注意采样时钟必须选 AXI 的 aclk也就是 FCLK_CLK0别选错了。探针信号建议至少包含这些awvalid、awready、awaddr、awlenwvalid、wready、wdata、wlast、wstrbbvalid、bready、bresparvalid、arready、araddr、arlenrvalid、rready、rdata、rlast、rresp你的状态机当前状态采样深度给到 8192 或者 16384触发条件设成awvalid awready写路径或者arvalid arready读路径。抓一次完整的事务对着波形检查握手。我排查问题时的固定顺序是这样的先看 AWVALID 和 AWREADY 是不是在同一拍同时为高时序图上是两个信号重叠一个时钟周期再看 WLAST 是不是在第 256 拍或者你设定的拍数出现最后看 BVALID 有没有来、BRESP 是不是 2b00。这三步走完百分之八十的问题都能定位。串口那边主要负责在 PS 侧打印关键节点flush 完成的时间戳、PL 启动的时间戳、读出数据的前几个字、带宽计算结果、以及数据校验是否通过。两边的时间戳对不上就说明是同步逻辑有问题而不是数据搬运本身有问题。5.4 带宽测试的代码框架带宽测试的关键是计时要精确并且要把 Cache 操作的时间排除在外。因为 flush 一个 4MB 的缓冲区本身也要花时间如果算进去你测到的就不是 PL 的搬运速度了。计时用 ZYNQ 的全局定时器XTime tStart, tEnd, tDelta; double elapsed_us, bandwidth_mbps; XTime_GetTime(tStart); /* 只在这个窗口内启动 PL 并等它完成 */ /* Xil_Out32(BASEADDR, 0x1); */ /* while ((Xil_In32(BASEADDR 4) 0x1) 0); */ XTime_GetTime(tEnd); tDelta tEnd - tStart; elapsed_us (double)tDelta / (COUNTS_PER_SECOND / 1000000.0); bandwidth_mbps ((double)SHARED_SIZE / 1024.0 / 1024.0) / (elapsed_us / 1000000.0); xil_printf(elapsed %.2f us\r\n, elapsed_us); xil_printf(bandwidth %.2f MB/s\r\n, bandwidth_mbps);COUNTS_PER_SECOND这个宏在xtime_l.h里ZYNQ-7000 一般是 CPU 频率的一半比如 CPU 跑 666MHz它就是 333333333。别自己去猜直接用宏就行。测的时候建议跑三轮第一轮是小数据量比如 64KB主要看延迟第二轮是中等4MB看稳态带宽第三轮是大数据量64MB 以上看长时间运行的稳定性。三轮数据对比一下如果小数据量的等效带宽远低于大数据量说明握手开销占比高要么把突发拉长要么把 PL 时钟提上去。6. 实测数据与常见故障速查6.1 我的实测记录与解读在 150MHz FCLK、单 HP 口、64bit 位宽、256 拍突发的配置下我这边测到的持续写带宽稳定在950MB/s 到 1100MB/s之间读带宽略低一点大概 850MB/s 到 1000MB/s。为什么读比写慢因为读操作有个往返延迟PL 发 AR从机要去 DDR 取数据中间的延迟无法被流水线完全掩盖。写操作则是「单向灌数据」只要从机的写 FIFO 没满PL 就可以一直往前冲更容易打满。理论值 1200MB/s实测 1000MB/s 左右也就是 83% 的效率。剩下的 17% 花在哪了主要是三块SmartConnect 把 AXI4 拆成 AXI3 的开销、AXI 握手的空闲周期、以及 DDR 控制器在 PS 和 PL 之间做仲裁时的切换开销。这个效率我认为是合理的没必要再抠。另外提一句我顺手测了 PS 端用memcpy搬同样 4MB 数据的速度大概在 1.5GB/s 上下纯 CPU 拷贝缓存在起作用。也就是说 PL 通过 HP 口搬数据的效率和 CPU 自己 memcpy 是一个量级的。那 PL 搬的意义在哪在于搬运的整个过程 CPU 不参与它可以去干别的活。这个价值远大于那点速度差。6.2 故障速查表下面这张表是我这些年积累下来的遇到问题先对着表查一遍能省掉大量翻手册的时间。现象可能原因排查动作BVALID 永远不来状态机挂死WLAST 没拉高或拍数不匹配ILA 抓 W 通道数一下有效拍数是否等于 AWLEN1BVALID 来了但 BRESP 2b10 (SLVERR)访问了未映射的地址核对地址是否在 DDR 物理范围内首字节对后面的数据整体错位多发或少发了一拍数据检查 WLAST 生成条件和 beat_cnt 的边界数据每 2KB 就出现一段异常跨了 4KB 边界检查起始地址低 12 位 突发字节数是否超 4096写进去的数据只有低 4 字节有效WSTRB 只接了低 4 位检查 WSTRB 是否恒为 8hFFCPU 读出来全是 0 或旧值Cache 没 invalidate读之前加 Xil_DCacheInvalidateRangePL 读到的数据不是 CPU 刚写的Cache 没 flush启动 PL 之前加 Xil_DCacheFlushRange偶发性数据丢失几个小时一次Cache Line 冲突相邻变量被误失效共享缓冲区独立划分32 字节对齐带宽只有理论值的三分之一突发长度太短或 PL 时钟太低检查 AWLEN 是否拉满FCLK 是否 150MHz时序报告 WNS 为负150MHz 下逻辑路径太长插入流水寄存器或降到 100MHz 先跑通综合报 latch 推断警告状态机 case 缺 default 分支补全 default 和所有分支赋值PL 逻辑完全不工作复位没释放或 bitstream 没加载检查 FCLK_RESET0_N 极性重新下载比特流JTAG 能识别 PS 但连不上 PLPL 电源或复位时序未稳检查板子供电确认 POR 释放后再连固化到 Flash 后 PL 不工作BOOT.bin 里没包含比特流检查 FSBL 构建流程确认 bit 已打包6.3 三个见效最快的优化手段如果测下来带宽不理想别急着改架构先试这三个成本最低。第一个把突发长度拉满。前面反复说了这是收益最大的单项优化。确保 AWLEN 设成 8d255并且地址对齐到 4KB 边界内部让每次突发都能完整发出去。第二个把 PL 时钟从 100MHz 提到 150MHz。直接带来 50% 的带宽提升代价是时序收敛难度上升。如果提不上去看看关键路径在哪一般是在地址计算或者 FIFO 输出那段插一级流水寄存器就能救回来。第三个把 AXI-Lite 控制路径和 AXI-FULL 数据路径彻底分开。我见过有人把控制寄存器塞在 AXI-FULL 的数据通路里每次读写寄存器都要走一遍完整的总线事务白白占用带宽。控制走 GP 口或者 IP 内部独立的一小段 AXI-Lite数据走 HP 口两条路互不干扰。7. 踩坑记录几个我希望早点知道的事写完主体逻辑之后我把这次项目里印象最深的几个坑记下来都是那种「文档上看不出、只有真跑过才知道」的。第一个坑是状态机没加超时。有一次调试地址算错了一位访问到了 DDR 范围之外的地址。结果从机没返回任何响应BVALID 永远不来我的状态机就卡在 S_B 状态出不来整个 PL 逻辑瘫痪连重新下载比特流都得先断电。后来我在所有等待握手的环节都加了一个计数器超时之后就跳回 IDLE 并且拉高一个 error 标志虽然不能修正错误但至少不会把整个系统拖死。第二个坑是忘记给 IP 重新打包。前面提过一次但值得再强调。改完 IP 源码之后一定要 Re-Package然后回到主工程右键 IP 选 Upgrade IP再重新综合。我有整整一个下午都在改代码、看波形没变化、再改代码的循环里最后发现是这个原因。第三个坑是把共享变量定义成了全局常量数组。编译器会把只读的全局数组放到.rodata段而我在链接脚本里把.rodata放到了 DDR 的一段固定区域。这段区域和 PL 的缓冲区有重叠PL 一写就把常量数据踩了。表现为程序跑到某个位置突然跑飞。教训是链接脚本里的段地址和 PL 的缓冲区地址一定要在纸上画一遍确保不重叠。第四个坑和 Cache 有关。我一开始图省事每次启动 PL 之前都用Xil_DCacheFlush()全局版本结果发现 CPU 那边的性能掉得厉害因为整个缓存都被刷了一遍后面所有的数据访问都要重新从 DDR 加载。改成Xil_DCacheFlushRange()只刷共享段之后CPU 侧的负担立刻就下来了。数据量越大这个差别越明显。第五个坑是忽略了 DDR 的温度。满负荷跑了两三个小时之后DDR 颗粒摸上去烫手然后开始出现偶发的一位错误。加了块小散热片之后问题消失。AX7020 是学习板定位散热设计不算激进长时间满载的时候还是要注意一下。最后分享一个我自己用着很顺的小习惯在 PL 逻辑里加一个计数器 IP专门统计 AXI 事务的实际拍数和空闲周期数通过 AXI-Lite 读出来。跑完之后算一下「有效拍数 / 总周期数」就是这条链路的实际利用率。这个数字比任何理论计算都实在调优的时候盯着它看改一个参数看它涨没涨方向立刻就有了。这个做法后来我在好几个项目上都用比盲猜高效得多。
返回列表