做了这么多年的FPGA开发,DDR几乎是躲不开的一块硬骨头。相信不少朋友的第一个高难项目,就是从“用FPGA往DDR里写点数据再读出来”开始的——图像采集、高速数据记录、片上数据库缓存,数据量一大,片内RAM就撑不住了,DDR基本成了标配外设。但这个内存颗粒确实难伺候:要按时初始化、要定期刷新、读写前要激活行、操作完要预充电,时序窗口用纳秒算,频率动不动几百兆,根本没有办法靠几条简单的逻辑去裸操作。好在主流FPGA厂商都提供了现成的DDR控制器IP,把物理层那些繁琐的细节全部收拢,给用户的是一个相对友好的标准接口,绝大多数情况下,这个接口就是AXI4。这篇博文,我想把“FPGA通过AXI4总线读写DDR”这个项目从前到后完整拆一遍,包括硬件选型、控制器IP配置、AXI4协议要点、状态机设计、地址映射以及调试过程中那些不写在文档里的坑,希望能给正在做或者准备做相关项目的朋友一些参考。
1. 先搞清楚三个角色:DDR颗粒、控制器和AXI4总线
1.1 DDR为什么这么难“直接玩”
DDR的全称是Double Data Rate SDRAM,核心特点是时钟上下沿都传输数据,所以同样频率下带宽是普通SDRAM的两倍。听起来是个加分项,但麻烦也在这:你操作它的时序逻辑,比平时操作Block RAM复杂得多。
一颗DDR颗粒内部,存储单元按Bank、行、列三层组织。你要读写某个数据,得先激活对应的Bank和行(ACT命令),等tRCD时间;然后才能发出读或写命令,紧接着按列地址连续收发数据;操作完了,还要预充电(PRE命令),等tRP时间;过一阵子还得做一次刷新(REF命令),防止电容漏电把数据弄丢。再加上tRFC、tREFI、tWTR、tRTW这些参数,每一个都是“硬性规定”,少了哪一步,数据就是错的。
这就像你去老式档案室查资料:不能直接推开档案柜拿文件,得先填写申请单(激活行)、等管理员找(tRCD)、拿到文件后还要归还登记(预充电),而且管理员每隔一段时间必须盘点全库(刷新)。你要自己写逻辑去伺候这些流程,还要在几百兆赫兹下保证不出错,工程量非常大,而且很容易把时序做崩。
还有一个更现实的问题:DDR的工作频率很高。DDR3-1600的数据率是1600Mbps,物理层信号摆率、片内端接、时序裕量这些,靠FPGA普通IO是搞不定的,必须用专用的高速IO接口,配合校准、训练逻辑才能跑稳。这已经不是“写代码”能解决的问题了,而是芯片级、板级的设计难题。
1.2 控制器IP到底替你扛了多少活
所以,Xilinx和Altera(现在是Intel)都推出了现成的DDR控制器IP。Xilinx这边叫MIG,Memory Interface Generator;Altera/Intel那边叫EMIF,External Memory Interface。这些IP是经过了硅验证的硬核方案,内部包含了物理层(PHY)、时序训练、初始化、刷新调度、bank管理等完整逻辑。
你只需要做两件事:第一,在配置界面里告诉它“我用的什么颗粒、什么频率、数据位宽多少、工作在什么模式”;第二,把用户侧接口的信号按协议接对。MIG会自动完成DDR颗粒的上电初始化、DQS/DQ校准、读写训练,并在后台定期处理刷新请求。校准完成后,MIG会拉高一个信号告诉你“可以干活了”——Xilinx这边是init_calib_complete,Intel那边是mem_clk_pll_lock加local_init_done。
这里有个概念要理清:MIG的用户侧接口有几种选择,常见的有AXI4、Native(也叫UI接口,User Interface)。Native接口更接近控制器底层,信号多,控制灵活,但用起来麻烦;AXI4接口则是把这些信号封装成了标准总线,你只需要处理五个通道的握手,难度大大降低。现在的趋势是,新设计优先用AXI4接口,因为不仅兼容性好,而且后面接了其他AXI4外设(比如DMA、PCIe、图像处理IP)也方便直接互联。
1.3 AXI4总线:把“杂乱请求”变成“标准化快递”
AXI4是ARM公司定义的AMBA总线协议家族里的一员。它的核心思想很简单:读和写完全分离,各自成一套通道,互不干扰。完整的一套AXI4接口,分为五个通道:
- 写地址通道(AW):主机告诉从机“我这次要写到哪里、写多长”。
- 写数据通道(W):主机把数据发过去,可以拆成多拍。
- 写响应通道(B):从机写完后回复“写完了/写错了”,主机确认。
- 读地址通道(AR):主机告诉从机“我这次要读哪里、读多长”。
- 读数据通道(R):从机把数据返回,伴随最后一路的LAST信号。
每个通道都是独立的握手机制,用VALID和READY两个信号配合。大致规则是:数据源拉高VALID,表示“我这拍数据有效”;数据接收方拉高READY,表示“我这拍能收”。只有VALID和READY同时为高,这一拍才算真正传输完成。这个机制的好处是天然带反压,接收方忙不过来就拉低READY,整条链路不会丢数据。
在DDR读写这种场景里,AXI4最常用的特性是突发传输(Burst)。你可以一次申请读或写一整段连续地址,数据以固定节奏连续传输,从而大幅度提升带宽利用率。另外,AXI4还支持多个未完成请求(Outstanding),意思是我可以一口气发好几个读请求,不用等第一个读完再发第二个,控制器内部可以并行调度,效率明显提高。
所以,整个链路的角色就清楚了:DDR是最终的存储介质,MIG是围绕DDR的“管家”,AXI4总线是管理者和管家之间统一的标准对接口。你写的逻辑,本质上就是个AXI4主设备,负责根据业务需求发起读写请求。
2. 动手前的准备:硬件选型、工具链与MIG配置
2.1 板子和颗粒怎么选,直接影响你的工作量
做DDR项目,硬件选型是第一道坎。市面上的FPGA开发板,比如黑金、正点原子、Digilent、Xilinx原厂评估板,绝大多数都带了DDR颗粒。选板子时重点看三样:FPGA型号、DDR型号和数量、以及板级连接方式。
FPGA型号决定了你能用哪一代DDR、支持多大位宽和频率。中低端Artix-7、Cyclone V系列一般配DDR3,跑800-1066MHz;中高端Kintex-7、Virtex-7、Kintex UltraScale系列可以上DDR3/DDR4,甚至DDR4跑到2400MHz以上。Intel这边对应的是Arria、Stratix系列。开发板如果自带了DDR,厂家一般会提供例程和引脚约束,这能省掉很大一部分工作量。如果是自己画的板子,一定要核对FPGA厂商的引脚规划文档,DDR的引脚不是随便连的,误接会导致信号完整性问题,校准不过。
颗粒类型选择上,DDR3和DDR4的主要区别在于工作电压(DDR3是1.5V,DDR4是1.2V)、预取位数(8bit vs 16bit)、以及DQ/DQS的拓扑结构。DDR4的VREF内部生成、命令信号对差分等特性,对SI(信号完整性)要求更高,新手建议先用DDR3练手,调通整套流程后再上DDR4。
2.2 Vivado里配置MIG,关键参数一次说清
以Vivado为例,在IP Catalog里搜索MIG,双击打开配置界面。核心参数有以下几项:
- 控制器数量:MIG支持在一颗FPGA里例化多个独立的DDR控制器,每个控制器对应一组DDR引脚。一般开发板只有一组DDR,选1就行。
- DDR类型与型号:选择DDR3或者DDR4,然后填上颗粒的型号、容量、位宽。MIG会自动换算Bank、行、列地址位数。这里要注意填写的数据要与实际颗粒严格对应,搞错一个参数,初始化校准会直接失败。
- 数据位宽:常见的有8、16、32、64位。位宽越宽,理论带宽越高,但占用的FPGA引脚越多,PCB走线压力也越大。配合AXI4使用时,数据位宽直接影响后面的地址对齐关系,这一点特别重要。
- 时钟频率:这里指的是DDR物理时钟频率,比如DDR3可以选500MHz、667MHz、800MHz等。用户侧接口时钟(UI Clock)通常是这个频率的固定比值,比如DDR3 800MHz数据率的MIG,UI时钟可能是½频率,也就是400MHz。控制器频率选得越高,时序裕量越小,对PCB和电源的要求也越高,首次做项目建议先保守一点。
- CAS Latency、追加延迟等时序参数:这些参数在配置界面里会做成下拉框。MIG会根据你选的频率和颗粒型号给出推荐值,不建议轻易改动,除非你知道自己在做什么。
- 地址映射顺序:即ROW、COLUMN、BANK在地址总线上的排列顺序。默认通常是Bank-Row-Column,但不同业务场景可能会有讲究。比如图像处理时,如果希望连续帧存满后再换行,可能需要调整BANK和COLUMN的位置。第一次做,用默认即可。
配置完会生成一些文件,MIG会自动帮你设置好引脚约束、时序约束,还会生成一个详细的example design——这个例子太珍贵了,强烈建议先在example design上跑仿真和上板测试,确认整条链路没问题之后,再基于它去改自己的逻辑。
2.3 时钟、复位和跨时钟域:最容易出问题的三个点
DDR控制器IP会输出一组用户侧时钟和复位信号,比如Xilinx MIG的ui_clk和ui_clk_sync_rst。你的AXI4主控逻辑,必须老老实实跑在这个时钟域里,千万不要自作聪明用别的时钟去操作AXI4接口信号。
这里有个细节容易被忽略:ui_clk_sync_rst是高有效复位,而且这个复位信号在DDR校准完成前一直处于拉高状态。你的逻辑设计要把“复位释放”和“校准完成”两个条件都考虑进去。一般做法是把ui_clk_sync_rst反向后和init_calib_complete做与门,生成一个cal_done_and_rst_released信号,作为整个用户逻辑的全局复位释放。
跨时钟域问题也是重灾区。比如你的业务主时钟是200MHz,但MIG的UI时钟是400MHz,两边交换数据时,必须经过异步FIFO或者寄存器打拍同步。最简单稳妥的方案是使用Xilinx的FIFO Generator IP,一端接业务时钟,一端接UI时钟,数据自然就跨过去了。很多初学者直接在这两个时钟域之间用寄存器直连,出来的数据时对时错,排查了很久才发现是跨时钟域问题。
另外,MIG初始化需要时间,从FPGA配置完成到init_calib_complete拉高,最快也要几微秒到几十微秒。你的用户逻辑如果在校准完成之前就发起了AXI4请求,控制器根本不会响应,甚至可能导致总线挂死。所以一个质量合格的复位逻辑,必须等校准完成后再拉高用户逻辑复位释放信号。
3. 实操全流程:从零搭出一条AXI4读写通路
3.1 例化MIG并跑通example design
第一步,新建Vivado工程,选择你的FPGA型号。在IP Catalog中添加MIG IP,按上一节说的参数配置好。生成IP后,在Sources面板里右键MIG IP,选择“Open IP Example Design”,Vivado会帮你新建一个完整的example工程。这个工程里包含了时钟管理、复位逻辑、ILA调试核、以及一个简单的读写测试模块,非常完善。
建议先在example design上做行为仿真。MIG自带的是仿真模型,在仿真里你可以清晰地看到初始化时序、校准过程、以及AXI4接口上信号的变化。跑完仿真再综合实现、生成比特流,下载到板子上,用ILA抓信号看init_calib_complete是否拉高。这是最稳妥的上手路径,直接跳过这一步去写自己的逻辑,出了问题往往很难定位是Controller没配好,还是自己的主控逻辑有问题。
3.2 自己写一个最简AXI4主控:先从单笔读写开始
MIG的example design里自带了一个简单的测试逻辑,但它通常比较抽象。我建议你自己手写一个最简单的状态机,只做三件事:写一个数据,读同一个地址,对比是否一致。这个逻辑虽然简单,但能帮你彻底搞懂AXI4握手时序。
先看写通道。AXI4写需要同时或者先后进行地址握手和数据握手,然后等待响应。一个最简状态机可以这样设计:
- IDLE:空闲状态,产生一个写请求(拉高AWVALID和WVALID)。
- WAIT_AW_READY:等待AWREADY拉高,说明地址被接受。
- WAIT_BRESP:等待BVALID拉高,说明写操作完成。
当然,实际开发中你可以把地址和数据握手并行处理,但在学习阶段,分开处理更容易理解。
读通道更简洁:
- IDLE:拉高ARVALID,发出读请求。
- WAIT_RVALID:等待RVALID拉高,读取数据总线上的值。
这里要注意R通道的LAST信号,如果是突发读,LAST拉高才表示这轮读数据全部结束。单笔读的时候,RLAST通常和最后一个数据一起出现。
我附上一段极简的Verilog伪代码结构,实际使用要补全端口声明和时序逻辑:
// 写操作状态机 localparam [2:0] IDLE = 3'd0, AW_STATE = 3'd1, W_STATE = 3'd2, B_STATE = 3'd3; always @(posedge ui_clk) begin if (!rst_n) begin state <= IDLE; awvalid <= 1'b0; wvalid <= 1'b0; end else begin case (state) IDLE: begin awvalid <= 1'b1; // 发出写地址 wvalid <= 1'b1; // 同时发出写数据 wdata <= 32'hDEAD_BEEF; waddr <= 32'h0000_0000; state <= AW_STATE; end AW_STATE: begin if (awready && awvalid) begin awvalid <= 1'b0; state <= W_STATE; end end W_STATE: begin if (wready && wvalid) begin wvalid <= 1'b0; state <= B_STATE; end end B_STATE: begin if (bvalid && bready) begin // 写完成 state <= IDLE; end end endcase end end这段代码的重点在于:每次握手完成的条件都是VALID和READY同时有效。比如AW_STATE里,只有当awready && awvalid同时为高,地址才算发出去,否则要一直保持AWVALID为高。这就是AXI4的“打乒乓球”握手规则,双方都必须确认,才算完成一拍。
3.3 地址映射:为什么你的地址逻辑要乘个4
这是无数人栽跟头的地方。DDR控制器IP的AXI4地址,是字节地址,而DDR颗粒的存储逻辑地址是按“行、列、Bank”组织的,两者之间靠地址映射电路自动转换。你不需要手动算Bank和Row,但必须理解地址和数据位宽的对齐关系。
举个例子:如果你的DDR数据位宽是32bit(4字节),那么AXI4地址的0x0000对应第一个32bit数据,0x0004对应第二个32bit数据,0x0008对应第三个,每次地址增加4,就跳到下一个数据。如果你的数据位宽是16bit,那地址每次增加2。也就是说,地址步进 = 数据位宽/8。
很多新手写回环测试时,往地址0x0000写一个数据,然后读地址0x0000,发现读回来是对的;但连续写0x0000、0x0001、0x0002好几个地址时,数据就乱了。其实是因为他把地址当作了“第几个数据”,而没意识到0x0001和0x0000在字节地址上重叠了,相当于往同一个32bit字的第二个字节里写东西。正确的连续写地址应该是0x0000、0x0004、0x0008。
还有一个相关的概念是WSTRB(写数据选通)。当数据位宽是32bit,你只写了低8bit数据时,WSTRB就是4'b0001,表示只有第0字节有效。如果你用MIG的example design,它一般会把WSTRB全部拉高,也就是整字写。自己写逻辑时,如果WSTRB没接对,会导致数据只写入了一部分,读回时高字节还是旧数据。
3.4 突发读写:把效率拉起来的正确姿势
单笔读写能通,只代表功能正确,性能完全不合格。DDR的最佳工作方式是大块连续数据传输,体现在AXI4上就是突发传输。AXI4协议里,地址通道上有LEN和SIZE两个信号,LEN表示这次突发有多少拍,SIZE表示每一拍有多少字节。
举个例子,DDR数据位宽32bit,SIZE=32/8=4字节,LEN=15,就表示这次连续传输16拍,总共64字节。控制器收到这个请求后,会让DDR以高效的内部调度模式连续读写一整段数据,而不是一拍拍地重新激活行、预充电。所以,能用突发,就不要用单笔读写。特别是在图像处理这种需要大量数据吞吐的场景,突发长度不够,带宽利用率可能连一半都不到。
还有一个实用技巧是Outstanding Requests。AXI4协议允许你在不等待上一个响应的情况下,连续发出多个请求。这在读场景下尤其有用:你可以连续发出N个读请求,控制器会自行调度DDR,让读数据的返回节奏基本不停顿。如果每发一个读请求等读完再发下一个,整个链路就退化成“一问一答”,DDR的高速访问优势完全被浪费了。
这里需要提一下MIG的Native接口和AXI4接口在处理Outstanding时的差异。Native接口里,数据有效信号和读地址通道是严格同步的,发一个读地址就要等对应的数据回来;而AXI4接口内部带了FIFO缓冲,可以缓存多个请求,所以用AXI4接口天然更适合做高速流水。这是我在实际项目中明显感受到的一个区别,也是推荐大家用AXI4接口的重要原因之一。
4. 性能与调优:别把DDR带宽浪费在“等待”上
4.1 理论带宽和实际带宽之间的鸿沟在哪
一串DDR3-1600、数据位宽64bit的配置,理论带宽是多少?数据率1600Mbps,位宽64bit,理论峰值带宽就是1600Mbps × 64bit / 8 = 12.8GB/s。听着很高对不对?但你实际测下来,能跑到60%~70%就算不错了。
原因很多,最关键的是DDR读写的“换行代价”。DDR读取一行数据需要激活时间tRCD,写完需要预充电tRP,随机访问时这些时间会占据大量的时钟周期。连续访问不同行时,控制器需要频繁在这几个状态之间跳转,有效数据传输时间被压缩了。AXI4协议的Outstanding机制可以在一定程度上缓解这个问题,让控制器提前调度后续请求,但不能完全消除。
所以,调优的核心思路是:尽量让控制器在同一个行地址上连续传输大量的数据,减少换行次数。落实到AXI4主控设计上,就是尽量发长突发、尽量发连续地址、尽量在多个Bank之间交错操作。
4.2 读写并行和Bank交错
DDR内部有多个Bank,每个Bank都有自己的行缓存。如果连续访问的是不同Bank,控制器可以在一个Bank预充电的同时,去另一个Bank激活行,把等待时间掩盖住。这就是Bank交错调度的思想,MIG控制器内部会做一定程度的自动调度,但前提是你从AXI4接口发过来的请求要有足够的并发度。
读和写之间的切换成本也很高。DDR总线上从读到写切换,需要插入“写转读”的间隔,反之亦然。你在写一个大规模数据块后立刻切到读,控制器可能要浪费几个周期来切换方向。如果业务允许,最好把读写分成两个时段批量处理,比如先写一大段,读完一小段,再写一大段,避免频繁交叉。
有一种纯用AXI4接口就能实现的调优方法:用多通道FIFO把读写请求分开缓冲,然后在一个调度器里按批次把读请求或写请求一次性发给DDR控制器。比如你有四个数据源需要写DDR,如果每个数据源直接往AXI4接口发请求,控制器会被打乱;但如果你用一个仲裁器把四个源的写请求先汇聚到一个大FIFO里,再批量发出去,DDR的换行次数明显减少,整体带宽能提升不少。
4.3 实际项目中的带宽测试方法
做性能调优,不能光靠感觉,要量化。最简单的测试方法是在逻辑里加入计数器:统计一段固定时间内,AXI4写数据通道上完成了多少拍,乘以每拍字节数,再除以时间,就得到了实际写带宽。读带宽同理。
我一般会这么测:设置一个时间窗口(比如10ms),用计数器记录WVALID和WREADY同时有效时的WLAST出现的次数,这样能精确得到完成写的传输次数。然后算一下平均带宽,再和理论带宽做比值,就能直观地看到调优效果。这个方法虽然粗糙,但在项目里非常实用。
有一个容易被忽略的地方是:MIG的example design自带的测试逻辑,会有意地做大量的连续地址读写,所以它测出来的带宽通常很高。你换成自己的真实业务地址流,比如图像处理中常见的隔行扫描、随机地址更新,带宽会掉得很明显。这个时候不要慌,先优化地址流模式,再考虑写更复杂的AXI4调度器。
5. 问题排查与调试心得:这些坑我都替你踩过
5.1 Calibration Fail:项目启动的第一个拦路虎
init_calib_complete一直不拉高,或者干脆在Vivado的Hardware Manager里看到MIG报calibration fail,这是DDR项目里最经典的问题。原因可能五花八门,但排查思路是有套路的。
首先查时钟。MIG需要一个参考时钟,一般要求是200MHz或者300MHz,具体用多少取决于你的配置。这个时钟必须干净、稳定,抖动不能太大。很多开发板会用一个专用时钟源给FPGA提供参考时钟,但如果你自己画的板子,参考时钟是从别的PLL分出来的,校准失败的概率会高很多。其次查电源。DDR有VDD、VDDQ、VTT等几路供电,电压容差一般在±5%以内,VTT的纹波不能太大。电源噪声大、上电时序不对,都会导致训练失败。
环境因素排除后,再看引脚约束。MIG生成的XDC文件里,会绑定具体的FPGA引脚位置。如果你用的板子和MIG配置的BANK不对应,或者PCB走线过长、阻抗不匹配,校准就会挂。这里推荐用厂商官方支持列表里的内存颗粒型号,市场上有些兼容颗粒参数虽然标的一样,但实际训练起来就是不稳定。
调试手段方面,Vivado提供了一些调试选项:可以修改MIG配置里的“调试端口和选项”,打开debug接口,将训练阶段的信息抓出来看。也可以调整VREF电压、内部延迟参数,逐步逼近正常状态。DDR3时代经常需要手动调ODT和片内端接值,DDR4时代MIG自动训练的能力强了很多,人工介入少了,但参考电压和端接还是要关注。
5.2 读回来的数据不对:先怀疑地址,再怀疑握手
读写回环测试数据不一致,是另一个高频问题。我见过的大量案例,不是DDR坏了,而是用户逻辑的地址计算错了。
最典型的错误就是我前面说的字节地址和数据位宽不对齐。比如32bit位宽下,数据是按0、4、8、C递增的,你觉得写0、1、2、3没问题,其实地址重叠了。这种错误在仿真里往往不被察觉,因为仿真模型不会报警,误打误撞还能读到数据,但真相是数据覆盖了前面的内容。
还有一种情况是突发传输的地址递增逻辑写错了。AXI4协议里,突发可以支持INCR(递增)、FIXED(固定)和WRAP(回卷)三种类型。DDR控制器一般只支持INCR,如果你的地址递增步进没有按SIZE信号的值来调整,读回来的数据就会错位。
握手信号的错误也会导致数据错乱。比如你的主控在发送读地址后,不等ARREADY拉高就认为地址被接受,直接在同一个周期去读R通道,这时候数据肯定不对。再比如WSTRB没有根据字节位置正确置位,高字节数据被丢弃了。这些问题在仿真波形里对比着抓一次就能发现,强烈建议用ILA把AXI4接口的主要信号都抓出来对照协议手册检查。
5.3 时序不收敛:跑不快才是真难受
实现的时候,如果MIG的时序报告出现大量违例,常见原因有几个。一是约束文件没写对或没加进去,MIG生成的XDC文件必须通过约束文件列表正确引入工程,而不是路径写错了。二是跨时钟域路径没有正确约束,如果你在UI时钟域和业务时钟域之间直连了信号,工具不知道怎么分析时序,很容易出现时序假违例或者真违例。
解决办法是:所有跨时钟域路径,要么用异步FIFO保证同步,要么用set_false_path/set_max_delay等约束告诉工具“这是安全的跨时钟路径,不用按严格时序分析”。直接硬调逻辑来满足时序,通常是事倍功半。
如果时序始终不收敛,还可以考虑降低DDR运行频率。比如DDR3原本跑800MHz,降到667MHz,这会同时降低UI时钟频率,给用户逻辑留出更充足的时序裕量。代价是理论带宽下降,但至少系统能稳定工作。项目迭代过程中先跑稳功能,再逐步优化频率,是一个稳妥的策略。
5.4 我建议的新手调试流程
总结我自己的调试流程,大概是这么几步:先仿真,后上板;先单笔,后突发;先读后写,再读写混合。
在仿真阶段,一定要用厂商提供的DDR仿真模型。Xilinx MIG在生成时会自动带上DDR颗粒的Verilog模型,写入激励,跑出波形,确认初始化、校准、读写握手全程正常。仿真能覆盖大部分逻辑错误,上板后只测硬件相关的部分就行。
上板第一步,用ILA抓init_calib_complete信号,看到它拉高了再做后续操作。如果它没拉高,优先排查时钟、电源、引脚约束。第二步,跑单笔读写回环,数据比对通过再上突发。第三步,突发正确后再跑多地址连续读写,最后再切到真实业务逻辑。
最后说两个容易被忽略的细节:MIG初始化需要一段时间,上电后不要立刻发读写请求,等init_calib_complete稳定拉高一会儿再开始;另外,你的用户逻辑复位信号应该持续高电平直到校准完成,而不是用FPGA全局复位直接释放。这两个点做对了,能避免很多深藏不露的异常。
我在实际项目中还发现,Xilinx MIG的example design虽然是官方出品,但它的测试逻辑写得比较“理想化”——默认数据位宽、参数配置都是最顺滑那种。你自己改配置后,尤其改了数据位宽或者地址映射顺序,example design里的很多地址假设就不成立了,务必认真对照生成的引脚文件和地址表去理解,不要直接拿example跑,跑好了也不代表你理解了,理解了才是自己的。
一次项目做完,把整个链路捋顺之后,你会发现DDR也没那么神秘。它就像一个规矩很多的仓库管理员,你不需要去盯他每一步在干什么,只需要通过AXI4这个标准接口,把明确的读写请求递进去,然后耐心地从响应通道里拿结果就行。多跑几次仿真,多抓几轮波形,那点协议上的陌生感很快就没有了。