ZYNQ开发实战:PL端通过AXI4读写DDR完整链路与避坑指南
简介:本资源是面向嵌入式FPGA开发者的ZYNQ 7020平台AXI4-DDR内存读写驱动实战项目,适用于已掌握Vivado基础设计与SDK软件开发流程的中级工程师及高校高年级学生,解决软硬件协同下DDR高速存取驱动开发这一典型痛点。压缩包共961个文件,涵盖140个头文件(.h)与90个C源码(.c)构成的完整SDK驱动工程,86个Verilog(.v)与45个SystemVerilog(.sv)文件支撑硬件接口定义,另有TCL脚本、XDC约束、BIT配置文件及HAL库编译产物(.a/.so/.elf),整体大小24.83MB。已有943人学习下载。资源提供可直接编译运行的AXI4-DDR初始化与读写函数实现,包含Vivado生成的HDF硬件描述、SDK自动生成的BSP工程结构、多轮调试验证用的runme.bat批处理脚本,以及__synthesis_is_complete__等关键构建标记文件,便于快速复现软硬联调全流程并理解AXI总线时序与DDR控制器寄存器映射机制。 很多刚开始接触ZYNQ的工程师,第一个真正卡住的项目需求往往不是跑个Hello World或者点亮LED,而是“让PL(可编程逻辑)端能读写DDR”。这个“能读写”三个字背后,牵扯出的是AXI4总线协议的理解、Vivado里Block Design的搭建、MIG(Memory Interface Generator)IP的配置、SDK里地址映射和Cache一致性的坑,一整套链路走下来,纯靠啃手册得耗掉不少时间。这篇博文就基于我实际调试ZYNQ 7020的AXI4 DDR读写驱动(SDK驱动)这个项目,把这个过程的完整链路、关键原理和踩过的坑一次说清楚。
1. 为什么PL端读写DDR会成为第一个拦路虎
1.1 ZYNQ的内存架构:DDR控制器不在PL里
先明确一个和纯FPGA完全不同的概念。在传统的FPGA开发中,如果要外挂DDR,通常需要在FPGA逻辑内部例化一个DDR控制器软核(比如MIG生成的硬核或者第三方IP),由PL逻辑直接控制DDR颗粒的管脚、时序和刷新。
但在ZYNQ中,情况完全不同。DDR控制器硬核是集成在PS(Processing System,处理系统)内部的,和ARM Cortex-A9处理器共用一套存储系统。PL端没有直接连到DDR颗粒的管脚,它必须通过AXI接口,经过PS内部的互联逻辑,才能访问DDR控制器,最终完成对DDR的读写。
这就带来一个关键认知:PL访问DDR,本质上是作为AXI主机(Master),发起到PS端从机(Slave)端口的AXI事务。
1.2 AXI接口类型选择:为什么用AXI HP而不是AXI GP
ZYNQ 7020的PS端对外提供了几类AXI接口,它们的能力差异非常明显:
- AXI_GP(General Purpose):通用目的接口,一共两组,数据位宽32位,理论最高频率150MHz,理论带宽约600MB/s(单向)。它主要用于访问PS端的寄存器、少量控制数据交换,不适合大数据量吞吐。
- AXI_HP(High Performance):高性能接口,一共4组,数据位宽64位,支持AXI3/AXI4协议,理论带宽更高,并且带有读写FIFO缓冲,是PL访问DDR的推荐通道。
- AXI_ACP(Accelerator Coherency Port):加速器一致性端口,可以介入CPU的Cache一致性域,适合需要和CPU频繁共享数据但数据量适中的场景。
这个项目里要实现的DDR读写驱动,核心目标是让PL端能够把大量数据高速写入DDR,或者从DDR读出大量数据,所以接口选型上毫无疑问要用AXI_HP。
这里要特别提醒一下:很多人以为在Block Design里把AXI_HP接口引出来,然后连一个AXI SmartConnect或者AXI Interconnect,再接到PL端的自定义逻辑就行了。这个流程方向是对的,但仅仅这样还不够。AXI协议的事务类型(读/写)、突发长度(Burst Length)、突发大小(Burst Size)、地址对齐要求、响应信号的处理,每一个环节都会影响驱动能不能稳定工作。
1.3 地址映射:DDR在PS地址空间中的位置
PL端发起AXI事务时,地址总线上的地址并不是DDR颗粒的物理地址,而是PS端地址空间中的一个映射地址。在典型的Vivado Block Design配置中,PS端的DDR地址空间位于0x00100000到0x3FFFFFFF(约1GB,具体取决于DDR容量和配置)。这个地址映射关系要在Vivado里通过Address Editor确认。
实操中我遇到过有人把地址当成DDR物理地址从0x00000000开始算,结果发现写入的数据和读取的数据完全对不上,又排查了半天才发现是地址空间理解错了。
2. Vivado侧Block Design的搭建与MIG核配置细节
2.1 Block Design中PS和MIG的连接方式
接下来要明确一个容易混淆的问题:既然DDR控制器已经集成在PS内部了,那PL端还需要例化MIG吗?
答案是:不需要,但是你得知道为什么。在纯PS启动模式下,DDR控制器由PS内部的ROM代码(BootROM)完成初始化,然后由FSBL(First Stage Boot Loader)继续配置,用户可以通过Vivado的Address Editor确认DDR地址映射,但不需要在Block Design里单独例化MIG。
但是,有一种情况需要在Block Design中处理DDR相关的东西:如果你需要让PL端可以在PS尚未完成DDR初始化之前就访问DDR(这很不常见,通常不会这么干),或者内部需要知道DDR控制器初始化完成的状态,那可以通过DDR_HP相关信号或者某些状态寄存器来获取。一般项目中,直接在Block Design中连接AXI_HP Slave端口即可。
典型的连接方式如下:
- 添加
ZYNQ7 Processing SystemIP,启用S AXI HP0接口(以及HP1/2/3中需要用到的接口)。 - 添加自己的AXI主机逻辑,或者使用Vivado IP库中的
AXI DMA、AXI DataMover等IP作为数据搬运引擎。 - 添加
AXI SmartConnect(推荐)或AXI Interconnect,把多个AXI主机或单个主机连接到PS端HP0接口。 - 在
Run Connection Automation时,把时钟和复位信号自动连接好。 - 在Address Editor中把PL端主机IP的地址段映射到DDR地址范围内(比如
0x00100000起始的某一段)。
2.2 AXI SmartConnect与AXI Interconnect的选择
Vivado中老版本常用AXI Interconnect,新版本推荐AXI SmartConnect。两者功能类似,都是AXI总线互联结构,但SmartConnect做了不少优化,更容易满足时序收敛,也支持自动协议转换(AXI4到AXI3、AXI4到AXI4-Lite等)。
在实际项目中,我建议直接用AXI SmartConnect。原因有几个:
- 配置界面更简洁,不需要手动指定每个接口的协议、数据宽度、时钟域。
- 对于单主机到单从机的简单连接,SmartConnect可以自动裁剪不必要的逻辑,资源占用更低。
- 支持多个AXI Slave接口和多个AXI Master接口的灵活连接。
这里还要注意一下时钟域问题。PL端的AXI主机逻辑时钟可以和PS端的CPU时钟不同步。SmartConnect可以工作在异步模式:一侧输入PL逻辑时钟(比如150MHz),另一侧输入PS端FCLK_CLK0(比如100MHz)。跨时钟域的处理由SmartConnect内部的异步FIFO完成。如果你的PL逻辑时钟和PS时钟最终来自同一个MMCM/PLL,也可以直接跑同步模式,但为了减少时钟约束的麻烦,我一般还是选异步模式。
2.3 Block Design里最容易漏掉的DDR相关信号
在Block Design的连线过程中,有几个DDR相关的状态信号容易被忽略:
- DDR仲裁和优先级:ZYNQ PS端的DDR控制器内部有QoS(Quality of Service)机制,但PL端的AXI_HP端口访问DDR时,PS内部会自动仲裁,正常情况下不需要额外配置。只有当CPU和PL同时大量访问DDR导致CPU实时性受影响时,才需要去调整QoS优先级。
- FCLK频率:Block Design中会生成
FCLK_CLK0、FCLK_CLK1等PS输出时钟。注意,这些时钟的频率会直接影响AXI_HP接口的工作频率。默认情况下FCLK_CLK0是100MHz,如果你希望PL端高速访问DDR,建议把FCLK_CLK0提高到150MHz或更高,但前提是PL逻辑时序能收敛。
我遇到过一个情况:Block Design默认FCLK_CLK0为100MHz,PL端逻辑跑150MHz时钟,SmartConnect自动做了跨时钟域,功能正常,但带宽只有设计预期的三分之二。后面把FCLK_CLK0改成150MHz,AXI HP接口和PL逻辑同频,带宽提升到接近理论值。
2.4 MIG配置中的DDR3/DDR4参数:以7020开发板为例
很多人会问,既然不需要PL端例化MIG,那Vivado里还要配置MIG吗?这得分场景。
如果你的板卡是标准的ZYNQ 7020开发板,板载DDR3内存颗粒,那么DDR控制器的初始化由PS完成,你不需要在Vivado里例化MIG。但如果你用的是基于ZYNQ的定制板,PS端BSP(Board Support Package)中需要正确的DDR参数,这些参数通常在创建Vivado工程时通过Board Part或者ps7_init.tcl来配置。
在Vivado中,新建ZYNQ工程时选择对应的开发板Board Part,PS配置向导会自动设置DDR型号、位宽、时序参数。如果选错了DDR型号或者位宽不对,FSBL启动时DDR training会失败,现象就是串口打印DDR init failed或者一直在反复重启。
DDR参数的关键项包括:
- DDR型号(如MT41K256M16 HA-125,DDR3L)
- 数据位宽(16bit、32bit、64bit)
- ECC支持(启用后地址空间会变化)
- 内存颗粒数(rank数量)
- 速度等级(如DDR3-1066、DDR3-1600)
这些参数最终会通过ps7_init.tcl在FSBL阶段配置到DDR控制器寄存器中。如果实际板卡参数和配置不一致,轻则DDR training失败,重则内存访问不稳定、数据随机出错。所以拿到一块不熟悉的板子时,第一件事一定是确认DDR颗粒型号和参考设计配置,不要想当然。
3. SDK驱动开发:地址映射与Cache一致性是踩坑重灾区
3.1 裸机SDK下DDR读写驱动的整体思路
PS端的FSBL完成初始化后,DDR已经可以正常访问。SDK中裸机工程里,CPU直接访问DDR地址空间。PL端的AXI主机会通过HP0接口访问同一段DDR空间。
裸机驱动要做的事情,本质上就是:
- 确定DDR映射基址和要操作的地址段。
- 让PL端AXI主机发起写事务(数据从PL到DDR)或读事务(数据从DDR到PL)。
- 在PS端验证数据的正确性,或者触发PL端发数据。
大部分项目里,PL端的数据搬运引擎是AXI DMA或AXI DataMover。两者都支持在SDK中通过寄存器配置来控制搬运任务。如果不需要复杂的DMA搬移,也可以直接在PL端用状态机实现简单的AXI4主接口,把数据写到固定地址。
下面会分两种路径来讲驱动实现:一种是数据完全由PL端发起,SDK只负责配置和状态监测;另一种是PS端主动发起读写,通过SDK中的Xil_In32/Xil_Out32这些函数访问DDR。
3.2 Xil_In32/Xil_Out32访问DDR的局限性
很多初学者在SDK中写这样一段代码来测试DDR:
#include "xil_io.h" #include "xil_cache.h" #define DDR_BASE_ADDR (0x00100000) void ddr_test(void) { u32 i; for (i = 0; i < 1024; i++) { Xil_Out32(DDR_BASE_ADDR + i * 4, 0xDEADBEEF); } for (i = 0; i < 1024; i++) { u32 val = Xil_In32(DDR_BASE_ADDR + i * 4); if (val != 0xDEADBEEF) { xil_printf("DDR test failed at offset %d, val 0x%08x\r\n", i, val); return; } } xil_printf("DDR test passed\r\n"); }这段代码在纯PS模式下确实能工作,但它的局限很明显:Xil_In32和Xil_Out32是单次32位读写,一条指令只能处理4字节。要测试大块数据或者验证PL端带宽,这种方式效率太低。而且,Xil_Out32没有缓存问题,因为它是直接通过AXI总线发起的,不会经过CPU的D-Cache。
但如果使用内存拷贝(memcpy)或者对普通数组赋值的方式去访问DDR,就一定要考虑Cache一致性问题。
3.3 Cache一致性问题:PL写入DDR后CPU读到的是旧数据
这是ZYNQ开发中最高频的坑。ARM Cortex-A9的D-Cache是写回(writeback)模式,CPU读数据时优先从Cache读,如果Cache miss再从DDR读。写数据时先写入Cache,标记为脏(dirty),在合适时机才写回DDR。
这就导致一个问题:PL端AXI主机往DDR的某段地址写入了新数据,但CPU可能一读还是旧数据,因为D-Cache中缓存的旧副本还没有失效。
反之,如果CPU用普通memcpy往DDR写入数据,但数据还停留在D-Cache中没有写回,PL端AXI主机去读取DDR时读到的也可能是旧数据。
解决方法是对操作区域进行Cache维护:
#include "xil_cache.h" #define DDR_BASE_ADDR (0x00100000) #define BUFFER_SIZE (4096) void pl_to_cpu_transfer(u32 *src_pl, u32 *dst_cpu, u32 length) { // 先将目的地址对应的D-Cache行无效化,确保后续读取会从DDR中取最新数据 Xil_DCacheInvalidateRange((UINTPTR)dst_cpu, length); // 触发PL端DMA/逻辑开始写入... // 等待写入完成... // 再次无效化,确保CPU从DDR读取到PL写入的最新数据 Xil_DCacheInvalidateRange((UINTPTR)dst_cpu, length); }Xil_DCacheInvalidateRange的作用是把指定地址范围内的D-Cache行标记为无效,下次CPU访问时强制从DDR重新读取。注意,这个操作是有对齐要求的,地址需要按Cache Line(Cortex-A9通常是32字节)对齐,长度也最好对齐。
如果数据是从CPU写入DDR然后由PL端读取,需要做Flush(写回+无效化):
void cpu_to_pl_transfer(u32 *src_cpu, u32 *dst_pl, u32 length) { // 将CPU写好的数据从D-Cache写回DDR Xil_DCacheFlushRange((UINTPTR)src_cpu, length); // 触发PL端DMA/逻辑读取... // 等待读取完成... }很多人在第一次调ZYNQ的AXI DMA和DDR时,发现CPU中断里收到的数据全是错的,去掉Cache优化就好了,但实际上正确的做法不是去掉Cache,而是正确使用Cache维护函数。数据量小的时候可以关Cache,数据量一大,关Cache性能损失明显,还可能出现不可预期的行为。
3.4 自定义AXI4 DDR读写驱动的一个最小示例
如果PL端逻辑自己实现了AXI4主接口,不借助AXI DMA,那么在SDK中驱动代码相对简单。假设PL端逻辑的基址在0x43C00000(AXI-Lite从机寄存器空间),PL端通过AXI4接口把数据写入DDR的0x20000000开始的缓冲区,SDK驱动需要做的是:
- 配置PL端逻辑的控制寄存器(起始地址、数据长度、启动命令)。
- 等待PL端逻辑的状态寄存器返回“完成”标志。
- 对DDR缓冲区做
Xil_DCacheInvalidateRange,然后读取校验。
下面给出一段实际的裸机驱动代码结构(只写出关键部分):
#include "xil_io.h" #include "xil_cache.h" #include "sleep.h" #define PL_BASE_ADDR (0x43C00000) #define PL_CTRL_REG (PL_BASE_ADDR + 0x00) #define PL_STATUS_REG (PL_BASE_ADDR + 0x04) #define PL_DDR_ADDR_REG (PL_BASE_ADDR + 0x08) #define PL_LENGTH_REG (PL_BASE_ADDR + 0x0C) #define DDR_BUF_ADDR (0x20000000) #define BUF_SIZE (4096) #define CTRL_START (0x1) #define STATUS_DONE (0x1) void pl_write_to_ddr(void) { u32 i, val; // 设置PL端要写入的DDR目标地址和长度 Xil_Out32(PL_DDR_ADDR_REG, DDR_BUF_ADDR); Xil_Out32(PL_LENGTH_REG, BUF_SIZE); // 启动PL端AXI写事务 Xil_Out32(PL_CTRL_REG, CTRL_START); // 等待完成,实际工程建议加超时 i = 0; while ((Xil_In32(PL_STATUS_REG) & STATUS_DONE) != STATUS_DONE) { i++; if (i > 1000000) { xil_printf("Timeout waiting PL write done!\r\n"); return; } } // 关键:读取前无效化D-Cache,确保拿到DDR中的最新数据 Xil_DCacheInvalidateRange(DDR_BUF_ADDR, BUF_SIZE); // 校验数据 for (i = 0; i < BUF_SIZE / 4; i++) { val = Xil_In32(DDR_BUF_ADDR + i * 4); // 这里按PL端预设的模式校验,比如递增序列 if (val != i) { xil_printf("Data mismatch at offset %d, expect 0x%08x, actual 0x%08x\r\n", i, i, val); return; } } xil_printf("PL -> DDR write verified successfully!\r\n"); }这个例子的核心步骤非常典型:寄存器配置、启动、轮询状态、Cache无效化、数据校验。如果PL端逻辑换成AXI DMA,驱动逻辑也基本一致,只是把寄存器换成AXI DMA SG(Scatter Gather)描述符的配置。
3.5 中断方式与轮询方式的选择
上文例子用了轮询等待状态位,这在调试阶段最直观,也最容易定位问题。但如果PL端搬运大量数据耗时较长,轮询会阻塞CPU,影响系统实时性。工程上建议改用中断:
- PL端逻辑或AXI DMA在搬运完成后,通过中断信号连接到PS端的
IRQ_F2P[7:0]。 - SDK的
XScuGic中断控制器驱动注册中断回调函数。 - CPU中断响应后在回调函数里做Cache无效化和数据处理。
这个方案在数据量大、需要持续搬运的场景下优势明显。不过调试时先跑通轮询,再加中断,是个更稳的路径。
4. 实测数据、性能瓶颈与常见Bug的排查链路
4.1 实际读写带宽能达到多少
很多人关心ZYNQ 7020的PL端通过AXI_HP写DDR,实际带宽能到多少。理论峰值其实只是参考,实际受限于多个因素:
- AXI_HP接口在FCLK_CLK0=150MHz时,64位数据位宽的理论带宽为150MHz * 8字节 = 1200MB/s。但AXI协议每次读写都有握手开销,实际能到80%左右就已经不错了。
- 使用AXI DMA时,突发长度(Burst Length)影响很大。AXI4支持突发长度16(每个突发16拍,每拍8字节 = 128字节),这是最大化带宽的关键。如果误用AXI3模式,突发长度被限制为16拍但协议不同,或者IP配置成了INCR类型但一次只发几拍,性能会严重下降。
- PS端的DDR频率、DDR控制器的仲裁策略也会影响最终吞吐。如果CPU同时在大量操作DDR(比如跑Linux系统),PL端带宽会被分走一部分。
在我实际测试中,7020上PL端通过AXI DMA以64位AXI4、突发16、FCLK_CLK0=150MHz向DDR连续写入,实测带宽大约在900MB/s到1000MB/s之间。读方向略低一点,大约800MB/s左右。这已经满足大多数图像采集、数据采集类项目需求。
如果你需要更高的带宽,可以并行使用多个HP端口(比如HP0写、HP1读),或者提升FCLK频率,但注意PL逻辑时序收敛风险。
4.2 排查链路一:写入DDR后读出来全是0x00或0xFF
这是PL端访问DDR时最常见的故障现象,按下面的链路一步步排查:
- 确认地址映射正确:Vivado的Address Editor里,PL端AXI主机的地址段是否映射到了DDR地址范围,起始地址和大小是否匹配。常见错误是映射到了
0x40000000的QSPI或者0xE0000000的外设区。 - 确认AXI事务类型正确:如果PL端逻辑实现了AXI4主接口,要检查是否发送了
AWVALID、WVALID、BREADY这些关键握手信号。AXI协议要求数据通道(W)必须在最后一个数据拍时置高WLAST,如果漏了这个信号,从机端会一直等待,事务永远不会完成。 - 确认DDR初始化成功:在SDK的FSBL阶段,串口日志中会打印DDR training相关信息。如果DDR初始化失败,PS端自身都不能访问DDR,更不用说PL端。可以通过简单的DDR读写测试确认PS端访问正常。
- 确认Cache一致性:如果是从PL读出来的数据全为0x00,反而是“有数据”的表现——读到的可能是DDR上电初始值或旧数据,而不是PL刚写入的数据。先做Flush,再做Invalidate,再重新读取对比。
4.3 排查链路二:DDR training失败该怎么办
DDR training失败时的现象一般是串口打印类似DDR init failed或者系统卡在FSBL阶段无法进入应用。
排查步骤:
- 确认板卡上DDR颗粒的型号、位宽、rank数量和Vivado中PS配置一致。尤其是某些开发板用的是DDR3L(1.35V),某些老板子用DDR3(1.5V),配置错了training必失败。
- 确认PS端的DDR引脚约束正确。ZYNQ的DDR引脚是固定引脚,不需要在XDC里指定,但DDR芯片的型号选择必须和实际颗粒一致。如果Vivado中没有完全一样的型号,选择一个时序参数接近的型号,并做必要的时序修改,但这一步风险较大,不推荐新手操作。
- 确认电源和时钟。DDR VCC、VCCIO、VTT电压是否正常,PS_PLL时钟源是否正常。用示波器量一下DDR的CLK有没有输出。
- 检查DDR时钟频率配置。ZYNQ PS支持DDR3最高到1066MHz(数据速率),但具体取决于PS PLL配置。如果PS端DDR PLL配置过高,颗粒体质跟不上,training可能偶尔失败,表现为上电后有时能启动有时不能启动。这种情况可以适当降低DDR频率,或者检查终端电阻和走线质量。
如果板子是自行设计的,DDR走线不等长、阻抗不匹配、参考层不连续,都可能导致training失败或者高低温环境下不稳定。这类硬件问题不是软件能完全解决的,软件上能做的是适当降低频率、增加时序余量。
4.4 排查链路三:不带DDR的ZYNQ如何用OCM加载
确实存在一些极简的ZYNQ板卡,没有外挂DDR,完全靠片内OCM(On-Chip Memory,256KB)运行程序。这时候PS端的启动流程、地址映射都不同,PL端AXI_HP访问DDR的路径也没有了。但依然可以跑裸机程序,只是要特别注意:
- BootROM阶段,FSBL会把应用程序加载到OCM中运行。OCM地址范围是
0x00000000到0x0003FFFF,大小256KB。 - 如果应用程序镜像超过256KB,加载会失败。解决方案是裁剪BSP、减小程序体积,或者把程序分阶段运行。
- PL端如果还需要访问存储器,只能访问OCM。OCM本身也在PS端地址空间中,PL端通过AXI_HP一样可以访问,但是地址不在DDR区,而是OCM区。
- 这种情况下Cache策略要更小心,OCM所在的地址空间映射在正常内存区,依然涉及Cache一致性问题。
在没有DDR的系统中,调试AXI访问存储器的流程和DDR类似,只是把目标地址换成OCM地址。这个场景特别适合验证PL端AXI主接口逻辑的正确性,因为OCM的时序比DDR简单,也不需要DDR training,问题定位更快。
4.5 排查链路四:PL写DDR后CPU读数据不一致
当PL端通过AXI DMA写入DDR后,CPU通过普通C代码(比如数组访问、memcpy)读取时数据不一致,原因几乎都是Cache一致性问题。
一个非常隐蔽的情况是:CPU第一次读取时数据是正确的(Cache miss,从DDR读取),所以读到PL写的新数据。第二次读取同一个地址时,Cache命中,读到的还是第一次的数据。如果PL端不断更新这段内存,CPU拿到的始终是旧数据。这种“看起来偶尔对、偶尔错”的现象非常迷惑人。
应对方法:
- 在每次PL端写完后,CPU读取前执行
Xil_DCacheInvalidateRange。 - 在每次CPU写完后,PL端读取前执行
Xil_DCacheFlushRange。 - 调试阶段可以先用
Xil_DCacheDisable()关闭D-Cache,排除Cache问题后再恢复,并逐个检查Cache维护调用是否正确。
4.6 一个真实的调试案例:AXI突发写入偶发数据错乱
这个案例值得单独拿出来讲,因为它暴露了AXI时序中的一个细节问题。
PL端逻辑实现了一个AXI4写主机,向DDR连续写入递增数据。轮询方式校验时发现,前几KB数据完全正确,但跑到某个位置就会出现连续多个数据错乱,再往后又恢复正常。最初怀疑是DDR控制器错误,但同样数据量用PS端写一遍完全没问题。
后来用ILA(Integrated Logic Analyzer)观察PL端AXI写通道的时序,发现了一个问题:PL端的写数据通道在WLAST拉高后,下一拍立刻发送了新一次突发传输的第一个数据,WVALID 保持拉高,中间没有插入任何空闲周期。这本身在AXI协议上是允许的,但从机的写缓存处理不过来时,可能造成数据不能及时吸收。真正的根因是:PL端逻辑在突发之间未处理BVALID/BREADY握手完成之前就开始了下一笔写传输,而AXI协议要求下一笔突发传输的地址(AW),可以和当前突发的数据(W)流水,但数据拍(W通道)必须严格顺序发送,不能跨越不同突发乱序。我的逻辑中W通道在突发边界上出现了重叠,导致数据被错误关联。
修复方式很简单:在W通道逻辑中,遇到WLAST && WREADY时,强制等待拍,同时确保AW通道已经在BVALID && BREADY之后才允许新突发开始。具体实现里加了一个状态位,保证相邻两个突发之间有清晰的边界。
这个问题给到大家的经验是:AXI协议的四个通道(AW、W、B、AR、R)在逻辑实现时,不能只看单通道时序,必须盯着跨通道的握手关系。很多自己手写AXI主机的工程师都会踩到类似问题,而且纯功能仿真不一定能发现(因为仿真环境的从机模型对协议容忍度较高),一旦上板和真正的DDR控制器交互,问题就暴露了。
5. SDK驱动调试的辅助手段与常见误区
5.1 用ILA实时观察AXI总线交易
调试PL端AXI逻辑时,ILA是必备工具。在Block Design中,在AXI接口上添加ILA即可,Vivado会插入探针逻辑。
操作步骤:
- 在Block Design中,右键点击需要调试的AXI接口,选择
Debug。 - 综合后打开Implemented Design,在
Setup Debug中确认要采集的信号,设置采样深度(一般采集4096或8192个时钟周期)。 - 生成Bitstream,在SDK中上电运行,通过Hardware Manager连接目标板。
ILA可以抓到AW、W、B、AR、R各个通道的波形,确认事务类型、地址、数据、突发长度、握手信号是否正常。在排查数据错乱或事务挂起问题时,ILA比仿真更直接,因为它检测的是实际板卡上的DDR控制器行为。
这里有个技巧:ILA的采样时钟要选择和AXI接口工作时钟一致(通常就是FCLK或PL逻辑时钟),否则采到的信号会有亚稳态风险。采样深度不要一次开太大,4K~8K够用,开太大会增加资源消耗和布线压力。
5.2 利用SDK的Xil_DCache统计和调试宏
Xilinx提供了xil_cache.h中的多个函数,除了Flush和Invalidate,还有Xil_DCacheEnable()、Xil_DCacheDisable()、Xil_DCacheSync()等。调试Cache问题时,可以临时禁用D-Cache,验证DDR读写是否正确,排除Cache因素后再逐步恢复。
另一个容易忽略的点:Xil_DCacheInvalidateRange和Xil_DCacheFlushRange内部是用mcr协处理器指令操作Cache,执行时间随操作数据量线性增长。如果对超大缓冲区(比如几MB)做Invalidate,会消耗不少CPU周期,影响实时性。工程上要尽量缩小需要维护Cache的区域,而不是整片内存随便一套。
5.3 误区:认为AXI DMA操作DDR不需要Cache维护
AXI DMA的SG描述符本身可能放在内存中,SDK中通过XAxiDma_BdRing等接口创建描述符时,底层会通过Xil_DCacheFlushRange来维护一致性。如果你自己实现SG描述符管理,一定要记得Flush描述符。另外,DMA搬运的数据缓冲区,如果是CPU写入后由DMA读出,需要Flush;如果是由DMA写入后CPU读出,需要Invalidate。这和PL端逻辑访问DDR的Cache维护规则完全一样。
5.4 误区:看到DDR地址乱码就怀疑DDR颗粒损坏
调试中经常发现数据错乱,第一反应是DDR颗粒坏了。实际上DDR颗粒损坏的概率很低,更多是以下原因:
- Cache一致性问题导致数据是旧的。
- AXI地址不对,访问到了错误区域。
- PL端AXI逻辑跨突发乱序。
- DDR电压不稳或温度过高(不常见,但存在)。
建议按照“PS端先自测DDR、再加入PL端AXI、最后加DMA”的顺序逐步排查。这个顺序能让问题边界清晰,避免一次引入太多变量。
具体做法:
第一步,在SDK中跑一个简单的DDR读写测试(用Xil_In32/Xil_Out32即可),确认PS端访问DDR正常。
第二步,在Block Design中只连PL端自定义逻辑,让逻辑向DDR写一个固定模式(如递增序列),SDK轮询完成后校验。
第三步,接入AXI DMA,测试搬移和中断。
每一步都验证通过再进入下一步,基本能很快锁定问题区域。
6. 实用经验补充:从Vivado到SDK的完整流程优化
6.1 地址分配的一点建议
在Address Editor中给PL端主机分配DDR地址时,建议不要在DDR最低地址(0x00100000)往下密排。因为FSBL、应用程序镜像、DDR堆栈往往也从低地址区开始运行,如果PL端写的缓冲区覆盖到了已使用的地址,轻则数据覆盖,重则程序崩溃。
一般做法是,把PL端buffer分配在DDR的高地址区域,比如7020开发板DDR容量为1GB时,可以把PL缓冲区放在0x3F000000往后,只占用最后16MB。这样即使程序镜像和堆栈在低地址区,冲突概率也最小。
6.2 内存屏障的补充
在多核(Cortex-A9双核)场景下,一个核做Cache维护操作后,另一个核不一定立刻看到效果。ZYNQ的裸机AMP(非对称多处理)场景中,往往需要配合dsb(数据同步屏障)和isb(指令同步屏障)指令。Xilinx提供的Xil_DCacheFlushRange内部其实已经包含了必要的屏障操作,但如果你直接使用汇编优化代码,一定要记得加。
在单核裸机开发中,通常不需要显式添加屏障,因为编译器和处理器的乱序执行没有复杂到需要额外处理的程度。但一旦使用多核,这个坑就非常深。
6.3 双核AMP场景下的共享内存
如果PS端两个核分别跑裸机程序,通过共享DDR内存通信,此时Cache一致性维护就更复杂。一个常见的方案是:
- 每个核都关闭D-Cache,牺牲性能换取简单正确。
- 或者使用
Xil_SetTlbAttributes将共享内存区域设置为非Cacheable(不缓存),这样CPU访问直接走DDR,PL端通过AXI访问DDR也不会有一致性问题。
具体代码:
#define SHARE_MEM_BASE (0x20000000) #define SHARE_MEM_SIZE (0x1000) // 将共享内存区域设为非Cacheable Xil_SetTlbAttributes(SHARE_MEM_BASE, 0x0);将区域设置为非Cacheable后,CPU的读写都绕过D-Cache,PL端和CPU看到的数据始终一致。代价是该区域的访问延迟会变高,但因为DDR本身延迟就不小,这个代价通常在可接受范围内。这个做法在共享内存通信场景中非常简单可靠,推荐使用。
6.4 波形与Data Viewer的配合使用
在SDK调试界面里,可以通过Memory窗口直接查看DDR某地址的数据。配合ILA抓到的AXI波形,可以快速判断“数据到底有没有写入DDR”。如果ILA显示W通道已经发送成功(WREADY为高,WLAST出现),但Memory窗口显示的数据不对,那么基本就是Cache问题或者地址映射问题。
一个小技巧:在SDK的Memory窗口中,勾选Cache选项后再查看,可以看到Cache中的数据和DDR中的真实数据的差异。这比反复做Invalidate测试效率高得多。
6.5 关于ZYNQ中DDR4的问题
7020只支持DDR3,但ZYNQ UltraScale+系列支持DDR4。如果你用的是UltraScale+,需要注意的是:
- DDR4的初始化时序和DDR3差异很大,PS端地址映射也不同。
- DDR4的
VREF校准、ZQ校准、ODT配置都是由PS端控制器自动完成的,不需要PL端干预。 - Vivado中PS配置界面会显示DDR4颗粒的型号选择,务必选对。
- PL端的AXI访问DDR的流程和7020大同小异,Cache一致性问题依然存在。
对DDR4不太熟的朋友,可以先搜一下“zynq ps端ddr4降速怎么配置”,在UltraScale+的PS配置中调整DDR频率即可,不需要改硬件。低速率有利于DDR training成功率,特别是在定制板走线质量一般时。
7. 最后的几个建议
从Vivado建工程到SDK驱动跑通,这个流程其实不复杂,但坑全是细节。把几个关键点再重复一遍:
第一, 理解地址映射。PL端访问DDR是通过PS端HP接口,地址是PS地址空间的映射,不是DDR物理地址。Vivado的Address Editor一定要检查。
第二, Cache一致性是必答题。在PL和CPU共享DDR数据时,Flush和Invalidate缺一不可。裸机工程调试阶段如果图省事,可以先关D-Cache跑通流程,但正式设计里一定按正确姿势处理。
第三, 用ILA抓AXI波形是最高效的排除手段。所有“偶发错乱”的诡异问题,最终都是通过波形定位的。仿真过不代表的板卡一定正确,AXI协议在真实验证环境下的容忍度完全不同。
第四, 调试顺序遵循“PS自测DDR — PL简单AXI写 — 加入DMA — 加入中断”的渐进路线。每次只引入一个变量,问题定位会快很多。
如果这篇文章能帮你少走几个弯路,那就很值得了。ZYNQ的存储系统设计是PS和PL协同工作的基石,把AXI4 DDR读写这块啃透,后面做图像采集、高速数据记录、信号处理都顺理成章。祝调试顺利。
本文还有配套的精品资源,点击获取
