搞FPGA的工程师,基本都绕不过DDR3。我第一次在Kintex-7上接DDR3颗粒时,天真地想自己写控制器状态机,结果光是把刷新、预充电、激活、突发读写的调度规则理清楚,就花了一个多星期,最后还是回到了Xilinx的MIG IP核这条路。MIG(Memory Interface Generator)把DDR3控制器和物理层PHY全部封装好,用户侧给你AXI4或Native UI接口。这篇文章就是我完整走过一遍之后整理的实战记录:从MIG帮你做了什么,到Vivado里怎么配置,再到仿真、上板、调性能,一路的坑我都会展开讲。适合第一次碰MIG、想用AXI接口读写DDR3的FPGA开发者,也适合那些已经跑通但总遇到偶发问题、想看排查思路的工程师。
1. MIG到底替你干了多少活
1.1 为什么自己写DDR3控制器是条回头路
DDR3不是一个大SRAM,不是给地址、给片选、等一会儿就能读数据。一次完整的读操作,要经过激活(ACT)选行,再发CAS选列,等待CL列选延迟之后,数据才在DQS源同步时钟的配合下回到控制器;如果下次要访问的行不在当前打开的bank行里,还必须先预充电(PRE)关闭当前行。这还只是单个操作的时序,真正麻烦的是背后的调度:
- 每64ms必须完成8192次刷新,折合tREFI约7.8us一次,超过85摄氏度时刷新间隔还要缩短到3.9us;
- 刷新命令的插入时机不能影响正在进行的读写突发,也不能因为连续burst太久而把刷新卡住;
- 上电后要等颗粒完成ZQ校准,之后控制器自己还要做写调平(Write Leveling)、读训练(Read Calibration)等一连串初始化。
这些逻辑层面的事咬咬牙还能写,物理层就没那么简单了。DDR3的数据线是源同步双向的,DQS在写的时候是时钟送出去,在读的时候又要变成输入采样信号;为了信号完整性,片内端接ODT必须根据操作方向动态切换。控制器内部要对每个字节通道的延迟做训练和校准,才能在高速率下把数据正确采回来。真要从零写一套能上板跑稳的DDR3控制器加PHY,业界通常要一个团队干大半年。MIG把控制器、PHY、训练算法、初始化状态机都做好了,留给用户的是标准接口,这才是它存在的价值。
1.2 刷新和仲裁才是DDR的生命线
我之前自己写控制器项目死在哪儿?其实就死在仲裁。DDR3颗粒要求必须按时刷新,否则电容漏电,存储单元的数据就丢了,而且这种错误不是检错纠错能救回来的,是整片数据静默损坏。MIG内部有一个专门的调度器,把刷新命令排在优先级较高的位置,同时在空闲时隙插入预充电,让bank尽可能处于适合下一次访问的状态。
在AXI侧感知不到刷新停顿,因为MIG会尽量在两次读写之间把刷新插进去,实在时间紧张就强制中断一下普通访问。如果自己实现,最怕遇到的情况就是某个master发了很长的burst,把刷新一直往后挤,最后tREFI超时,数据全盘崩溃。这种问题和逻辑正确性无关,纯粹是长期运行才暴露的可靠性问题,也是我后来坚定用MIG的原因之一。
1.3 布线等长与颗粒配置是控制器的另一半
再强调一点:MIG只能保证逻辑和时序训练算法正确,DDR3的信号完整性取决于PCB布线。DQ与DQS之间要等长,地址控制信号组要等长,差分时钟CK/CK#要等长,VTT端接电压要稳定,ODT阻值要和走线阻抗匹配。
我见过一块板子,MIG配置、时钟、电源全都正常,但init_calib_done信号始终不拉高,最后查出来是DQ某一位的走线比其他线长了将近500mil,训练算法怎么都收敛不了。所以说,DDR3里控制器能解决的事,和PCB该解决的事是分不开的。下板之前先确认板厂给的等长约束报告,能省掉后面一大半的调试时间。
2. 为什么接口要选AXI4而不是Native UI
2.1 Native UI接口的原始面貌
MIG生成的时候,接口类型有很多选择,其中原生UI(User Interface)直接把控制器的命令和数据总线暴露出来。你会看到一大堆app_开头的信号:命令通道有app_addr、app_cmd、app_en、app_rdy,写数据通道有app_wdf_data、app_wdf_wren、app_wdf_rdy,读数据通道有app_rd_data、app_rd_data_valid。
这组接口需要自己维护一个状态机:在app_rdy和app_en同时为高时发送命令,写数据要按固定burst长度准备好,命令通道和数据通道的握手相互独立,还得处理命令已经发出但写数据没跟上的情况。每笔访问的burst长度实际上是固定的,由地址的低位宏参数决定,想变成长度不同的突发也没那么灵活。用这套接口不是不行,但所有访问控制、异常处理都要自己写。
2.2 AXI4五个通道如何映射到DDR访问
接AXI4之后,MIG内部已经把这套原生接口封装成了一个标准从机。AXI4 Memory-Mapped协议有5个通道:
- AW通道:写地址,一次发送一个突发请求的起始地址和长度;
- W通道:写数据,携带字节使能,最后一次传输要拉高WLAST;
- B通道:写响应,DDR控制器写完数据后返回BVALID;
- AR通道:读地址;
- R通道:读数据,同样以RLAST标记最后一拍。
对用户来说,一次DDR写操作就是"发AW、发W、等B"三个步骤,读操作就是"发AR、等R",逻辑非常清晰。MIG在内部把AXI突发按DDR颗粒的实际组织方式拆成对应长度的访问,比如AXI一次32字节突发可能对应DDR3一次BL8的访问。AXI的地址映射到bank、row、column的拆分方式,MIG也通过配置项帮你算好了。
2.3 实际项目里选AXI的另一个理由:生态互通
从实际工程角度看,选AXI4最大的好处是生态。你的系统里大概率还有MicroBlaze软核、DMA、VDMA、自定义的视频流IP,这些IP天然都挂AXI总线。DDR3的MIG用AXI接口,整个系统的互联就是统一的AXI拓扑,后面换Zynq、换Versal,甚至换DDR4的MIG,用户逻辑完全不用动。
Native UI虽然接口更底层、转换路径更短,但一旦换了控制器IP,所有访问逻辑都要重写。我在项目里就把底层的DDR访问封装成一个AXI接口的模块,上面接的所有master都只认识AXI,后续维护非常省心。
3. Vivado里逐项配置MIG:每步都不踩坑
3.1 创建前先定数据位宽、颗粒型号与时钟
打开Vivado,在IP Catalog里搜索MIG,创建IP核之后第一步就是选Memory Type为DDR3。这一步之前,心里得先把三件事定下来:数据位宽、内存颗粒型号、运行时钟频率。
数据位宽和颗粒组织强相关。一个DDR3颗粒通常是x8或x16位宽,64位数据总线意味着要并8颗x8颗粒组成一个rank。MIG的AXI数据位宽默认和DDR数据位宽一致,如果DDR是32位,AXI接口也是32位。视频缓存类应用用32位可能够,但如果是DMA大量搬数据或以太网高速收发,我建议直接上64位,毕竟带宽翻倍带来的收益很直接。
颗粒型号要选MIG支持的,常用的是Micron的MT41K系列。配置窗口里可以直接搜颗粒型号,如果列表里没有,还要手动用Add new Part的流程去编辑颗粒参数。时钟频率这块,一般DDR3运行在1066MT/s到1600MT/s之间,即核心时钟533MHz到800MHz。频率越高,时序收敛越难,PCB要求也越高,不要一上来就冲到最高。
3.2 参考时钟、输入时钟与系统时钟的关系
MIG配置里有一个很容易看晕的概念:Input Clock Period、Reference Clock和System Clock。实际经验是这样理解:
- Input Clock是板子上给MIG提供的基础时钟,一般定义成200MHz或100MHz,它经过MIG内部的PLL/MMCM倍频出DDR核心时钟;在配置页里填的是周期,200MHz就是5000ps,100MHz就是10000ps;
- Reference Clock是训练算法使用的相位参考,通常要求200MHz的差分或单端信号,有的板子上没有200MHz外部时钟,可以从Fabric时钟转过来;
- System Clock代表控制器内部时钟,也就是MIG用户侧接口(比如AXI的时钟
ui_clk),它的频率一般是DDR核心时钟的1/2或1/4。
我习惯把外部提供200MHz差分时钟直接喂给MIG的sys_clk_p/n,同时让MIG的ui_clk作为我用户逻辑的主时钟,这样时序约束简单,也不会有跨时钟域的分叉。配置完成后,MIG会把内部锁相环和MMCM的结构生成好,用户只需要在顶层例化时保证sys_clk输入有效,复位信号aresetn在时钟稳定后再释放即可。
3.3 AXI Burst长度与Address Mapping的取舍
MIG的AXI接口配置里有Burst Length选项,一般DDR3固定BL8,也就是一次颗粒突发8个bit位宽的数据。比如DDR3数据位宽32位,BL8对应一次颗粒访问是32字节。AXI一侧的突发长度可以是1、2、4、8、16等,MIG会把AXI突发拆成一个或多个DDR原子访问。
另一个关键配置是Address Mapping。DDR3颗粒内部地址是bank、row、column三层结构,AXI地址的哪些bit映射到row、哪些映射到bank、哪些映射到column,直接影响顺序访问的page hit率。MIG默认的映射方式在配置文件里能看到,例如可能把地址bit[15:13]映射为bank、bit[22:16]映射为row。如果应用是大块顺序数据搬运,这种映射通常已经够用;但如果访问模式是随机的,可以手动调整映射把bank字段放在较低位,让连续地址能落在不同bank里,从而支持bank交错。
具体选哪种好,我的办法是先用默认配置上板跑一轮性能测试,再用仿真或实际数据带宽数据对比。盲调映射没有意义,因为实际性能除了映射还跟outstanding能力、burst长度、刷新策略相关。
3.4 生成后值得优先读的文件
MIG生成完IP之后,只接IP本身也可以,但我强烈建议先打开Example Design。在IP Sources里右键MIG,选择Open IP Example Design,Vivado会生成一个独立的参考工程,里面包括:
- example_top.v:顶层,把DDR3接口、时钟、复位、ILA都连好了;
- traffic_gen模块:一个简单的AXI读写发生器,能做基本的数据检查和led控制;
- ddr3_model:仿真用的DDR3颗粒模型;
- ILA例化:把若干关键信号拉出来准备上板调试。
读这个工程的价值在于知道MIG例化的正确姿势,尤其是时钟和复位怎么连、用户侧数据总线怎么接,比自己闷头从头写测试框架快得多。我第一次做的时候直接把example_top里的traffic_gen替换成自己的AXI模块,仿真和上板一次性跑通。
4. 仿真阶段先把协议跑透:别等到上板再学握手
4.1 仿真模型怎么选,初始化的速度陷阱
MIG的example design生成后,直接Run Behavioral Simulation就能看到DDR3初始化到读写的完整过程。仿真之所以快,是因为example工程里用了DDR3_SIMULATION这类宏定义,把内部的一些计数和训练时序缩短了。如果直接在自建工程里例化MIG,不做仿真优化,DDR初始化的时间在仿真里会非常漫长,因为训练算法里面有很多真实的延迟和时钟周期等待。
自己搭仿真环境时,别忘了把MIG相关仿真宏加上,或者干脆先借用example design的tcl脚本和约束,改掉顶层模块再跑。仿真成功后,你会在波形里看到几个关键节点:
- 系统复位释放;
- MIG内部PLL锁定;
init_calib_done从0跳变到1,代表PHY训练和颗粒校准完成;- 之后AXI通道才开始接受事务。
如果在仿真里init_calib_done始终不拉高,不要急着查业务逻辑,先查时钟和复位配置,这是所有问题的根源。
4.2 在波形里看懂VALID/READY背压
AXI协议的流控基础就是VALID和READY握手,只有两者同时为高时一拍数据传输才成立。DDR控制器作为从机,什么时候拉低READY就是背压信号,背后原因基本是内部FIFO满了或者DDR颗粒正忙。
仿真时专门去找几段背压波形看:
- 写方向:主设备拉WVALID,但MIG把WREADY拉低,此时WVALID和数据必须保持不变,直到WREADY拉高完成握手;
- 读方向:AR通道正常发出后,R通道的RVALID可能不会立刻拉高,控制器需要时间完成DDR读操作;
- B通道:写数据完成后,BVALID要等DDR真正写完并返回响应才拉高。
我见过不少第一次写AXI主设备的人,在WREADY拉低时把WVALID和WLAST丢掉,结果仿真报数据不完整或事务挂死。AXI里"从机没准备好,主设备就得原地等待"这个规则非常简单但极其重要,仿真是理解它的最好方式。
4.3 自己写个简单的AXI主设备做读写回环
用example里现成的traffic_gen能跑通仿真,但理解不深。我建议自己写一个极简的AXI主设备,功能只有三个状态:先写一片数据,再读回来,然后比对。这个模块核心逻辑如下:
- 等待
init_calib_done和MIG AXI接口的aresetn释放; - 状态0:发AW和W,地址从0开始,数据为递增序列,WLAST标记最后一拍;
- 状态1:等待BVALID返回,确认写完成;
- 状态2:发AR,等RVALID,收集R数据和RLAST;
- 状态3:比对读写数据,输出通过/失败标志。
这个模块别看简单,能帮你把AXI写地址、写数据、写响应、读地址、读数据五条通道的时序全部过一遍。在仿真里把变量打出来,跑上几百次随机地址,比直接上板查ILA效率高太多。
4.4 关于AXI VIP的一个小提醒
如果是做高性能验证,用Synopsys AXI VIP这类商业验证IP当然更专业,它可以自动检测协议违例、UVM环境集成、随机约束生成。唯一的烦恼是默认打印太多,transaction一条条刷屏。常见的抑制办法是在Vivado或实际工具链里根据VIP版本调整报文打印级别,或者在你的test里把事务打印相关的verbosity设高一些,让低优先级的transaction日志不再输出。具体参数名以你使用的VIP版本文档为准,这里只提思路。
5. 上板调试:从init_calib_done到数据正确的完整链路
5.1 上电后第一个要抓的信号
把example design综合、实现、生成bitstream,下载到板子上之后,第一步不是看读写结果,而是用ILA抓c0_ddr3_ui_clk、aresetn、init_calib_done。这三个信号正常了,说明DDR控制器自身已经跑起来。ILA的采样时钟建议用MIG的ui_clk,这样能直接看到AXI通道上的所有时序,不需要跨时钟域。
我自己遇到过init_calib_done一直为0的情况,排查链路是这样的:
- 先用万用表确认DDR3供电:VDD 1.5V,VTT 0.75V,VREF 0.75V。VTT最容易出问题,曾经有板子VTT做成1.5V,MIG的PHY IO电平都不对,训练完全跑不起来;
- 确认外部时钟输入,用示波器量
sys_clk_p/n; - 确认复位芯片时序,
aresetn是否在时钟稳定后才释放; - 如果以上正常,用ILA抓
c0_ddr3_init_done这类内部状态信号,看卡在training的哪个阶段。
5.2 初始化过了但读写失败:先查地址映射
init_calib_done拉高后,最常见的现象是写一个地址,读回来数据不对或者全零。这时候第一嫌疑不是时序,而是AXI地址到DDR地址的映射没有对齐。
MIG把AXI地址映射到DDR行列bank有一套固定规则,具体看IP核配置里的Address Mapping。一个典型误区是:以为DDR容量16MB,所以地址bit[23:0]就是颗粒内部寻址,其实bit划分中有一部分会映射到bank或row,你写入地址0x0000和0x1000可能落在同一个row的不同column,也可能落在不同bank,取决于数据宽度和映射表。
排查方法是先用一小组地址写读回环,把地址差值从小到大遍历,找出数据开始错位的地址边界,再和MIG提供的映射表对照。我调K7时就是发现0x000到0x7FF正常,0x800开始读回全是FF,最后对照发现是row映射位算错了,调整数据在系统里的起始偏移后立刻正常。
5.3 偶发数据错误:时序余量不足的典型表现
如果读写不是全错,而是偶发某一位翻转、某个burst数据错位,那基本就是时序余量不够。处理手段从软到硬包括:
- 降低DDR运行频率,比如1600MT/s降到1333MT/s,这是最直观的改善余量的方法;
- 调整ODT阻值配置,DDR3常见RTT_NOM有RZQ/2(120欧)、RZQ/4(60欧)、RZQ/6(40欧),不同阻值对信号反射的抑制效果不同,具体看PCB走线,一般高速板用40或60欧;
- 检查训练参数是否到达边界,MIG部分版本支持调整Read DQS延迟等级和写调平值;
- 仔细看板级SI问题:VTT去耦电容是否足够、DQ/DQS等长约束是否真实满足。
我的项目曾遇到DDR偶尔死机,降到1333MT/s稳定跑了一整夜不会挂,后来查出是某个数据组的等长误差太大,改版后回到1600MT/s也完全正常。这种问题别指望靠逻辑解决,根源就在信号质量。
5.4 跨4KB边界的事务一定要处理
AXI4协议规定,一个burst不能跨越4KB地址边界。这个限制的起因和系统页表有关,但在接DDR3时无论什么master都得遵守。如果DMA要搬运一大块数据,刚好跨过4KB,硬件必须自己把事务拆成两段,否则协议违例,表现可能是数据错乱或者通道挂死。
很多现成DMA IP会在内部做4KB边界分割,但自己写的master就很容易忽略。我的建议是:所有AXI主设备在发送burst前,必须算一下本burst是否会跨4KB边界,如果会,就缩短到4KB边界处的最后一个节拍。这个逻辑写在发送状态机里,代码量不大,但能避免一大堆莫名其妙的问题。
6. 性能细节:从跑通到跑得快
6.1 顺序访问与bank交错
跑通DDR之后,下一个话题是效率。DDR3实际带宽和访问模式强相关,顺序地址访问时,控制器可以打开一个行然后连续读取,带宽能接近理论峰值;随机地址访问时,几乎每次都要激活新行、预充电旧行,吞吐率可能只有顺序访问的三分之一。
设计应用时尽量做到:
- 一次AXI burst尽量长,比如16拍甚至更长,而不是频繁发短突发;
- 多个master同时访问时,让它们访问不同的bank区域,MIG的bank调度能自动把不同bank的操作交错起来;
- 数据排列上,把需要并行处理的数据块放在不同bank起始地址。
6.2 outstanding事务数量的作用
AXI主设备如果每一笔都等B通道返回再发下一笔,DDR控制器就会有空闲周期。打开outstanding能力,即不等响应就连续发多笔AW或AR,MIG内部FIFO会缓存这些请求并自行调度,能显著提高带宽利用。
在MIG的AXI配置里,可以设置outstanding(或可接受outstanding传输)的深度上限。主设备侧也要支持这个行为。比如读方向,主设备可以连发4笔AR,然后一次性接收4笔R数据,控制器在内部把这4个读请求排到不同bank或同一行,省去中间等待。
我做过一个简单测试:同样的DDR3,每笔读都串行等待时带宽只有约60%,开启8笔outstanding后能跑到约85%以上。如果你的性能测试不达标,先检查AXI主设备的outstanding能力,这一项往往比地址映射的影响还要大。
6.3 多master访问DDR时的QoS配置
当CPU、DMA、视频流处理器同时通过AXI Interconnect访问DDR时,MIG的QoS优先级寄存器就派上用场了。MIG的AXI接口有优先级输入信号,可以从高到低设置每个通道的优先级权重,低优先级请求在高优先级密集时会被延后。
我在一个小型SoC里调试过视频帧率不稳定,最后就是把视频DMA对应的AXI端口优先级提高,读和写权重都加大,CPU做控制平面的访问优先级降一档,问题立刻缓解。这里有个容易忽略的点:优先级影响的是同一时刻多个等待事务的仲裁顺序,如果某个master根本没发事务,优先级再高也没用,所以要先确认带宽够不够,再加QoS。
6.4 用ILA看到的几种异常波形
上板调试时,我总结了几种常见异常:
- WREADY长时间为零:MIG写FIFO满或DDR正忙,正常情况背压不会超过几个节拍,如果长时间卡住,检查是否某笔事务没有正确完成;
- RVALID迟迟不拉高:读地址是否发出且被接受,acc检查ARVALID和ARREADY是否正常握手;
- BVALID一直不返回:可能是写数据通道和写地址通道跨了很久,比如发了AW但W数据没跟上;
- 总线hang死:所有通道都停在等待状态,大概率是某个从机没有接受或没有响应,复位后重新抓波形。
调试时ILA采样深度别设太小,我一般设16384以上,配合触发条件在事务边界去抓,波形里能看到完整的事务序列,而不是一堆碎片。
写到这里,DDR3控制器从配置到上板调试的关键环节都过了一遍。最后再说一个我自己的习惯:每次接到新板子,我不会直接跑业务逻辑,而是先让MIG的example design在板子上跑一遍,确认DDR训练和基本读写没问题。这个小步骤看起来多花二十分钟,但它能帮你把"板子/DDR硬件问题"和"自己逻辑问题"清晰隔离开,后面排查业务异常时省下的时间远不止二十分钟。DDR3的性能和稳定性是设计出来的,不是调出来的,配置阶段多想一步,上板阶段就能少熬一个夜。