ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA四原语实战:IODELAY、ODDR、BUFGMUX与BRAM避坑

FPGA四原语实战:IODELAY、ODDR、BUFGMUX与BRAM避坑 1. 这四个原语为什么总在同一块板子上一起出现先把场景摆出来。一块中等规模的板子前端挂了一颗并行输出的ADC采样时钟和数据一起送过来板子另一侧要把数据转成DDR形式打出去同时还要给外部器件送一路随路时钟系统里有两路参考时钟需要按工作模式切换最后数据要缓存一段用片上RAM顶住突发。四个需求看起来八竿子打不着实际上它们在FPGA里全部落在同一类资源上——IOB里的延迟链、OLOGIC/ILOGIC里的双沿寄存器、时钟网络上的全局缓冲器以及Block RAM硬核。这四个东西分别对应标题里的IODELAY、ODDR、BUFGMUX和VIVADO BRAM。我见过太多人在这四个地方翻车原因高度一致把它们当成普通逻辑来写。用assign直接把时钟送到输出引脚用always (*)写个三目运算当时钟切换用一堆寄存器拼一个FIFO当缓存然后在时序报告里看到一堆红色的路径接着开始怀疑是板子画得不好。问题从来不在板子在于没有意识到这四类资源是硅片上已经做好的硬核它们有专用路径、专用参考时钟、专用约束方式你只能按它的规矩用不能按你的想象用。1.1 一次源同步采集的翻车现场最早让我把 IODELAY 和 ODDR 放在一起看的是一个源同步采集的项目。ADC的输出是一路随路时钟加多路DDR数据我在接收端用 IDDR 采数用了几个不同的tap值试了试发现只要把延迟调到某个值就能采对于是写死了一个tap值交差。实验室里跑了三天没问题到了现场温度一上来误码率开始往上飘飘到某个程度就整帧丢。后来我做了两件事一是把接收端的采样点从能采对改到采样窗口正中间二是把输出侧的转发时钟用 ODDR 重新做了一遍让它和输出数据的相对关系可调。做完之后我才意识到这两个动作本质上是同一件事的两面——输入侧要挪采样沿输出侧要挪时钟沿。而在这中间BUFGMUX 决定了哪一路时钟在给这套收发逻辑供血BRAM 决定了数据进FPGA之后能顶多久。四个原语其实是一条完整数据通路上的四个关卡。1.2 原语和普通RTL的职责边界我的经验判断很直接凡是跨越芯片边界、或者直接操纵专用时钟网络、或者调用硬核存储块的逻辑都必须用原语或者让工具明确推断到硬核上剩下的组合逻辑和普通寄存器才交给综合器自由发挥。这里有个很容易被忽略的点Vivado 对时钟路径非常敏感。你用 LUT 做时钟选择综合器不会拦你但它会报一个时钟被普通布线资源驱动的警告然后你的时钟skew、抖动、占空比全都失控时序分析结果也不可信。同理你用assign把内部时钟接到输出引脚Vivado 会提示这个输出时钟没有经过专用转发路径接收端拿到的时钟和数据之间没有确定的相位关系。下面这张表是我自己整理的分工速览先建立整体概念后面每一节再展开。原语归属资源解决的问题不用它的后果IODELAYE2IOB内的延迟链输入采样沿位置微调采样点靠运气温漂后失锁ODDROLOGIC时钟/数据双沿输出输出时钟质量差、相位不可控BUFGMUX全局时钟网络时钟源无毛刺切换切换瞬间产生窄脉冲后端逻辑误触发BRAMBlock RAM硬核大容量片内存储用LUT拼RAM资源爆炸且时序差1.3 这四个原语的共同特征约束比代码重要写这四个原语代码量都不大加起来可能不到一百行。但每个原语背后都跟着一串必须配套做的事这才是真正花时间的地方。IODELAY 必须配一个 200MHz 的参考时钟和 IDELAYCTRL少一个它就不工作ODDR 要配合 PLL 的相移设置不然转发时钟的边沿位置是随机的BUFGMUX 要保证两路输入时钟在切换时满足它的内部时序要求否则可能截断一个周期BRAM 的写模式、输出寄存器、字节使能配置直接决定了你的读出延迟是1拍还是2拍改配置不等于改代码是改IP。我在项目里的做法是先把这些配套条件列成一张检查清单再动手写RTL。清单没打勾之前代码写得多漂亮都是白搭。2. IODELAY把输入采样点做成可调旋钮先纠正一个常见误解IDELAY 不是给信号加个固定延迟它是一根可以精确量化的延迟链你调的是一个整数tap值。在7系列里一根IDELAY的延迟范围大约是2.5ns分成32个tap每个tap约78ps200MHz参考时钟下。78ps是什么概念一个200MHz周期是5ns相当于你能把采样沿在整个位窗口里挪动 1/64 的精度。这个精度对于几百Mbps量级的源同步接口来说是够用的对于上G的接口就得靠ISERDES的位对齐配合了。2.1 DELAYE2内部结构与IDELAYCTRL的硬性约束IDELAYE2 内部是一条由32个延迟单元串起来的链输入信号从链的头部进入从第N个抽头取出来N就是你设置的IDELAY_VALUE。问题在于每个延迟单元的绝对延迟量会随工艺、电压、温度变化所以芯片上专门放了一个IDELAYCTRL模块用一个稳定的参考时钟去持续测量并校准这条链保证一个tap始终约等于78ps。硬性约束有两条踩过的人不少参考时钟必须是 200MHzHR Bank 只支持200MHzHP Bank 支持200或300MHz频率偏差要控制在很小范围内否则 RDY 不拉高IDELAY 输出直接是x。每个时钟区域clock region至少要有一个 IDELAYCTRL。如果你在多个Bank上用了IDELAY要么共用一个IDELAYCTRL前提是这些Bank在同一个时钟区域要么每个区域各放一个。放在哪个区域由综合器自动决定如果想手动控制得用IODELAY_GROUP属性把IDELAY和IDELAYCTRL绑在一起。提示IODELAY_GROUP这个属性很多人不知道。当你跨区域例化多个IDELAYCTRL时不加这个属性Vivado 可能把某几个IDELAY分到没有对应CTRL的区域报no IDELAYCTRL found这类错误。加上(* IODELAY_GROUP grp_adc *)之后布局时就会把这些资源放在一起。2.2 FIXED、VARIABLE、VAR_LOAD三种模式怎么选IDELAYE2 的IDELAY_TYPE有四个取值实际常用的三种FIXED上电后tap值就是IDELAY_VALUE运行中不可改。适合已经量产标定过的固定接口最省资源没有额外的控制逻辑。VARIABLE通过CE和INC两个脚递增/递减tap一次走一步。适合做动态校准——比如检测到误码率上升就往一个方向试探。VAR_LOAD通过LD把CNTVALUEIN上的5位值直接载入一步到位。做调试扫描用这个因为你可以在几十个时钟周期内把32个tap全扫一遍而用VARIABLE模式得一步步挪慢得多。还有一个VAR_LOAD_PIPE在VAR_LOAD的基础上多一级流水寄存器LDPIPEEN控制。一般设计用不到除非你的LD信号本身就是高频跳变的。我的选型逻辑是调试阶段一律用VAR_LOAD量产固化成FIXED带动态校准需求的用VARIABLE。这个分阶段的做法可以让你在调试时获得最大的灵活性同时又不会把调试逻辑带进最终版本。2.3 一个可扫tap的IDELAYE2实例与参数逐条说明下面这段是我调试时用的模板接收通道用VAR_LOADtap值可以从外部比如ILA的VIO直接写入。(* IODELAY_GROUP grp_adc *) IDELAYE2 #( .CINVCTRL_SEL (FALSE), // 不需要动态反相 .DELAY_SRC (IDATAIN), // 数据来自引脚必须选IDATAIN .HIGH_PERFORMANCE_MODE (TRUE), // 高速模式下抖动更小、功耗更高 .IDELAY_TYPE (VAR_LOAD),// 调试期用VAR_LOAD量产改FIXED .IDELAY_VALUE (16), // FIXED模式下的初始tap居中起步 .PIPE_SEL (FALSE), .REFCLK_FREQUENCY (200.0), // 必须与IDELAYCTRL的参考时钟一致 .SIGNAL_PATTERN (DATA) // 数据通道选DATA时钟通道才选CLOCK ) u_idelay_adc_d0 ( .CNTVALUEOUT (tap_mon_d0), // 回读当前tap接ILA观察 .DATAOUT (adc_d0_dly), // 延迟后的数据送IDDR .C (clk200), // 与REFCLK同域的200MHz时钟 .CE (1b0), // VARIABLE模式才用 .CINVCTRL (1b0), .CNTVALUEIN (tap_in_d0), // 5位外部写入的目标tap .DATAIN (1b0), // DELAY_SRC为IDATAIN时此脚不用 .IDATAIN (adc_d0_ibuf), // 来自IBUF的数据 .INC (1b0), .LD (tap_ld_d0), // 高电平载入CNTVALUEIN .LDPIPEEN (1b0), .REGRST (1b0) ); IDELAYCTRL u_idelayctrl ( .RDY (idelay_rdy), // 没拉高就说明参考时钟有问题 .REFCLK (clk200), .RST (~rst_n) );几个参数需要特别说明DELAY_SRC选IDATAIN还是DATAIN取决于信号是来自引脚还是来自内部布线。走引脚进来的数据必须用IDATAIN否则延迟链根本不在信号路径上你会得到一个完全没有延迟效果的正常波形然后怀疑人生。HIGH_PERFORMANCE_MODE设为 TRUE 时延迟链的抖动更小但静态功耗会上去一点。对于超过200MHz的接口我一般直接开对低速接口比如几十兆的SPI模拟关掉省点功耗也没问题。REFCLK_FREQUENCY必须和实际给的参考时钟频率严格对应写 200.0 就必须给200MHz。写错了的话 IDELAYCTRL 的 RDY 可能还是拉高但每个tap的实际延迟量算错你标定出来的窗口就是错的——这个坑特别隐蔽因为波形看起来能工作。2.4 标定tap值与温度漂移的实测处理标定的方法很朴素写一个已知的伪随机序列从ADC或者测试模式发过来扫tap 0到31把每个tap下的误码个数记下来画出误码率随tap变化的曲线。曲线中间会有一段平坦区域这就是采样窗口把tap设到窗口正中间。我实测下来的几个经验第一窗口宽度和信号速率强相关。100MHz左右的源同步接口窗口可能有十几个tap到了400MHz以上窗口缩到三五个tap很常见。如果你发现窗口只有1到2个tap说明布线延迟和时钟延迟已经快把窗口吃完了这时候应该考虑用PLL的相移先把大范围的偏差调掉再用IDELAY做精细调整。粗调和细调分开做这是我踩过坑之后总结出来的。第二窗口中心会随温度漂移。我在一个项目里做过测试常温下标定好的tap15加热到70度后最佳tap变成了13偏移两个tap大概150ps。这个量对低速接口无所谓对高速接口就是能不能过的区别。所以高速接口上我会用VARIABLE模式做周期性校准——每隔一段时间重新扫一次窗口或者用误码检测反向推动tap。第三标定时一定要让整个链路工作在最坏条件附近再扫。用常温、标称电压标出来的中心到了极限条件下可能已经偏出窗口了。3. ODDR输出时钟和数据必须成对送出输入侧调好之后输出侧的问题马上就来了。很多人第一次做源同步输出时都会写出这样一行代码assign data_clk_out clk_200m; // 危险写法然后综合通过、实现通过、上板也能用——如果你运气好的话。这行代码的问题在于它让一个内部时钟经由普通布线资源走到了输出引脚。Vivado 会给你一个警告但很多人不看警告。结果就是输出时钟的抖动被布线和LUT的延迟放大和数据之间的skew随布局变化换一版工程重新实现时序又不一样了。3.1 输出时钟为什么必须走ODDRFPGA 的IOB里有一组专用结构叫OLOGIC其中的 ODDR 就是为双沿输出设计的。它有两个数据输入 D1、D2一个输出 Q输出在每个时钟周期的上升沿和下降沿各更新一次。把 D1 固定接1、D2 固定接0Q 就变成了一个完整的时钟而且这个时钟是从OLOGIC的专用路径直接到输出引脚的延迟确定、抖动小、与其他ODDR输出的数据之间的相对关系可控。用ODDR转发时钟的第二个好处是相位可调。你输出的数据也是通过ODDR发出去的两者走同样的专用路径延迟基本一致如果接收端需要在数据眼中心采样只需要在PLL的时钟输出上加一点相移这个相移会同时作用在数据路径和时钟路径上相对关系保持不变。这就是源同步的本质——时钟和数据一起走让它们之间的相对关系说了算而不是让接收端去猜。3.2 SAME_EDGE和OPPOSITE_EDGE的差别到底在哪ODDR 的DDR_CLK_EDGE参数有两个值这是最容易搞混的地方我用一段话把它说清楚OPPOSITE_EDGED1 在时钟上升沿被采样并送到输出D2 在时钟下降沿被采样并送到输出。这意味着你的逻辑必须在两个边沿都提供有效数据对上游逻辑的时序要求高半个周期。SAME_EDGED1 和 D2 都在时钟上升沿被采样输出侧再由内部结构分配到两个半周期上。上游逻辑只需要在上升沿提供两个数据时序压力小一半。对转发时钟这个用法来说两种模式转发出来的时钟相位相差半个周期这个差异不是随便选的——它正好用来调整接收端的采样边沿位置。我在做输出时钟转发时的做法是先用SAME_EDGE因为它的上游时序最好满足D11、D20然后看接收端的采样边沿落在哪如果落在数据跳变沿附近就在PLL上补一个相移。ODDR #( .DDR_CLK_EDGE (SAME_EDGE), .INIT (1b0), .SRTYPE (SYNC) ) u_oddr_fwd_clk ( .Q (fwd_clk_out), // 送到OBUF再到引脚 .C (clk_tx), // 与数据同源的发送时钟 .CE (1b1), .D1 (1b1), .D2 (1b0), .R (~rst_n), .S (1b0) );注意SRTYPE选SYNC时复位脚R需要至少一个时钟周期的宽度才能可靠复位。选ASYNC的话复位更快但会引入复位释放的时序问题。我一般选SYNC让复位逻辑统一走同步复位。3.3 输出数据的DDR化三种写法对比数据侧的DDR输出有三种做法各有适用场景做法资源占用适用速率灵活性手动例化ODDR每个bit一个ODDR中低速600Mbps高完全可控手动例化OSERDESE2每个bit一个SERDES高速可达1Gbps以上高但要处理位对齐SelectIO Wizard生成自动生成整套全速率低改配置要重生成我个人的习惯是速率不高、通道数不多的场合手动例化ODDR代码短、看得懂、好调通道多又要求高速的场合用SelectIO Wizard因为它会自动帮你处理IDELAY、ISERDES、bit slip这些麻烦事代价是生成出来的代码不好读。手动例化ODDR输出数据时有一个细节必须注意D1和D2对应的是同一个时钟周期的两个半周期谁在前谁在后取决于模式。如果搞反了你会看到输出数据的高低半周期交换表现在接收端就是每隔一位错一次。我踩过这个坑当时以为是时钟相位问题调了半天PLL相移最后发现是D1/D2接反了。3.4 输出侧的复位与初始状态处理ODDR 的INIT参数决定上电初值R和S分别是复位和置位。转发时钟的ODDR如果初值不对上电后会先在引脚上输出一段不确定的电平接收端可能误判成有效时钟。我的做法是把转发时钟的ODDR初值设为0并且用系统复位拉一段时间等PLL锁定之后再释放。等锁定再释放这一步很关键——PLL没锁的时候时钟是乱的这时候输出到引脚上接收端可能已经跑飞了。4. BUFGMUX时钟切换不能只用LUT时钟源切换的需求在很多系统里都有工作模式A用本地晶振模式B用外部恢复时钟或者主时钟失效时切到备用时钟。最直觉的写法是assign clk_sel sel ? clk_b : clk_a; // 千万别这么写这行代码的问题不在于功能而在于切换瞬间的毛刺。当sel变化时如果clk_a正好是高电平、clk_b正好是低电平输出会出现一个宽度不受控的窄脉冲。这个脉冲足以让下游的寄存器采到一个错误值而且这种错误极难复现——它依赖sel变化时两路时钟的相位可能几千次切换才出现一次。4.1 BUFGCTRL和BUFGMUX到底是什么关系BUFGMUX 是 BUFGCTRL 的一个封装。BUFGCTRL 是底层的通用时钟选择模块有 I0/I1 两路输入S0/S1 两个选择脚CE0/CE1 两个使能脚还有 IGNORE0/IGNORE1 两个忽略控制脚。BUFGMUX 把 S0/S1 合并成一个 S 脚CE 固定为使能对外只有 I0/I1/S/O 四个脚。真正保证无毛刺的机制是这样的内部有一个小的状态机只有当被选中的那路时钟处于低电平时切换才会真正发生。所以输出永远不会在时钟的高电平期间被切断也就不会产生窄脉冲。这里有个关键限制必须说清楚BUFGMUX 能保证无毛刺但不能保证无窄脉冲。如果两路时钟是异步的切换时新选中的时钟可能正好在上升沿附近输出会出现一个被截断的短周期。对于纯数字逻辑短周期通常只是让某个周期变快逻辑本身不会出错但如果后端有时钟分频器、或者有对周期敏感的电路这个短周期就是灾难。4.2 异步时钟切换的正确做法我的做法分两种情况情况一两路时钟同源比如都来自同一个MMCM的不同输出。这种情况下两路时钟有确定的相位关系用BUFGMUX直接切就是干净的不用担心短周期。这是最省事的方案能用就用。做法是利用MMCM同时产生两路输出或者用两个BUFG从同一路时钟分频出来。情况二两路时钟完全异步。这时候我不用裸的BUFGMUX而是用BUFGCTRL 握手。顺序是先在当前时钟域里同步要切换这个请求然后在当前时钟域确认已经停止输出用一个计数器数够几个周期再关闭当前路的CE确认关闭完成后再打开新路的CE。整个过程在旧时钟还活着的时候完成等新时钟稳定输出后再释放后级逻辑的复位。// 骨架示意具体的手势时序请对照官方文档的BUFGCTRL时序要求逐条确认 BUFGCTRL #( .INIT_OUT (0), .PRESELECT_I0 (FALSE), .PRESELECT_I1 (FALSE) ) u_clk_mux ( .O (clk_sel), .I0 (clk_a), .I1 (clk_b), .S0 (1b1), // 选择逻辑由CE控制 .S1 (1b0), .CE0 (ce_clk_a), // A路使能 .CE1 (ce_clk_b), // B路使能 .IGNORE0 (1b0), .IGNORE1 (1b0) );IGNORE0/IGNORE1这两个引脚的设计意图很有意思当某一路时钟已经停了比如外部时钟源失效但它的输入端还在翻转或者抖动时把对应的 IGNORE 拉高可以让内部状态机忽略这路输入的状态直接按剩余那路来决策。用在主时钟失效、切换备用时钟的场景里非常合适。4.3 切换之后别忘了复位这是我要重点强调的一条经验时钟切换完成后必须对切换后的时钟域做一次复位。原因是两路时钟的相位关系是不确定的。切换前的时钟域里所有寄存器的状态是相对于旧时钟建立的切换后新时钟的边沿位置和旧时钟没有任何关系某些寄存器可能刚好处于亚稳态或者某个分频计数器的值变得没有意义。这时候如果不对整个时钟域复位你会看到一个非常诡异的现象——系统能跑但偶尔某个计数器跳变或者状态机卡在某个不该停留的状态。我的处理方式是把时钟已经稳定切换完成作为一个复位源给后级逻辑一个几拍的复位脉冲。等复位释放整个域的起点就统一了。5. VIVADO BRAMIP配置里的每个选项都在影响时序最后一个原语是BRAM。严格说Vivado里大部分人用的是Block Memory Generator这个IP不是直接例化RAMB36E1。但理解IP配置背后的硬核行为才能知道为什么你的读数据晚了一拍、为什么综合出来的RAM没进BRAM。5.1 三种端口模式对应的真实场景模式端口结构典型场景Single-port RAM一套读写端口单时钟域的缓存、查找表Simple Dual Port一个写口一个读口跨时钟域的异步FIFO主体、乒乓缓冲True Dual Port两套完整读写口两个时钟域都要读写同一块RAM选错模式会浪费资源。比如你只需要一个口写、一个口读用 Simple Dual Port 就够了如果用 True Dual Port工具可能会给你多占一个BRAM因为真双口需要两套地址译码和输出寄存器。我踩过的一个坑早期做跨时钟域缓存我选了 Simple Dual Port然后天真地以为写口写完读口就能读到。实际上写数据从写时钟域进入BRAM到读时钟域能看到中间隔着至少一个读时钟周期如果开了输出寄存器就是两个而且这个延迟关系需要在读侧的读使能逻辑里扣掉。如果不扣你读到的永远是上一个地址的数据。5.2 WRITE_FIRST、READ_FIRST、NO_CHANGE的实际差别这三种写模式只在同一个端口同拍对同一地址读写时才有区别WRITE_FIRST写入的数据同时出现在读输出上。读端口会看到新值。READ_FIRST读输出保持旧值写入的数据下一个周期才能读到。NO_CHANGE写操作期间读输出保持不变不是旧值是不变。看起来只是差一拍但实际影响很大。READ_FIRST 是最容易推断的——综合器看到标准的先读后写结构几乎总能正确推断出BRAMWRITE_FIRST 需要综合器理解你的读优先语义有时候推断不出来就退化成LUT拼的RAM了。我的建议是写代码时按READ_FIRST的语义写把读数据延迟一拍明确地在逻辑里体现出来。这样综合结果稳定不会因为换一版工具就变。5.3 位宽深度换算与输出寄存器BRAM 的容量是36KbRAMB36或18KbRAMB18。换算逻辑很简单深度乘以位宽不超过容量同时深度和位宽要落在允许的配置范围内。举个实际例子。我需要一块 2048 x 16 的缓存算一下2048 × 16 32768 bit 32Kb小于36Kb所以一个RAMB36就够了。但如果我要 4096 x 16 64Kb超过36Kb就需要两个RAMB36级联或者用两块RAM并行。这里有个技巧位宽在9位以上时可以使用字节写使能。比如16位位宽可以分成两个8位字节写使能这样写一半的时候不需要读-改-写。位数是8的倍数的时候字节使能最省事如果位宽是12位、20位这种字节使能的粒度就对不齐需要额外的处理逻辑。关于**输出寄存器Primitives Output Register**这个选项它的作用是在BRAM输出加一级寄存器缩短从BRAM到fabric的组合路径改善时序。代价是读延迟从1拍变成2拍。我的判断标准是如果这块RAM的工作频率超过200MHz或者读路径后面接的逻辑比较深就把输出寄存器打开如果是低速、读延迟敏感的场景就关掉。5.4 COE初始化文件格式与常见报错用COE文件给BRAM预置内容格式比较严格最容易错的是分隔符和行尾。memory_initialization_radix16; memory_initialization_vector 0001,0002,0003,0004, 0005,0006,0007,0008, ... FFFF;几个注意事项radix后面必须是分号vector后面必须是等号加分号再换行最后一个数据后面必须是分号不是逗号。数据个数必须严格等于IP配置的深度多一个少一个都会报错而且报错信息不一定直观——有时候提示的是init file size mismatch你得自己回去数。另外如果位宽超过一个radix位能表示的范围比如16位宽、radix16数据要用逗号分隔、每个数据用完整的十六进制表示。我见过有人把16位数据拆成两个4位十六进制数用逗号分开写那样会被当成两个独立地址的数据纯粹是浪费地址空间。5.5 RAM没进BRAM的排查顺序综合完之后发现资源报告里LUT用量异常高BRAM用量是0说明你的RAM被推断成分布式RAM了。按这个顺序查排查项现象处理复位逻辑读地址被异步复位去掉读地址复位或改成同步复位读数据初值声明了初值或用了复位去掉读数据寄存器的复位读写时序组合读、同拍写读混用改成标准的同步读结构位宽深度太小工具自动选LUTRAM加属性强制指定属性标注没有明确标注加(* ram_style block *)最省事的做法是直接给信号加属性(* ram_style block *) reg [15:0] mem [0:2047];加了之后综合器会优先用BRAM。当然前提是你的代码结构本身是可推断的——属性只是建议不是命令结构不对的话加了属性也没用。6. 把四个原语串成一条通路搭建顺序与约束前面四节是分开讲的实际项目里它们是一条链。这一节说搭建顺序和约束这部分做错了前面四个原语调得再好也是零。6.1 先定时钟架构再定IO时序最后做存储我的搭建顺序是固定的三步第一步时钟架构。先把PLL/MMCM配好确定有几路时钟、分别走BUFG还是BUFGMUX、有没有时钟切换需求。时钟树是所有约束的基础时钟没定下来就写IO约束纯属浪费时间。这一步要产出的东西是一张时钟拓扑图标注每一路时钟的来源、频率、经过哪些buffer、驱动哪些逻辑。第二步IO时序。等时钟定了再决定输入侧用不用IDELAY、输出侧用不用ODDR。这一步的关键是把IDELAY和ODDR的时钟与PLL的输出对应起来——IDELAY的参考时钟必须是一个独立的、稳定的200MHz不能是运行时会被切换的时钟ODDR的C输入必须和输出数据的时钟同源同相。第三步存储与流水线。最后才处理BRAM因为BRAM的位置会影响布线进而影响IO路径的时序。如果先做BRAM后做IO很可能出现IO时序满足不了、回头再改BRAM布局的情况来回折腾。6.2 输入输出延迟约束怎么写输入侧约束以源同步输入为例create_clock -name adc_clk -period 5.000 [get_ports adc_clk_in] set_input_delay -clock adc_clk -max 1.500 [get_ports adc_data_in*] set_input_delay -clock adc_clk -min 0.500 [get_ports adc_data_in*] set_input_delay -clock adc_clk -clock_fall -max 1.500 [get_ports adc_data_in*] set_input_delay -clock adc_clk -clock_fall -min 0.500 [get_ports adc_data_in*]注意最后两行源同步DDR数据的约束必须成对写上升沿和下降沿少一组的话另一半周期的路径完全没有约束工具会当成无关路径处理然后你会发现时序全绿但功能不对。输出侧转发时钟需要生成一个虚拟时钟来描述接收端的时钟create_generated_clock -name fwd_clk \ -source [get_pins u_oddr_fwd_clk/C] \ -divide_by 1 \ [get_ports data_clk_out] set_output_delay -clock fwd_clk -max 1.200 [get_ports dout*] set_output_delay -clock fwd_clk -min -0.200 [get_ports dout*]-source指向的是ODDR的C脚不是输出端口本身这样工具才知道转发时钟是从哪个内部时钟派生出来的才能正确计算输出路径的延迟关系。6.3 用ILA观测tap扫描结果调试IODELAY最有效的方法是把tap扫描的结果可视化。我的做法是把CNTVALUEOUT和误码计数一起接进ILA用VIO控制tap值和LD信号每扫一个tap就让VIO记一次数。具体步骤在VIO里放一个5位的tap_sel和一个tap_ld按钮。把tap_sel接到IDELAYE2的CNTVALUEINtap_ld接到LD。在ILA里采误码计数器和tap_sel。手动或者用一个简单的状态机自动扫过0到31每个tap停留几千个周期。把误码计数导出来找窗口中心。自动化扫描我一般用小状态机做比手动点VIO快得多// 简化示意每个tap停留一段时间后自动加一 always (posedge clk200 or negedge rst_n) begin if (!rst_n) begin tap_cnt 5d0; dwell 20d0; end else if (scan_en) begin if (dwell 20d200_000) begin dwell 20d0; tap_cnt tap_cnt 1b1; end else begin dwell dwell 1b1; end end end6.4 联调速查表把前面几节的高频问题汇总成一张表出问题的时候按这个顺序过一遍能省不少时间。现象最可能的原因先查这里IDELAY不工作输出无延迟参考时钟不是200MHz或没接对IDELAYCTRL的RDY是否拉高输入采样偶尔错温度变化后变差tap值没取窗口中心重新扫窗口检查窗口宽度输出时钟抖动大直接用assign输出时钟改成ODDR转发输出数据每隔一位错ODDR的D1/D2接反核对DDR_CLK_EDGE与D1/D2对应时钟切换后系统偶发异常切换后没复位或有短周期加切换后复位核对两路时钟是否同源读数据总是旧值BRAM写模式与时序理解不一致核对WRITE_FIRST/READ_FIRST与读延迟BRAM用量为0RAM被推断成LUTRAM按5.5的清单逐项排查最后再说一句关于约束的态度。这四个原语的约束文件我建议写完之后逐条问自己这条约束在描述什么物理事实。比如set_input_delay -min描述的是数据最早可能到达的时间这个值来自器件手册和数据手册的最坏情况计算不是你随便填的。约束写错比不写约束更危险因为它会让你以为时序已经收敛了。我个人在这些原语上花的时间大概有三成在写代码七成在标定延迟、扫窗口、对相位、看波形。如果你现在正卡在某个IO接口上建议先别急着改RTL把IDELAY的tap扫一遍、把ODDR转发的时钟打出来看一眼、把时钟切换前后的波形对着看很多时候问题就自己浮出来了。
返回列表