SDC时钟约束避坑指南:create_clock、generated clock与clock group实战
数字后端和FPGA设计里时序收敛的起点从来都不是布局布线而是SDC约束。我见过太多项目综合报告看着干干净净一到PrimeTime里全是setup违例最后追根溯源发现是create_clock少写了一个generated clock或者set_clock_groups把本该同步的时钟域给切断了。这类问题排查起来极其痛苦因为工具不会报错它只会忠实地按照你给的约束去算算出来的结果和你的设计意图南辕北辙。这篇内容面向的是已经写过一些SDC、但经常在时钟约束上踩坑的工程师。我会从create_clock的基本语义讲起把generated clock、clock group、clock uncertainty这几个最容易出问题的约束类型串起来重点讲清楚每个约束背后工具到底在做什么、为什么这么写、写错了会怎样。不会停留在语法手册的层面而是把实际项目中反复验证过的约束写法和排查思路摊开来讲。1. 从create_clock说起时钟定义的起点与常见误用1.1 create_clock到底在定义什么很多人把create_clock理解成告诉工具时钟频率是多少这个理解只对了一半。create_clock本质上是在定义时序分析的参考基准——它创建了一个理想时钟波形后续所有的setup、hold检查都是基于这个波形来计算的。你写的周期、占空比、边沿时间直接决定了工具怎么去算建立时间和保持时间的窗口。一个最基本的create_clock写法create_clock -name sys_clk -period 10 -waveform {0 5} [get_ports clk_in]这行约束的含义是在clk_in端口上定义一个周期10ns、上升沿在0ns、下降沿在5ns的时钟命名为sys_clk。注意这里-waveform指定的是上升沿和下降沿的时间点不是占空比百分比。如果你写-waveform {2 7}周期还是10ns但上升沿移到了2ns下降沿在7ns相位就变了。实际项目里最容易犯的错误是周期单位搞混。SDC默认的时间单位跟工艺库有关一般在库文件里定义。我遇到过有人以为单位是ns结果库是ps写了个-period 10实际定义出来是10ps的时钟工具跑完还纳闷为什么时序全违例。确认单位的方法很简单在PrimeTime里执行printvar time_unit就能看到当前的时间单位。另一个常见问题是create_clock加在了错误的对象上。对于纯内部产生的时钟应该加在时钟根节点上对于从端口进来的时钟加在端口上。如果你把create_clock加在了一个内部net上而这个net又被其他逻辑驱动工具会把这个net当成时钟源但它前面的逻辑就不在时钟路径上了时序分析会漏掉一大块。1.2 虚拟时钟的适用场景虚拟时钟virtual clock是不绑定到任何物理对象的时钟用create_clock -name vclk -period 8这种不带端口或net的写法来定义。它的典型用途是I/O接口的时序约束——当输入输出信号参考的时钟在芯片外部芯片内部没有对应的物理时钟时就需要用虚拟时钟来建立参考。举个例子假设有个输入数据总线外部器件用的是100MHz时钟发送但芯片内部没有这个时钟的引脚。这时候你需要create_clock -name ext_clk -period 10 set_input_delay -clock ext_clk -max 3 [get_ports data_in*] set_input_delay -clock ext_clk -min 1 [get_ports data_in*]虚拟时钟只参与时序计算不会驱动任何实际逻辑。它的价值在于让工具知道这些输入信号是相对于哪个时钟沿到来的从而正确计算input delay的约束窗口。注意虚拟时钟的周期必须和外部器件的实际时钟周期一致否则input/output delay的计算基准就错了。我见过有人虚拟时钟写10ns实际外部器件跑的是8ns结果接口时序怎么都收敛不了。1.3 时钟命名与后续约束的关联create_clock的-name参数看起来不起眼但它直接影响后续所有约束的引用方式。如果你不指定-name工具会自动用端口名或net名作为时钟名。问题在于当你后续要写set_clock_groups或者set_false_path的时候引用的时钟名必须和create_clock定义的一致。我的习惯是始终显式指定-name并且用有意义的命名规则比如sys_clk、ddr_clk、pcie_clk这种。这样做的好处是当时钟经过时钟分频、倍频或者MUX之后generated clock的命名可以基于源时钟名派生整个时钟树的名字体系清晰可追溯。如果时钟名重复了会怎样工具会报warning但不同工具的处理方式不一样。有些工具会覆盖前一个定义有些会保留两个同名时钟导致后续约束引用时产生歧义。所以每次写完create_clock建议用report_clocks检查一下当前所有时钟的定义状态确认没有重复或遗漏。2. generated clock派生时钟的约束逻辑与级联处理2.1 什么时候必须用create_generated_clock只要时钟不是直接从端口或晶振进来的而是经过芯片内部的逻辑产生的——比如分频器、倍频器、时钟MUX、时钟门控——就必须用create_generated_clock来约束。不用generated clock的后果是工具不知道这个派生时钟和源时钟的相位关系时序分析要么漏算要么算错。一个典型的分频场景create_clock -name clk_src -period 4 [get_ports clk_in] create_generated_clock -name clk_div2 -source [get_ports clk_in] \ -divide_by 2 [get_pins div_reg/Q]这里clk_div2是从clk_src经过2分频得到的周期是8ns。工具会自动推导出clk_div2的上升沿和clk_src上升沿的相位关系从而正确分析两个时钟域之间的时序路径。-source参数指定的是源时钟的物理对象不是时钟名。这一点很多人搞混。你要填的是源时钟所在的端口或引脚工具会根据这个对象找到对应的时钟定义。如果源时钟本身也是generated clock那-source要指向那个generated clock的源对象。2.2 -divide_by、-multiply_by与-edges的取舍create_generated_clock提供了几种描述派生关系的方式最常用的是-divide_by和-multiply_by。分频用-divide_by倍频用-multiply_by这两个参数直观易懂。但在某些复杂场景下比如非整数分频或者有特定相位要求的场合就需要用-edges参数来精确描述。-edges参数的写法是-edges {1 3 5}数字代表源时钟的边沿序号。源时钟的上升沿编号为1下降沿编号为2下一个上升沿编号为3以此类推。比如你要描述一个3分频、占空比50%的时钟源时钟周期4nscreate_generated_clock -name clk_div3 -source [get_ports clk_in] \ -edges {1 4 7} [get_pins div3_reg/Q]这里1是源时钟第1个上升沿0ns4是第2个下降沿6ns7是第3个上升沿12ns。所以生成的时钟周期是12ns上升沿在0ns下降沿在6ns占空比50%。-divide_by和-edges的区别在于-divide_by只能描述整数分频且占空比固定的情况-edges可以描述任意边沿关系。但-edges的可读性差容易写错。我的建议是能用-divide_by就用-divide_by只有在-divide_by表达不了的时候才用-edges。2.3 级联generated clock的源对象选择当设计中有多级时钟派生时比如PLL输出经过分频器再经过另一个分频器每一级都需要单独约束。关键问题是第二级generated clock的-source应该指向哪里正确的做法是指向上一级generated clock的源对象。比如create_clock -name pll_ref -period 20 [get_ports ref_clk] create_generated_clock -name pll_out -source [get_ports ref_clk] \ -multiply_by 4 [get_pins pll/CLKOUT] create_generated_clock -name div_out -source [get_pins pll/CLKOUT] \ -divide_by 2 [get_pins div_reg/Q]这里div_out的-source指向的是pll/CLKOUT也就是pll_out这个generated clock的源对象。工具会沿着这个链条追溯到pll_ref建立完整的相位关系。如果-source指错了比如直接指向ref_clk端口工具会认为div_out是从pll_ref直接分频8得到的虽然周期算出来一样但相位关系可能不对特别是当PLL有相移或者抖动特性时分析结果会有偏差。提示写完generated clock后用report_clocks -generated检查每个generated clock的源对象和派生关系是否正确。这个报告会列出每个generated clock的master clock和边沿映射关系一眼就能看出有没有指错。3. set_clock_groups异步时钟域的正确隔离方式3.1 为什么需要clock group同步设计中工具默认会分析所有时钟之间的时序路径。但实际芯片里往往存在多个异步时钟域——比如CPU时钟和DDR时钟、高速接口时钟和低速外设时钟——它们之间没有固定的相位关系强行做时序分析没有意义还会产生大量虚假违例。set_clock_groups的作用就是告诉工具这些时钟组之间的路径不需要分析。它的语法是set_clock_groups -asynchronous \ -group {clk_cpu} \ -group {clk_ddr} \ -group {clk_pcie}这行约束表示clk_cpu、clk_ddr、clk_pcie三个时钟组之间都是异步关系组间的时序路径全部忽略。注意-asynchronous是必须的它告诉工具这些组之间是异步的不需要做时序检查。3.2 -asynchronous与-exclusive的区别set_clock_groups有两个常用的选项-asynchronous和-exclusive。很多人分不清它们的区别用错了会导致时序分析出现漏洞。-asynchronous表示组间时钟是异步关系工具完全不做时序分析。适用于真正异步的时钟域比如不同晶振产生的时钟。-exclusive表示组间时钟是互斥关系同一时刻只有一个时钟活跃。适用于时钟MUX的输出——比如一个MUX有两个输入时钟但同一时刻只选一个。这种情况下工具需要分析每个时钟单独活跃时的时序但不需要分析两个时钟同时活跃的情况。# 异步时钟域 set_clock_groups -asynchronous -group {clk_a} -group {clk_b} # 时钟MUX的互斥关系 set_clock_groups -exclusive -group {clk_src1} -group {clk_src2}用错的情况很常见。比如把MUX的互斥时钟写成了-asynchronous工具就完全不分析这两个时钟域之间的路径了但实际上当MUX选择clk_src1时从clk_src1域到MUX输出域的路径是需要分析的。这样就会漏掉真实的时序违例。3.3 时钟MUX约束的实战处理时钟MUX是SDC约束里最容易出问题的结构之一。一个典型的时钟MUX有两个输入时钟和一个选择信号输出时钟根据选择信号切换。约束的关键是既要让工具知道两个输入时钟是互斥的又要保证每个输入时钟到输出的路径都被正确分析。假设MUX的两个输入是clk_a和clk_b输出是clk_muxcreate_clock -name clk_a -period 10 [get_ports clk_a_in] create_clock -name clk_b -period 8 [get_ports clk_b_in] # 在MUX输出端定义generated clock create_generated_clock -name clk_mux_a -source [get_ports clk_a_in] \ -master_clock clk_a [get_pins mux/Z] create_generated_clock -name clk_mux_b -source [get_ports clk_b_in] \ -master_clock clk_b [get_pins mux/Z] # 声明两个输入时钟互斥 set_clock_groups -exclusive -group {clk_a} -group {clk_b}这里的关键点是在MUX输出端定义了两个generated clock分别对应两个输入时钟。然后用-exclusive声明clk_a和clk_b互斥。这样工具会分别分析clk_a活跃时和clk_b活跃时的时序路径但不会分析两者同时活跃的情况。如果MUX输出端只定义了一个generated clock或者没有用-exclusive工具可能会把两个输入时钟的路径混在一起分析产生错误的时序结果。我实际项目中遇到过MUX约束写错导致hold违例漏报的情况最后在硅后测试才发现问题代价很大。注意时钟MUX的约束写完以后一定要用report_timing -from clk_a -to clk_mux_a和report_timing -from clk_b -to clk_mux_b分别检查两条路径的时序分析是否正常。如果报告为空说明约束有问题工具没有正确识别路径。4. set_clock_uncertainty给时序分析留出合理余量4.1 uncertainty的物理含义set_clock_uncertainty定义的是时钟的不确定度包括时钟抖动jitter和时钟偏斜skew的预估余量。在时序分析中setup检查会加上uncertaintyhold检查会减去uncertainty相当于给时序留出安全边际。set_clock_uncertainty -setup 0.15 [get_clocks sys_clk] set_clock_uncertainty -hold 0.05 [get_clocks sys_clk]这表示sys_clk的setup uncertainty是150pshold uncertainty是50ps。工具在计算setup slack时会从数据到达时间里减去150ps计算hold slack时会给数据到达时间加上50ps。uncertainty的取值需要根据时钟源的抖动特性和时钟树的预估偏斜来确定。对于PLL产生的时钟jitter通常来自PLL的周期抖动和相位噪声一般在几十到几百ps量级。对于外部晶振直接输入的时钟jitter相对较小。时钟树的skew在布局布线前是未知的需要根据经验预估通常按周期的5%到10%来设置。4.2 setup与hold uncertainty的差异化设置setup和hold的uncertainty应该分开设置因为它们的物理来源不同。setup uncertainty主要反映时钟抖动和偏斜对建立时间的影响通常取值较大。hold uncertainty主要反映时钟偏斜对保持时间的影响通常取值较小。我通常的设置策略是时钟类型setup uncertaintyhold uncertaintyPLL输出时钟周期的8%-10%周期的3%-5%外部晶振时钟周期的5%-8%周期的2%-3%分频时钟继承源时钟的uncertainty继承源时钟的uncertainty虚拟时钟根据外部器件特性设置根据外部器件特性设置这个表格是经验值具体项目需要根据时钟源的datasheet和时钟树综合的结果来调整。关键原则是uncertainty不能设得太小否则时序收敛了但实际芯片可能因为抖动而失效也不能设得太大否则会过度约束增加不必要的面积和功耗。4.3 uncertainty与clock latency的关系set_clock_uncertainty和set_clock_latency是两个容易混淆的约束。latency描述的是时钟从源到寄存器的传播延迟uncertainty描述的是这个延迟的不确定部分。在时钟树综合之前工具不知道实际的时钟延迟需要用latency来预估时钟树综合之后工具会从实际的时钟树中提取延迟latency约束就不再需要了。uncertainty则不同它反映的是时钟抖动等固有特性即使时钟树综合完成了uncertainty仍然需要保留。所以典型的约束流程是综合前设置latency和uncertainty布局布线后去掉latency但保留uncertainty。# 综合前 set_clock_latency -source 2 [get_clocks sys_clk] set_clock_uncertainty -setup 0.15 [get_clocks sys_clk] # 布局布线后 # set_clock_latency -source 2 [get_clocks sys_clk] # 注释掉 set_clock_uncertainty -setup 0.15 [get_clocks sys_clk] # 保留提示有些团队在布局布线后忘记去掉latency约束导致工具在计算时序时叠加了预估延迟和实际延迟时序结果偏乐观。建议在PR后的SDC里显式注释掉latency约束并在review时重点检查。5. 约束验证与常见问题排查链路5.1 用report_clocks做第一轮检查写完SDC后第一件事是跑report_clocks。这个报告会列出所有已定义的时钟包括它们的周期、波形、源对象和generated关系。重点检查三项时钟数量是否和设计预期一致、周期是否正确、generated clock的源对象是否指向正确的上级时钟。如果发现时钟数量不对比如少了一个分频时钟说明create_generated_clock可能没写或者写在了错误的对象上。如果周期不对检查create_clock的-period参数和单位。如果generated clock的源对象不对检查-source参数指向的对象。5.2 用check_timing定位约束遗漏check_timing是PrimeTime里最实用的约束检查命令之一。它会扫描整个设计报告哪些路径没有约束、哪些时钟没有定义、哪些输入输出没有delay约束。典型的输出包括no_clock存在没有时钟定义的寄存器unconstrained_endpoints存在没有约束的时序终点no_input_delay输入端口没有设置input delayno_output_delay输出端口没有设置output delay每一条报告都对应一个具体的约束遗漏。比如no_clock说明有寄存器的时钟引脚没有被任何create_clock或create_generated_clock覆盖这些寄存器的时序完全没被分析。unconstrained_endpoints说明有些路径的终点没有约束工具不知道用什么时钟去检查。我习惯在每次SDC更新后都跑一遍check_timing把所有的warning逐条清理掉。这个过程很枯燥但能避免90%以上的约束遗漏问题。5.3 时钟MUX约束错误的排查实例前面讲了时钟MUX的正确约束方法这里分享一个我实际踩过的坑。项目里有一个时钟MUX两个输入时钟分别是100MHz和125MHz输出给一个SPI模块。我最初的约束是create_clock -name clk_100 -period 10 [get_ports clk_100_in] create_clock -name clk_125 -period 8 [get_ports clk_125_in] create_generated_clock -name clk_mux -source [get_ports clk_100_in] \ -master_clock clk_100 [get_pins mux/Z] set_clock_groups -exclusive -group {clk_100} -group {clk_125}问题出在create_generated_clock只定义了一个源对象指向clk_100_in。这样工具只知道clk_mux是从clk_100派生的当MUX选择clk_125时工具仍然用clk_100的时序去分析clk_mux域导致125MHz下的时序违例被漏报。修复方法是在MUX输出端定义两个generated clock分别对应两个输入create_generated_clock -name clk_mux_100 -source [get_ports clk_100_in] \ -master_clock clk_100 [get_pins mux/Z] create_generated_clock -name clk_mux_125 -source [get_ports clk_125_in] \ -master_clock clk_125 [get_pins mux/Z]然后用report_timing -from clk_125 -to clk_mux_125验证125MHz路径的时序分析是否正常。如果报告为空或者报错说明约束还有问题。这个坑的教训是时钟MUX的输出端必须为每个输入时钟定义独立的generated clock不能只定义一个。而且-source要指向对应的输入时钟源对象不能指错。5.4 跨时钟域路径的约束检查跨时钟域CDC路径的约束是另一个容易出问题的地方。对于真正的异步CDC路径应该用set_false_path或者set_clock_groups来隔离。但有些设计里CDC路径上加了同步器比如两级触发器这时候路径的起点和终点虽然属于不同时钟域但经过同步器后时序路径的终点应该落在同步器的第一级触发器上。约束的写法是set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]或者更精确地set_false_path -from [get_clocks clk_a] -to [get_pins sync_reg1/D]后者只切断到同步器第一级的路径同步器内部的路径仍然会被分析。这样既能避免虚假的跨时钟域违例又能保证同步器本身的时序正确。检查CDC约束是否完整的方法是用report_timing -from [get_clocks clk_a] -to [get_clocks clk_b]如果报告为空说明路径被正确切断了。如果报告里有路径说明还有遗漏的CDC路径没有被约束。6. 约束文件的组织与版本管理6.1 SDC文件的模块化拆分一个大型项目的SDC文件往往有几千行全部写在一个文件里维护起来很痛苦。我的做法是按功能模块拆分成多个SDC文件比如clocks.sdc所有create_clock和create_generated_clockclock_groups.sdc所有set_clock_groupsio_constraints.sdc所有set_input_delay和set_output_delayexceptions.sdc所有set_false_path和set_multicycle_pathuncertainty.sdc所有set_clock_uncertainty然后在顶层文件里用source命令按顺序加载。顺序很重要先定义时钟再定义时钟组最后定义例外。因为set_clock_groups和set_false_path都依赖于时钟定义如果时钟还没定义就引用工具会报错。# top.sdc source ./clocks.sdc source ./clock_groups.sdc source ./io_constraints.sdc source ./exceptions.sdc source ./uncertainty.sdc这种拆分方式的好处是每个文件的职责清晰修改时钟约束时只需要动clocks.sdc不会影响其他约束。而且便于多人协作不同模块的约束可以分给不同的人维护。6.2 约束版本与RTL版本的对应关系SDC约束必须和RTL版本严格对应。RTL改了时钟结构SDC没跟着改时序分析结果就完全不可信。我的做法是在SDC文件头部加注释记录这个约束文件对应的RTL版本号和修改日期# SDC Version: 1.3 # RTL Version: rev_20240115 # Last Modified: 2024-01-20 # Author: xxx # Change Log: # v1.3 - 增加DDR时钟域的generated clock约束 # v1.2 - 修正时钟MUX的exclusive group设置 # v1.1 - 初始版本这样在review的时候一眼就能看出约束文件和RTL是否匹配。如果RTL更新了但SDC版本没变就需要重点检查时钟结构有没有变化。6.3 约束review的检查清单每次SDC更新后我都会按照以下清单做一遍reviewreport_clocks确认所有时钟定义完整周期正确check_timing确认没有no_clock和unconstrained_endpointsreport_clock_groups确认异步和互斥关系设置正确检查所有generated clock的-source是否指向正确的上级时钟检查时钟MUX是否每个输入都有对应的generated clock检查CDC路径是否都有false_path或clock_groups覆盖确认uncertainty设置合理没有遗漏确认latency约束在PR后已移除这个清单看起来简单但每一条都对应着实际项目中踩过的坑。特别是第4条和第5条时钟MUX和级联generated clock的问题往往在项目后期才暴露排查成本很高。7. 写在最后SDC约束这件事工具不会告诉你哪里写错了它只会按照你给的约束去算。所以约束的正确性完全依赖于工程师对设计意图的理解和对工具行为的把握。我自己的经验是每次写完约束后不要急着跑时序先花十分钟把report_clocks和check_timing的报告仔细看一遍把所有的warning都清理掉。这十分钟能省掉后面几小时的debug时间。另外时钟MUX和级联generated clock这两个场景建议在项目初期就建立约束模板后续直接复用。我见过太多项目在这两个地方反复踩坑每次都要重新排查一遍。把正确的约束写法固化下来比每次临时写要靠谱得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →