简介:这份PDF资料围绕静态时序分析(STA)与逻辑设计展开,面向数字集成电路设计初学者、ASIC/FPGA 方向的学生与工程人员,帮助读者建立从时序约束概念到 timing closure 的完整认知框架。内容涉及时序约束中 Setup、Hold 时间的含义与 Clock-to-Output、tMET 等参数的关系,静态时序分析流程中的网表生成、分析执行与报告解读,以及建立时间与保持时间违例的成因和关键路径定位思路,同时简要介绍 PrimeTime、SST Velocity、Blast 等主流 STA 工具与动态时序分析、形式验证等方法的差异,并延伸至流水线、重定时等逻辑优化手段。资源包内仅含1个PDF文件,大小约592KB,篇幅紧凑,适合作为时序分析入门梳理或面试前复习的提纲式读物。目前已有710人浏览学习,可作为理解 STA 基本概念与术语体系的轻量参考资料。
1. 从一张时序报告说起:这份 STA 讲义解决的到底是哪类问题
很多人第一次做时序签核,流程是先运行一遍工具,看见一屏 slack 数字,然后才开始猜哪个数才算合格。真正容易出问题的地方通常不在工具会不会用,而在于能否用一条T = tCO + tDELAY + tSU把报告里那一行数字还原回具体电路。《静态时序分析与逻辑设计》这份 PDF 采用的是另一种讲法:先把建立时间、保持时间、时钟到输出延迟这几个参数固定下来,再讲时序约束怎么写、关键路径怎么找、timing closure 怎么收。它适合已经能画时序图、但对“约束为什么这么写”没底的人,也适合从 FPGA 逻辑设计转向 ASIC 签核、需要补齐流程概念的工程师。静态时序分析不是跑一次报告就结束,而是一套从约束到报告再到优化的闭环,这份材料把闭环的每一段都落到了具体参数上。
2. 时序路径的两个不等式:T = tCO + tDELAY + tSU 怎么落到网表上
在同步电路里,几乎所有时序问题最终都会被化简成两个不等式:一个管“数据来得够不够早”,一个管“数据走得够不够慢”。前者是建立时间检查,后者是保持时间检查。这两个式子在教材上只有一行,落到真实网表上却要牵扯库单元延迟、互连延迟、时钟树偏斜和工作条件。这一章把公式拆开,拆到能拿到单个路径上逐个数字对得上的程度。
2.1 把 tCO、tDELAY、tSU 三个量固定住
一条从触发器 A 到触发器 B 的路径,延迟由三段组成。tCO是 A 的时钟沿到 Q 端输出稳定所花的时间,这个值不在网表里,而在标准单元库或器件手册的时序表里,并且分 min/typ/max 三档。tDELAY是 A 的 Q 到 B 的 D 之间那段组合逻辑加互连的延迟,由综合与布局布线决定,同样分档。tSU是 B 的 D 端数据必须在时钟沿之前稳定的时间窗口,方向与 tCO 相反,属于“必须提前量”。
把这三点串起来,就得到单周期路径的周期下限:时钟周期必须大于等于tCO(max) + tDELAY(max) + tSU,再扣掉时钟不确定度。这里最容易被忽略的是 max 与 min 的选择:建立时间检查走最大延迟(最慢那条路),保持时间检查走最小延迟(最快那条路)。同一份网表,两次检查用的根本不是同一组数字,这一点如果没建立直觉,后面看报告会一直觉得工具在乱算。
| 参数 | 数据来源 | 建立检查取值 | 保持检查取值 | 典型量级(示例工艺) |
|---|---|---|---|---|
| tCO | 库单元时序表 | max | min | 0.08 ~ 0.20 ns |
| tDELAY | 综合/布线后的 SDF 或寄生参数 | max | min | 0.1 ~ 数 ns |
| tSU | 库单元时序表 | max | — | 0.02 ~ 0.10 ns |
| tH | 库单元时序表 | — | max | 0.01 ~ 0.05 ns |
| 时钟不确定度 | 约束文件给定 | 加到 required 上扣减 | 加到 required 上扣减 | 0.05 ~ 0.20 ns |
提示:表格里的量级只是示意,实际值必须以手上那套库的
.lib或器件手册为准。拿别人的数字套自己的工艺,是最常见的翻车起点。
有了这三段,就能回答一个很实际的问题:这颗芯片或这块板子最高能跑到多少频率。把f = 1/T代进去,T 取下限,得到的就是理论频率上限。注意这个上限只对单周期路径成立,多周期路径、跨时钟域路径、带使能的路径都不能直接套用。
2.2 slack 的加减法:arrival 与 required 到底谁减谁
工具报告里那三列数字,本质是两个时间点的比较。arrival time是数据实际到达 D 端的时刻,从发射时钟沿算起,等于发射沿时刻加上tCO + tDELAY。required time是数据最晚必须到达的时刻,从捕获时钟沿倒推,等于捕获沿时刻减去tSU和时钟不确定度。两个一减就是 slack:slack = required - arrival,正数表示还有余量,负数表示已经违例。
手算一遍比看十遍报告有用。下面这段脚本把一条路径的 setup slack 算出来,用来和工具输出对齐:
# 单条 setup 路径的 slack 手算,用于对照工具报告 tco = 0.12 # 触发器 clock-to-Q,取库中 max 档 tdelay = 0.85 # 组合逻辑 + 互连延迟,取 max 档(含布线) tsu = 0.08 # 目的触发器 setup time,取 max 档 tclk = 2.00 # 时钟周期,对应 500 MHz uncertainty = 0.15 # 时钟不确定度:jitter + skew 预留 arrival = tco + tdelay # 数据实际到达时刻 required = tclk - tsu - uncertainty # 数据最晚允许到达时刻 slack = required - arrival print(f"arrival={arrival:.3f} required={required:.3f} slack={slack:.3f}") # arrival=0.970 required=1.770 slack=0.800 -> 正余量,这条路径不是瓶颈这段脚本里最值得琢磨的是uncertainty这一项。它不是工艺参数,而是设计者主动留出的安全垫,通常会涵盖时钟抖动、时钟树偏斜预估、以及 OCV 降额。留多少没有统一答案,我一般的做法是先把时钟树综合前的偏斜预估放进去,时钟树做完之后再用实际偏斜回填,把多余的部分收回去,否则后期会被过大的余量拖住,白白多做一堆没必要的优化。
对 hold 检查,加减法方向反过来:arrival = tCO(min) + tDELAY(min),required = tH,两者相差slack = arrival - required。同样是正数合格,但含义变成“数据不能来得太早”。
2.3 保持时间为什么和频率无关
初学者常有的误会是:hold 违例降频就能救。实际上把周期从 2 ns 拉到 4 ns,hold 的 slack 一点都不会变。原因在检查的两个沿:setup 检查比较的是“这个沿发射、下个沿捕获”,两个沿之间隔着一整个周期,周期变长余量变大;hold 检查比较的是“同一个沿发射、同一个沿捕获”,路径两端的时钟沿是同一个时刻,周期长短根本没进公式。
所以 hold 违例的修复手段只有三种:在数据路径上插延迟(buf 或延迟单元)、缩短时钟到捕获触发器的偏斜、换驱动更弱的单元让 min 延迟变大。这三种手段都在改路径本身,跟频率无关。反过来,setup 违例的修复空间就宽得多:降频、砍组合逻辑级数、加流水、改约束,随便挑。
| 维度 | setup 检查 | hold 检查 |
|---|---|---|
| 发射沿 / 捕获沿 | 不同沿(隔一个周期) | 同一沿 |
| 用的延迟档位 | max | min |
| 与频率的关系 | 周期越紧越容易违例 | 无关 |
| 修复手段 | 降频、减逻辑级数、加流水、松约束 | 插延迟、调偏斜、换弱驱动 |
| 高发 corner | 慢 corner(低压高温) | 快 corner(高压低温) |
表格最后一行是签核时的第二个高发点:hold 违例经常只在快 corner 冒出来,如果只跑一个慢 corner 就下结论说时序干净,流片回来大概率会遇到问题。
2.4 时钟不确定度和工作条件:余量不能混着留
约束文件里能影响 slack 的余量项有好几个:clock uncertainty、clock latency(source 与 network 分开)、以及各类 derate 系数。它们不是同义反复,压的也不是同一个东西。uncertainty 压的是“我不确定时钟沿什么时候来”,latency 补的是“时钟从端口到触发器实际走了多久”,derate 则是对延迟值做悲观放大。
实践中常见的错误是把所有悲观都塞进 uncertainty。这么做 slack 会集体变负,然后开始盲目优化,最后发现优化完还是负的,因为负的根源是约束里的常数项。我一般的分配是:uncertainty 只放抖动和偏斜预估,通常 0.1 ns 上下;OCV 通过 derate 表达,早路径和晚路径分开设;latency 在时钟树综合之后用提取出的实际值覆盖预估。这三项各走各的通道,出了问题才能定位到具体是哪一项在起作用。
3. 约束到报告的四步链路:一份能跑的最小 SDC 与报告读法
工具跑起来不难,难的是让它算的东西和你脑子里想的是同一件事。这一步的差距全在约束上:约束写漏一条,报告会给你一个漂亮的假绿;约束写重一条,你会花两周去优化一条本来就不需要的路径。下面按“约束 → 网表与库 → 报告 → 路径分组”这个顺序把链路走一遍。
3.1 时序约束:一份能跑的最小 SDC 骨架
一份能真正跑出有意义结果的约束文件,最少要覆盖四类信息:时钟定义、输入延迟、输出延迟、驱动与负载。缺任何一类,工具都会用默认值兜底,而默认值往往是把端口当成理想环境,报告自然好看。
# 最小可跑的 SDC 骨架:时钟 -> 不确定性 -> 端口延迟 -> 驱动/负载 # 1) 主时钟:周期 2.0 ns,占空比 50%,上升沿在 0、下降沿在 1.0 create_clock -name clk_core -period 2.0 -waveform {0 1.0} [get_ports clk] # 2) 时钟不确定度与转换时间:setup 留 0.15,hold 留 0.05 set_clock_uncertainty -setup 0.15 [get_clocks clk_core] set_clock_uncertainty -hold 0.05 [get_clocks clk_core] set_clock_transition 0.10 [get_clocks clk_core] # 3) 输入延迟:上游器件的 tCO + 板级走线,max/min 分别给 set_input_delay -clock clk_core -max 0.60 [remove_from_collection [all_inputs] [get_ports clk]] set_input_delay -clock clk_core -min 0.20 [remove_from_collection [all_inputs] [get_ports clk]] # 4) 输出延迟:下游器件的 tSU/tH,max 对应 setup、min 对应 hold set_output_delay -clock clk_core -max 0.45 [remove_from_collection [all_outputs] [get_ports clk]] set_output_delay -clock clk_core -min 0.10 [remove_from_collection [all_outputs] [get_ports clk]] # 5) 驱动能力与负载:不写这两行,端口延迟算出来偏乐观 set_driving_cell -lib_cell BUF_X2 -pin Z [remove_from_collection [all_inputs] [get_ports clk]] set_load 0.05 [all_outputs]逐段解释一下。create_clock的-waveform决定上升沿与下降沿位置,如果设计里同时用到双沿,这里写错会让一半路径的检查沿错位,报告全错但看起来很正常。set_clock_uncertainty分 setup/hold 两条,是因为两个检查的悲观程度本来就不该一样。set_input_delay和set_output_delay里的 max/min 不是随便写的,max 参与 setup、min 参与 hold,少写一条 min,输入侧的所有 hold 检查就等于没做。
set_driving_cell和set_load是最容易被跳过、又最容易造成假绿的两行。前者告诉工具输入端有多强的驱动,后者告诉工具输出端挂了多少电容。不写这两行,工具按理想驱动和零负载算,延迟偏小,slack 偏大,报告漂亮但没意义。
注意:端口列表里一定要把时钟端口排除掉,否则时钟会被当成普通数据端口加输入延迟,时钟路径的检查会全部失效。
3.2 网表与库的匹配:别让工具在错的单元上算延迟
约束写完,下一步是把网表和库对起来。这一步出问题的表现很有特征:工具能跑完,报告也能出,但延迟数字明显不合常理,比如一个反相器算出 3 ns。原因通常是网表里的单元名在库里找不到,工具悄悄替换成默认单元或者直接跳过。
# 网表与库一致性检查:抽出网表里用到的单元名,逐个到库里核对 grep -oE '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*' netlist.v \ | tr -d ' ' | sort -u > cells_in_netlist.txt # 从 .lib 里抽出所有 cell 定义名 grep -oE 'cell[[:space:]]*\([[:space:]]*[A-Za-z0-9_]+' slow.lib \ | sed -E 's/cell[[:space:]]*\([[:space:]]*//' | sort -u > cells_in_lib.txt # 差集:出现在网表但库里没有的单元,就是隐患清单 comm -23 cells_in_netlist.txt cells_in_lib.txt这段脚本的思路很直接:网表里出现的单元名集合,减去库里定义的单元名集合,剩下的就是“工具认识网表、但不认识这个单元”的部分。实际项目里这个差集通常不会为空,因为网表里混着电源单元、填充单元、物理only单元,这些本来就没有时序模型。所以真正要看的是差集里有没有带时序的单元,比如触发器、锁存器、时钟门控单元。这几类如果缺库,工具会给出完全错误的路径延迟。
除了单元名,还有两项必须核对:工作条件(corner)和库版本。网表是某个 corner 下综合出来的,STA 却拿另一个 corner 的库去算,两边的延迟模型对不上,结果没有参考价值。我一般的做法是在脚本开头把 corner 名字打印出来,跑之前肉眼确认一遍,比事后翻日志省事得多。
3.3 报告三列怎么读:一份真实报告的拆解
报告里真正需要的只有三列:起点时刻、到达时刻、余量。其余信息都是辅助定位。下面是一段典型的建立时间报告结构(各工具字段名有差异,语义一致):
Startpoint: u_core/u_ff0 (rising edge-triggered flip-flop clocked by clk_core) Endpoint: u_core/u_ff1 (rising edge-triggered flip-flop clocked by clk_core) Path Group: clk_core Path Type: max Point Incr Path --------------------------------------------------------- clock clk_core (rise edge) 0.000 0.000 u_ff0/CK 0.000 0.000 u_ff0/Q 0.120 0.120 <- tCO u_core/u_and0/Z 0.310 0.430 <- 组合逻辑 u_core/u_and1/Z 0.280 0.710 u_ff1/D 0.000 0.710 <- arrival --------------------------------------------------------- data arrival time 0.710 clock clk_core (rise edge) 2.000 2.000 clock uncertainty -0.150 1.850 u_ff1/setup -0.080 1.770 <- required --------------------------------------------------------- data required time 1.770 slack (MET) 1.060读这份报告的顺序是:先看 Path Group 和 Path Type,确认这是不是你想检查的那组路径;再看 arrival 那一行是 tCO 加了几段组合逻辑;最后看 required 那一行的三个减项。三段组合逻辑一共贡献 0.59 ns,占了整条路径延迟的大头,如果要优化,目标很明确——把 and0 和 and1 合并或者拆到两级流水里。
真正需要警惕的是相反的情况:报告显示 slack 很大,但路径上的组合逻辑段数明显偏少。这说明这条路径根本没被走通,工具是在一段不完整的路径上算出的余量。判断方法是看 startpoint 和 endpoint 是不是都在设计内部,只要有一端落在端口上,就要回头检查 3.1 里的输入输出延迟有没有覆盖到。
3.4 路径分组与报告生成:把上千条路径收敛成几条
一颗稍大的设计,时序路径动辄几十万条,全打印出来没人看。收敛的办法是分组:按时钟域分、按路径类型分、按 slack 阈值筛。分组之后每组只保留最差的那几条,问题就集中了。
# 按时钟域分组,只抓违例路径,每组保留最差的 5 条 set clk_list [all_clocks] foreach clk $clk_list { set grp [get_path_group -of_clocks $clk] # -slack_lesser_than 0 只留违例;-nworst 控制每端点条数 report_timing -path_group $grp \ -delay_type max \ -max_paths 5 \ -nworst 1 \ -slack_lesser_than 0 \ > ./rpt/${clk}_setup.rpt }-slack_lesser_than 0是关键参数,它把所有合格路径过滤掉,只留下需要动手的。-nworst 1表示每个端点只报最差的一条,避免同一端点因为多条输入路径重复刷屏。-max_paths 5控制总量。三个参数配合,能把一个时钟域的报告压到一页以内。
生成完报告不要急着看,先做一次覆盖率检查:约束里定义的时钟数量、端口加了延迟的数量、以及设计里实际存在的时钟数量,三者应该对得上。对不上说明有约束没覆盖到,报告是残缺的。
4. FPGA 与 ASIC 的边界:同一套 STA 方法,两套参数要重算
方法在两边是通用的,参数几乎全要重算。从 FPGA 转过来的人最容易犯的错,是把 FPGA 上那套“时序报告全绿就完事”的习惯直接搬到 ASIC 上。两边的延迟构成、可用资源、签核标准差得很远,同一个设计在两边的瓶颈路径经常不是同一条。
4.1 工具档位对照:签核级 STA 与内置时序分析器
项目正文里提到的几款 EDA 工具,按能力可以分成两档:签核级 STA 工具,和 FPGA 厂商自带的时序分析器。两档的定位完全不同,混用会出问题。
| 维度 | 签核级 STA 工具 | FPGA 内置时序分析器 |
|---|---|---|
| 输入 | 门级网表 + 标准单元库 + 寄生参数 | 布局布线后的器件网表 + 器件时序模型 |
| 约束方式 | SDC 全量约束,需自行编写与校验 | 部分自动生成,时钟由工具识别 |
| 延迟模型 | 库单元 + 提取的互连 RC | 器件内部固定模型,按行列位置查表 |
| 多 corner 支持 | 完整,可批量扫 | 有限,通常按速度等级给定 |
| 典型使用阶段 | 综合后、布局布线后、签核前 | 综合后预估、布局布线后确认 |
| 常见误用 | 约束漏项导致假绿 | 把预估结果当签核结果 |
表格里最后一行值得展开。FPGA 侧的分析器在布局布线前给的是估算值,估算用的互连模型和最终布线结果能差出不少,尤其在高利用率设计上。把估算报告当签核结论,上板之后才发现问题,这种属于典型的流程坑。
4.2 跨时钟域、伪路径、多周期:三个高发误用点
约束里最危险的不是写错,是写得太“宽松”。三条命令能让报告瞬间全绿,也能让真实问题全部消失。
# 危险区:这三条命令都能让报告变绿,用错就是掩盖问题 # 1) 跨时钟域:只对真正的异步跨越生效,且必须配合同步器 set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b] # 2) 多周期:只有逻辑上真的允许 N 拍才写,setup 和 hold 要成对给 set_multicycle_path 2 -setup -from [get_pins u_ff0/Q] -to [get_pins u_ff1/D] set_multicycle_path 1 -hold -from [get_pins u_ff0/Q] -to [get_pins u_ff1/D] # 3) 逻辑互斥:需要配合 design rule 检查,否则会被综合优化掉 set_case_analysis 0 [get_ports test_mode]第一条set_false_path的正确用法,是跨越的双方确实属于异步时钟域,且路径上已经有同步器(两级触发器或异步 FIFO)。如果只是“不想看到这条违例”就加 false_path,等于把真实的数据路径从检查里删掉,上板后表现为极低概率的功能错误,极难复现。
第二条的多周期约束,setup 和 hold 必须成对。只给 setup 不给 hold,工具会把 hold 检查的捕获沿也往后推一个周期,导致 hold 检查完全失效。这是我在实际项目里见过最多的约束错误,没有之一。
第三条set_case_analysis在测试模式下很有用,但它会把相关路径直接剪掉,如果这条路径在功能模式下是活的,等于漏检。
4.3 从 FPGA 迁到 ASIC 要重算的四个量
同一份 RTL,从 FPGA 换到 ASIC,下面四个量必须重新评估,不能沿用旧数字。
第一个是单元延迟模型。FPGA 的查找表和进位链延迟是固定的,ASIC 的标准单元延迟随驱动强度、输入转换时间、输出负载连续变化,同一种逻辑功能在不同驱动下能差好几倍。
第二个是互连延迟占比。FPGA 的布线资源是固定的,延迟相对可预测;ASIC 的互连延迟由实际走线长度和层决定,在先进工艺下占比经常超过单元延迟本身。这意味着 ASIC 上优化单元延迟的收益可能远小于预期。
第三个是时钟树。FPGA 的时钟走专用网络,偏斜很小;ASIC 需要自己插入时钟树,偏斜、功耗、拥塞都要自己平衡,时钟树综合前后的时序差别很大。
第四个是工作条件 corner 的数量。FPGA 通常按速度等级给一组参数,ASIC 要覆盖多个工艺角、多个电压温度组合,每个组合都要单独签核。签核报告的数量是 FPGA 的数倍。
5. 避坑与排查:五条时序签核里翻过的车
时序这块的坑有个共同特征:犯的时候毫无感觉,发现的时候已经烧掉一周。下面五条都是按“现象 → 原因 → 解决”记下来的,每条对应一个很具体的信号。
5.1 报告全绿但上板跑飞:端口约束漏了一整片
现象:STA 报告 setup 和 hold 全部为正,最差余量还有 0.3 ns,但板子跑起来功能偶发错误,换个批次又好了。
原因:只约束了内部时钟域之间的路径,输入输出端口一条延迟都没加。工具把端口当成理想环境,外部器件的 tCO 和板级走线延迟完全没进检查。内部路径确实没问题,问题出在芯片边界上,而边界根本没被检查。
解决:在约束末尾加一段覆盖率自检,统计端口总数与已加延迟的端口数,两者不等就中断流程。另外把外部器件的 tCO、tSU、tH 和板级走线延迟整理成一张表,直接从表里取值,不靠记忆。
# 端口约束覆盖率自检:数量对不上直接报错退出 set all_ports [remove_from_collection [all_inputs] [get_ports clk]] set constrained [get_ports -filter "input_delay != {}"] if {[llength $all_ports] != [llength $constrained]} { puts "ERROR: [expr {[llength $all_ports] - [llength $constrained]}] 个输入端口缺少 input_delay" exit 1 }5.2 约束写对了 slack 还是负的:库和网表来自不同 corner
现象:约束逐条核对过,和上一版工程一模一样,但这一版跑出来一片负 slack,最差的 -1.2 ns。把约束回退到上一版,结果还是负的。
原因:网表是慢 corner 下综合出来的,STA 却加载了快 corner 的库。两边的延迟模型对不上,工具算出来的路径延迟比实际偏大,slack 自然全线告负。这种情况很隐蔽,因为工具不会报错,只会安静地按加载的库算。
解决:在脚本开头把库路径、corner 名称、网表版本三者一起打印到日志首行,跑完先看这一行。更进一步的做法是把 corner 名称写进库文件名,让加载错库的行为在路径上就暴露出来。
5.3 关键路径越优化越多:拆散的组合逻辑被布线延迟反噬
现象:针对一条违例路径做逻辑重构,把 6 级组合压到 3 级,综合报告显示单元延迟降了 40%,但布局布线后这条路径的 slack 反而更差了。
原因:级数减少意味着每级的扇出和驱动要求变高,综合出来的单元尺寸变大,布局时被推得更开,互连延迟上升。在互连占主导的工艺节点上,单元延迟的收益被布线延迟吃干净还倒贴。
解决:判断该不该动一条路径,先看报告里单元延迟与互连延迟的比例。互连占比超过一半,就不要在逻辑级数上做文章,改用寄存器打拍(pipeline)把长路径切断,让每一段的物理距离缩短。这个判断在布局布线前的报告里看不到,必须用带寄生参数的版本。
5.4 保持时间只在快 corner 违例:只跑了一个慢 corner
现象:慢 corner 下 hold 全部通过,签核前顺手补跑了一个快 corner,冒出一批 hold 违例,最差的 -0.15 ns。
原因:hold 检查用的是最小延迟路径,工艺越快、电压越高、温度越低,单元延迟越小,数据越容易提前到达。只跑慢 corner,等于只检查了 setup 的最坏情况,hold 的最坏情况完全没覆盖。
解决:签核脚本里把 corner 列表写成显式数组,setup 扫慢角、hold 扫快角,两组结果分开汇总。任何一组缺失就判定签核不完整。这条现在已经是我的固定动作,不再靠记忆。
5.5 报告里整组路径消失:一条 false_path 把它们全剪了
现象:某个时钟域下的路径在报告里一条都找不到,既没有违例也没有合格路径,像是这个域根本不存在。
原因:约束文件里有一条范围过宽的set_false_path,用了-from [get_clocks clk_a]这种粗粒度写法,把该时钟域下所有路径连同真实的同步路径一起剪掉了。工具不会提示“你剪掉了 N 条路径”,只会安静地不检查。
解决:所有set_false_path必须写到具体路径或具体引脚,禁止用时钟到时钟的粗粒度写法,除非确认整对时钟域完全异步。另外在报告生成后统计每个 path group 的路径条数,某组为 0 就回头查约束。这个检查很土,但救过我两次。
6. 关键路径之后的收口:pipeline 与 retiming 什么时候值得动
找到关键路径只是开始,真正决定项目周期的是“改哪里”和“改到什么程度停手”。我的判断顺序是:先看这条路径是不是真的需要优化,再看能不能靠约束澄清,最后才动 RTL 结构。
第一步永远是确认这条路径在功能上必须单周期完成。带多周期语义的路径(比如乘法器的后级、状态机的慢速分支)如果被当成单周期在优化,投入的每一分力气都是浪费。确认的方式是回到 RTL 看数据依赖性,不是看报告。
确认必须单周期之后,才轮到三个手段:
| 手段 | 适用场景 | 代价 | 见效速度 |
|---|---|---|---|
| 逻辑重构(合并/提取公因式) | 组合级数多但每级很浅 | 改动小,可能引入新路径 | 快,综合即可验证 |
| 插入流水级(pipeline) | 路径跨越多个模块,物理距离长 | 增加一拍延迟,接口时序要改 | 中,需重新验证功能 |
| 寄存器重定时(retiming) | 组合逻辑夹在寄存器之间,前后负载不均 | 工具自动搬移,调试信息变差 | 慢,需多轮迭代收敛 |
逻辑重构的收益上限通常在 20% 到 30%,再往上就很难,因为剩下的延迟主要是互连。插入流水级的收益最大,但会改变接口时序,凡是跟外部器件对接的路径都不能随便加,这一点如果没提前跟系统侧对齐,后期返工代价很高。寄存器重定时在数据通路上效果好,在控制逻辑上经常把可读性搞没,调试时找不到信号,我一般只在对性能极度敏感的数据通路上开。
还有一个容易被忽略的收口动作:优化完之后重新跑一遍完整签核,不要只看被优化那条路径的新 slack。逻辑重构和流水级插入都会改变布局,周边路径的延迟跟着变,经常出现“修好一条、坏了两条”的情况。全量重跑一次的成本,远低于上板之后定位一个偶发错误的成本。
从那以后我每次拿到一份新网表,都强制先跑一遍端口约束覆盖率检查和 corner 完整性检查,两项都过了才允许自己去看 slack 数字。这个习惯看着笨,但它把“报告好看”和“时序真的干净”这两件事分开了。希望帮到你。
本文还有配套的精品资源,点击获取