ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA/ASIC时序约束实战:从create_clock到时钟分组与不确定性

FPGA/ASIC时序约束实战:从create_clock到时钟分组与不确定性 1. 时钟约束到底在解决什么问题做FPGA或者ASIC后端的人迟早都会撞上SDC这堵墙。你综合出来的网表功能仿真全过时序报告一跑满屏的红色slack工具告诉你setup违例2.3nshold违例0.4ns。这时候你回头去看约束文件发现里面只有一行create_clock剩下的全是工具自动推导出来的默认行为。问题就出在这里——SDC不是写给工具看的说明书而是你向时序引擎描述真实硬件意图的唯一通道。SDC全称Synopsys Design Constraints是一套基于Tcl语法的约束描述语言。它的核心任务只有一件事告诉时序分析引擎你的设计里哪些路径需要检查、按什么频率检查、哪些路径不需要检查。听起来简单但实际操作中一个中等规模的FPGA设计动辄几十个时钟域、上百条跨时钟路径约束写错一条要么过度约束导致工具拼命优化浪费资源要么约束不足导致硅片上跑不起来。这篇文章面向的是已经写过一些SDC、但总觉得心里没底的工程师。你可能知道create_clock要写在时钟输入端口上也知道跨时钟域要用set_clock_groups但为什么这么写、什么情况下会出问题、工具报的warning到底该不该管这些细节往往决定了你的设计是一次流片成功还是反复迭代。我会从最基础的时钟定义开始一路讲到时钟分组、不确定性、时钟切换这些实战中高频出现的场景把每条约束背后的逻辑拆开揉碎。1.1 时序分析的底层逻辑工具到底在算什么要理解SDC先得理解静态时序分析STA在算什么。STA不跑仿真它把所有路径抽象成起点和终点之间的延时链。起点通常是寄存器的时钟引脚或者输入端口终点是寄存器的数据引脚或者输出端口。工具计算的是数据从起点出发经过组合逻辑到达终点时相对于时钟沿是早了还是晚了。这个计算依赖三个核心参数时钟周期、时钟到起点的延时、数据路径延时。其中时钟周期和时钟延时都来自SDC约束。如果你不写create_clock工具根本不知道时钟周期是多少它只能假设一个默认值或者直接报错。这就是为什么create_clock是所有约束的起点——没有时钟时序分析无从谈起。再往深一层工具还需要知道时钟之间的相位关系。两个时钟如果同源它们的相位差是确定的如果不同源相位差就是随机的。对于随机相位关系的时钟域工具默认会检查它们之间的路径但检查的方式是假设最坏情况——两个时钟沿可能同时到达也可能错开任意相位。这种检查方式往往过于悲观导致大量虚假违例。set_clock_groups的作用就是告诉工具这两个时钟域之间的路径不用检查因为我在设计上已经做了异步处理。1.2 一个真实案例约束缺失导致的连锁反应我经手过一个图像处理项目前端是MIPI接口进来的像素时钟频率74.25MHz后端是DDR控制器时钟200MHz。两个时钟完全异步。最初版本的SDC只写了两个create_clock没有做时钟分组。综合报告显示跨时钟路径有大量setup违例最差slack达到-3.2ns。工具为了修复这些违例拼命在跨时钟路径上插入寄存器和调整布局结果布线拥塞度飙升到95%最终布局布线跑不完。后来加上set_clock_groups -asynchronous之后跨时钟路径不再被检查工具把优化精力集中在真正的同步路径上拥塞度降到72%时序轻松收敛。这个案例说明一个道理约束不是越多越好而是越准确越好。错误的约束比没有约束更可怕因为它会让工具朝着错误的方向优化。2. create_clock一切约束的起点create_clock是SDC里最基础也最容易被低估的命令。很多人觉得它简单——不就是定义一个时钟吗但实际操作中时钟定义的位置、周期、波形参数、命名方式每一个细节都会影响后续所有约束的生效范围。2.1 时钟定义的三要素源、周期、波形create_clock的基本语法是create_clock -name 时钟名 -period 周期 -waveform {上升沿 下降沿} [源对象]三个核心参数缺一不可。-name给时钟起个名字后续所有约束都通过这个名字引用它。-period定义时钟周期单位默认是纳秒。-waveform定义上升沿和下降沿的时间点默认是{0 周期/2}也就是占空比50%。源对象通常是一个端口或者一个引脚。对于输入时钟源对象就是时钟输入端口create_clock -name sys_clk -period 10.000 -waveform {0 5.000} [get_ports sys_clk]这条约束的意思是在sys_clk端口上定义一个周期10ns、上升沿在0ns、下降沿在5ns的时钟。工具会以此为基础推导出时钟树上的所有时序关系。注意-waveform的第一个值不一定是0。如果你定义的是差分时钟的负端上升沿可能从半个周期开始。但大多数情况下保持默认的{0 周期/2}即可。2.2 时钟源的选择端口、引脚还是内部节点一个常见的困惑是时钟应该定义在端口上还是定义在时钟树的某个内部节点上这取决于你的约束策略。策略一定义在输入端口。这是最直接的方式适用于时钟从外部晶振或时钟芯片直接进入FPGA的情况。优点是简单直观工具会自动推导端口到寄存器的时钟树延时。缺点是如果时钟在内部经过了MMCM或PLL你需要额外约束这些时钟处理模块的输出时钟。策略二定义在时钟处理模块的输出。当设计中使用MMCM、PLL或者时钟分频器时工具通常会自动从输入时钟推导出输出时钟。但自动推导的结果不一定符合你的预期特别是当MMCM配置了非整数分频比或者多个输出时钟时。这时候手动在MMCM输出引脚上定义时钟更可靠create_clock -name clk_100m -period 10.000 [get_pins mmcm_inst/CLKOUT0] create_clock -name clk_200m -period 5.000 [get_pins mmcm_inst/CLKOUT1]策略三虚拟时钟。虚拟时钟不对应任何实际端口或引脚它用于描述外部器件的时钟特性。比如你的FPGA通过SPI接口与外部ADC通信ADC的时钟是25MHz但FPGA内部没有这个时钟。这时候你需要定义一个虚拟时钟来约束SPI接口的输入输出延时create_clock -name vclk_adc -period 40.000 set_input_delay -clock vclk_adc -max 5.000 [get_ports adc_data]虚拟时钟不驱动任何寄存器它只作为set_input_delay和set_output_delay的参考时钟存在。2.3 时钟命名与后续约束的关联时钟名不是随便起的。后续的set_clock_groups、set_false_path、set_multicycle_path都要通过时钟名来引用。如果命名混乱约束的可维护性会急剧下降。我建议的命名规范是来源_频率_用途。比如sys_100m_main、ddr_200m_ctrl、adc_25m_sample。这样一眼就能看出时钟的来源、频率和用途。避免使用clk1、clk2这种无意义的命名三个月后你自己都记不清哪个是哪个。另外要注意工具在综合和实现阶段可能会自动重命名时钟。比如MMCM输出的时钟工具可能命名为clk_out1_mmcm_inst。如果你在SDC里用get_pins手动定义了时钟名后续约束要用你定义的名字如果依赖工具自动推导就要用工具生成的名字。混用会导致约束不生效。2.4 生成时钟自动推导与手动定义的选择生成时钟generated clock是从主时钟派生出来的时钟通常由MMCM、PLL、分频器或者时钟门控产生。工具默认会自动推导生成时钟但自动推导的结果有时不够精确。自动推导的逻辑是工具找到时钟处理模块的输入时钟根据模块的配置参数计算输出时钟的频率和相位。对于MMCM和PLL这个计算是准确的。但对于简单的分频器或者时钟门控工具可能无法正确识别分频比。手动定义生成时钟的语法是create_generated_clock -name clk_div2 -source [get_pins mmcm_inst/CLKIN] -divide_by 2 [get_pins div_inst/Q]-source指定源时钟的引脚-divide_by指定分频比。也可以用-multiply_by指定倍频比或者用-edges指定边沿映射关系。实操心得对于MMCM和PLL我通常依赖工具自动推导因为工具对Xilinx和Intel的时钟模块支持很好。但对于自己写的分频器或者时钟门控一定要手动定义生成时钟否则工具可能把分频后的时钟当成主时钟的同步时钟来处理导致时序检查错误。3. set_clock_groups跨时钟域约束的核心跨时钟域路径是时序约束中最容易出问题的地方。两个异步时钟域之间的路径如果被工具当作同步路径来检查会产生大量虚假违例如果完全不约束又可能漏掉真正的亚稳态风险。set_clock_groups就是用来精确控制哪些时钟域之间的路径需要检查、哪些不需要。3.1 异步时钟域的本质为什么不能当同步路径检查同步路径的时序检查基于一个假设起点寄存器和终点寄存器由同一个时钟或者有确定相位关系的时钟驱动。工具计算的是数据从起点发出后在下一个时钟沿到达终点之前是否稳定。异步时钟域之间没有固定的相位关系。两个时钟可能同时跳变也可能错开任意时间。如果工具按照同步路径来检查它会假设最坏情况——两个时钟沿同时到达然后计算数据路径延时是否满足建立时间和保持时间。这个假设在实际中几乎不可能发生但工具必须按最坏情况检查结果就是大量虚假违例。更严重的是工具会尝试修复这些违例。它会在跨时钟路径上插入寄存器、调整布局、增加缓冲器消耗大量资源去解决一个根本不存在的问题。所以对于真正的异步时钟域必须用set_clock_groups把它们分开。3.2 三种分组模式asynchronous、exclusive、physically_exclusiveset_clock_groups有三种分组模式对应不同的物理场景。-asynchronous最常用的模式表示两个时钟域完全异步相位关系不确定。工具会完全忽略这两个时钟域之间的路径。适用于大多数跨时钟域场景比如不同晶振产生的时钟、经过不同MMCM输出的时钟。set_clock_groups -asynchronous -group {clk_a} -group {clk_b}-exclusive表示两个时钟域在时间上互斥同一时刻只有一个时钟活跃。比如时钟切换电路的两个输入时钟虽然它们可能同源但同一时刻只有一个被选中。工具会检查每个时钟域内部的路径但忽略它们之间的路径。set_clock_groups -exclusive -group {clk_fast} -group {clk_slow}-physically_exclusive表示两个时钟域在物理上互斥它们不能同时存在。比如芯片测试模式和功能模式下的时钟同一时刻只有一个时钟源被连接到时钟树。这种模式比-exclusive更严格工具不仅忽略跨域路径还会检查时钟树上的物理冲突。set_clock_groups -physically_exclusive -group {func_clk} -group {test_clk}注意-exclusive和-physically_exclusive的区别在于前者假设两个时钟可能同时存在但不同时活跃后者假设两个时钟在物理上不可能同时存在。选错了会导致约束过松或过紧。3.3 分组策略全异步、部分异步与时钟MUX场景实际项目中时钟域之间的关系往往不是简单的两两异步。一个设计可能有五六个时钟域其中一些是同源的一些是异步的。这时候分组策略就很重要。全异步分组把所有时钟域两两分组每组一个时钟。适用于所有时钟域都互不相关的场景。set_clock_groups -asynchronous \ -group {clk_cpu} \ -group {clk_ddr} \ -group {clk_pcie} \ -group {clk_video}部分异步分组把同源的时钟放在同一组不同源的时钟分在不同组。适用于有多个时钟来自同一个MMCM的场景。set_clock_groups -asynchronous \ -group {clk_100m clk_200m} \ -group {clk_74m}这里clk_100m和clk_200m来自同一个MMCM它们之间有确定的相位关系所以放在同一组工具会检查它们之间的路径。clk_74m来自另一个晶振与它们异步所以分在另一组。时钟MUX场景这是最容易出问题的地方。当时钟经过MUX选择时MUX的输出时钟在不同时刻可能来自不同的输入时钟。工具默认会把MUX的输出时钟和所有输入时钟都建立关联导致约束混乱。正确的做法是在MUX输出引脚上定义生成时钟然后用-exclusive或-physically_exclusive把输入时钟分组。create_clock -name clk_a -period 10.000 [get_ports clk_a] create_clock -name clk_b -period 6.667 [get_ports clk_b] create_generated_clock -name clk_mux -source [get_pins mux_inst/I0] [get_pins mux_inst/O] set_clock_groups -exclusive -group {clk_a} -group {clk_b}这样工具知道clk_a和clk_b不会同时驱动MUX输出跨域路径不需要检查。3.4 分组顺序与优先级工具如何处理重叠分组当多条set_clock_groups约束存在时工具会按顺序处理。如果两个时钟既出现在-asynchronous分组里又出现在-exclusive分组里工具的行为可能不符合预期。我建议的做法是把所有时钟分组约束集中放在SDC文件的一个段落里按从宽到严的顺序排列。先写全异步分组再写互斥分组。避免在不同位置零散地写分组约束那样很难排查冲突。另外set_clock_groups会覆盖之前对同一对时钟的set_false_path约束。如果你先写了set_false_path -from clk_a -to clk_b又写了set_clock_groups -asynchronous -group {clk_a} -group {clk_b}后者会生效前者被忽略。所以不要混用这两种约束来描述同一对时钟关系。4. set_clock_uncertainty给时序留出安全余量时钟不确定性clock uncertainty是SDC里最容易被忽视、但对时序收敛影响最大的参数之一。它描述的是时钟沿到达时间的不确定程度包括时钟抖动、时钟树偏斜、以及工艺角变化带来的偏差。设置得太小时序报告看起来很美实际芯片跑不起来设置得太大工具拼命优化浪费资源和时间。4.1 不确定性的来源抖动、偏斜与工艺偏差时钟不确定性主要来自三个方面。时钟抖动jitter时钟源本身的相位噪声导致的周期变化。晶振的抖动通常在几十皮秒量级MMCM输出的时钟抖动会更大一些。抖动分为随机抖动和确定性抖动前者服从高斯分布后者与电源噪声、串扰等有关。时钟树偏斜skew时钟信号到达不同寄存器的时间差异。即使时钟树做了平衡仍然会有几十到几百皮秒的偏斜。偏斜的大小取决于时钟树的深度、布局布线的质量、以及工艺角。工艺偏差process variation芯片制造过程中的参数波动导致时钟缓冲器延时变化。先进工艺节点下工艺偏差的影响越来越显著。set_clock_uncertainty就是把这些因素综合成一个数值告诉工具在时序检查时预留多少余量。4.2 setup与hold的差异化设置建立时间setup和保持时间hold对不确定性的敏感度不同。setup检查的是数据在时钟沿之前是否稳定不确定性会减少可用的数据路径延时hold检查的是数据在时钟沿之后是否保持稳定不确定性会增加对数据路径延时的要求。因此setup和hold的不确定性应该分别设置set_clock_uncertainty -setup 0.200 [get_clocks sys_clk] set_clock_uncertainty -hold 0.100 [get_clocks sys_clk]setup的不确定性通常比hold大因为setup受时钟抖动和偏斜的影响更明显。hold的不确定性主要来自时钟树偏斜抖动的影响较小。对于跨时钟域路径如果已经用set_clock_groups分开了不确定性设置就不重要了因为路径根本不检查。但对于同源时钟域之间的路径不确定性设置直接影响时序收敛难度。4.3 不确定性数值的估算方法不确定性数值不是拍脑袋定的。我通常按以下步骤估算查时钟源手册晶振或时钟芯片的数据手册会给出周期抖动period jitter和相位抖动phase jitter的典型值和最大值。取最大值作为抖动分量。估算时钟树偏斜根据时钟树的级数和布局布线的预期质量估算偏斜。一般来说FPGA内部时钟树的偏斜在50-150ps之间ASIC的偏斜取决于时钟树综合的结果。加工艺余量根据工艺节点的特性加10%-20%的余量。先进工艺节点下这个余量要更大。综合取值把抖动、偏斜、工艺余量相加得到不确定性的初始值。然后在时序收敛过程中根据实际情况调整。实操心得我通常先设一个偏保守的值比如setup 0.3ns、hold 0.15ns跑一遍时序。如果违例很多先检查约束是否正确再考虑放宽不确定性。如果时序轻松收敛可以适当收紧不确定性给实际芯片留更多余量。但不要为了追求报告好看而把不确定性设得过小那是自欺欺人。4.4 跨时钟域路径的不确定性处理对于已经用set_clock_groups分开的异步时钟域不确定性设置不影响时序检查因为路径被忽略了。但对于没有分开的跨时钟域路径比如同源但不同频率的时钟不确定性设置需要特别注意。同源时钟之间的相位关系是确定的但时钟树偏斜和抖动仍然存在。这时候不确定性的设置应该比同频同相时钟更大一些因为频率不同意味着时钟沿的对齐关系更复杂。set_clock_uncertainty -setup 0.250 -from clk_100m -to clk_200m set_clock_uncertainty -hold 0.120 -from clk_100m -to clk_200m-from和-to参数可以针对特定的时钟对设置不确定性比全局设置更精确。5. 实战约束编写流程与调试技巧前面几章把SDC的核心命令拆开讲了一遍这一章把整个流程串起来从拿到设计到约束收敛一步步说明每个阶段该做什么、怎么做、遇到问题怎么排查。5.1 从设计规格到约束文件完整流程拆解约束编写不是一上来就打开文本编辑器写Tcl。我通常按以下流程操作第一步梳理时钟架构。画出设计的时钟树标出每个时钟的来源、频率、去向。对于MMCM和PLL记录输入频率、输出频率、分频比、相位偏移。对于时钟MUX记录选择逻辑和各个输入时钟的特性。第二步确定时钟域关系。把时钟按来源分组同源的放在一起不同源的分开。确定哪些时钟域之间是异步的哪些是互斥的哪些有确定的相位关系。第三步编写基础时钟约束。先写create_clock把所有输入时钟定义好。再写create_generated_clock把MMCM、PLL、分频器的输出时钟定义好。这一步完成后跑一遍综合看工具是否能正确识别所有时钟。第四步编写时钟分组约束。根据第二步的分析写set_clock_groups。先写异步分组再写互斥分组。写完后再跑综合看跨时钟路径的违例是否消失。第五步编写输入输出延时约束。根据外部器件的时序手册写set_input_delay和set_output_delay。这一步需要外部器件的建立时间和保持时间参数。第六步编写不确定性约束。根据时钟源手册和时钟树预期写set_clock_uncertainty。先设保守值后续根据时序收敛情况调整。第七步编写例外约束。对于不需要检查的路径写set_false_path或set_multicycle_path。这一步要非常谨慎每一条例外约束都要有明确的理由。第八步时序收敛与迭代。跑完整的时序分析根据报告调整约束。重点关注跨时钟域路径、输入输出路径、以及例外约束覆盖的路径。5.2 约束文件的组织与维护SDC文件的可维护性很重要。我建议按以下结构组织# # 时钟定义 # create_clock -name sys_clk -period 10.000 [get_ports sys_clk] create_clock -name adc_clk -period 40.000 [get_ports adc_clk] # # 生成时钟 # create_generated_clock -name clk_100m -source [get_pins mmcm_inst/CLKIN] [get_pins mmcm_inst/CLKOUT0] create_generated_clock -name clk_200m -source [get_pins mmcm_inst/CLKIN] [get_pins mmcm_inst/CLKOUT1] # # 时钟分组 # set_clock_groups -asynchronous \ -group {sys_clk clk_100m clk_200m} \ -group {adc_clk} # # 输入输出延时 # set_input_delay -clock adc_clk -max 5.000 [get_ports adc_data*] set_input_delay -clock adc_clk -min 2.000 [get_ports adc_data*] # # 时钟不确定性 # set_clock_uncertainty -setup 0.200 [get_clocks sys_clk] set_clock_uncertainty -hold 0.100 [get_clocks sys_clk] # # 例外约束 # set_false_path -from [get_ports rst_n]每个段落用注释分隔方便后续查找和修改。约束文件不要写得太长超过500行的SDC文件应该考虑拆分成多个文件按功能模块组织。5.3 时序报告解读从slack反推约束问题时序报告是约束质量的直接反馈。看到违例时不要急着改约束先分析违例的性质。setup违例数据到达太晚。可能的原因包括组合逻辑太长、时钟频率设得太高、不确定性设得太大。先检查数据路径上的逻辑级数如果逻辑级数正常再检查时钟约束是否合理。hold违例数据到达太早。通常是因为数据路径太短或者时钟树偏斜太大。hold违例比setup违例更难修复因为不能通过降低频率来解决。如果hold违例集中在跨时钟域路径上检查是否漏了set_clock_groups。跨时钟域违例如果异步时钟域之间有违例说明set_clock_groups没写对或者没生效。检查时钟名是否正确、分组是否覆盖了所有时钟对。输入输出违例检查set_input_delay和set_output_delay的值是否与外部器件手册一致。如果外部器件手册给的是建立时间和保持时间需要转换成input delay和output delay。5.4 常见约束错误与排查清单以下是我在实际项目中遇到的高频错误整理成速查表错误现象可能原因排查方法工具报找不到时钟create_clock的源对象写错用get_ports或get_pins确认对象存在跨时钟域仍有违例set_clock_groups未生效检查时钟名是否与create_clock一致生成时钟频率不对自动推导错误手动写create_generated_clock输入延时约束不生效虚拟时钟未定义先create_clock定义虚拟时钟例外约束覆盖不全路径匹配不完整用get_paths或report_timing确认路径时序报告与预期不符约束优先级冲突检查是否有重复或矛盾的约束注意每次修改约束后一定要重新跑综合和实现不要只跑时序分析。约束的变化会影响工具的优化策略只跑时序分析看不到优化结果的变化。5.5 约束收敛的迭代策略约束收敛不是一次性的工作而是一个迭代过程。我的迭代策略是第一轮粗调。先把所有时钟定义好分组写好不确定性设保守值。跑综合看整体时序情况。如果违例数量在可接受范围内比如小于总路径数的5%进入第二轮。如果违例数量很大先检查约束是否有明显错误。第二轮细调。针对违例集中的路径分析是约束问题还是设计问题。如果是约束问题调整不确定性、输入输出延时、例外约束。如果是设计问题反馈给前端修改逻辑。第三轮收紧。时序基本收敛后逐步收紧不确定性给实际芯片留更多余量。每次收紧后跑一遍实现确认时序仍然收敛。第四轮验证。用最终约束跑完整的时序分析生成时序报告。检查所有时钟域、所有输入输出接口、所有例外约束覆盖的路径。确认没有遗漏。这个迭代过程通常需要三到五轮复杂设计可能需要更多。关键是每轮都要有明确的目标不要盲目调整约束。6. 时钟MUX与切换场景的约束处理时钟MUX和时钟切换是SDC约束中最容易出问题的场景之一。工具对MUX的处理逻辑与普通组合逻辑不同如果约束写得不准确要么产生大量虚假违例要么漏掉真正的时序风险。6.1 时钟MUX的约束陷阱当时钟经过MUX时工具默认会把MUX的输出时钟与所有输入时钟建立关联。这意味着工具会检查输出时钟域与每个输入时钟域之间的路径。如果MUX的两个输入时钟是异步的这些跨域路径检查会产生大量虚假违例。更麻烦的是工具可能会把MUX的输出时钟当作一个独立的时钟与输入时钟分别建立相位关系。如果输入时钟频率不同工具会按最坏情况检查导致时序报告完全失真。正确的处理方式是在MUX输出引脚上定义生成时钟然后用set_clock_groups -exclusive把输入时钟分组。create_clock -name clk_a -period 10.000 [get_ports clk_a] create_clock -name clk_b -period 6.667 [get_ports clk_b] create_generated_clock -name clk_mux -source [get_pins mux_inst/I0] [get_pins mux_inst/O] set_clock_groups -exclusive -group {clk_a} -group {clk_b}这样工具知道clk_a和clk_b不会同时驱动MUX输出跨域路径不需要检查。同时clk_mux作为生成时钟会继承clk_a或clk_b的时序特性具体继承哪个取决于MUX的选择信号。6.2 时钟切换电路的约束方法时钟切换电路比简单的MUX更复杂因为它涉及到切换过程中的毛刺抑制和同步处理。常见的时钟切换电路包括打两拍同步器、握手协议、以及专用的时钟切换IP。对于打两拍同步器约束的关键是确保选择信号在切换时不会产生毛刺。这通常通过set_false_path或set_max_delay来约束选择信号的路径。set_false_path -from [get_pins sync_inst/Q] -to [get_pins mux_inst/S]这条约束告诉工具选择信号从同步器到MUX的路径不需要做时序检查因为同步器已经保证了信号的稳定性。对于握手协议约束的重点是握手信号的跨时钟域路径。这些路径通常需要用set_max_delay来约束而不是完全忽略。set_max_delay -from [get_pins req_sync_inst/Q] -to [get_pins ack_sync_inst/D] 5.000set_max_delay比set_false_path更精确它允许路径存在一定的延时但不允许延时超过指定值。这对于握手协议很重要因为握手信号的延时会影响切换的响应时间。6.3 多时钟域设计的约束组织一个设计如果有多个时钟域约束的组织方式直接影响可维护性。我通常按以下原则组织按时钟域分组把同一个时钟域相关的约束放在一起。比如clk_cpu域的所有约束放在一个段落clk_ddr域的约束放在另一个段落。跨域约束集中管理所有set_clock_groups、set_false_path、set_max_delay等跨域约束集中放在文件末尾方便统一查看和修改。注释说明约束理由每条例外约束都要写注释说明为什么这条路径不需要检查。三个月后你回头看没有注释的例外约束就是定时炸弹。# clk_cpu与clk_ddr异步跨域路径已做异步处理 set_clock_groups -asynchronous -group {clk_cpu} -group {clk_ddr} # 复位信号经过同步器不需要时序检查 set_false_path -from [get_ports rst_n] -to [get_pins sync_inst/D]6.4 时钟切换的时序验证要点时钟切换电路的时序验证不能只看STA报告。STA只能验证静态的时序关系无法验证切换过程中的动态行为。我通常补充以下验证功能仿真在切换时刻附近跑功能仿真确认选择信号的变化不会导致输出时钟出现毛刺。门级仿真用综合后的网表跑门级仿真确认在实际门延时下切换行为仍然正确。时序例外检查确认所有跨域路径都被正确的例外约束覆盖。用report_timing -from [get_clocks clk_a] -to [get_clocks clk_b]检查是否有遗漏的路径。切换频率检查如果时钟切换频繁发生需要确认切换电路的响应时间满足系统要求。这通常通过set_max_delay来约束。实操心得时钟切换电路是芯片调试中最容易出问题的地方之一。我经手过一个项目功能仿真全过门级仿真也过但芯片回来后发现切换时刻偶尔出现时钟毛刺。后来排查发现是选择信号的同步器少打了一拍导致亚稳态传播到了MUX。所以时钟切换电路一定要留足余量同步器至少打两拍最好打三拍。7. 约束验证与签核检查约束写完不是终点验证约束的正确性和完整性同样重要。这一章讲怎么做约束验证以及签核前需要检查哪些项目。7.1 约束覆盖率检查约束覆盖率检查的目的是确认所有时序路径都被适当的约束覆盖。工具通常提供report_clock_networks、report_timing_requirements等命令来检查约束覆盖情况。我通常按以下步骤检查列出所有时钟用report_clocks确认所有时钟都被正确定义没有遗漏。检查时钟关系用report_clock_interaction查看时钟之间的检查关系确认异步时钟域之间没有时序检查。检查例外约束用report_exceptions列出所有例外约束确认每条约束都有明确的理由。检查未约束路径用report_timing -unconstrained查看是否有未约束的路径。如果有需要补充约束。7.2 时序例外约束的审计时序例外约束是约束中最危险的部分。一条错误的set_false_path可能掩盖真正的时序风险导致芯片失效。我通常按以下清单审计例外约束每条set_false_path是否有明确的理由理由是否记录在注释中例外约束的源和目的是否精确是否使用了通配符导致覆盖范围过大例外约束是否与set_clock_groups冲突例外约束是否覆盖了所有需要忽略的路径例外约束是否影响了不需要忽略的路径审计过程中我通常会用report_timing -from 源 -to 目的来确认例外约束的效果。如果路径被正确忽略报告会显示no timing path或者false path。7.3 跨时钟域路径的最终确认跨时钟域路径是签核检查的重点。我通常按以下步骤确认列出所有跨时钟域路径用report_timing -from [get_clocks clk_a] -to [get_clocks clk_b]列出所有跨域路径。确认异步处理对于每条跨域路径确认设计上已经做了异步处理如双打拍同步器、异步FIFO、握手协议。确认约束覆盖确认每条跨域路径都被set_clock_groups或set_false_path覆盖。检查同步器约束对于同步器路径确认set_max_delay或set_false_path约束正确。检查亚稳态风险对于没有做异步处理的跨域路径确认是否有亚稳态风险。如果有反馈给设计修改。7.4 签核清单与常见遗漏项签核前我通常按以下清单逐项检查检查项检查方法通过标准所有时钟已定义report_clocks无遗漏时钟生成时钟正确report_clocks -generated频率和相位正确异步时钟已分组report_clock_interaction异步域之间无检查输入延时已约束report_timing -from [all_inputs]无未约束输入输出延时已约束report_timing -to [all_outputs]无未约束输出例外约束已审计report_exceptions每条约束有理由跨域路径已确认report_timing -from [get_clocks *] -to [get_clocks *]无遗漏跨域路径不确定性已设置report_clock_uncertainty所有时钟有不确定性时序报告无违例report_timing_summaryWNS和WHS为正常见遗漏项包括虚拟时钟未定义、生成时钟未手动定义、时钟MUX未分组、复位信号未约束、以及跨时钟域路径未完全覆盖。这些遗漏项在签核前一定要逐项确认。8. 从约束到硅片经验与教训写了这么多约束最后聊一些实际项目中的经验和教训。这些内容在工具手册里找不到但往往是决定项目成败的关键。8.1 约束过紧与过松的代价约束过紧的代价是资源浪费和编译时间增加。工具为了满足过紧的约束会拼命优化布局布线消耗大量逻辑资源和布线资源。我见过一个设计因为不确定性设得过大工具在跨时钟路径上插入了大量寄存器导致逻辑资源利用率从60%飙升到85%编译时间从2小时增加到8小时。约束过松的代价是芯片失效。时序报告看起来很美但实际芯片跑不起来。我经手过一个项目约束里漏了一条跨时钟域路径的set_clock_groups工具按同步路径检查后报告时序收敛。但实际芯片回来后跨时钟域数据传输偶尔出错。后来排查发现是亚稳态导致的如果当初约束写对了工具会忽略这条路径设计上也会更重视异步处理。约束的黄金法则是准确描述硬件意图不多不少。每一条约束都要有明确的物理意义不要为了报告好看而随意调整。8.2 工具差异与版本兼容性不同工具对SDC的支持有差异。Synopsys Design Compiler、Cadence Genus、Xilinx Vivado、Intel Quartus对SDC命令的解析不完全一致。比如set_clock_groups的-physically_exclusive选项在某些工具中不支持需要用-exclusive替代。版本兼容性也是问题。新版本工具可能引入新的SDC命令或者改变现有命令的行为。我通常的做法是在项目开始时确认工具版本查阅对应版本的SDC手册不要依赖记忆中的语法。跨工具移植约束时一定要做兼容性测试。把约束从一个工具移植到另一个工具后跑一遍时序分析对比报告是否一致。如果不一致逐条排查约束的差异。8.3 团队协作中的约束管理约束文件是团队协作的产物不是个人作品。我建议版本控制SDC文件必须纳入版本控制每次修改都要有提交记录。修改约束时在提交信息中说明修改原因和影响范围。代码审查约束修改需要经过审查特别是例外约束的修改。审查时重点关注修改是否有明确理由、是否影响其他时钟域、是否与现有约束冲突。文档化每条约束都要有注释说明约束的理由和预期效果。复杂约束如时钟MUX、时钟切换需要额外的文档说明。定期审计每隔一段时间对约束文件做一次全面审计清理不再需要的约束更新过时的注释。8.4 个人踩坑记录与实用建议最后分享几个我踩过的坑和对应的建议坑一时钟名不一致。create_clock定义的时钟名和后续约束引用的时钟名不一致导致约束不生效。建议定义时钟后立即用report_clocks确认时钟名后续约束统一使用这个名称。坑二生成时钟自动推导错误。MMCM配置了非整数分频比工具自动推导的生成时钟频率不对。建议对于非整数分频比手动写create_generated_clock用-edges参数精确指定边沿映射。坑三例外约束覆盖不全。set_false_path只写了-from没写-to导致路径只被部分忽略。建议例外约束尽量写全-from和-to避免使用通配符。坑四不确定性设置一刀切。所有时钟用同一个不确定性值导致高频时钟过紧、低频时钟过松。建议按时钟频率和来源分别设置不确定性高频时钟和抖动大的时钟设更大的值。坑五忽略工具warning。工具报的约束warning往往指向真正的问题但很多人直接忽略。建议每次跑时序分析后仔细阅读所有warning确认每条warning的原因。约束编写是一门实践性很强的技能看再多文档不如亲手写一遍、跑一遍、调一遍。希望这篇文章能帮你少走一些弯路让你的设计一次流片成功。
返回列表