拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

分段CTS实战:解决SoC中DDRPHY与Memory Controller的时钟树难题

分段CTS实战:解决SoC中DDRPHY与Memory Controller的时钟树难题

1. 项目概述:一颗SoC里,DDRPHY与Memory Controller为什么总在打架

做SoC芯片的后端物理设计,最让人头疼的往往不是CPU主频那棵大时钟树,也不是总线上那些跑得飞快的接口逻辑,而是DDRPHY和Memory Controller这一对冤家。DDRPHY是买来的硬核IP,Memory Controller是自研数字逻辑,两者通过DFI接口对接。回片之后DDR跑不稳、跑不到标称频率、温度和电压一变化就报错,十有八九问题出在这两棵时钟树的关系没理顺。

这篇文章要讲的,就是我在实际项目中怎么用分段CTS,也就是Segmented Clock Tree Synthesis的思路,把DDRPHY和Memory Controller之间的时序难题拆开、分而治之。方案落地后,DFI接口的时钟skew从78ps压到了38ps,原来大批量的hold violation清零,芯片在DDR4-2400跑到标称速率,高低温下长时间压力测试也没再出问题。适合给正在做SoC后端集成、DDR接口物理设计,或者准备接DDRPHY硬核IP的工程师参考。

1.1 先捋清楚:DDRPHY、Memory Controller和DFI接口各自扮演什么角色

很多做数字前端的朋友对这三个名词的分工其实有点模糊,我先用最直白的方式讲清楚。

Memory Controller,简称MC,是SoC内部负责管理DDR存储器的数字逻辑。CPU、GPU、NPU这些主控发出的读写请求,首先汇聚到MC,由它转换成DRAM能理解的命令序列,比如ACT、READ、WRITE、REF这些。MC本身是一大坨标准的数字逻辑,挂在SoC的全局时钟域下面,里面动辄几千个寄存器,时钟树要灌到所有寄存器上。

DDRPHY则是连接MC与外部DRAM芯片的物理层接口IP。它内部包含IO驱动器、接收器、DLL延迟锁相环、per-lane delay line这套模拟电路,以及一部分用来做命令和数据整形的数字逻辑。DDRPHY通常是硬核IP,内部已经有一套完整的、经过硅验证的时钟树,外部不需要也不应该再往里面插buffer。它对外暴露的时钟接口一般只有一个时钟输入引脚,甚至有时候只要求输入一个干净的时钟源,剩下的全部自己搞定。

MC和DDRPHY之间靠DFI接口,也就是DDR PHY Interface互联。DFI总线包括dfi_clk、dfi_cs_n、dfi_cke、dfi_odt、dfi_addr、dfi_ba、dfi_ras_n、dfi_cas_n、dfi_we_n、dfi_wrdata、dfi_wrdata_mask、dfi_rddata、dfi_rddata_valid等一大组信号。以DDR4-2400为例,内存接口频率是1200MHz,DFI接口时钟一般也是这个频率或者两分频,周期在1.66ns到0.83ns这个量级。DDRPHY的DQ位宽如果是32bit,DFI的wrdata/rddata位宽往往会扩展到64bit甚至128bit,接口总信号数轻松超过100根。

问题就出现在这100多根信号的发射和接收上。MC端是普通数字逻辑,用MC的时钟信号去发射dfi信号,DDRPHY端再用它接收到的时钟去采样。DDRPHY的采样时钟本质上来自同一个源头,但经过了DDRPHY内部那棵树。对于DFI接口这类高扇出、高频率的同步接口来说,发射时钟和采样时钟之间必须保持高度的同相性,这就是整个时钟树设计里最微妙也最致命的地方。

1.2 传统单棵时钟树为什么搞不定DFI接口

如果你第一次接触SoC集成,最容易想到的方案很自然:全局一个时钟源,拉一棵大树,把所有寄存器和DDRPHY的时钟输入引脚都当成sink,让工具统一做平衡。这个思路在纯数字逻辑内部是没问题的,但一旦涉及DDRPHY硬核IP,就会暴露出一堆问题。

第一个问题是负载数量悬殊。MC内部几千个寄存器flop,DDRPHY外部能看到的sink只有一个时钟输入引脚,两者的扇出量差了三个数量级。标准CTS工具在做平衡时,会为了满足MC那几千个flop的skew要求,把树拉得很深很长,DDRPHY那个孤零零的时钟引脚只能跟在后面一起等。最后的结果是,DDRPHY的时钟输入路径飘得又长又远,大量共享路径上的寄生电容和IR drop差异会直接转化为DFI接口上的skew。

第二个问题是干扰。DDRPHY内部的模拟电路对时钟质量极其敏感,特别是DLL。DDRPHY需要用DLL把内部时钟对齐到外部DRAM的DQS信号上,如果输入时钟沿上有大的jitter或者duty cycle失真,DLL锁相之后会把噪声放大,最终表现为读数据时的tDQSCK、tDQSQ这些参数全面恶化。MC那几千个flop同时翻转,会在电源网络上制造很大的动态压降,如果DDRPHY的时钟树和MC的时钟树共享了太多的buffer和长线,这些压降会直接污染DDRPHY的时钟沿。

第三个问题是耦合。MC做读写调度时,DFI总线上的信号翻转密度极高,尤其是wrdata和rddata,几乎每个cycle都在跳。传统大树方案里,CTS为了平衡skew会把时钟线拉到离DFI数据线很近的地方,SI耦合的噪声会注入时钟网络。我在项目里实测过,时钟线旁边如果平行跑着一组高速DFI数据线,耦合造成的时钟jitter能占到总jitter预算的三成以上。

所以问题的本质很清楚:MC需要一棵大树来平衡几千个寄存器的延迟,DDRPHY需要一条干净、短、负载轻的时钟路径来保证模拟电路的时钟质量,而DFI接口又要求这两者之间保持严格的同相关系。这三件事靠一棵树同时满足,在物理上就是互相掣肘的。分段CTS,正是为打破这个死结而生的。

2. 分段CTS的整体设计思路:把一棵树拆成两头各自舒服

分段CTS的核心思想不复杂:不要试图用一棵树解决所有sink,而是把时钟树按照功能域和物理域切成几段,每一段独立规划、独立平衡,然后通过设计好的分叉点保证段与段之间的时序关系。

放到DDRPHY和MC这个场景里,我会把时钟树切成三段。第一段是从PLL输出到MC时钟根节点的全局主干,这一段要保证低抖动、低延迟,把干净的时钟送到MC的入口。第二段是MC内部的时钟子树,从MC时钟根节点灌入几千个寄存器,这一段按照普通数字逻辑的skew标准去平衡。第三段是从同一分叉点引到DDRPHY时钟输入引脚的独立分支,这一段只带一个sink,要求路径短、负载轻、远离吵扰源。

这个切法背后有几个工程上的讲究,得一个个说清楚。

2.1 分段的第一层逻辑:负载隔离与独立优化

先说最直观的负载隔离。MC内部几千个flop的时钟负载,是DDRPHY时钟输入的几百倍。如果两段共享从分叉点到叶子之间的长路径,MC那部分负载翻转引起的电源噪声、buffer驱动能力波动,会通过共享节点传导到DDRPHY分支上。分段之后,MC的时钟子树和DDRPHY的时钟分支从分叉点开始就走不同的物理路径,由不同的buffer驱动,MC再怎么闹腾,对DDRPHY分支的影响也被削弱到了可接受范围。

我习惯用供电系统来理解这件事。一个变电站同时给工厂区和居民区供电,工厂区的大型设备频繁启停,会导致整条线路电压波动,居民区的灯跟着忽明忽暗。解决办法不是在居民区加个稳压器,而是给工厂区单独拉一条专线。分段CTS里,DDRPHY的分支就是那条专线,它换来的是DDRPHY时钟域的清静。

独立优化还有另一层好处:MC内部时钟树和DDRPHY分支的优化目标可以不一样。MC子树追求的是把所有寄存器的clock latency压到同一个窄区间,哪怕多插几级buffer也不心疼。DDRPHY分支追求的则是延迟敏感,路径上能少一级buffer就少一级,因为这直接影响DFI接口launch与capture之间的相对关系。一棵大树框架下,工具根本没法同时满足这两种差异化目标。

2.2 分叉点与分组的选法,直接决定成败

分段方案里有一个极其关键的参数,分叉点选在哪里。分叉点选得太靠前,比如直接从PLL输出端分开,那么MC树干和DDRPHY分支的common path太短,PVT变化对两边延迟的影响差异会直接暴露成skew,OCV这只老虎会咬人。分叉点选得太靠后,比如塞到MC时钟树深处,那么到DDRPHY分支的路径会被MC的负载拖累,又回到了开头说的那种互相干扰局面。

我的经验是,分叉点放在MC时钟树的根节点,也就是MC时钟入口的第一个buffer之后。这个位置的common path已经覆盖了PLL到MC根节点的延迟,足够长;同时它又位于MC大规模扇出之前,DDRPHY分支不会背上MC的负载。具体操作上,我一般会在MC根节点后面放一个两级buffer的小树,第一级用大驱动buffer,比如CLKBUFX32,专门负责把时钟能量抬起来,然后从这个节点分出两路,一路进MC,一路去DDRPHY。

这里有一条铁律:分叉点前端的clock tree,在布局上必须尽量做成一条直线,不要绕弯,不要在中途挂任何其他负载。因为分叉点之前的路径是MC时钟与DDRPHY时钟的公共路径,这条路径上的任何不平衡都会同时加在两棵子树上,而且无法通过子树的deskew去补偿。公共路径越干净,后端的CPPR补偿越省心。

另外,DFI接口的launch和capture时钟必须明确指定为concurrent delay模式,也就是让工具知道这两个时钟是同源且要求同时到达,不能当成两个独立时钟去做异步处理。很多项目翻车就翻在这里,工具如果不知道DFI是concurrent关系,它会自由优化MC时钟和DDRPHY时钟的相对延迟,导致DFI接口时序一团糟。

2.3 给DDRPHY的树枝,要额外守住三个时钟质量参数

DDRPHY对时钟的要求和MC纯数字逻辑很不一样。MC只关心skew,只要每个寄存器的时钟沿对齐就行,哪怕这个沿比理想沿晚来1ns,只要大家都晚1ns,功能照样正确。DDRPHY不行,它的模拟电路对时钟的绝对质量有硬性要求。

第一个参数是时钟jitter。DDRPHY的DLL做相位对齐时,参考时钟上的随机抖动会通过DLL的环路传递到输出,形成相位噪声。实测下来,DDR4-2400的参考时钟jitter如果超过30ps RMS,读写裕量就会明显恶化。所以DDRPHY分支上,我会避免让时钟路径和MC那边的高活跃信号线平行走太长距离,跨层时也要选噪声隔离好的走线层。

第二个参数是duty cycle。DDRPHY内部很多双沿电路依赖时钟的占空比,如果时钟路径上buffer级数太多、上升沿和下降沿的延迟差被放大,duty cycle失真会直接破坏DQS和CK的相位关系。给DDRPHY分支用的buffer,我一般会特别指定用duty-cycle-preserve的类型,并且在DRC里对duty cycle做专项检查。

第三个参数是时钟沿速率,也就是转换时间transition time。DDRPHY时钟输入引脚的transition如果太慢,时钟沿经过输入保护电路时会产生幅度噪声,DLL的锁定精度随之下降。我在代码里会针对DDRPHY的时钟输入pin单独设max_transition约束,比普通逻辑的默认值收紧至少20%。

三根支柱撑住之后,DDRPHY分支才能算是一条合格的专线,而不只是一个普通的skew balanced sink。

3. 分段CTS实战:约束、脚本与收敛全过程

方案说完了,接下来是动手环节。我会把从SDC约束到工具配置、再到收敛记录的完整过程走一遍,中间用到的命令和参数都是主流的STS工具,比如ICC2和Innovus都支持的分层CTS思路,区别只是具体语法,逻辑完全通用。

3.1 时钟约束与DFI接口时序预算

分段CTS的第一步不是画时钟树,而是把SDC里DFI接口的约束定义清楚。时钟定义必须明确告诉工具:MC时钟和DDRPHY采样时钟同源,并且要求同时到达。

示意约束大致长这样:

# 定义MC主时钟 create_clock -name mcu_clk -period 1.666 [get_ports clk_in] # 定义DFI接口的虚拟参考时钟,用于concurrent delay检查 create_clock -name dfi_ref_clk -period 1.666 [get_pins ddr_phy/pll_ref_clk] # 声明MC时钟树与DFI参考时钟为concurrent关系 set_clock_tree_options -clock_concurrent_mode true

DFI接口的时序预算要看DDR的速率来定。以DDR4-2400为例,DFI时钟周期1.66ns,我一般把接口总的uncertainty拆成三份:时钟skew预算50ps,jitter预算30ps,余量留给IR drop和SI,大概20ps。总uncertainty初始设到100ps,跑通流程之后再逐步收紧到70ps。

这里要提醒一句,不要一上来就把uncertainty设成那50ps的理论值。工具在CTS阶段看到的uncertainty越紧,就会越激进地插buffer去补偿,结果树又高又密,后布线阶段反而无从下手。我见过不少项目的后端,DFI接口的timing report看着很漂亮,但布线完之后整个区域congestion爆掉,所有时序全部重来。正确做法是给工具留一点呼吸空间,先确保功能收敛,再用ECO补丁把裕量追回来。

3.2 分段时钟树的spec与工具配置

接下来是CTS的spec配置。需要告诉工具,整棵时钟树在哪些点分段,每一段独立平衡,段与段之间的物理路径由谁负责。ICC2里的实现思路是创建两组独立的clock tree,分别指定source和target,然后通过分段点连接。

示意脚本:

# 第一段:全局主干,从时钟源到MC根节点 set_clock_tree_options -tree_name GLOBAL_TRUNK \ -output_stage_on_clock_pin none # 第二段:MC内部时钟子树 create_clock_tree -name MC_TREE \ -source [get_pins u_mc/clk_root_buf/Z] \ -targets [get_pins u_mc/genblk*/clk] # 第三段:DDRPHY专用分支 create_clock_tree -name DDRPHY_TREE \ -source [get_pins u_mc/clk_root_buf/Z] \ -targets [get_pins u_ddr_phy/clk_in]

中间还有个关键步骤是指定分段点两侧的buffer类型和驱动强度。MC_TREE这边我用逐级倍增的驱动方案,CLKBUFX8进CLKBUFX16再进CLKBUFX32,保证MC的大扇出能被稳定地推开。DDRPHY_TREE这边我反而刻意压小了驱动,用CLKBUFX16就够,因为只带一个sink,过大的驱动反而会把transition做太陡,产生不必要的串扰噪声。

工具配置里还要显式关掉跨组的自动平衡。如果让工具同时优化MC_TREE和DDRPHY_TREE的绝对延迟,它很有可能因为追求两边latency一致,往DDRPHY分支上猛塞buffer,这完全违背了我们分段的初衷。正确做法是让两棵子树各自平衡,然后靠分叉点之前common path的时序关系来保证DFI接口的对齐。

在布局层面,DDRPHY和MC的摆放也要配合分段方案。DDRPHY的时钟输入引脚要朝向MC的方向,尽量拉短两段分叉点之间的物理距离。DFI接口的100多根信号线要排布在DDRPHY朝向MC的那一侧,避免DFI数据线和两棵时钟树的长路径交叉。我在项目里是把DDRPHY放在SoC的右下角,MC紧贴它上方,PLL放在MC的左侧,这样时钟从左侧进来,先经过MC根节点,再分叉往右下进DDRPHY,物理走向非常干净。

3.3 一版真实收敛记录:从78ps skew到38ps

讲一个具体的数据。这颗SoC的DDR子系统,在改用分段CTS之前,DFI接口的时钟skew被工具报告到了78ps,密密麻麻的hold violation接近300条。根本原因是MC和DDRPHY在同一棵大树上,工具为了平衡MC内部几千个flop,把到DDRPHY时钟引脚的路径拖得特别长,尤其是跨过MC核心区域的那一段,绕了将近400um。

切换到分段CTS之后,我先把全局主干上的绕线全部拉直,分叉点固定在MC根节点处,DDRPHY分支完全独立走线。第一轮跑完,DFI接口的skew降到了45ps,violation还有零星几条。接着我把DFI接口的uncertainty从100ps收到80ps,额外做了一轮PBA分析,定位到几条长路径上还有约7ps的OCV悲观度没有被CPPR剥干净,原因是DDRPHY时钟输入pin的布局位置和MC的最终寄存器相隔太远,导致common path比例偏低。

最后一轮,我把DDRPHY的时钟输入pin微调了20um,使它更贴近MC根节点所在的那条直线路径,同时给DFI接口的wrdata和rddata两组信号做了group约束,让它们在CTS之后能共享更多的物理走线段。跑完PBA signoff,DFI接口的时钟skew停在38ps,hold violation清零,setup还有接近正常水平的裕量。这个状态在后续的布线、ECO、签核迭代里一直保持稳定,没有被其他阶段的改动打破。

4. 问题排查、避坑清单与经验心得

分段CTS不是配完脚本就能躺平的,实际迭代过程中会遇到各种奇奇怪怪的现象。我把这次项目中踩过的坑和排查思路整理一下,按问题现象分类,方便大家对照参考。

4.1 DFI接口hold violation成片出现的排查顺序

DFI接口hold violation成片出现,是在CTS之后最常见的翻车方式。现象很有特点:violation集中在wrdata、rddata、addr这些DFI信号上,数量大,路径报告显示launch和capture的clock edge几乎纹丝不动,但data path上差了一截。

我的排查顺序是固定的。第一步看report_clock_tree的summary,确认MC_TREE和DDRPHY_TREE的真实物理路径是否如设计预期。如果DDRPHY分支的tree level比预期多了一两级,多半是工具在DDRPHY时钟输入pin前面自动插了balance buffer,这会直接吃掉DFI接口的hold margin。处理方法是在CTS spec里把DDRPHY时钟输入pin设成leaf pin,禁止工具在它前面再插任何delay cell。

第二步检查clock concurrent delay模式有没有真的生效。有些版本的工具,concurrent mode只对显式声明了关系的时钟对起作用,如果DFI接口的capture clock被工具识别成了另一个域,两边延迟就会放飞自我。判断方法很简单,在时序报告里查看DFI接口路径的clock path delay,launch和capture的clock path delay如果差异超过10%,基本可以断定concurrent mode没生效。

第三步才是看data path本身。DFI接口的data path上如果串联了太多buffer,或者是跨了MC和DDRPHY两个区域的长线,即使时钟完全对齐,hold也好不了。这时候的处理就不是时钟问题了,要么把寄存器往接口附近移,要么把数据路径拉到更高层的金属去走,让RC延迟降下来。

这里有一个非常实用的小技巧:在CTS结束后,先把DFI接口的hold margin全部解掉,也就是临时把uncertainty设到最大,确保data path本身是一张白纸,再逐步收紧uncertainty来观察工具对时钟树的调整。这能快速区分问题是出在时钟树上还是出在数据通路上,省掉大量翻报告的时间。

4.2 时钟jitter超标与CTS后congestion同时发生的解法

项目中有一次,DDRPHY分支的时钟jitter在PLL输出端明明很正常,到了DDRPHY时钟输入引脚却超标了。同时DFI接口那一带区域congestion也非常严重,绕线资源亮红。

排查下来发现是同一个根因:我最初给DDRPHY分支设置了过于宽松的走线层范围,工具为了把这条分支从MC区域旁边绕过去,让它和DFI的wrdata总线在相邻层平行走了很长一段。wrdata每个cycle都在翻转,耦合电容把大量噪声灌进了时钟线。而DFI信号本身为了避让这条时钟线,也被迫绕路,进一步恶化了congestion。

解法分两步。第一步把DDRPHY时钟分支的走线层约束到更高层,并且和DFI信号线至少隔一层走线层,同时在时钟线两侧加shielding,也就是接地线隔离。第二步重新排列DFI信号组,把wrdata、rddata这类高活跃信号全部集中到DDRPHY的另一侧进出,让它们彻底避开DDRPHY时钟输入pin那一侧的走线通道。

做完之后jitter回到预算范围内,congestion也从红变绿。这件事给我的教训是,分段CTS的物理实现不能只看树的拓扑,还要考虑与周边数据通路的电磁兼容。DDRPHY时钟分支是一条高敏感路径,在布局布线上要给足隔离,不要舍不得那几um的间距。

4.3 一个容易被忽略的signoff细节:PBA与CPPR

最后说一个所有做DDR接口时序收敛的人都应该重视的细节:路径基分析PBA(Path Based Analysis)和公共路径悲观去除CPPR(Common Path Pessimism Removal)在DFI接口上的应用。

MC时钟和DDRPHY时钟同源,两者之间有很长的公共时钟路径。GBA(Graph Based Analysis)在做时序分析时,会悲观地假设公共路径两端分别取不同corner的延迟,给DFI接口的时序报告人为加了不少悲观度。我遇到过的情况是,GBA报DFI接口hold有近10ps的violation,但PBA算下来完全干净。这就是CPPR在发挥作用。

分段CTS恰恰能放大CPPR的收益,因为在设计上你刻意做了分叉点,分叉点前的公共路径覆盖了PLL到MC根节点的大部分延迟,这段路径上的PVT变化是MC和DDRPHY共享的,通过CPPR剥掉这些悲观度之后,实际可用的时序裕量会显著增加。

但PBA不是免费的,它需要对所有path做真实物理路径分析,在大规模SoC上跑起来很慢。DFI接口路径数量有限,完全值得单独开PBA模式做signoff。我在项目里的做法是,主时钟网络用GBA快速迭代,DFI接口单独用PBA做最终签核。这样既保住了迭代速度,又拿到了真正可信的时序结论。

做完这棵分段时钟树之后,我的体会是,DFI接口的时钟问题本质上是一个取舍问题:要负载隔离还是要公共路径长度,要绝对延迟匹配还是要时钟质量,要快速迭代还是要签核精度。分段CTS给了工程师一个灵活的杠杆,让你能针对每一段单独做权衡,而不是被一棵巨大混沌的时钟树绑架所有选择。把这个思路吃透了,不只是DDRPHY,任何带着硬核IP和自研逻辑的SoC时钟架构,都可以用同样的方法拆解出清晰的物理设计路径。

返回列表