
做数字IC前端或者后端验证的兄弟对SDC应该都不陌生。可以说时序约束是连接RTL设计和物理实现的桥梁而SDC里的时钟约束又是这座桥梁的桥墩。我见过太多项目功能仿真跑得飞起一到综合或者STA就炸出一堆时序违例最后排查下来很大一部分问题不是逻辑写错了而是时钟分组没写对或者干脆没写。“SDC设计约束——时钟分组”这个话题我打算一次性讲透。从为什么需要分组到核心命令的语法细节再到不同场景下的实操示例最后把我在项目里踩过的坑和排查思路也一并整理出来。这篇内容适合刚接触STA的初级工程师也适合写了不少约束但想理清概念、避免踩坑的同行。1. 时钟分组到底在解决什么问题很多初学者觉得时钟分组就是把几个时钟用命令圈在一起让工具别去分析它们之间的路径。这个理解方向对但远远不够。如果不理解分组背后的物理意义和设计意图很容易写出“看似正确、实则危险”的约束。先想一个最基本的场景一个芯片里有两个独立的时钟域比如一个跑USB 480MHz一个跑I2C 100kHz。它们之间隔着一级异步FIFO或者打了两拍同步器这两个时钟之间天然就是异步关系不存在需要被严格分析的时序路径。如果你不告诉工具这件事工具会傻乎乎地把两个时钟域的寄存器到寄存器的路径全部拿去做时序分析在480MHz的约束下那条穿越异步FIFO的路径几乎必挂。更麻烦的是工具为了收敛这些假路径可能在不该插缓冲的地方插缓冲白白浪费面积和功耗。1.1 时钟关系的四种基本类型要正确分组先把时钟之间的关系分清楚。我习惯把它们归为四类同步时钟Synchronous同源或者有明确相位关系的时钟比如同一个PLL出来的不同分频时钟或者同一个时钟通过不同BUFFER驱动的时钟。这些时钟之间的路径需要被完整分析。异步时钟Asynchronous没有固定相位关系频率也可能完全无关。比如两个独立晶振产生的时钟或者不同PLL的输出。它们之间的路径不需要做精确的时序收敛。逻辑互斥时钟Logically Exclusive由同一个MUX根据配置选择输出的时钟物理上两个时钟都存在但逻辑上同一时刻只有一个生效。比如一个模块既能跑SPI的时钟又能跑I2C的时钟由配置位选择。物理互斥时钟Physically Exclusive物理管脚上就不会同时存在的时钟通常出现在芯片的测试模式或者不同的工作模式切换时。后续要讲的set_clock_groups处理的核心就是后三种异步、逻辑互斥、物理互斥。同步时钟之间的约束靠的是create_clock、create_generated_clock和set_clock_uncertainty这些命令来定义不是靠分组。1.2 不写时钟分组的后果可能有人觉得时序违例跑出来我再针对性修路径就行了。但现实是不写分组工具会默认所有时钟都是相关的然后做大量无意义的分析导致三个直接后果第一编译时间暴涨。我曾在一个中等规模模块上测试过加了正确的时钟分组之后综合时间直接缩短了三分之一。对于全芯片规模的设计差距更明显。第二出现大量假性时序违例干扰真实问题定位。你花了两天时间去优化一条根本不需要优化的异步路径最后发现是时钟没分组那种挫败感我太熟悉了。第三影响布局布线的质量。工具为了满足一条虚假路径的约束可能会调整寄存器的位置导致真实的时序关键路径反而变长。这个影响非常隐蔽往往到后期修 timing 的时候才暴露。所以时钟分组不是可写可不写的“锦上添花”而是必须写对的“基础设施”。2. set_clock_groups命令深度拆解时钟分组最核心的命令就是set_clock_groups。它有两种主要模式-asynchronous和-exclusive。很多工程师把二者混用觉得反正都是false_path的效果差别不大。这个念头非常危险。2.1 命令语法与基础参数先看基本语法这里不罗列所有选项只讲常用的部分set_clock_groups -asynchronous \ -group {clk_a} \ -group {clk_b} set_clock_groups -logically_exclusive \ -group {clk_mux_sel_0} \ -group {clk_mux_sel_1} set_clock_groups -physically_exclusive \ -group {func_clk} \ -group {test_clk}-group后面要用大括号括起来列出属于该组的时钟对象。注意这里列的是时钟对象也就是你通过create_clock或者create_generated_clock创建出来的时钟不是时钟端口名字更不是模块时钟信号名。2.2 asynchronous与exclusive的本质区别这两个词从字面上看似乎差不多实际含义完全不同。-asynchronous描述的是时钟之间的关系——它们之间没有稳定的相位关系信号穿越时钟域边界时无法保证在目的寄存器的建立保持时间内稳定。工具看到这个约束会屏蔽两个时钟域之间所有路径的时序分析包括组合逻辑路径和寄存器到寄存器的路径。-logically_exclusive描述的是时钟本身的工作行为——它们物理上可能共用一条走线或者由同一个MUX输出但逻辑功能保证同一时刻只有一个时钟在工作。工具不会去分析这两个时钟之间的路径但会保留每个时钟自身相关的约束。用一个具体例子说明某个IP内部有一个配置寄存器选择输出A时钟还是B时钟。A和B明明是从同一个PLL的同一根线分出来的频率相同相位对齐物理上完全同步。这种情况下如果我用-asynchronous等于告诉工具A和B之间路径都不分析了。但实际上这两个时钟在功能上永远不可能同时出现分析它们之间的路径没意义。用-logically_exclusive才是准确的。反过来如果两个时钟确实物理独立、频率无关比如板上两颗晶振分别驱动的时钟你用-logically_exclusive就不得要领因为逻辑上没有任何机制保证它们互斥它们只是单纯的异步。这种场景该用-asynchronous。2.3 与set_false_path的异同很多老工程师更习惯用set_false_path来处理异步路径set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]这个写法在某些场景下和set_clock_groups -asynchronous等效。但有一个重要差异set_false_path默认是单向的你只屏蔽了A到B的方向B到A的路径仍然会被分析。除非你补一条反向的set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b] set_false_path -from [get_clocks clk_b] -to [get_clocks clk_a]如果时钟域之间还有通过门控、MUX绕回来的环路路径数量会爆炸你很难用set_false_path覆盖完整。set_clock_groups则是一次性把两个时钟域之间的所有路径双向都屏蔽掉。在大型设计里这是更可靠、更简洁的写法。我个人的经验是凡是描述两个时钟域之间整体关系的优先用set_clock_groups凡是针对某一条具体路径做豁免的才用set_false_path。两者在SDC中允许混合使用工具会先处理更具体的约束。3. 常见场景下的时钟分组实操全部用命令语法讲一遍等于白讲真正能帮到人的是场景化拆解。我按项目里最常见的四种场景走一遍每个场景给出约束写法、分析思路和容易犯错的地方。3.1 场景一两个完全异步的时钟域典型情况通信芯片里接收路径用恢复出来的时钟发送路径用本地PLL产生的时钟。这两个时钟频率完全无关数据靠FIFO跨域。约束写法set_clock_groups -asynchronous \ -group {clk_rx_rec} \ -group {clk_tx_pll}如果接收时钟还派生出了分频时钟比如clk_rx_rec_div2而这个分频时钟也参与了跨域那需要把所有同源的时钟都归到同一个组里set_clock_groups -asynchronous \ -group {clk_rx_rec clk_rx_rec_div2} \ -group {clk_tx_pll clk_tx_pll_div4}这个细节特别重要。同源生成时钟要放在同一个组里否则工具会认为clk_rx_rec和clk_rx_rec_div2之间也是异步的从而屏蔽它们之间的路径。如果这两个时钟内部存在真实的同步路径比如分频时钟采样原始时钟域的信号但设计上已经做了同步处理屏蔽了反而掩盖了潜在问题。正确的做法是先按时钟源分组再在源组内部分析同步关系。3.2 场景二一个MUX选择的两个时钟高速接口里很常见。某个模块的时钟可以选择PLL0的2分频或者PLL1的4分频由寄存器配置。这两个时钟不会同时工作但它们有可能来自同一个PLL也可能来自不同PLL。如果来自同一个PLL且相位对齐逻辑上互斥set_clock_groups -logically_exclusive \ -group {clk_pll0_div2} \ -group {clk_pll1_div4}如果这两个时钟本身频率、相位都完全独立且由MUX选择用-logically_exclusive更准确。因为从物理实现来看MUX输出端只会有一个时钟波形但在网表里两个时钟都连接到了MUX的两个输入端。工具在STA时需要知道这两个输入端口不会同时活动这正是-logically_exclusive的用途。如果两个时钟不仅逻辑互斥而且物理上就不出现在同一个管脚比如芯片测试模式用的低速时钟和功能模式的高速时钟由一个芯片外部信号切换那用set_clock_groups -physically_exclusive \ -group {func_clk_500m} \ -group {test_clk_100m}3.3 场景三时钟分组与异步FIFO的配合带异步FIFO的设计跨时钟域的路径已经被FIFO内部的同步器逻辑隔断。此时设置set_clock_groups -asynchronous是合理的因为FIFO读写侧确实跨时钟域。但要注意一个技术细节FIFO的写指针到读指针之间经过格雷码转换后仍有组合逻辑路径。这部分路径工具没法自动识别为“可以忽略”需要靠时钟分组来豁免。所以不写分组FIFO的跨域路径一定会出现在时序报告里而且很难收敛。但是如果设计里用了某些特殊的FIFO IPIP内部自带跨域约束集成时再写一层set_clock_groups -asynchronous会不会冲突一般来说不会工具会取更保守的约束但仍然建议查看IP的约束文件避免重复约束造成行为异常。我曾经遇到过一个情况IP约束已经对跨域路径打了set_false_path我又在其上层加了一组set_clock_groups -asynchronous结果导致原本被豁免的一条关键路径重新出现在报告里。排查半天发现是SDC处理顺序的问题。后来调整了约束文件的读入顺序问题解决。3.4 场景四多个异步域混合在一个模块如果一个模块同时有USB、MIPI、Audio三个异步时钟域还带一个低速配置时钟怎么分组正确的思路是把同源的时钟域归到一组不同源的各自成组set_clock_groups -asynchronous \ -group {usb_clk usb_clk_div2} \ -group {mipi_clk} \ -group {aud_clk aud_clk_div4} \ -group {cfg_clk}对于多个组工具会依次对任意两组之间做异步处理。这个写法简洁也能保证USB内部时钟之间的同步分析不受影响。关于-group里要不要包含生成的时钟我的建议是所有通过create_generated_clock从同一个源时钟派生出来的时钟都要源时钟放在同一组。这样既能保证组内时钟的同步分析完整进行又能让异步分组覆盖到所有相关路径。漏掉生成时钟的唯一原因是这个生成时钟只用于局部门控不参与跨域。但即便如此把它放在源组里也不会有副作用顶多多分析几条无关路径。安全起见宁可多含不要漏写。4. 时钟分组相关的工具与流程配合写SDC不是写TCL脚本交到工具里就能跑。时钟分组约束在逻辑综合、物理实现、STA、形式验证等环节的作用范围和优先级各不相同。这块容易踩坑值得单独说。4.1 综合阶段和实现阶段的分工差异逻辑综合阶段DCDesign Compiler吃入SDC后时钟分组会影响时序路径的优化优先级。被分组的异步路径不会被工具视为优化目标工具会把资源集中在需要收敛的真实路径上。单元优化和面积优化。没有被分析的假路径工具不会为它插入缓冲或调整驱动强度。对理想时钟网络的处理。综合阶段时钟还是理想的分组约束更多的价值在于“减少无效路径干扰”。物理实现阶段ICC2或Innovus里时钟分组的意义就更大了CTS时钟树综合会参考约束判断哪些时钟需要做平衡。布局布线时分组之间的路径不会被布局器视为关键路径布局器的热力图上它们就是冷点不会阻碍真实关键路径的布线。所以如果你在综合阶段不写分组等到后端才补上综合网表的优化方向和布局器看到的路径压力点可能完全不同。从RTL冻结那一刻起SDC要尽早定稿时钟分组更是第一版就要带上。4.2 SDC文件的读入顺序与优先级SDC里如果同时存在set_clock_groups和set_false_path谁优先生效从我的经验看工具在执行时遇到两者重叠的约束set_false_path的粒度更细通常优先级更高。但不同工具、不同版本的处理机制存在细微差异。为了规避不确定性我习惯在写SDC时遵循两条原则第一避免一个时钟域既被set_clock_groups分组又对其中某些路径额外设置set_false_path。如果确实有特殊情况比如异步时钟域内有一条路径虽然跨域但做了同步处理需要被分析那就不能用set_clock_groups应该用更细粒度的set_false_path在跨域路径上逐个豁免。第二把set_clock_groups尽量写在一个独立文件里在读到其他具体约束之前先读入。这样可以在流程早期就把“哪些跨域路径不用管”的边界框定下来后续再补充个别例外。4.3 验证手段报告分组效果约束写完了怎么确认它发挥了预期作用这里分享两个方法。方法一用report_clock_groups查看分组结果确认每个组包含哪些时钟组与组之间的关系是异步还是互斥。这个方法最快但只验证了“约束本身”。方法二用report_timing检查某条跨域路径的时序报告。比如想确认A、B两个时钟域之间确实没有分析了可以这样查report_timing -from [get_clocks clk_a] -to [get_clocks clk_b] -path_type full_clock_expanded如果这两个时钟域确实被分组了报告里会显示Startpoint and Endpoint have no path或者直接报出Path is false。如果报告里还能看到完整路径说明分组没有生效需要检查时钟名字是否写错、组名是否匹配、约束是否被后续更高优先级命令覆盖了。5. 实际项目中踩过的坑与排查思路这块是我最想写的部分。SDC的语法很简单真正难的是在复杂设计里保证约束和设计意图完全一致。下面几个坑是真实项目中非常典型的。5.1 坑一分组命令里写了时钟端口名而不是时钟对象名get_clocks返回的是时钟对象如果在SDC里写set_clock_groups -asynchronous -group {clk_a_port} -group {clk_b_port}而clk_a_port只是端口名字没有在这个端口上创建时钟对象那工具会报一个warning告诉你在该对象上找不到时钟定义这一组约束也会被跳过。更隐蔽的是如果端口的clock对象名字和端口名字相同这是默认行为看起来好像没报错但实际上你使用的是端口本身而不是时钟定义。一旦后续在另一层约束里改了时钟名称分组就失效了。我现在写每一组set_clock_groups之前都会先跑一遍report_clocks把实际的时钟对象名字复制粘贴到命令里。别手敲。5.2 坑二同源时钟分到不同组假定一个PLL输出pll_out然后又通过一个分频器生成了pll_out_div2。如果在分组时把pll_out放在组A把pll_out_div2放在组B那么工具会认为它们之间是异步的。PLL输出和它的分频时钟之间如果存在寄存器直接互连路径本来应该被同步分析的被错误豁免后潜在的功能问题就很难被时序分析暴露。这个坑在有人用多个create_generated_clock定义同一个分频器的不同输出时特别容易触发。解决方案是所有生成时钟一定归属于同一个源域组不要因为名字不同就拆到不同组。5.3 坑三先写了set_clock_groups后面又写了更宽松的约束覆盖了它SDC约束文件中后读入的约束可能覆盖先前的设置尤其是当后续用了remove_clock_groups或者设置了片选set_false_path但方向匹配出错或者对某个时钟域重新定义了时钟关系。工具对同一对象的多次约束会采用合并策略但规则很复杂不同工具版本行为不完全一致。在大型流程里不同的IP、不同的模块作者会各自提交一份SDC片段拼接时很容易出现后一方覆盖前一方的现象。我的排查方法是跑完约束后专门生成一份report_clock_group_relationships如果发现预期分组的时钟域没有被屏蔽先查时钟名字再查约束文件的读入顺序用二分法定位是哪两行命令在互相打架。5.4 坑四分组范围过大导致真实路径被豁免有时候某个模块内部存在两个时钟域之间的同步握手逻辑设计上这部分路径需要满足时序要求以便握手信号能可靠传递。如果因为这两个时钟域来自不同源你想都没想就写了一个set_clock_groups -asynchronous那么同步握手路径也被豁免了。一旦握手时序不满足硬件上可能出现偶发故障而且极难复现。这类问题在后端design review里被发现时通常已经进入signoff阶段修改的成本极高。我的建议是在给两个时钟域做分组之前先确认它们之间的确不存在需要被分析的同步逻辑。可以通过查看CDCClock Domain Crossing报告来辅助判断。如果确实需要保留部分同步路径的时序检查那么这两个时钟域之间不能用set_clock_groups -asynchronous而应该用更精准的set_false_path只豁免那些真正的异步路径。5.5 坑五生成时钟与主时钟之间误加互斥在做DFT或低功耗设计时有时会为测试时钟和功能时钟分别创建时钟定义然后在分组里设置它们为physically_exclusive。这样没问题。问题是你如果同时对主时钟和它自己的测试分频时钟也设了physically_exclusive而它们的来源其实相同只在选择时切换设计意图上是互斥的但如果测试时钟是从功能时钟分频得到的它们之间就存在相关路径物理互斥的设定会屏蔽掉这层关系导致测试路径的时序约束缺失。处理方案确认测试时钟是独立PLL生成的还是由功能时钟分频而来。前者用physically_exclusive没问题后者要仔细分析是否真的需要分组还是仅仅需要让STA知道工作模式不同。6. 时钟分组的检查清单与最佳实践最后这部分更像是我自己项目里沉淀下来的一个checklist分享出来供参考。每次流片前的约束审查我都会按照这个清单逐项核对。6.1 约束书写阶段的七个检查点时钟对象名是否从report_clocks输出里复制避免手写拼写错误。每个时钟域内所有同源时钟是否都在同一个分组里。-asynchronous和-logically_exclusive的使用场景是否与设计意图匹配。分组是否覆盖到了所有跨异步域的路径包括通过MUX、门控返回的绕行路径。是否用了set_false_path辅助处理个别路径而不是受影响的大组路径。分组之间是否在功能模式下确实不存在需要分析的同步路径。是否通过report_clock_groups做了最终确认。6.2 跨时钟域分析的工具配合团队里做CDC验证的同事使用专用的CDC工具比如Cadence的Conformal CDC或者Synopsys的SpyGlass CDC生成异步路径报告STA的时钟分组应当与CDC分析结论保持一致。如果CDC工具认为某条跨时钟路径有同步器保护STA的时序分析也应该豁免它反之如果CDC工具报出潜在的问题STA不应通过分组把这条路径隐藏掉。两边结论矛盾时以CDC结果为准先处理功能正确性再优化timing。每次写完时钟分组我都会把report_clock_groups的结果导出来发给做CDC验证的同学两方对齐一下各自工具里分析出来的异步域边界。这个动作成本很低收益非常高能在流片前避免很多潜在的signoff争议。6.3 经验之谈分组宁可保守不要激进如果对某个时钟域之间的关系不够确定我的建议是先不分组让STA把完整路径报告跑出来人工确认哪些是真实不需要分析的。虽然临时跑出来的违例数量可能多一点但至少不会因为错误分组掩盖了真实问题。后续确认清楚了再加上分组约束重新跑一轮。有个经验数据可以参考一个中等规模的SoC完整STA报告里的路径数通常在百万级时钟分组影响的是其中可能被错误优化的路径比例不一定大但影响面极广。与其追逐完美收敛的数字不如让每一行约束都经得起推敲。还有一点我想提醒所有写SDC的人时钟分组不是一锤子买卖。综合前、布局后、CTS后时钟树的实际情况一直在变分组约束是否需要update要跟着设计走得看。尤其是CTS之后如果发现某个group里的时钟实际走线存在耦合或者共用路径STA还是要重新评估。写在最后时钟分组看起来只是几句命令但它背后是整个跨时钟域设计意图的体现。写得好后端流程顺畅STA结论可信写漏了或者写错了轻则浪费一版综合时间重则带着错误约束进入signoff芯片回来跑不出功能。希望这篇整理能帮你在写SDC的时候少走弯路。我的经验是每次在项目里遇到奇怪的timing问题先别急着优化逻辑回头把时钟分组重新捋一遍往往会有惊喜。