很多人在ZYNQ上跑DDR3第一步都是先在PS端用Linux或裸机把内存跑通因为SDK里面现成的驱动和DDR配置已经有了DDR颗粒的参数、地址映射、时序训练这些都被工具链处理好了基本上几步就能进系统。但一旦到了PL端情况就完全不一样MIG核要自己配、AXI通道要自己接、初始化校准信号要看读写时序要自己抓问题一下子全部暴露出来。这篇东西就是针对这个场景写的——PL端DDR3读写测试到底要怎么搭工程、怎么写测试逻辑、怎么排查问题把我调试过程中的实际操作和踩坑经验都放进来适合正在学ZYNQ、或者拿到了新板子准备验证DDR3焊得对不对的工程师参考。1. 为什么DDR3测试要放在PL端架构差异和验证目标1.1 ZYNQ里DDR3的两条通路PS端和PL端的本质区别ZYNQ-7000系列在硬件上把处理器系统和可编程逻辑放在同一个芯片里DDR3接口则是硬件上复用的一组引脚既可以接给PS端的DDR控制器也可以通过MIG IP核接到PL端。从物理上看DDR3颗粒是同一片或者同一组但从逻辑上看PS和PL是两套互相独立的控制器两者共用引脚但不共用数据通路。这意味着PS端的内存测试通过只能说明DDR3颗粒本身能工作、PCB焊接到位、供电和参考电压正常不能说明PL端的MIG控制器配置正确更不能说明PL内的AXI逻辑能和DDR3正常交互。很多开发板出厂测试固件都只是验证了PS端DDRPL端读写测试往往需要自己写。再加上PL端的DDR3接口和PS端在管脚分配上经常有部分重合如果你在设计中同时用了PS和PL就必须仔细核对引脚分配确认PL端MIG生成的引脚约束没有和PS端占用的引脚冲突。也就是说PL端DDR3读写测试本质上是在验证三件事MIG IP核的参数配置是否和实际DDR3颗粒匹配、PCB布线和信号完整性在高速读写下是否可靠、用户逻辑和MIG之间的交互时序是否正确。1.2 PL端读写测试到底在验证什么从测试角度讲DDR3读写测试不是写几个数读出来对比就完事的。DDR3是DDRDouble Data Rate接口时钟上升沿和下降沿都在传数据内部还有预取prefetch和bank管理机制地址和数据的映射关系远比SRAM复杂。真正意义的PL端DDR3测试至少要覆盖以下内容MIG初始化校准是否成功DDR3上电后需要经过复位、等待时钟稳定、训练、校准等多个阶段最终由init_calib_done信号标识完成。这个信号拉不拉高是判断MIG配置是否正确的第一关。连续地址的写读一致性写入一组递增数据再逐个读回比较可以验证地址线、数据线连接和内部bank切换是否正常。突发传输Burst的完整性DDR3本身就是突发传输设计MIG的AXI4接口支持不同长度的burst。测试中必须让burst长度和实际配置一致否则容易出现数据重叠或错位。跨时钟域处理的稳定性MIG的用户接口工作在ui_clk时钟域而用户逻辑可能工作在另一个时钟域。中间如果没有处理跨时钟域偶发错误是必然的。长时间压力写读高速信号在长时间运行后会有热噪声、电源纹波等问题短时间测试通过不代表长时间稳定。我见过不少人直接把MIG的example design跑通就认为DDR3没问题了其实example design只是验证了控制器本身并没有覆盖你自己写的用户逻辑和引脚约束。后面我写的操作步骤都是围绕一个最核心的目标让读者看到一个从RTL设计到上板抓信号的完整闭环。2. 测试方案选型MIG IP AXI4是最稳的组合2.1 MIG IP核配置里最容易出错的几个参数PL端DDR3控制器在Vivado里一般直接用MIGMemory Interface GeneratorIP核生成。配置界面看起来复杂但真正影响能不能跑通的就那么几个参数。选择芯片和封装ZYNQ-7000的DDR3引脚在PS和PL之间是有复用的量产板上通常已经确定了哪个引脚接PS、哪个接PL。如果选错封装或者没有勾选正确的DDR接口后面的引脚约束会完全对不上。DDR3颗粒型号或参数Vivado里可以通过搜索颗粒型号来预填参数也可以自己手动填列数、行数、bank数、数据位宽等。我建议尽量选具体颗粒型号比如常见的MT41K256M16TW-107因为内存厂商的参数文档对参考时钟频率、CL值、tRCD等参数定义得很明确就算手填也容易核对。手动填的话一旦行地址数或列地址数和实际颗粒不匹配读出来的数据就会完全错乱而且MIG初始化可能还会报错。数据位宽一般开发板DDR3是16位或32位。注意AXI4数据总线的位宽和DDR3颗粒数据位宽的关系MIG内部会自动处理转换但会直接影响地址最低几位怎么用。如果你在代码里直接操作32位数据却接了个16位颗粒的MIG地址对齐就会出问题。频率和时序参数DDR3的接口时钟频率取决于颗粒的Speed Grade和PCB布线质量。不要一上来就按最高频率如DDR3-1600配很多时候板子布线质量不够高频下初始化都过不去。我调试新板子习惯先按保守频率比如400MHz或533MHz的接口时钟等跑完硬件测试再逐步抬高。配置完成后Vivado会生成一个包含DDR3读写控制器的example design里面有MIG原语实例化、时钟复位处理、引脚约束等。我的经验是不要直接复制example design的用户逻辑而是只看它的顶层接口和引脚约束怎么写的然后自己写用户逻辑。理由很简单example design里大量的模块都是为仿真和演示服务的直接拿来改成自己的设计容易把时序约束也带歪。2.2 AXI4 Full还是AXI4-Lite读写测试场景下的取舍MIG在PL侧对外提供Native接口和AXI4接口两种选择。做读写测试最简单的方式是用Xilinx官方给出的AXI4接口因为MIG IP内部已经为你实现了从AXI4请求到DDR3命令调度的转换不用自己跟bank管理打交道。但AXI4也分Full和Lite。**Lite接口没有Burst传输地址和数据通道都不支持发送多个节拍的数据每次只能读写一个字节或一个字。**对于简单的读测试Lite够用但如果你想测DDR3的突发能力和高速读写稳定性Lite显然不合适。所以PL端DDR3测试我一般直接上AXI4 Full设置为不定长Burst用户逻辑通过状态机控制burst长度这样既贴近真实应用又能验证DDR3吞吐率和时序。如果只是想把DDR3当SRAM来用做一个简易接口那不用AXI4也是可以的。MIG IP还提供了Native接口本质上是一堆类似SRAM的信号上手更快。但问题是Native接口需要自己去处理不同延迟、读数据返回的时序而且后续如果想在系统中通过AXI总线访问DDR3还是要把Native逻辑包一层AXI转换器。对于学习阶段和实际项目我都推荐直接用AXI4接口因为这套接口在ZYNQ PL端应用极广学会之后很多IP核的互联都能顺手搞定。2.3 为什么我推荐自定义一个轻量级AXI4读写状态机MIG IP自带的example design中有一个mig_axi_master模块可以发起写读操作。但那个模块为了演示通常包含了多个测试数据模式、随机地址跳变、状态打印等功能代码量非常大新手看起来容易晕。我的做法是写一个精简的AXI4读写状态机只做三件事初始化后等待MIG校准完成按预设地址写入一轮递增数据然后按同样的地址读回并比对。这个状态机可以拆成两个模块一个用作AXI4 Full Master另一个用作数据比较逻辑。整体思路是状态机在IDLE等待init_calib_done信号拉高然后进入WRITE状态。写状态中把写地址通道的awaddr、awlen、awsize准备好同时把写数据通道的数据准备好等待握手完成后进入读状态。读状态中发起读请求然后等待读数据通道返回数据每拍数据和内部生成的期望数据比较不匹配就拉高一个error_flag。这个状态机的代码量大概在一百行左右比example design简单得多而且方便加ILA观察信号后续在真机上调试也不需要去理解不相干的逻辑。后面我在3.2节里会给出一个可以直接用的简化版本。3. 搭建测试工程MIG实例化到ILA抓信号全流程3.1 新建工程和例化MIG核在Vivado中新建一个RTL工程目标器件选你手上的ZYNQ型号然后从IP Catalog中添加Memory Interface Generator (MIG 7 Series)IP核。进入配置界面后重点设置以下内容Component Name改成容易识别的名字如ddr3_mig。Mode and Interface选择AXI4。如果选Native后面的接口信号就要自己处理。Controller Options里选择DDR3以及颗粒型号或手动设置参数。Memory Options中确认数据位宽、地址位宽、颗粒数量等和开发板原理图配合。注意这里“地址位宽”通常包括Row、Column和Bank三部分。Clock Options设置参考时钟频率和系统时钟频率。参考时钟一般来自板上的差分晶振或PS端的时钟输出建议频率选200MHz这是MIG在多数ZYNQ板卡上默认支持的参考时钟频率。AXI Options可以设置AXI数据位宽为32bit或64bit地址宽度会自动生成也可以勾选AXI窄突发支持。配置完成后Vivado会生成IP核文件同时生成一个示例工程。我们不需要改动MIG内部逻辑只需要把MIG的顶层信号引出并在顶层模块中调用。MIG IP生成后还要检查ddr3_mig相关的时钟约束和引脚约束。Vivado通常会为MIG生成一个ddr3_mig_pins.xdc文件里面根据你选的器件和封装自动分配了DDR3引脚这个文件是固定的不能轻易改。如果开发板的DDR3引脚和Vivado自动分配的引脚有出入需要手动修改所以买开发板一定要看原理图。我自己第一次就是直接用了自动约束结果因为开发板是从PS端复用引脚PL端MIG没有对应管脚最后不得不重新分配。3.2 AXI4读写状态机的Verilog简化实现这里给出一个“能跑”的读写状态机设计思路不贴完整大段代码因为完整代码通常还要配合你自己的架构。核心逻辑主要分三块。写通道发起逻辑状态机在收到写命令后先拉高awvalid给出awaddr一个32位递增的地址awlen设置为指定burst长度减1awsize设置为3对应8字节如果数据位宽是64bit或2对应4字节32bit数据位宽。等到awready拉高再拉低。写数据通道则要提前把wdata准备好拉高wvalid每个wready有效的时钟周期送一个数据直到burst长度为止。最后一个数据时拉高wlast。然后等待bvalid有效写操作完成。读通道获取逻辑读操作的发起过程和写通道类似先发araddr和arlen然后等待rvalid有效和rready握手。每个时钟周期读出一个数据直到收到rlast这次读操作就算结束。需要注意的是DDR3读延迟不是固定的MIG会根据时序在内部调整所以读数据返回时间和你的命令之间可能有几个周期的延迟状态机必须做等待不能假设数据在下个周期一定会到。数据比较逻辑我习惯在读数据通道上加一个期望数据生成器用同一个地址和burst序号计算出期望值比如expected addr burst_index然后每个节拍比较一次。如果不等就将错误计数器加一并且把错误地址锁存下来方便ILA观察。实现时还要处理一个跨时钟域的问题。MIG的ui_clk是AXI接口时钟状态机运行在这个时钟域即可。但如果你在顶层还用了另一个时钟跑用户逻辑建议在状态机前加一个异步FIFO把写入数据和地址同步过去读回的数据也用FIFO同步回来。不要图省事直接打两拍因为DDR3的数据量和速度决定了打两拍只能用于控制信号不能用于数据。3.3 使用ILA观测关键信号Vivado的ILAIntegrated Logic Analyzer是调试PL端DDR3的最佳工具。用ILA之前先要给被测信号分好组不要让ILA的采样深度被不重要的信号浪费。我在PL端DDR3测试里加的ILA信号一般是init_calib_done、状态机当前状态、awaddr和araddr、wdata和rdata、error_flag、写读完成计数。如果采样深度够建议把读写数据都加上否则还是优先加地址和错误标志。例化ILA时可以设两个探针探针0用来观察初始化阶段触发条件设为init_calib_done上升沿这样上板后能够看到MIG初始化时序。探针1用来观察读写数据阶段触发条件可以是写状态结束时或error_flag上升沿。这样正常跑数据时我们能抓到读回的数据流出错了也能第一时间定位在哪一拍出错。ILA的采样时钟直接接MIG的ui_clk这样看到的信号都是同步于AXI时钟域的。如果你接到别的时钟域比如系统时钟可能抓到亚稳态或者看到信号跳变不同步非常难分析。这一点我踩过坑后面专门再讲。3.4 综合实现前的约束和检查代码写好后需要做几项常规检查引脚约束MIG生成的DDR3引脚约束不能删用户自己加的LED、按键等引脚也必须在XDC中明确分配。时钟约束如果用户逻辑使用了独立的时钟需要创建create_clock如果和MIG共用ui_clk通常不需要额外约束但必须确保用户逻辑在综合时能把时钟域分开。复位逻辑MIG的复位信号最好在外部做一个异步复位同步释放电路或者在初始化完成后解除复位。很多老手在这块吃了亏——一直拉高复位MIG永远不会完成校准。综合实现完成后生成Bitstream然后打开Hardware Manager连接开发板。下载bit文件之后首先观察ILA里init_calib_done有没有拉高。如果没有问题大概率出在电源、引脚约束或DDR3配置上。如果拉高了再触发读写状态机观察数据。这一步特别提醒连接ILA可能影响时序收敛因为ILA会插入探针逻辑降低布局布线质量。所以如果你发现ILA抓到的数据偶发错误可以先把ILA去掉仅用板载LED显示error_flag和完成标志验证是否仍然是偶发错误。如果LED显示正常而ILA显示错误就可能不是DDR3本身的问题而是ILA时序对设计产生了扰动。4. 实测中的坑初始化失败、数据错位和偶发读回异常4.1 初始化校准信号拉不高的排查链路我拿到一块新板子第一次上电跑MIG最常遇到的就是init_calib_done一直低。这时候不要去乱猜按下面的顺序排查第一步确认MIG复位有没有释放。MIG的复位默认是高电平有效要拉低才释放。检查外围逻辑是否在初始化完成前反复拉高复位。如果你用了proc_sys_resetIP注意它的dcm_locked信号是否有效以及外部时钟是否稳定。第二步检查MIG的参考时钟和系统时钟是否真的送进来了。用ILA观察MIG内部时钟信号或者用util_ds_buf把差分时钟转成单端时钟再接到一个GPIO上通过示波器量。有时候PCB上参考时钟电路设计得比较脆弱示波器探头一上去时钟就停了但其实不是设计问题而是测量方式不当。最好在板上保留一个测试点用差分探头测。第三步检查引脚约束和实际硬件是否一致。MIG生成的XDC文件里会列出所有DDR3引脚的bank、IO标准等。特别是VREF引脚的设置很多ZYNQ开发板用内部VREF但MIG默认可能要求外部VREF不一致会导致DDR3颗粒的输入阈值不对初始化训练时无法锁定。这个问题在原理图设计不规范的小板子上非常普遍。最简单的办法是查MIG配置中System VREF的设置把它调整成开发板实际支持的选项。第四步如果以上都没问题把MIG的时钟频率往下调重新生成IP重新综合实现。有很多情况是板上布线质量不行高频率下初始化训练不通过。我遇到过一次在533MHz下初始化偶尔不过降到400MHz后就一直稳定说明高频时序裕量不足不是逻辑问题。4.2 Burst长度和地址映射导致的数据错位初始化过了但读回的数据和写入的数据对不上这个问题排查起来比初始化失败更费时间。先说我遇到的一个典型案例我配置了32bit的AXI数据位宽DDR3颗粒也是16bit但MIG内部会把两个16bit拼成32bit。我在代码里让地址每次加1然后期望数据是地址值结果读回的数据总是隔几个数错位一次。后来发现AXI4协议里的地址是按字节寻址的32bit数据的对齐地址最低2位必须为0。我每次awaddr加1等于是在2字节边界上访问MIG接受但会把地址折算到实际颗粒的word地址上导致读写同一地址时数据错位。解决办法是规定地址递增步长为数据位宽除以8即32bit位宽时步长464bit位宽时步长8。不要用地址本身直接作为期望数据而应该用写入数据的序号来生成期望值这样即使地址映射复杂只要读写地址一致数据比对仍然有意义。另外AXI burst长度和地址递增模式也容易出错。AXI4支持INCR模式即突发内地址不断累加但MIG对突发地址的处理可能受DDR3行、列、bank结构的影响地址累加穿越行边界时MIG需要额外几百个周期的延迟。如果状态机在同一个burst内没有等够延迟会覆盖前面还没写完成的bank导致数据丢失。这个在时序上不会出错因为AXI ready/valid协议有反压但会让状态机变得很慢有时会超时。我见过一个案例是burst长度为256时DDR3因为要频繁换行导致吞吐率急剧下降数据错位偶发。最简单的验证方法是把burst长度改成8或16先排除长burst对地址映射的影响。4.3 偶发读回错误时钟域和FIFO深度的平衡整个测试工程跑起来之后遇到的最头疼的问题不是完全错而是“偶尔错”。可能写了10次数据前9次都正确第10次读回来有一个字节不对。这种偶发错误往往和DDR3颗粒本身关系不大多半出在接口跨时钟域处理上。我一开始用的简单状态机把用户时钟直接接成MIG的ui_clk这样确实没有跨时钟域。但后来为了模拟真实应用把用户逻辑时钟改成独立的100MHz再用FIFO和MIG交互问题就来了。因为FIFO深度不够当读数据通道返回到达率高于用户逻辑处理率时FIFO被写满之后数据就丢了。由于DDR3返回数据的延迟是动态的并不是固定周期所以表现出来的就是偶发多丢一拍。解决办法有两个方向。一是加大FIFO深度确保读返回数据能缓冲但这治标不治本。二是让用户逻辑的读请求速率永远低于DDR3能够返回的速率并在FIFO写满时不发起新的读操作。我在状态机里增加了一个fifo_prog_full信号当FIFO剩余空间小于本次burst长度时就停在IDLE这样彻底避免了溢出。这种偶发错误在只用ILA看单次抓拍的时候很难定位因为你看到的那次数据可能恰好是正常的。我建议在RTL里加一个连续错误计数寄存器如果连续出错超过一定次数比如16次就关闭测试并点亮错误LED然后通过ILA的触发条件只抓错误之后的数据。这样比随机触发靠谱得多。4.4 信号完整性对测试成功率的影响PL端DDR3读写测试最终考验的其实是PCB设计。MIG初始化训练能过不代表长时间读写稳定短时间稳定跑完也不代表高温下能稳定跑。和热词里提到的“ddr3布线规则”直接相关这里把常见的硬件层面的坑也提一下因为软件工程师如果板子Layout有问题再怎么调逻辑都没用。DDR3布线最核心的是等长、阻抗和参考平面。地址/控制线和数据线的分组等长要求不同DQS与对应的DQ信号之间必须做差分等长时钟差分对也要严格控制。很多开发板为了节省过孔数量DDR3布线并不规范导致不同频率下读写裕量不同。在测试端我们可以通过调整MIG的Read DQS Training和Write Leveling选项来改善时序裕量但最终稳定性和PCB是直接挂钩的。如果你发现同一套bit文件在不同温度下偶发错误大概率是信号完整性问题自适应均衡equalization又没开。这时可以尝试把MIG的nCK_per_clk调低或者降低DDR3工作频率保证硬件余量。对做系统集成的朋友来说记住一条经验PL端DDR3测试是硬件设计最灵敏的试金石——如果连测试都过不了软件层面的优化都是徒劳。5. 让测试更有说服力压力测试方法和DDR3/DDR4的对比5.1 从功能测试到压力测试数据模型怎么选简单的递增数据读写只能验证基本通路要评估DDR3稳定性必须上压力测试。压力测试分两个维度地址覆盖和数据模型。地址覆盖方面不要只在低地址区域跑。DDR3内部有多个Bank每个Bank有对应的行和列不同Bank之间的切换延迟、激活和预充电策略都不同。建议写一个伪随机地址生成器让地址在整个DDR3空间中随机跳变覆盖行冲突和各种边界情况。在实现时用LFSR生成地址地址可以限制在某个范围内比如从0到2^24-1之间跳变确保会频繁跨越行和Bank边界。数据模型方面除了常见的递增/递减还需要跑以下几类数据全0、全1、5A5A、A5A5用于验证数据线是否存在短路或断路。尤其全0和全1能快速暴露DQ线在PCB上的虚焊问题。地址反走将地址线的每一位取反作为数据比如地址0写入0xFFFF_FFFF地址1写入0xFFFF_FFFE。这样能快速发现地址线和数据线之间的串扰或错位。March C算法这是内存测试中最经典的算法能有效发现固定故障、转换故障和耦合故障。虽然实现起来比递增数据复杂但跑一轮就能覆盖绝大多数颗粒级故障。我在实际项目里会把压力测试分成两个阶段先跑一轮March C再跑一轮随机地址随机数据。随机数据通过LFSR生成读回时用相同LFSR种子重新生成期望值进行比对。这种方式比保存整个数据块更省资源适合PL内部逻辑实现。测试结果可以通过一个计数器累计错误次数测试中途发生错误时把错误地址锁存下来通过串口或ILA读出即可。5.2 DDR3布线规则对测试结果的直接映射热词里出现的“ddr3布线规则和实例”和PL端读写测试的关联非常大。常见DDR3布线规则包括数据组DQ、DQS、DM与地址/控制组尽量分开走线每组内保持等长误差控制在±25mil以内。差分DQS与时钟走差分线在源端加串联电阻或端接电阻。VREF电源必须干净去耦电容尽量靠近颗粒电源引脚。数据线和地址线不要跨越分割的参考平面否则会造成阻抗不连续。这些规则如果不满足在MIG初始化时可能还有一定的自适应能力但到了高负载读写或者温度变化时就会暴露。你可能会发现一个简单读写测试在低温环境下通过率很高到了高温比如长时间运行后芯片发热就开始出错误。这就是因为布线导致时序裕量不足温度影响了信号上升沿和延迟。如果你手里的板子已经做出来了布线没法改那测试策略上要做的是在不同频率和不同温度下分别跑压力测试记录最大频率下的出错率。这可以为最终产品挑选合适的DDR3工作频率提供依据也可以作为硬件改版的参考。总的来说PL端DDR3测试不只是“能不能用”更是“稳不稳定”的重要标尺。5.3 从DDR3迁移到DDR4测试思路要改变什么很多新项目已经在用DDR4尤其是ZYNQ UltraScale平台。热词里有“ddr4读写测试”这里也顺便做一个对比。DDR4和DDR3在PL端测试上有很多不同参考电压和电压标准不同DDR4使用VDDQ1.2V和VREFCA内部生成不再像DDR3那样需要外部精确的VREF。MIG配置时要注意IO标准选择比如DDR4通常用SSTL12DDR3则用SSTL15。频率更高训练机制更复杂DDR4引入CMD/ADDR训练、Vref训练等机制MIG IP内部自动处理但初始化时间更长对外表现就是init_calib_done信号出现得更晚。做测试时要有耐心不要在复位后立即判断初始化失败。Bank数量更多刷新周期管理不同DDR4每个Bank Group内部独立测试数据模型需要覆盖更多Bank切换场景否则很容易在某些特定地址组合下才出错。但核心测试方法是一样的就是用AXI4接口发起读写再用ILA或RTL逻辑比较数据。如果已经在ZYNQ-7000上把PL端DDR3测试这套流程跑熟了换到DDR4平台只是换一套MIG配置和引脚约束用户逻辑状态机完全可以复用。我刚换到DDR4时花时间最多的反而是DDR4颗粒型号选择不同颗粒公司对时序训练的支持差异比较大导致MIG初始化偶尔不过后来选了一款官方参考设计验证过的颗粒问题就消失了。对想在DDR3测试阶段就把基础打牢的朋友我的建议是不要只测完“能读写”就高兴一定要把压力测试和错误统计模块写进去这样后续换平台、换频率、换颗粒都会受益。最后再分享一个我自己的操作习惯上板测DDR3之前我会把MIG的AXI接口信号名字写在草稿纸上标清楚哪个信号属于哪个通道。调试的时候在ILA里分组观察比一个信号一个信号地翻要快得多。还有状态机的超时计数器一定要加万一DDR3卡在某一步计数器能让状态机自动复位否则板子就只能重新下载bit文件调试效率太低。这套方法我用到现在从ZYNQ到MPSoC都适用效率确实不错。