我做了快十年的SoC后端实现,最怕听到的一句话就是“这版DDRPHY时序崩了”。DDRPHY和Memory Controller(MC)这对组合,几乎每一个做过SoC物理设计的人都绕不开,而围绕它们的时钟树综合(CTS)更是考验基本功的试金石。传统做法是一棵全局时钟树从头撑到尾,结果往往是MC内部的建立时间修不完,DDRPHY那边的时钟偏差又越压越难,最后只能靠一堆useful skew硬凑,既难看又脆弱。这篇内容我打算把分段CTS这套思路完整拆开,从为什么单棵树撑不住,到分段点怎么选、约束怎么写、时序怎么收敛,再到实际流片前最容易踩的坑,一次性说透。适合正在做SoC集成的后端工程师、准备做DDR子系统签核的团队,以及想系统理解时钟树优化的在校学生。
1. DDR子系统的时序难题,到底难在哪
1.1 PHY与MC对时钟树的“诉求”天生相反
先看清问题的根源。DDRPHY负责和外部DRAM颗粒通信,它内部有大量和DQS、DQ、地址命令总线相关的双沿采样逻辑,时钟频率动辄跑在DDR4 2400MT/s甚至DDR5 4800MT/s以上。这个频率并不是PHY内部所有逻辑都要跑满,但PHY内部那条关键时钟路径的物理距离往往很长,从PLL出来经过时钟分频、DCC校准、IO pad驱动,再到内部同步逻辑,插入延迟天然就大。
Memory Controller则是完全另一种诉求。它主要处理命令仲裁、读写调度、数据通路缓冲,逻辑规模庞大、寄存器级数多,对时钟频率的要求没那么极端,但对片上时钟的jitter容忍度有限。MC内部有大量跨时钟域路径,比如命令队列和AXI接口之间的异步握手,这些路径本身走的是异步逻辑,真正需要关注的是同步域内的建立时间,而同步域内的逻辑层次很深,很容易出现setup violation。
这两者放到一棵时钟树上,就会出现一个尴尬局面:PHY需要短而尖锐的树,MC需要深而宽的树,但芯片顶层只会给你一个时钟源入口。如果不做分段,让MC那棵又大又宽的树带着PHY的IO负载一起走,PHY端时钟路径上的偏差会被放大,DDR接口的时序余量会迅速被吃掉。
1.2 单棵全局树的极限:一条长tree的连锁反应
我在第一次独立负责一颗集成LPDDR4的SoC时,最初就是老老实实按传统CTS做:PLL出来的时钟统一分频,然后让CTS工具从同一个sink点长出一棵覆盖PHY和MC的大树。结果一跑出来,PHY内部时钟偏差倒是勉强压到了40ps以内,但MC那边因为负载太重,底层skew完全压不住,跑到了120ps以上,MC的setup violation一堆。
更麻烦的是,这种单树结构会引发连锁反应。MC的树大,意味着时钟buffer级数多,插入延迟大;插入延迟大了,hold修复就更困难,因为数据路径上的cell delay相对时钟路径的buffer delay来说占比变小了。于是工具在修hold时,会往MC的数据路径上插入大量的delay cell,功耗和面积直线上升,而且每修一个violation都会影响周边路径,收敛过程非常痛苦。
到这一步我才意识到,问题的本质不是“skew压得不够好”,而是“一棵树不可能同时满足两种完全不同的诉求”。分段CTS的出发点,就是允许你把一棵全局时钟树拆成多条相对独立的子树,各自针对自己的sink集合做优化,再通过精心设计的分段点和约束,保证跨树路径仍然能够成立。
2. 分段CTS的设计思路与分段点选择
2.1 分段CTS不等同于简单的“两棵树”
很多人一听分段CTS,第一反应是“不就是把时钟域分成两棵树分别做吗”。实际完全不是这么回事。分段CTS的核心在于分段点(split point)的选取和跨段路径的处理,而不是简单地把sink分成两组。如果你只是把PHY和MC的sink拆开,各自长树,然后互相之间不做任何约束,跨段路径大概率会乱掉。
正确的理解是:分段CTS是在同一个时钟源下,通过定义一个明确的物理分界点——通常是一级时钟缓冲器或者一个专门的时钟门控单元——让这个分界点下游分别长出PHY子树和MC子树。分界点本身必须是物理上可达的、时序上可控的,并且要作为跨路径的公共参考点参与时序分析。
我常用的做法是:先让MC的子树承担主要的时钟生成功能,因为MC对时钟树面积和功耗更敏感;然后把PHY子树设计成一个相对独立的“时钟岛”,由同一PLL分出的时钟在某个物理位置分叉,PHY岛内的树全部自己长,岛边界处再加一段均衡延迟来解决跨岛路径。
2.2 时间预算与Skew Budget拆分方法
分段点位置的确定,不能拍脑袋。我一般先把整个DDR域的逻辑按物理位置和时序关系画出来,把PHY和MC之间的所有交互路径都列出来,标清楚哪些是跨树路径、哪些是树内路径。然后做一轮初步的pre-CTS STA,拿到两类路径的时序裕量,再根据裕量反推每棵子树的skew budget。
举个例子,假设DDRPHY的接口时钟目标周期是1.25ns(对应800MHz),MC内部逻辑时钟是625MHz(1.6ns周期)。从PLL出来到PHY子树的插入延迟,我预算能容忍的skew是50ps;MC子树因为负载大,我给的skew预算是100ps。这两者之间的跨树路径,必须保证公共参考点到两端sink的延迟差被约束在一个可接受的范围里。具体我会在SDC里用set_clock_latency和set_clock_uncertainty把budget先钉死,再让CTS去跑。
这一步容易被忽略的是PHY和MC之间的异步跨时钟域(CDC)路径也要纳入预算。很多工程师只关心同频同步路径,但DDRPHY和MC之间往往存在一些CDC握手信号,比如写数据从MC转到PHY的FIFO、读数据从PHY回到MC的通道,这些路径虽然在STA里常用false_path或max_delay约束处理,但它们同样依赖两棵子树之间在一个较长窗口内的相对对齐。分段点选得不好,这些CDC路径在物理上会绕很远,导致max_delay约束根本凑不齐。
3. 实战落地:从约束到收敛的完整流程
3.1 时钟定义与约束准备
分段CTS第一步,是把时钟结构在SDC里定义清楚。我会为同一物理时钟源定义多个“逻辑时钟视图”,分别标记给PHY子树和MC子树。比如:
# 物理时钟从PLL输出 create_clock -name clk_pll_out -period 1.25 [get_pins u_pll/clk_out] # MC子树逻辑时钟 create_generated_clock -name clk_mc -source [get_pins u_pll/clk_out] \ -divide_by 1 [get_pins u_clock_splitter/clk_mc_out] # PHY子树逻辑时钟 create_generated_clock -name clk_phy -source [get_pins u_pll/clk_out] \ -divide_by 1 [get_pins u_clock_splitter/clk_phy_out]这里的关键是u_clock_splitter,也就是我选定的分段点单元。它可以是普通的CLK_BUF,也可以是一个clock mux或clock gate。我用得最多的是CLK_GATE,因为它既能做分段点,又能在低功耗模式下直接关掉PHY侧时钟,一举两得。如果你的DDRPHY有独立的时钟使能控制,这个方案尤其合适。
约束方面,最需要花心思的是跨树路径的约束。所有从MC子树到PHY子树的路径,以及反过来从PHY子树到MC子树的路径,我都会单独建一个约束组:
# 跨树路径约束 set_clock_groups -asynchronous -group [list clk_mc] -group [list clk_phy] set_multicycle_path -setup 2 -through [get_pins u_mc_to_phy/*/C] \ -from [get_clocks clk_mc] -to [get_clocks clk_phy] set_bus_skew -from [get_clocks clk_mc] -to [get_clocks clk_phy] 150ps注意set_clock_groups我这里用的是asynchronous,但如果在物理设计阶段就把它们设成异步,工具很可能直接忽略两条时钟之间的所有路径,导致时序签名报告里看不到跨树路径。所以我的习惯是先用set_clock_groups定义成logically_async,但单独把需要同步的路径拎出来做multicycle和max_delay约束,保留STA可见性。只有确认了CDC是安全握手而非同步路径之后,才彻底打false_path。
3.2 分段点的物理实现与tree结构
分段点在物理上怎么落,直接影响后面所有步骤。我的经验是:分段点放在MC和PHY物理边界的中间偏PHY侧一点。为什么偏PHY侧?因为PHY子树负载轻、路径少,晚一点分叉可以把PHY的插入延迟做得短一些;而MC子树负载重,早一点分叉可以让CTS工具更早开始平衡MC内部。
具体操作时,我通常手动摆u_clock_splitter,不交给CTS工具自动处理。手动摆的好处是可控性极强。摆好之后,我会在Innovus或ICC2里给这个单元设置dont_touch属性,然后分别对两棵子树做时钟树综合:
# 例子使用的是Innovus的命令风格 set_db clock_tree_options -split_clock_network true set_db clock_tree_split_clock -from u_clock_splitter/clk_phy_out \ -to ... -target_skew 50ps set_db clock_tree_split_clock -from u_clock_splitter/clk_mc_out \ -to ... -target_skew 100ps如果你用的是ICC2,对应的命令风格略有不同,但逻辑一致。关键在于,分段之后两棵子树是并行综合的,工具会自动在两端插入各自的buffer chain,而不会在一棵树上从头串到尾。你会明显看到PHY子树里的clock buffer层级比MC那边浅很多。
3.3 LL流程中CTS常用选项与DB设置
分段CTS不是把命令敲完就完事的,CTS选项和delay budgeting要做很多配合。以下是我每次做DDR时钟树都会反复检查的选项列表:
| 项目 | 推荐设置/值 | 说明 |
|---|---|---|
| target_skew(PHY子树) | 40~60ps | PHY保证DQS/DQ双沿采样的稳定性 |
| target_skew(MC子树) | 80~120ps | MC面积功耗优先,skew宽松一些 |
| max_insertion_delay | PHY≤600ps,MC≤1ns | 防止树过长导致hold难修 |
| useful_skew | 谨慎开启 | 只在MC子树开启,PHY子树保持保守 |
| clock_gate_aware | 开 | 让工具识别PHY内的gate结构 |
| cts_fix_hold | 先关,后开 | 分段树结构稳定后再修hold |
这个表里有两个点需要特别解释。一是PHY子树的target_skew不能压太狠。有人觉得PHY越短越好,skew压到20ps,结果CTS工具为了平衡skew插了很多buffer,反而把jitter放大。DDRPHY内部本身有训练和校准机制,一定范围的不对称可以被DQS中心对齐算法吸收,所以PHY子树的skew目标要给到合理区间,而不是越极端越好。二是MC子树的useful_skew可以放开,因为MC内部大都是普通同步路径,useful_skew工具可以自动在时序紧张的路径上借skew,收敛效率高很多,但PHY侧千万不要开,PHY内部的双沿路径一旦被useful_skew偏移,DQS对齐就乱了。
4. 分段后的时序收敛与偏差分析
4.1 分段树的时序验证:不只是看skew
很多工程师判断CTS做得好不好,只看两个数字:total negative slack和skew。但分段CTS之后,还必须看更多维度的指标,尤其是两棵子树之间的相对偏差(inter-tree skew)。这个值决定跨树路径能不能过。
我一般会跑两轮STA。第一轮是常规的setup/hold full signoff,第二轮我会特意把跨树路径单独抓出来,对比每一条跨树路径的launch和capture时钟到达时间。具体操作可以是,写一个小的TCL脚本,遍历所有跨树路径,计算每一对launch/capture的clock arrival差值,画一个分布直方图,我人工判断这个分布是否有明显的长尾。长尾往往意味着分段点附近有局部的负载不均衡。
为了验证PHY子树的稳定性,还会额外做一次DDR接口时序的脉冲宽度检查。DDRPHY内部的很多路径对占空比失真很敏感,如果时钟树上有太多buffer级联,脉冲宽度会被压缩,这时即使setup/hold都过了,实际芯片上DDR训练也可能失败。
4.2 Useful Skew与PLL配置的配合使用
前面我提到MC子树开useful_skew,这里展开讲一下怎么配合PLL配置使用,效果更好。MC里最典型的问题是:命令路径从仲裁逻辑出发,穿过队列、编码器,最后到达PHY的接口寄存器,逻辑级数往往超过20级。你用useful_skew给这条路径借200ps的capture clock偏移,就能把一个原本必挂的setup violation救回来。
但借skew有个前提:要保证两端都在MC子树内。如果路径的端点涉及PHY,就不能借。所以我在开启useful_skew之前,会在CTS配置里用exclude_pin把PHY子树的全部sink排除掉,确保工具绝不会动PHY侧的时钟到达时间。
PLL配置是另一个容易被忽略的收敛手段。DDRPHY和MC常常工作在不同频率,比如MC跑625MHz、PHY跑800MHz,两者的相位关系由PLL的divider决定。我曾经遇到过一个项目,MC和PHY逻辑上同源,但PLL的VCO频率设置导致两者之间有一个固定的相位偏移,STS总是差40ps过不了。后来调整了PLL的post divider,让MC和PHY的边缘对齐,时序立刻松了100多ps。所以每次流片之前,我都会把PLL各种分频配置下MC和PHY时钟沿的关系做一个仿真确认,而不是只依赖STA。
4.3 功耗、面积与电迁移的连带影响
分段CTS天然会带来一些额外的面积和功耗开销,因为相当于多了一条或多条buffer chain。但相比单棵大树,分段后的总功耗往往反而是下降的。原因很好理解:PHY子树变短了,高频率翻转的时钟网络短了很多;MC子树虽然还是那么长,但它的clock buffer频率低,单位时间翻转次数少。整体下来,动态功耗会降低。
真正的坑在电迁移(EM)。PHY子树的时钟频率高、sink密度大,如果分段点下游的某一级buffer驱动能力不足,很容易出现EM违规。我的习惯是,在物理验证阶段专门检查PHY子树里所有clock net的EM margin,尤其是靠近分段点的那几级,把驱动强度比普通设计再放大一档。
面积方面,分段点本身需要加一个或多个splitter单元,占的面积很小,可以忽略。但要注意的是,如果MC子树和PHY子树在物理上离得太远,用来绕接的时钟线会占用额外的布线资源,可能引起局部拥塞。所以我在布局阶段就会提前规划:分段点放中间,两棵子树尽量呈扇形展开,避免出现交叉。
5. 实际流片前必须避开的几个坑
5.1 分段点处的hold修复:最容易被忽略的噩梦
分段之后,跨树路径的hold修复是个难点。因为MC和PHY两棵子树的插入延迟差异可能很大,假设MC插入延迟800ps,PHY插入延迟500ps,跨树路径从MC到PHY,如果数据路径只有两三百ps的延迟,hold必然违规。
CTS工具在修这些跨树hold时,往往会在数据路径上加delay buffer,但这个buffer加在MC侧还是PHY侧,结果差异很大。我的经验是,跨树hold优先在MC侧(launch侧)数据路径上修,不要在PHY侧接收端修。因为MC侧逻辑时刻相对宽裕,加delay cell对功能时序影响小;如果在PHY侧接收端加delay,很容易破坏PHY内部已经做好的时序关系。
为了控制这些修复,我通常在CTS完成之后,专门写一个hold修复脚本,把跨树路径单独遍历出来,对每条路径做一个max_delay和min_delay的区间约束,再跑一轮eco_hold。同时我会人工抽查几条最关键的路径,确认工具没有把delay cell加在PHY的接收端。
5.2 片外负载与板级时钟风险
很多人做DDR时钟树时,眼睛里只有芯片内部,忘了DDRPHY还要驱动片外DRAM颗粒。DDRPHY的时钟树末端往往要穿过IO pad,经过封装走线、PCB,到达DRAM芯片。这一段的延迟和误差,虽然由板级设计负责,但芯片内部的时钟树设计必须给板级留出足够余量。
我遇到过的情况是,芯片内部PHY树收敛得非常好,skew只有30ps,但系统跑起来DDR训练总是不稳定。后来查了板级设计才发现,PHY的输出时钟在PCB上走了两条长度差了800mil的线,DQS和CLK之间的skew接近100ps。从那之后,我在给PHY子树定skew budget时,会专门预留一部分给板级不确定性,而不是把全部预算都压在片内。
对于集成DDR的Arm SoC这类芯片,片内PHY和MC都在同一颗die上,板级路径相对可控,但仍然要关注封装基板上的走线。有条件的话,在signoff阶段做一次封装模型和芯片模型的协同仿真,能提前暴露很多问题。
5.3 跨时钟域路径的约束反复
分段CTS最消耗时间的部分,其实是CDC路径的约束反复。前面说了,MC和PHY之间有不少跨时钟域握手,这些路径在分段之前可以简单设成false_path,分段之后由于两棵树的物理差异,很多CDC路径不再满足max_delay,你需要回到前端去重新确认这些CDC设计是否仍然安全。
特别是当PHY子树很短、MC子树很长时,从MC到PHY的CDC路径,数据到达时间会明显提前于PHY采样时钟,这本身没问题;但从PHY回MC的路径,数据可能晚到,如果CDC握手的级数不够,可能会丢失。我见过一个工程团队,前端确认了CDC握手是两级同步器,结果分段CTS之后握手信号的延迟变了,同步器采样窗口不够,功能仿真直接出错。
我的经验是,在分段CTS做完之后,必须做一轮RTL+门级混合仿真或至少做一轮带sdf的后仿真,把所有DDRPHY和MC之间的CDC握手路径都刺激一遍。这一步虽然耗时,但能挽回流片后可能出现的灭顶之灾。
5.4 多电压域与DVFS下的时钟树兼容
现在很多SoC有DVFS需求,DDRPHY和MC可能工作在不一样的电压域。分段CTS对这个情况其实很友好:你把两棵子树分别落在各自电压域里,各自做level shifter到边界,比单棵树跨电压域要干净得多。
但有个细节必须注意:电压域边界上的时钟路径,一定要有专门的电平转换器(level shifter)且这个转换器的时序延迟要纳入skew预算。我习惯把level shifter放在分段点之后、每一棵子树内部,也就是分段点先把时钟电平统一到一个参考电压下,再分发给PHY和MC,这样跨树路径最安全。反之,如果分段点之前就有电压变化,时钟会在splitter前后产生额外的偏差,这个偏差你是压不掉的。
多电压域下的分段CTS,还需要在UPF里把两棵子树的power domain定义清楚,CTS工具才能正确识别isolation cell和level shifter的位置。我见过有人在UPF里没定义清楚,CTS工具把level shifter当成普通cell做timing derating,结果signoff报告完全不可信。
6. 一些再细一点的实操技巧
分段CTS不是一次就能收敛的,建议第一次做的人多预留一些迭代时间。我整理几个纯实操层面的技巧,按我个人经验排序:
先修一棵树,再修另一棵。不要两棵一起来,否则出问题都不知道是谁的锅。我通常是先把MC这棵大树做到收敛,PHY子树保持相对宽松,确认MC的收敛状态足够稳定,再压缩PHY子树的skew目标去优化。如果反过来先做PHY,后面MC一旦有大的ECO改动,PHY树容易被牵连。
分段点设置dont_touch还不够,建议再加一个user attribute标记。因为CTS做完之后,后续的ECO布线或者时钟树优化工具可能会试图改变分段点的sink connection,你在前端脚本里用名字匹配就能轻松检查到分段点是否被误动。
每一轮iteration之后,用增量方式重新做一次跨树路径统计。我写了一个简单的TCL函数,每次CTS结束后自动打印出PHY子树和MC子树的插入延迟平均值、最大最小skew、跨树路径总数和最大inter-tree skew。这套输出我会贴到每日review的邮件里,方便和前端、封装同事对齐进度。
保留一份完整的FF/SS双角下的树形对比。只做单角收敛,到了多角签核往往会被打回原形。DDRPHY的IO路径在FF角下保持时间紧张、SS角下建立时间紧张,分段点周围的buffer链必须在两个角下都保持稳定,不然你的分段点就像搭在流沙上。
对PHY子树的时钟网络,建议关闭时钟门控优化(ICG optimization)。PHY内部供电控制通常是由PHY本身逻辑控制的,CTS工具自动插ICG反而会引入额外的时钟脉冲毛刺风险。我在PHY子树里显式设置dont_use ICG,MC子树里保留,各取所需。
分段CTS这套思路,说到底就是尊重物理现实——不同的模块对时钟的需求不同,与其硬生生用一棵树去妥协,不如通过合理的分段点把问题隔开、各自优化。我从第一次被DDR时序折磨到凌晨三点,到现在可以比较从容地在项目初期就规划好DDR时钟树的整体架构,最大的体会是:时序问题从来不是等它爆了再修,而是在方案设计阶段就把树的形态想清楚。希望这篇内容对正在和DDRPHY、Memory Controller时序搏斗的同行们有帮助。如果你也在做类似项目,欢迎来交流一下你们的分段点策略,尤其是遇到DDR5这类更高频率接口时,我相信大家的经验能互相印证出更好的做法。