学FPGA学到第42天,终于要碰MIG了。如果你也正在自己啃FPGA,会发现前面做的组合逻辑、状态机、时序约束都还算是“自己能控制”的东西,到了MIG这里就完全不一样了——它是一个复杂的IP核,是FPGA和DDR内存之间的桥梁。第一次点开配置界面,满屏的选项、频率、时序参数,很容易让人懵。这篇文章就围绕MIG是什么、它内部怎么工作、配置的时候每个选项到底在问什么、以及最关键的怎么把它跑起来,做一个完整的梳理。
要提前说明的是,这篇文章虽然是学习系列的第42天,但我不会假设你已经看过前41天的内容。你只需要懂FPGA的基本概念,比如时钟、复位、状态机、FIFO,并且有在Vivado里建工程的经验,就能顺着这篇文章把一个MIG控制器的读写流程摸清楚。我自己是一边看官方文档,一边在开发板上反复试,加起来折腾了差不多一周才把DDR3真正跑稳。文章里很多注意事项都是当时踩过的坑,直接跳过的代价是上板后一片黑屏或者数据全错。
1. 先搞清楚一件事:MIG到底替你干了哪些活
1.1 DDR不是按个按钮就能读的
很多初学者以为DDR内存就像一片大号的RAM,给个地址和数据就能读写了。真实情况远没那么简单。DDR SDRAM内部是一大片存储阵列,存取数据之前需要先激活行、再读写列,读出来的数据还要经过物理层信号采集、训练、校准才能落到用户逻辑里。除了这些,还有时钟同步、命令时序、刷新、预充电、模式寄存器配置,任何一个环节不对,数据都是错的。
如果你知道SRAM怎么读写,可以把DDR理解成“需要提前开会、排班、定期检查的SRAM”。SRAM只管读和写,DDR不行,它有自己的状态机,比如IDLE、ACTIVATE、READ、WRITE、PRECHARGE、REFRESH等等,而且这些状态之间的切换有严格的时间要求,一般叫tRCD、tRP、tRAS这类时序参数。
MIG就是FPGA厂商提供的内存接口生成器,它负责把DDR这些复杂操作全部封装起来。你不再需要自己在逻辑里一行行地写刷新状态机,也不需要对着器件手册算每个时序参数要等多少个时钟周期。MIG对上给你一个干净的用户接口,对下直接连接DDR芯片的引脚。你告诉它“我要写地址0x1000的数据”,它就自动完成激活、写命令、写数据、预充电这一整套流程。
第一次听到这个解释,我的反应是“哦,就是个万能翻译官”。后来深入看了一下内部结构,才意识到翻译官这个比喻太轻了,它更像是一个自带物理层培训体系的调度中心。
1.2 MIG内部的两大块:控制器和物理层
MIG内部可以分为两大块,一块是控制器,一块是物理层。控制器处理的是协议层面的东西,比如接受用户的读写请求、把它们转换成符合DDR规范的命令序列、管理刷新周期、维护bank状态。物理层则负责真正把控制器的命令和数据变成DDR芯片引脚上的电信号。
物理层里有个东西叫延迟校准和训练逻辑。DDR的时钟频率很高,数据线和时钟线之间有相位偏差,不同芯片、不同温度下偏差还会变化。MIG会在上电初始化的时候自动发送训练序列,然后不断调整延迟,确保数据能准确采样。这个流程就是MIG初始化完成后输出的init_calib_done信号,只有看到这个信号拉高,你才能认为DDR已经进入了可以正常读写的工作状态。
理解这两块最大的帮助,是调试故障的时候能快速定位。比如上板后init_calib_done一直不拉高,那大概率问题出在物理层,比如时钟、引脚约束、电压不匹配,而不是你的读写逻辑写错了。反过来,如果init_calib_done正常拉高了但读写数据不对,那问题多半在应用层接口时序或者数据处理方式上。这个分类排查的思路,后面写仿真和上板验证时会反复用到。
1.3 什么时候你可以绕开MIG
聊一个很多初学者会纠结的问题:MIG是不是必须的?如果你的设计只是把DDR当个大数组用,存几帧图像或者一段音频,那MIG确实是最高效的选择。但有些场景MIG未必合适,比如你需要非常极致的读写延迟控制,想在同一时刻精确控制bank和行地址;又或者你的DDR数据位宽、频率超出了MIG的支持范围。这些情况属于少数派,大部分FPGA开发者,尤其是我这种学习阶段的,应该老老实实用MIG。
还有一种说法是“既然MIG这么复杂,那我直接写个DDR控制器不更体现水平吗”。我的建议是,如果你不是专门研究存储控制器这一行,真的没必要。MIG本身包含了大量经过验证的时序训练逻辑,这部分自己写成功率很低。光学MIG能怎么用,已经够我们消化很长一段时间了。
2. 例化MIG前,端口和时钟规划比配置界面更值得花时间
2.1 先确定DDR颗粒型号和位宽
打开MIG配置界面之前,最好先把手上的硬件信息理清楚。最重要的一点是你板上用的DDR芯片具体型号,以及它的数据位宽。同样是DDR3,可能有16位颗粒和8位颗粒的差别,位宽不同导致DDR芯片跟FPGA之间的连接方式也不同。
举个实例,我手头开发板用的是一片DDR3,数据线一共16根,型号里写着MT41K128M16。MT41K128M16代表的是单颗128M×16的颗粒。如果你的板子用两颗或更多颗粒拼成32位或者64位接口,那MIG配置里选择的位宽也要对应改。选错位宽最直接的后果是引脚约束错乱,严重的时候甚至会把DDR芯片接口搞出电气问题。
除了位宽,还要确认DDR的工作频率等级。DDR3颗粒标称可能有1066、1333、1600、1866这些等级,MIG配置界面会让你填一个内存时钟频率,比如400MHz、533MHz、800MHz。要注意的是,DDR3的标称数据速率是时钟频率的两倍,选400MHz就是DDR3-800。频率不是越高越好,还要看FPGA的速度等级能不能满足时序收敛。我一般先按开发板例程里的频率来,等跑通后再想办法往高了提。
2.2 时钟架构:一个容易出问题的接线方式
MIG需要的外部时钟通常是差分时钟,板上晶振通过时钟缓冲器分出一对差分信号,接到FPGA的专用时钟引脚上。配置MIG的时候,它会问你是用差分时钟还是单端时钟,然后自动把时钟引脚绑定到特定的MRCC或者SRCC引脚。很多人第一次跑MIG的例程会发现系统时钟只能接到固定位置,就是这个原因。
时钟频率之间是有换算关系的。MIG需要你提供系统时钟频率,以及DDR时钟频率,有时还要填内部参考时钟频率。系统时钟频率通常和DDR时钟频率一致,也可以不同,MIG内部会自动处理跨时钟域。比如DDR跑400MHz,系统时钟也可以给200MHz,MIG的控制器逻辑里会做时钟转换。但我个人的经验,初学阶段最稳妥的做法就是让系统时钟和DDR时钟保持一致,尽量降低变量。
还有一个容易踩的坑:时钟源选PLL还是MMCM。MIG通常会推荐你使用它自己生成的时钟方案,不要在外部再插一个PLL去调整MIG的输入时钟。因为MIG内部的训练逻辑对时钟相位关系很敏感,你插一层PLL,相位关系变化了,轻则校准时间变长,重则直接初始化失败。
2.3 AXI接口还是app接口:不要纠结太久的取舍
MIG对用户提供两种主流接口,一种是AXI4接口,一种是简单的app接口。AXI接口自带地址突发、数据通道分离等协议,适合接AXI总线系统,比如软核处理器通过AXI访问DDR。app接口则是MIG内部控制器接口的简化版,信号更少,读写控制方式更直白。
我见过很多学习资料里喜欢用AXI接口,理由是AXI结构清晰、可复用性高。但我自己想快速验证DDR能力的时候,用的反而是app接口。因为app接口没有复杂的握手协议规则,只需要拉高app_cmd和app_en,再把地址放到app_addr上,数据放到app_wdf_data上,写完数据等app_wdf_rdy,读的时候等app_rd_valid就行。对初学者来说,这是打开MIG世界最不陡峭的入门路径。
我自己的建议是:如果你还没有接触过AXI协议,或者只是想测DDR能不能用,先用app接口。等你理解了MIG的读写过程,再回到AXI接口就有底气得多。反过来一上来就套AXI,信号一堆,命令和数据混在一起,很容易把自己绕晕。
3. 在配置GUI里把每个问题都搞清楚再点Generate
3.1 配置界面的几个关键页面
MIG的配置界面,在Vivado里的位置是IP Catalog里搜MIG,找到Memory Interface Generator。打开之后第一个页面就是选择器件型号和内存类型,比如DDR3或者DDR4。不同器件支持的选项不一样,这个不用太纠结。
接下来是内存颗粒参数,要填行列地址数和bank数。这些参数跟DDR颗粒型号是对应的,你得去查芯片手册或者开发板原理图。比如我之前的开发板是A10系列,配套的DDR3行地址15位,列地址10位,bank位3位,数据位宽16位。这些参数填错的话,MIG生成的引脚数量会对不上,编译阶段就会报错。
还有几个选项这里值得注意,一个是Burst Length,通常选8;一个是Burst Type,选Sequential;还有Output Driver Impedance和RTT,这些跟PCB走线和端接有关,开发板例程一般会给出标准值。没有头绪的时候,千万别自己乱填,先找开发板厂家提供的MIG配置参考工程,或者用配置界面里的默认值。
3.2 频率、突发长度和时序参数的取舍逻辑
频率的选择直接影响训练时序能否收敛。DDR跑得太高,FPGA的IO时序裕量变小,跑Training的时候偶尔成功偶尔失败,这种“玄学”问题最容易出现在超频场景。我自己的心态是:先稳定,后性能。把频率降下来跑一个星期都没问题,再把频率提上去测试,比一开始就顶格跑更节约时间。
突发长度是另一个初学者容易忽略的点。DDR3的突发长度一般是8,意思是每次读写命令会连续传输8个数据。在MIG的应用接口上,一次app_cmd命令往往对应一个或几个突发传输,具体取决于你配置里的数据位宽。比如数据位宽是16位,突发长度是8,那一次命令传输的数据量是128位。应用逻辑设计的时候,要知道MIG每次传输的单位长度,不然数据处理会错位。
时序参数这块,配置界面里通常有tCK、tRCD、tRP、tRAS等一堆东西。除非你是专门做硬件调优的,否则这些参数直接参考芯片手册的建议值就行。MIG配置界面还会问你读写延迟和附加延迟。读延迟和写延迟会影响数据进出MIG的时钟周期,配置后最好记下来,后面读数据的时候要用它来推算数据什么时候有效。
3.3 生成后检查一下example design再动手
生成MIG IP之后,Vivado会给你一个example design的选项。很多人习惯直接忽略这个example直接开始自己的逻辑,我不是很建议这样。example design里包含了时钟初始化、复位时序、一个简单的读写测试逻辑,还有仿真用的testbench,这些都是开箱可用的。
我第一次就是拿example design来跑仿真的,在questasim里载入MIG仿真模型,把复位信号拉低再拉高,等init_calib_done拉高后,example design里的测试逻辑会自动往DDR里写几个地址的数据再读出来比对。这套流程跑通了,才说明你的MIG配置在仿真层面是没问题的,开始写自己的应用逻辑才有把握。
检查example design还有一个目的:看它怎么连接时钟和复位。MIG对外有一个sys_clk_i和一个sys_rst输入,example design里会给你一个标准的连接模板。尤其sys_rst不是普通的异步复位,它要求低电平有效,而且在MIG开始训练后不能随意去拉。这些细节在example design里有现成的处理,自己写的时候也照着做就不会错。
4. 用app接口手写一个最小读写流程
4.1 你需要关注的端口信号
app接口的端口数量不多,但每个都要弄明白含义。核心的有app_cmd、app_addr、app_en、app_rdy,这四个构成命令通道。app_cmd为0表示写命令,为1表示读命令。app_rdy是MIG给用户的反馈信号,只有app_rdy和app_en同时为高,命令才算真正被接收。
数据通道这边,写入时有app_wdf_data、app_wdf_wren、app_wdf_rdy和app_wdf_end。app_wdf_rdy表示MIG当前能接收写数据,app_wdf_wren和app_wdf_rdy同时为高时数据写入FIFO。读数据时有app_rd_data和app_rd_valid,app_rd_valid拉高时app_rd_data上的数据才有效。
还有一个状态信号是app_sr_req和app_sr_active,这是自刷新请求相关。一般应用里我们用不到,但要记得保持app_sr_req为低。我见过有人把这些信号悬空,结果MIG在训练完成后偶尔莫名卡死,查了很久才发现是自刷新请求引脚没处理。
4.2 先等init_calib_done再干活
MIG上电复位后最忙的一段时间就是执行DDR初始化和读写训练。在训练完成之前,所有用户读写请求都会被丢弃或者处于未定义状态。MIG对外提供一个init_calib_done信号,这个信号拉高,才代表DDR可以正常访问。
我看到很多新手代码里很随意地“延时了10ms就开始访问DDR”,这种做法很脏。且不说不同配置下训练时间不一样,就算这次能用,换一块板子、换一套环境温度就可能翻车。正确做法是把init_calib_done当成一个启动门控信号,所有读写逻辑都在它拉高之后再开始。
还有一个容易疏忽的地方,是复位信号的处理。MIG的sys_rst输入虽然是低电平有效,但它不是普通的异步复位,手册建议复位信号的释放要跟时钟同步。example design里通常会用一个复位同步器来解决。我一开始偷懒直接拿按键信号当复位给MIG,结果是训练稳定性很差,时好时坏,后来对照example design才发现问题在这里。
4.3 一次写操作和读操作的完整时序描述
写操作可以分成命令发送和数据发送两路,这两路是独立的、可以流水线重叠的。假设我要往地址0x100写入一个突发长度8的数据。先判断app_rdy和app_wdf_rdy是否都为高,如果都高,就可以拉高app_en并同时给出app_cmd=0、app_addr=0x100,同时把数据放到app_wdf_data上并且拉高app_wdf_wren。
读操作稍微麻烦一点点,因为读数据的返回时间跟读延迟有关。你发完读命令后,app_rd_valid不会立刻拉高,它会过一段固定延迟后才出现对应数据。这个延迟是在配置里确定的,比如读延迟是6个周期,那从app_en被接受开始数6个周期,app_rd_data上的数据就比较可疑了。实际设计里不用自己数周期,靠app_rd_valid来判断即可。
有个细节值得关注:命令通道和数据通道的时序关系。MIG允许数据比命令早一个周期到达,因为它的内部FIFO会暂存数据。但如果你想完全避免时序上的不确定性,最简单的办法就是让命令和写数据同拍发送。代价是突发传输效率略低,对于学习阶段的验证逻辑完全够用。
4.4 一个极简的读写状态机示例
阔以先看一段示意性的Verilog代码,只做一次固定地址的读写校验,后面再加上你自己的上层逻辑。
localparam IDLE = 3'd0; localparam WRITE_CMD = 3'd1; localparam WRITE_DATA = 3'd2; localparam READ_CMD = 3'd3; localparam WAIT_READ = 3'd4; localparam DONE = 3'd5; reg [2:0] state; reg [31:0] read_data; wire app_rdy_d = app_rdy; reg app_en_r; reg app_cmd_r; reg [27:0] app_addr_r; reg [63:0] app_wdf_data_r; reg app_wdf_wren_r; assign app_en = app_en_r; assign app_cmd = app_cmd_r; assign app_addr = app_addr_r; assign app_wdf_data = app_wdf_data_r; assign app_wdf_wren = app_wdf_wren_r; always @(posedge ui_clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; app_en_r <= 1'b0; app_wdf_wren_r <= 1'b0; end else if (init_calib_done) begin case (state) IDLE: begin app_en_r <= 1'b0; app_wdf_wren_r <= 1'b0; state <= WRITE_CMD; end WRITE_CMD: begin if (app_rdy && app_wdf_rdy) begin app_cmd_r <= 2'b00; // write app_addr_r <= 28'h0000100; app_wdf_data_r <= 64'h0123456789ABCDEF; app_en_r <= 1'b1; app_wdf_wren_r <= 1'b1; state <= WRITE_DATA; end end WRITE_DATA: begin app_en_r <= 1'b0; app_wdf_wren_r <= 1'b0; state <= READ_CMD; end READ_CMD: begin if (app_rdy) begin app_cmd_r <= 2'b01; // read app_addr_r <= 28'h0000100; app_en_r <= 1'b1; state <= WAIT_READ; end end WAIT_READ: begin app_en_r <= 1'b0; if (app_rd_valid) begin read_data <= app_rd_data; state <= DONE; end end DONE: state <= DONE; endcase end end这段代码为了可读性省略了write_data_burst_last等信号,实际应用里要按你配置的burst数量来补齐。重点是想说明:MIG读写不复杂,复杂的是你的状态机能不能处理握手信号之间的等待关系。很多问题都出在app_rdy拉低的瞬间你还继续给命令,导致命令被丢弃。
5. 仿真和上板会遇到的坑,我替你踩过一部分
5.1 仿真模型与example design的配合
MIG仿真有两种常见玩法。一种是把example design里的testbench直接用,这种最省心,仿真模型、激励、DDR行为模型全都给你准备好了,改都不用改,直接跑。另一种是自己写testbench,例化MIG IP和仿真模型。
自己写testbench的时候,记得把仿真模型提前编译好,不要指望在testbench里直接include那些IP生成文件就能跑。我在某次换版本环境时踩过一次坑:仿真一跑全是高阻态,后来发现是仿真库里少了ddr3_model这个器件库。凡是看到MIG仿真输出全是x或者高阻,优先怀疑仿真库没加载完全,而不是你的逻辑写错了。
跑仿真还有一个看板技巧:观察init_calib_done信号拉高的过程,配合MIG内部的calib_signals等调试信号可以清楚看到训练进度。如果你只是想要一个快速的仿真通过,可以把仿真加速选项打开,或者用缩短的刷新时序模型。但正式的验证还是要用完整的时序模型,至少要把初始化训练流程走完。
5.2 复位时序和时钟稳定问题
MIG的初始化依赖一个干净的复位信号。如果板上电时序乱,DDR的电源都没稳定你就释放复位,训练必然失败。我后面加了一个电源状态检测的延时逻辑,确保复位信号在FPGA配置完成且DDR电源稳定之后才开始释放,init_calib_done才稳定拉高。
时钟稳定同样重要。如果MIG的系统时钟在训练过程中短暂丢失或毛刺较多,训练结果可能是错误的但init_calib_done照样拉高。所以MIG的时钟来源最好经过专用的时钟缓冲器,并且在上板测试时用ila抓一下时钟频率是否准确。如果时钟频率偏了零点几个百分点,DDR读写偶发错误就会频繁出现。
还有一种情况是系统复位信号释放太快,比时钟稳定还早,导致MIG内部同步器采样到不确定状态。处理方案也很简单:用复位同步器,让复位释放沿和时钟对齐。example design里有现成的复位模块,直接拿过来用。
5.3 时序不收敛时先查什么
做时序分析的时候发现MIG路径不收敛,这是高频问题。首先查引脚约束是否正确,特别是DDR的时钟引脚和数据引脚是否绑到了对应的专用IO。数据引脚约束不对,实现工具根本没法把物理层逻辑布到正确位置,时序收敛无从谈起。
排除了引脚问题,再看时钟约束。MIG生成的XDC文件里已经包含了对DDR接口时钟的约束,如果你在顶层又加了多余的时钟约束,比如对同一个时钟声明了错误的周期,时序分析结果就会被污染。我建议生成MIG后,在约束文件里打开它自带的XDC,删掉跟自己顶层重复的clock constraint,只保留IP内部的约束。
如果这些都没问题,最后才考虑调整MIG的PHY配置,比如PLL位置、数据位宽训练模式等等。到这一步基本属于高级调优了,学习阶段很少手动去改。在动复杂的train参数之前,先检查自己的设计里有没有在MIG路径上插了跨时钟域的FIFO或者组合逻辑延迟。
5.4 上板验证的推进顺序
上板验证不要一上来就写一个大应用去访问DDR,最好分步骤来。第一步只看init_calib_done是否拉高,拉高了说明DDR训练通过,物理层没问题。第二步做固定地址的读写回环,写一个值进去再读出来比对。第三步再扩展成地址遍历,比如用伪随机地址连续读写一段时间。
我在第二步和第三步之间卡了比较久,原因是我写的数据位宽和突发长度没对齐,读出来的数据总是错位。后来我用ILA同时抓app_rd_valid和app_rd_data,发现数据在每次突发结束后会有几个周期的空窗,我的数据处理逻辑硬是把空窗数据也当成有效数据存了。加一个有效信号进行过滤就正常了。
如果方便的话,板上验证时可以用在线逻辑分析仪抓内部信号波形,不然就老老实实用自检逻辑把比对结果通过串口或者LED输出。这一步的逻辑花费不算多,但能省去很多拿眼睛盯着错误数据的痛苦。
说回到第42天这个节点,MIG确实是FPGA学习过程中一道不小的坎。但这不是因为它本身有多难,而是因为它把DDR的协议细节、物理训练、时钟复位全都打包在了一起,让你第一次感觉到了“一个IP的体量”有多重。我能给你的最实在的建议就是:先跑通example design,再写自己的状态机,每加一点功能就验证一次。DDR这种东西,一次改太多,出了问题你根本不知道是哪一步引入的。等你哪天看到init_calib_done拉高从“紧张期待”变成“理所当然”时,MIG这关就算真正过了。