4. 设计数据导入与初始化自动化

数字后端流程的起点不是布局布线,而是把一组来源不同、格式不同、语义不同的设计数据,准确无误地装载进 EDA 工具的内存模型里。网表(Verilog/VHDL)描述逻辑连接,LEF 描述物理抽象(pin、obstruction、site、layer),DEF 描述已有的物理实现(floorplan、placement、row、track),Liberty(.lib/.db)描述时序与功耗,SDC 描述时钟与约束,UPF 描述电源域与隔离。这些文件由不同团队、不同工具产出,命名习惯、单位、版本各不相同。手工在 Innovus/ICC2 里逐条敲 read_netlist、read_lef、read_def、read_liberty、read_sdc、read_upf,不仅低效,而且极易漏读、错序、错版本。本节的目标,是把这套导入动作封装成可重复执行的初始化脚本,并在导入后立即做一致性检查,让问题在流程最前端暴露,而不是等到 CTS 或 route 阶段才以时序违例、DRC 爆量的形式爆发。

导入顺序为什么不能随意

工具对数据有依赖关系。LEF 必须先于 DEF 读入,因为 DEF 中的 cell 引用需要 LEF 提供物理定义;Liberty 必须先于网表映射,否则 link_design 找不到 cell 的时序模型;SDC 必须在设计链接完成之后读入,否则 create_clock 找不到对应端口;UPF 通常要在网表和 Liberty 就绪后读入,才能把 supply net 与 power domain 正确绑定。一个稳妥的顺序是:LEF → Liberty → 网表 → link → DEF → SDC → UPF。如果设计是“网表 + floorplan DEF”的 handoff 模式,DEF 在 link 之后读入;如果是“仅网表”从零开始,则跳过 DEF,由脚本生成初始 floorplan。

下面这张图给出了一次完整初始化的数据流与检查点关系,注意每个导入动作后面都挂了一个校验节点,而不是全部读完再统一检查。

设计数据导入顺序与一致性检查点 输入数据 LEF (tech + cell) Liberty (.lib/.db) 网表 (.v) DEF (floorplan) SDC UPF 导入动作 read_lef tech + cells read_liberty / set_lib read_netlist + link_design read_def read_sdc read_upf / commit_upf 一致性检查点 LEF 层/site 与 tech 匹配 库名/工艺角/单位一致 未映射 cell / 黑盒统计 DEF 与网表实例名比对 时钟端口/周期是否命中 电源域/隔离单元覆盖 初始化报告 PASS / WARN / FAIL 未映射 cell 清单 约束覆盖率统计 任一检查点 FAIL 时脚本立即中止,避免错误数据进入后续流程 WARN 项记录到报告,由工程师确认后决定是否继续

用 Tcl 封装导入过程

Cadence Innovus 的原生控制语言是 Tcl,因此初始化脚本天然就是一段 Tcl 过程。核心思路是:把文件路径、库名、工艺角等易变信息抽成变量或配置文件,把导入动作写成过程,把检查写成独立函数。下面是一个可直接参考的骨架,参数为示例值,实际使用时替换为项目真实路径。

# init_design.tcl —— 设计初始化主流程
set NETLIST   "./data/top.v"
set LEF_FILES [list ./lef/tech.lef ./lef/stdcell.lef ./lef/macro.lef]
set LIB_FILES [list ./lib/ss_0p81v_125c.lib ./lib/ff_0p99v_m40c.lib]
set DEF_FILE  "./data/top_floorplan.def"
set SDC_FILE  "./data/top.sdc"
set UPF_FILE  "./data/top.upf"

# 1. 物理库
foreach f $LEF_FILES { read_lef $f }
# 2. 时序库
foreach f $LIB_FILES { read_liberty $f }
# 3. 网表与链接
read_netlist $NETLIST
set top_module "top"
link_design $top_module
# 4. 物理实现
if {[file exists $DEF_FILE]} { read_def $DEF_FILE }
# 5. 约束与电源意图
read_sdc $SDC_FILE
if {[file exists $UPF_FILE]} { read_upf $UPF_FILE ; commit_upf }

这段脚本本身不复杂,真正体现工程价值的是它后面的检查逻辑。导入动作只是“把数据放进工具”,检查才是“确认数据是对的”。

一致性检查:把问题挡在流程入口

第一类检查是单元映射完整性。网表里出现的每个 cell 都必须在已读入的 Liberty 和 LEF 中有定义。Innovus 提供 checkDesign -all,也可以自己遍历实例:

set unmapped [get_cells -hier -filter "is_mapped==false"]
if {[sizeof_collection $unmapped] > 0} {
    puts "ERROR: [sizeof_collection $unmapped] unmapped cells"
    foreach_in_collection c $unmapped { puts "  [get_object_name $c]" }
    exit 1
}

未映射 cell 通常来自三种原因:Liberty 库版本不对、网表里混入了综合工具特有的 cell(如 SYNTH_*)、或者宏单元 LEF 缺失。脚本应把清单打印出来并直接退出,而不是让工具带着黑盒继续跑。

第二类检查是DEF 与网表的一致性。DEF 中的实例名、端口名必须能在链接后的设计中找到对应对象。常见问题是 DEF 来自旧版本网表,实例被重命名或删除。可以用 get_cells 与 DEF 中的 COMPONENTS 段做集合比对,统计“DEF 有而设计无”和“设计有而 DEF 无”的数量。前者会导致 read_def 报 warning,后者意味着 floorplan 不完整,需要重新生成。

第三类检查是约束覆盖率。SDC 读入后,用 report_clocks 确认时钟数量与预期一致,用 get_ports 检查输入输出延迟是否覆盖了所有 I/O。一个实用的判断是:如果设计中存在时钟端口却没有对应 create_clock,或者存在 I/O 端口却没有 set_input_delay/set_output_delay,就应记为 WARN 并输出端口名。这类遗漏在时序分析阶段会表现为“unconstrained path”,越早发现越好。

第四类检查是UPF 电源域覆盖。读入并 commit 之后,用 report_power_domain 查看每个 domain 的 supply net、isolation cell 和 level shifter 是否按预期绑定。如果某个 domain 没有 isolation 策略,而它又存在跨域信号,后续插入隔离单元时就会失败。检查点应确认:每个 domain 有明确的 primary supply,跨域路径有 isolation 或 level shifter 规则,retention 寄存器(如有)已正确标记。

把检查结果落成报告

脚本不应只在终端打印,而应生成一份结构化报告,便于归档和对比。一个轻量做法是写一个 init_check.rpt,每行一条检查项,格式为 状态 | 检查项 | 数量 | 详情文件。例如:

PASS | LEF layers        | 12  | -
PASS | Liberty corners   | 2   | -
FAIL | unmapped cells    | 3   | unmapped_cells.txt
WARN | unconstrained IO  | 5   | unconstrained_ports.txt
PASS | DEF instance match| 0   | -
PASS | UPF domains       | 4   | -

这样,每次 handoff 或 nightly run 都能用同一套脚本产出可比对的报告。如果某天 unmapped cells 从 0 变成 3,diff 一下就能定位到是库更新还是网表变更引入的。

建议把文件路径、库列表、top module 名、检查阈值全部外置到 init_config.tcl,主脚本只负责流程。这样同一套脚本可以在不同项目、不同工艺角之间复用,切换配置而不改逻辑。
不要跳过 link_design 之后的检查直接读 SDC。未链接完成的设计中,端口和实例可能尚未建立,SDC 里的对象引用会静默失败,导致约束丢失却没有任何报错,直到时序报告出现大量 unconstrained path 才被发现。

完成上述导入与检查后,设计就进入了一个“已知状态”:单元已映射、物理信息已就位、约束已覆盖、电源意图已提交。后续的 floorplan 优化、placement、CTS 都建立在这个可信的起点上。初始化脚本的价值,不在于它省了多少次手工敲命令,而在于它把“数据是否正确”这件事变成了每次运行都会自动回答的问题。