3. 逻辑综合实战:编写综合脚本、分析综合报告、解决常见的综合问题

逻辑综合,说白了就是把 RTL 代码翻译成门级网表的过程。很多刚入行的朋友觉得这步就是跑个脚本完事,其实不然。我见过太多项目因为综合脚本里一个约束写错,导致后端跑了好几个月才发现时序收敛不了,最后不得不推倒重来。

今天我们就来聊聊综合实战中的三个核心环节:脚本怎么写、报告怎么看、问题怎么修。

3.1 综合脚本的骨架与血肉

一个标准的综合脚本,通常包含以下几个部分。我个人习惯把它们分成四块:

模块 内容 说明
环境设置 search_path, target_library, link_library, synthetic_library 告诉工具去哪找库文件
读入设计 analyze + elaborate 或 read_verilog / read_vhdl 把 RTL 代码加载进来
约束施加 create_clock, set_input_delay, set_output_delay, set_max_fanout 等 告诉工具你的设计目标
编译与输出 compile_ultra, write -f verilog, write_sdc, write_sdf 跑综合并保存结果

下面是一个我常用的综合脚本模板,你可以直接拿来改:

# ============================================
# 综合脚本模板 (DC Synthesis Script)
# ============================================

# 1. 库设置
set search_path        ". /path/to/liberty /path/to/db"
set target_library     "tcbn28hpcplusc35l.db"
set link_library       "* $target_library dw_foundation.sldb"
set synthetic_library  "dw_foundation.sldb"

# 2. 读入设计
analyze -format verilog {top.v sub_module_a.v sub_module_b.v}
elaborate top

# 3. 时钟约束
create_clock -name clk -period 10.0 [get_ports clk]
set_clock_uncertainty -setup 0.3 [get_clocks clk]
set_clock_uncertainty -hold  0.1 [get_clocks clk]
set_clock_transition 0.15 [get_clocks clk]

# 4. 输入输出延迟
set_input_delay  2.5 -clock clk [all_inputs]
set_output_delay 2.5 -clock clk [all_outputs]

# 5. 设计规则约束
set_max_fanout 20 [current_design]
set_max_transition 0.5 [current_design]
set_max_capacitance 0.3 [current_design]

# 6. 编译
compile_ultra -gate_clock -no_autoungroup

# 7. 报告与输出
report_timing > reports/timing.rpt
report_area   > reports/area.rpt
report_power  > reports/power.rpt
write -f verilog -output outputs/top_netlist.v
write_sdc     -output outputs/top_constraints.sdc
小技巧: 我习惯在脚本开头加一句 set_host_options -max_cores 8,让 DC 用多核并行编译。尤其是大模块,能省不少时间。不过别贪心,设太多核反而会因为内存争抢变慢。

3.2 综合报告:别只看时序

综合跑完后,工具会吐出一堆报告。很多新手只盯着 timing report 看,其实另外两份报告同样重要。

3.2.1 时序报告怎么看

时序报告的核心是看 slack。正 slack 说明时序有余量,负 slack 说明有违例。但这里有个坑:

  • Setup slack 负了:说明路径太长,需要优化逻辑深度或换用更快的 cell。
  • Hold slack 负了:说明路径太短,需要插入 buffer 或 delay cell。
  • Transition / Capacitance 违例:说明驱动能力不够,需要 upsizing cell。

我曾经在一个项目中,setup slack 全是正的,但 hold slack 在几个关键路径上负了 0.2ns。当时没在意,结果流片回来芯片在低温下频繁出错。嗯,从那以后我再也不敢忽略 hold 检查了。

3.2.2 面积报告里的猫腻

面积报告会告诉你设计用了多少 combinational cells、sequential cells、以及总 cell 面积。但要注意:

  • Combinational area 占比过高:说明逻辑太复杂,可以考虑 retiming 或重新划分模块。
  • Sequential area 占比过高:说明寄存器太多,可能是流水线级数过深。
  • Net area 很大:说明布线资源紧张,后端可能会遇到 routing congestion。

3.2.3 功耗报告里的门道

功耗报告分三部分:动态功耗、静态功耗、内部功耗。我个人习惯先看 动态功耗,因为它通常占大头。如果动态功耗超标,可以尝试:

  • 降低 clock gating 的使能信号切换率
  • 使用低功耗 cell(如 HVT cell)
  • 优化数据路径的翻转率
核心原则: 综合阶段的目标不是把所有 slack 都优化到正无穷,而是让设计在 时序、面积、功耗 三者之间取得平衡。过度优化某一项,往往会牺牲另外两项。

3.3 常见综合问题与解决方案

下面是我在实际项目中遇到最多的几个问题,以及对应的解决思路。

3.3.1 时序违例:setup 修不动

现象: 关键路径上的 slack 是负的,而且怎么优化都修不干净。

排查步骤:

  1. 先看路径报告,找到最长的逻辑级数(logic level)。
  2. 如果 logic level > 20,说明 RTL 代码写得太深,需要手动插入流水线。
  3. 如果 logic level 在 10-15 之间,可以尝试 compile_ultra -retime 让工具自动做 retiming。
  4. 如果还是不行,检查是不是 fanout 太大导致 cell 延迟过高。
避坑指南: 我曾经遇到过一个 case,setup 违例怎么修都修不掉,最后发现是 set_clock_uncertainty 设得太保守。把 uncertainty 从 0.5ns 降到 0.3ns 后,slack 就变正了。当然,这需要你对自己的时钟树质量有信心。

3.3.2 面积爆炸:综合出来的网表比预期大 30%

原因: 通常是 RTL 代码中使用了大量的 casez 或 casex,或者有未优化的 for 循环被综合成了并行逻辑。

解决方案:

  • 检查 RTL 中是否有 full_case 或 parallel_case 的误用。
  • 使用 set_flatten true 让工具尝试优化共享逻辑。
  • 如果面积还是大,考虑把大模块拆分成多个小模块,分别综合后再拼接。

3.3.3 功耗超标:动态功耗比预期高 50%

原因: 通常是 clock gating 没有生效,或者数据路径的翻转率过高。

排查方法:

  1. 在功耗报告中查看 clock gating 覆盖率。如果低于 80%,说明很多寄存器没有被门控。
  2. 检查 RTL 中是否写了 always @(posedge clk) if (enable) ... 这种手动门控,工具可能无法识别。
  3. 使用 set_clock_gating_style 指定门控风格,并开启 compile_ultra -gate_clock。

3.4 综合流程全景图

下面这张图总结了逻辑综合的完整流程,以及每个阶段的关键产出物:

逻辑综合流程全景图 逻辑综合流程全景图 RTL 代码 (.v / .vhd) 约束文件 (.sdc / .tcl) 工艺库 (.db / .lib) 综合引擎 (Design Compiler) analyze → elaborate → compile_ultra 门级网表 (.v) 约束文件 (.sdc) 延迟文件 (.sdf) 时序报告 (setup / hold) 面积报告 (cell / net) 功耗报告 (dynamic / static) 说明:综合阶段的核心产出是门级网表、约束文件和延迟文件,三者缺一不可,共同作为后端布局布线的输入。

3.5 综合脚本的调试技巧

写综合脚本就像写软件,总会遇到 bug。这里分享几个我常用的调试方法:

  • 分段执行:不要一次性跑完整个脚本。先跑库设置和读入设计,确认无误后再加约束,最后再跑编译。这样出问题时能快速定位。
  • 使用 check_timing:在编译前跑一下 check_timing,它会告诉你哪些路径没有被约束。我见过有人漏了异步时钟域的约束,结果综合出来的网表在后端根本没法用。
  • 保存中间状态:在关键步骤后使用 write -f ddc 保存设计状态。这样如果后面跑崩了,不用从头再来。
个人习惯: 我会在脚本里加一个 if { [file exists "outputs/top.ddc"] } { read_ddc outputs/top.ddc } 的判断。这样第二次跑脚本时,如果网表已经存在,就直接跳过读入和编译,只重新生成报告。省时又省力。

3.6 综合后的检查清单

综合跑完后,别急着往后端交接。先过一遍下面的检查清单:

检查项 检查方法 合格标准
时序检查 report_timing -max_paths 100 所有路径 slack ≥ 0
设计规则检查 report_constraint -all_violators 无 transition / capacitance 违例
面积检查 report_area 总面积 ≤ 预算的 110%
功耗检查 report_power 总功耗 ≤ 预算的 105%
网表完整性 check_design 无 dangling wire / unconnected pin
约束一致性 compare_sdc 输出 SDC 与输入 SDC 一致

嗯,以上就是逻辑综合实战的核心内容。脚本怎么写、报告怎么看、问题怎么修,这三块你掌握了,综合这关基本就稳了。记住,综合不是一次性的工作,它需要你反复迭代、不断调优。别怕麻烦,多跑几次,你自然就能找到感觉。