ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SDC时钟分组深度解析:从概念到工程实践与避坑指南

SDC时钟分组深度解析:从概念到工程实践与避坑指南

做数字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 IP,IP内部自带跨域约束,集成时再写一层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 综合阶段和实现阶段的分工差异

逻辑综合阶段,DC(Design 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阶段,修改的成本极高。

我的建议是:在给两个时钟域做分组之前,先确认它们之间的确不存在需要被分析的同步逻辑。可以通过查看CDC(Clock 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问题,先别急着优化逻辑,回头把时钟分组重新捋一遍,往往会有惊喜。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进