正在准备preparing。 上周帮一个项目查CTS报告OCCOn-Chip Clock输出附近的hold违例报到几千条插入延迟也大得离谱整整一个域的功能时钟树全线飘红。翻来覆去查约束、查库、查floorplan最后发现原因特别气人——CTS开始之前OCC这个宏在工具眼里压根就是一条“死路”时钟通路的透传关系和极性没有标清楚工具凭自己的猜测去平衡时钟树结果把整片树带偏了。这类问题我在不止一个项目里见过做数字IC后端的人基本都会在OCC上踩几脚。OCC是DFT做at-speed测试必需的控制电路它内部是一堆锁存器、MUX和门控逻辑但在CTS工具看来这些都不是标准时钟单元如果你不把“时钟从哪进、从哪出、哪些pin可以透传”交代清楚后面修hold、调skew全是瞎忙。这篇我按实战顺序把OCC电路时钟树综合最关键的5个技巧整理出来所有配置都以Synopsys ICC2/CCopt的写法为准PrimeTime的检查也会带一笔。适合正在做后端PR、CTS或者STA的工程师参考也适合刚接触数字IC后端、想搞明白OCC为什么难倒一大片人的人看。1. OCC电路为什么在CTS里总被特殊对待1.1 OCC到底干了什么先花三分钟补背景OCC全称是On-Chip Clock很多公司也叫它OCGOn-Chip Clock Generator或者Clock Controller一般挂在PLL输出和scan链路的捕获寄存器之间。做at-speed测试的时候ATE测试仪给的是低频时钟几百MHz都算快的了但芯片里真正的功能路径是跑到1GHz甚至更高。这时候不能直接拿低速时钟去打功能路径必须先在芯片内部产生一个或者几个高速功能时钟沿去捕捉被测路径的跳变。OCC就是干这个切换工作的。它内部会有一个glitch-free MUX在两个时钟源之间切换配合锁存器产生一个“测试时钟窗口”先用低速测试时钟把电路初始化等状态稳定之后切到高速功能时钟输出一串快速脉冲然后切回低速测试时钟把结果扫出去。整个过程中时钟沿不能出现毛刺一旦MUX切换时产生一个glitch后面的扫描寄存器就会采到错误数据整个测试就废了。这个逻辑听起来简单但在后端实现里非常讨厌。OCC里的锁存器和MUX是标准单元不是ICG、不是clock bufferCTS工具默认会按数据逻辑去分析它们不会主动把穿过OCC的时钟路径当成真正的时钟树来处理。你要是没在约束和工具设置里告诉它“这条路径是时钟通路”工具就会做出很多匪夷所思的优化比如在OCC输出后面插一堆delay buffer去补一个根本不存在的hold或者把OCC内部锁存器的D端和Q端都当成时钟端点来平衡。1.2 CTS在OCC上翻车的典型现象我见过最多的几种翻车现场排第一的就是hold违例批量爆炸而且集中在OCC输出往后接的几百个寄存器上。报出来的hold值动不动就是几百ps看着像数据路径少了一段CRPR实际去查发现是时钟路径的模型根本没建对工具把OCC输出当成了一个普通组合逻辑输出UTCuncertainty和clk latency算得乱七八糟。第二种常见现象是插入延迟insertion delay异常大。正常一条功能时钟树从PLL到寄存器也就几百ps到1ns但OCC相关的树能跑到2ns以上。原因通常是工具为了把OCC内部的两个MUX输入路径拉平衡绕了远路或者在OCC宏内部插了太多不该插的buffer。第三种是glitch检查不过。后端做完CTS之后DFT团队会拿网表做功能仿真在OCC切换沿附近抓到毛刺。这种问题严格说不能全怪CTS但时钟树的到达时间如果让锁存器透明窗口和MUX切换沿靠得太近毛刺概率会大幅上升。这三种现象根子上都是CTS开始之前对OCC的时钟语义定义不够完整。后面几个技巧一步一步把这个定义补全。2. 技巧一把OCC的输出建成时钟源而不是让工具裸跑2.1 不要在PLL出口一棵树打到底很多人第一版CTS脚本是这么写的给PLL的输出create_clock然后直接把所有寄存器当成同一个时钟域的sinkOCC就被当成路径上的一坨组合逻辑自动穿越了。这种做法在时钟频率低、OCC结构简单的时候勉强能跑一旦到了高频多域设计必然出问题。正确的第一步是在OCC的输出端单独创建一个generated clock。让工具明确知道OCC的输出是一个独立的时钟源点它的源是PLL或者其他上游时钟分频系数通常是1但它的物理起点在OCC的输出引脚上。# 功能时钟来自PLL输出 create_clock -name func_clk -period 1.2 [get_ports pll_out] # 低频测试时钟来自ATE输入 create_clock -name test_clk -period 10.0 [get_ports ate_clk] # OCC输出端创建generated clock源是OCC输入时钟引脚 create_generated_clock -name occ_clk \ -source [get_pins inst_occ/fclk_in] \ -divide_by 1 \ [get_pins inst_occ/clk_out]这样做的目的是把OCC输出的时钟树单独拎出来管理它的sink是后面几千个扫描寄存器它的source是OCC内部经过锁存器和MUX之后的时钟沿。工具在balance这棵子树的时候会从OCC输出开始算插入延迟而不会追溯到PLL那边把整条路径搅在一起。2.2 ICC2里必须做的through pin和stop pin设置建完时钟之后工具还是不知道OCC内部哪些pin是“可以穿过去”的时钟通路。这一步必须用ICC2的时钟树例外指令来标。# 指定OCC内部可以被时钟穿过的pin set_clock_tree_options -through_pins [get_pins -hier -filter ref_name ~ *OCC*MUX*] # 在OCC的控制信号上设置stop避免时钟沿从非时钟输入漏进树 set_clock_tree_exceptions -stop_pins [get_pins -hier -filter ref_name ~ *OCC*LAT*.D]有个点必须说清楚through pin要标在OCC内部真正的时钟通路上通常是glitch-free MUX的两个数据输入和一个输出以及锁存器的时钟输入。stop pin则要打在那些“看着像时钟输入但实际是控制信号”的pin上比如OCC的test_mode、scan_enable、pulse_cnt等控制端。你要是图省事把整个OCC设成opaque时钟确实不会乱穿了但OCC输出的时钟树也彻底找不到源头后面的CTS照样废。反过来全透传也不行锁存器的D端会被当成时钟路径去平衡树形完全乱套。我曾经在低功耗项目里见过有人用set_clock_tree_exceptions -dont_touch_subtree把OCC整个skip掉CTS倒是跑得快了结果OCC输出到寄存器的树完全没长出来后面全部靠post-CTS手动插buffer那叫一个酸爽。2.3 再配合set_clock_sense把极性标清楚OCC内部经过锁存器和MUX之后时钟极性和原时钟可能不一样。工具默认认为穿过锁存器之后时钟沿还是原来的样子但实际锁存器会在高电平或者低电平透明穿过去之后时钟的sense可能发生改变。在ICC2里用set_clock_sense显式告诉工具穿过某个pin之后时钟继续以正脉冲方式传递set_clock_sense -clock [get_clocks occ_clk] \ -pulse \ -through [get_pins inst_occ/mux_out]这步看起来只是约束实际影响的是CTS平衡时对时钟边沿的计算尤其影响hold和min pulse width检查。OCC切换窗口内部那些锁存器对时钟沿的到达时间极其敏感sense不标对工具算出来的到达时间就是错的后面修出来的树也必然不对。3. 技巧二时钟关系分组别把功能时钟和测试时钟搞成“路人”3.1 同一个PLL出来的时钟到底是异步还是同源OCC设计里最绕的就是时钟关系。功能时钟来自PLL测试时钟来自ATEOCC输出又由这两者切换产生。很多人拿到CTS约束模板看到“测试时钟”三个字就直接set_clock_groups -asynchronous把功能时钟和测试时钟劈成两半觉得只要分开了CTS就不会互相干扰。这个做法在纯粹的scan shift模式shift时钟完全由ATE提供和功能时钟毫无关系下是对的。但在at-speed capture模式下OCC输出的是功能时钟沿和PLL产生的功能时钟是同一个源头只不过中间穿过了OCC的锁存器和MUX。你要是把所有时钟全部设成异步CTS工具会认为它们之间没有任何时序关系自然也不会去平衡两条路径的skew。实际数据路径上发射沿用的是功能时钟捕获沿用的是OCC输出时钟这俩明明是同源的工具却当成两个worldhold检查就全乱了。3.2 该分组的分组该互斥的互斥我自己的处理习惯是这样的功能时钟和OCC输出时钟放在同一个同步组里让CTS知道它们之间存在公共路径需要做树平衡。真正的ATE低速测试时钟只在shift流程里用才设为asynchronous。如果两条时钟在物理上永远不可能同时有效但又是同源的用logical_exclusive而不是asynchronous。ICC2里的参考写法# 功能时钟与OCC输出时钟同源需要同步平衡 set_clock_groups -logical_exclusive \ -group {func_clk occ_clk} \ -group {test_shift_clk} # 或者更保守地按同步来处理 set_clock_groups -asynchronous \ -group {test_shift_clk} \ -group {func_clk occ_clk}注意这里的test_shift_clk是真正给scan shift用的ATE时钟不是OCC输出的测试时钟。很多项目里会出现多个OCC每个OCC的输出时钟分别对应不同的功能域这时候还要按domain去分set_clock_groups -asynchronous \ -group {occ_clk_a func_clk_a} \ -group {occ_clk_b func_clk_b}不同OCC域之间才是真正异步的因为它们的源可能来自不同的PLL或者不同的分频配置。混在一个组里平衡CTS工具会拼命去拉两个域的skew浪费绕线资源还有可能制造出匪夷所思的长路径。3.3 分组设错的实际案例之前有个项目OCC时钟域的功能路径setup和hold都是过了的但DFT团队做at-speed pattern仿真的时候一直抓到MUX切换附近的数据错误。后来查下来CTS脚本里把功能时钟和OCC输出时钟设成了两个异步组工具在平衡时钟树时完全不管这两条路径之间的相对延时。OCC内部MUX做切换的时候发射沿和捕获沿之间的相对skew有快200ps的偏差而功能模式下逻辑本来就只有300ps不到的裕量。把分组改成logical_exclusive之后CTS工具理解了这两条时钟“不在同一时刻出现但物理上必须对齐”开始主动去平衡它们的公共路径最终把相对skew压到了60ps以内问题消失。分组关系在约束文件里就是几行字但改错一行后面整个后端都要为它买单。4. 技巧三Skew目标要按OCC的测试/功能比例定不是越小越好4.1 target_skew的两难CTS工具里都有一个target_skew参数很多人拿到就填30ps、50ps恨不得全芯片所有时钟都做到完美平衡。但OCC相关的时钟树一味追求小skew反而会把自己坑了。原因很简单。OCC输出的时钟频率在at-speed模式下可能是功能频率在shift模式下则是低频测试频率。这两种模式下的时钟树负载几乎一样但时钟周期差了一个数量级。frequency高的时候setup是关键希望插入延迟尽量短好把大部分周期留给数据路径frequency低的时候hold是关键因为发射沿和捕获沿之间的时间差会被放大skew稍微大一点就可能造成hold违例。如果你把target_skew设得非常小CTS工具会为了拉平衡在树上插大量buffer插入延迟一路飙升。插入延迟一大功能模式下的setup立刻吃紧。这等于拿setup的命去换hold的命在一个小skew指标上两头受气。4.2 按预算来算别按感觉来填我给OCC相关时钟定skew目标时会先做一步简单预算计算。假设功能时钟周期是1.2nsOCC输出后的时钟树插入延迟水平和普通功能树保持同一水平比如400ps。数据路径本身有500psuncertainty去掉100pssetup余量就是1.2 - 0.4 - 0.5 - 0.1 0.2ns如果因为强拉skew导致插入延迟从400ps涨到600ps余量直接变成0。反过来hold路径上数据路径通常是短路径只要发射和捕获时钟的skew被控制在合理范围内hold不会成为主要矛盾。所以实操上我会把OCC时钟域的target_skew放得比其他功能时钟稍宽一点比如普通时钟域设50psOCC域设80到100ps然后重点关注插入延迟不超过某个上限。在ICC2里可以这样按时钟域单独设set_clock_tree_options -clock [get_clocks occ_clk] \ -target_skew 0.08 \ -max_insertion_delay 0.45max_insertion_delay这个参数很多人忽略。它对OCC这类高频路径特别重要限制了工具最多能在树上插多长的buffer链等于直接给setup兜底。4.3 NDR和绕线规则OCC的树不能随便跑OCC输出到后面寄存器的时钟树在线宽和间距上也应该和普通数据线区别对待。时钟翻转频率高又长如果旁边跑着一条摆幅很大的数据总线串扰引起的时钟抖动会很可观。OCC本身对毛刺敏感这个问题会被放大。我一般会在ICC2里给OCC相关的时钟树单独建一条NDRNon-Default Rule双倍线宽、双倍间距有空间的话再加shielding。define_routing_rule occ_clk_ndr \ -default_reference_rule \ -multiplier_width 2 \ -multiplier_spacing 2 set_clock_routing_rule -clock [get_clocks occ_clk] \ -rule occ_clk_ndr \ -layer_bottom M4 -layer_top M6layer_bottom和layer_top要根据实际工艺和floorplan来定一般让OCC的时钟树走中上层金属避开底层的标准单元绕线 congested区域。有一点要注意NDR的应用范围太大会造成绕线资源浪费所以建议只对OCC输出之后的这一段时钟树单独设其他功能时钟树保留默认规则。工具支持的设置方式各个版本略有差异老项目里用ICC的写法也没问题核心思路是让OCC相关时钟树的路由“宽、远、稳”。5. 技巧四用useful skew处理OCC透明窗口和hold的拉扯5.1 锁存器透明窗口CTS必须照顾的敏感点OCC里的锁存器是电平敏感器件在高电平窗口内输入直接通到输出。CTS结束之后时钟沿到达锁存器时钟端的时刻和到达后面捕获寄存器的时刻之间必须有明确的先后关系否则锁存器可能在一个错误的窗口内间采到不稳定的数据。这句话听起来绕实际做后端的时候常常表现为OCC输出后的路径在时序报告里setup和hold都干净但PR之后做晶体管级仿真或者后仿OCC切换时出现毛刺波形图上锁存器输出在透明窗口边缘抖了一下。CTS阶段能做的就是让OCC锁存器的时钟输入沿和它后面MUX的切换沿之间存在足够的裕量。这个裕量不是靠改RTL来的是靠在树上做有意的偏差useful skew来实现的。5.2 用useful skew而不是疯狂插buffer处理这类问题我建议优先用useful skew而不是靠标准单元库里的delay buffer硬怼。OCC输出到几百个寄存器如果每个寄存器前都插hold buffer面积、功耗、绕线资源全部爆炸而且插入延迟会被推得非常高。useful skew的意思是在setup仍然满足的前提下主动把捕获时钟沿往后拉一点给hold路径争取时间。或者在发射路径的时钟上提前一点减少发射沿和捕获沿之间的有效保持时间差。ICC2里可以针对OCC时钟域打开useful skew优化set_clock_tree_options -clock [get_clocks occ_clk] \ -useful_skew true \ -target_early_delay 0.2 \ -target_late_delay 0.25target_early_delay和target_late_delay分别约束了提前和推迟的边界。不要一上来就大跨度调整先给50ps的余量迭代看报告再逐步收紧。这个优化在修hold的同时还能缓解OCC锁存器透明窗口的问题因为锁存器关闭沿会跟着捕获时钟一起往后移动让透明窗口避开数据不稳定的区间。5.3 别在OCC输出后面修大hold那是拆东墙我见过一个项目OCC输出后面hold违例几百条工程师直接在OCC输出到寄存器的每一条路径上插delay buffer硬生生把hold修干净了。结果setup全线崩掉负责STA的同事骂了一下午。这个场景属于典型的“用战术上的勤奋掩盖战略上的懒惰”。OCC输出后面的hold问题很多并不是数据路径真的短到抓不住而是OCC内部时钟关系没处理干净两条同源时钟被设成了异步工具在计算hold时没有公共路径抵消。先把时钟分组和sense改对hold可能自动消失一大半。剩下的那部分用useful skew去拉实在不行再考虑在局部路径上加少量buffer。记住一个原则OCC输出后面的delay buffer每加一个功能时钟的插入延迟就涨一截setup就少一截。这两件事永远在打架你要做的是找平衡点而不是单方面压一边。6. 技巧五Signoff阶段给OCC单独过一遍体检6.1 CTS报告里要盯哪几列CTS跑完之后不要只看总表上的skew数字就收工。对OCC相关时钟一定要单独出一份详细的时钟树报告人工确认几点。ICC2里可以这样看report_clock_tree -clock [get_clocks occ_clk] -detail report_clock_timing -clock [get_clocks occ_clk] -type skew -late report_clock_timing -clock [get_clocks occ_clk] -type skew -early重点看插入延迟是否在预算范围内skew是否收敛到设定值以及时钟树在OCC输出之后有没有出现不合理的长链。我每次都会顺手查一份report_clock_tree -exceptions确认through pin、stop pin、clock sense这些例外没有被后续的ECO意外清掉。很多项目在post-CTS阶段跑ECO一版脚本下去把之前辛辛苦苦标好的时钟例外冲掉了时钟树悄悄变了形状到signoff阶段才暴露。6.2 PrimeTime里做OCC专项检查PR阶段报告干净不代表签核阶段也干净。OCC相关路径我会单独在PrimeTime里加两组命令去确认。第一组检查generated clock定义和时钟通路的完整性check_timing -include {generated_clock no_input_delay no_output_delay} report_timing -from [get_clocks occ_clk] -to [get_clocks func_clk] \ -path_type full_clock_expanded如果check_timing里报generated clock的source找不到或者路径无法展开到OCC内部说明时钟定义和through pin之间有断点要回溯到ICC2阶段修复。第二组专门看at-speed capture路径的holdreport_timing -delay_type hold \ -from [get_clocks func_clk] \ -to [get_clocks occ_clk] \ -path_type full_clock_expanded我还会额外扫一遍所有OCC相关的timing path看看有没有经过MUX两个输入端的路径被当成真实路径分析。OCC切换时两路时钟只有一个有效另一路处于关闭状态理论上应该设置false path。如果漏了primeTime会报出一堆根本不存在的违例干扰判断。6.3 ECO阶段动OCC的树一定要小心后端的ECO十有八九发生在时钟树上。改OCC相关时钟树和其他时钟树有个本质区别OCC后面挂的寄存器往往同时连接扫描链你挪动一个sink的位置或者改树的拓扑影响的不仅是功能时钟还有扫描链上的hold时序。EDA工具在ECO时自动修hold通常不会区分功能树和测试树。我的经验是ECO脚本里要显式把OCC相关时钟设为保护对象或者在做完ECO之后重新跑一遍OCC树的clock tree report和glitch仿真。不要以为PR整体收敛了ECO就只是局部加点buffer的事。IO端口那边的OCC也一样如果OCC输入时钟来自芯片外部ECO时不要轻易在端口后面加平衡buffer那段路径的延时对外部测试仪而言可能是精确校准好的。动了之后测试团队那边的timing calibration又得重来一遍。7. 常见问题速查表7.1 现象、原因和解决方案对照把之前几次项目里排查OCC CTS问题积累的结论整理成一张表遇到类似的情况可以快速对号入座。异常现象最常见原因优先排查方向OCC输出后hold违例集中爆发功能时钟和OCC输出时钟被错误设成异步检查clock groups同源时钟应放在同步组插入延迟比预期大几百psOCC内部透传pin没标工具绕远路平衡检查through_pins和stop_pins设置时钟树skew收敛但glitch仿真失败锁存器透明窗口和MUX切换沿靠太近检查set_clock_sense用useful skew拉开窗口CTS跑完功能setup大量违例target_skew设太小树上插了太多buffer按时钟域放宽skew限制max_insertion_delayECO之后OCC树形突然变化时钟例外被ECO流程冲掉重跑report_clock_tree -exceptions确认OCC内部两条输入路径被同时平衡没有设置时钟sense工具认为两路都有效显式声明clock sense和逻辑互斥关系多个OCC域互相干扰所有OCC时钟放在同一个group里强行平衡按PLL/domain拆分异步group这套表是我自己排查问题的固定起点每条背后都对应过一次真实项目里的熬夜经历。7.2 上线CTS之前我的5分钟自检清单现在每次跑CTS不管项目多急我都会花5分钟把下面几项过一遍确认没有低级错误再交给工具OCC输出端有没有创建generated clocksource是否指向正确的OCC输入引脚。OCC里面用于时钟透传的MUX、锁存器pin是否已经加到through_pins控制信号有没有加stop_pins。功能时钟、OCC输出时钟、测试时钟的分组关系是否符合OCC的实际切换逻辑。OCC时钟域的target_skew、max_insertion_delay是不是按预算填的还是沿用默认值。时钟sense有没有显式声明特别是锁存器穿越路径。这五项看着简单但每次帮别人排查问题最终十有八九都落在其中一项上。CTS跑起来动辄几小时与其跑完看一堆violation再返工不如开始前把这几分钟花掉。最后再分享一个小习惯。每次做完OCC相关CTS我都会把report_clock_tree的结果导出一份存档连同约束脚本、例外设置的list一起扔到项目目录里。后续ECO或者budget调整拿出来一对照就能看出树到底是哪一版开始变的。这个习惯帮我省了很多排查时间也推荐你们试一下。