3. 设计输入与准备:Verilog 网表准备、Liberty/LEF/DEF 文件解析、SDC 时序约束基础
各位同学,欢迎来到第三章。前两章我们把 OpenROAD 的环境搭好了,也跑通了第一个小例子。从这一章开始,我们要真正面对后端设计的“原材料”了。
后端设计不是从零画版图,而是把前端设计好的逻辑,变成物理实现。所以,你得先搞清楚:输入给我们的都是些什么文件?它们各自有什么用?
我个人习惯把后端输入文件分成三类:逻辑描述、物理库、约束。这三类文件缺一不可,而且任何一个出了问题,后面跑出来的结果都是错的。我曾经在一个项目里,因为 LEF 文件版本不对,导致 APR 工具报了一堆奇怪的 DRC 错误,排查了整整两天……嗯,从那以后我每次拿到新库都会先做一遍“文件体检”。
核心输入文件一览
- Verilog 网表 (.v):逻辑连接关系,即门级网表
- Liberty 库 (.lib):时序与功耗模型
- LEF 文件 (.lef):物理抽象视图(单元尺寸、Pin 位置、布线障碍)
- DEF 文件 (.def):设计交换格式(芯片尺寸、I/O 位置、初始布局)
- SDC 文件 (.sdc):时序约束(时钟定义、输入/输出延迟、例外路径)
3.1 Verilog 网表准备
前端综合完成后,会输出一个门级网表。这个网表里只有标准单元、宏单元和它们之间的连线。说白了,就是一张“零件清单 + 接线图”。
在 OpenROAD 里,我们通常用 read_verilog 命令读入网表。但这里有个坑:网表必须是展平的(flattened),不能有层次化模块嵌套。为什么?因为后端工具需要看到所有单元的路径,层次化网表会让工具在查找单元时变得非常慢,甚至报错。
我建议你在综合阶段就加上 -flatten 选项。如果拿到的已经是层次化网表,可以用以下命令展平:
# 在 OpenROAD 中展平网表
read_verilog -flatten inputs/top_hier.v
# 或者用 Yosys 展平
yosys -p "read_verilog top_hier.v; flatten; write_verilog top_flat.v"
小技巧:读入网表后,用 check_design 命令检查一下。如果工具报了“unresolved reference”之类的错误,多半是网表里引用了不存在的单元,或者库文件没加载全。
3.2 Liberty 文件解析
Liberty 文件(.lib)是标准单元的“时序身份证”。它描述了每个单元在不同输入转换时间(slew)和输出负载(load)下的延迟、功耗、建立/保持时间等参数。
你想想看,工具怎么知道一个 NAND2 门在驱动 50fF 负载时延迟是多少?就是从 .lib 里查表查出来的。.lib 里存的是二维查找表(NLDM),或者更高级的 CCS(Composite Current Source)模型。
在 OpenROAD 中,我们用 read_liberty 命令加载 .lib 文件:
read_liberty -max /path/to/slow.lib
read_liberty -min /path/to/fast.lib
这里要注意:通常需要加载两个库——一个最差工艺角(slow/slow,用于 setup 分析),一个最好工艺角(fast/fast,用于 hold 分析)。
| 工艺角 | 用途 | 典型 PVT 条件 |
|---|---|---|
| Slow/Slow (SS) | Setup 时序检查 | 0.72V, 125°C, 慢工艺 |
| Fast/Fast (FF) | Hold 时序检查 | 0.88V, -40°C, 快工艺 |
| Typical (TT) | 功耗估算 | 0.80V, 25°C, 典型工艺 |
避坑指南:我曾经遇到过 .lib 文件里某些单元的 cell_footprint 属性与 LEF 中的 MACRO 名称不匹配,导致工具在时序分析时找不到对应的物理单元。解决办法是:用 report_units 命令检查库中所有单元,确保 .lib 和 .lef 的单元名称完全一致。
3.3 LEF 文件解析
LEF(Library Exchange Format)是物理库的“抽象视图”。它不包含单元内部的晶体管细节,只告诉你:这个单元占多大面积?Pin 在哪个位置?哪些层可以走线?
LEF 文件分为两种:
- 技术 LEF:定义工艺信息,如金属层数、最小线宽、间距规则、通孔规则等
- 单元 LEF:定义每个标准单元和宏单元的物理尺寸、Pin 位置、布线障碍(OBS)
在 OpenROAD 中加载 LEF:
read_lef /path/to/tech.lef
read_lef /path/to/cells.lef
这里有个顺序问题:必须先加载技术 LEF,再加载单元 LEF。因为单元 LEF 里引用了技术 LEF 中定义的层和规则。如果顺序反了,工具会报“unknown layer”错误。
我个人习惯在加载完 LEF 后,用 report_objects 命令快速检查一下:
report_objects -type macro
# 查看所有宏单元的名称和尺寸
3.4 DEF 文件解析
DEF(Design Exchange Format)描述的是当前设计的物理信息。它不像 LEF 那样描述库单元,而是描述:芯片的尺寸是多少?I/O Pad 放在哪里?有哪些已经预放置的宏单元?
在项目初期,DEF 文件通常只包含芯片边界和 I/O 位置。随着流程推进,DEF 会逐步增加标准单元位置、布线信息等内容。
加载 DEF 的命令很简单:
read_def /path/to/initial.def
但要注意:DEF 文件中的坐标单位。OpenROAD 默认使用 1000 单位 = 1 微米(即 1nm 精度)。如果你的 DEF 文件使用其他单位(如 100 单位 = 1 微米),需要用 set_def_units 命令调整。
小技巧:用 report_design_area 命令可以快速查看芯片利用率。如果利用率超过 80%,后面布线可能会很困难。我一般建议在 60%-70% 之间留出足够的布线资源。
3.5 SDC 时序约束基础
SDC(Synopsys Design Constraints)是时序约束的标准格式。说白了,就是告诉工具:时钟频率是多少?输入信号什么时候到?输出信号什么时候要?哪些路径不需要检查?
一个最基本的 SDC 文件至少包含以下内容:
# 创建时钟
create_clock -name clk -period 10.0 [get_ports clk]
# 设置输入延迟
set_input_delay -clock clk -max 2.0 [get_ports data_in]
set_input_delay -clock clk -min 0.5 [get_ports data_in]
# 设置输出延迟
set_output_delay -clock clk -max 3.0 [get_ports data_out]
set_output_delay -clock clk -min 1.0 [get_ports data_out]
# 设置伪路径(异步时钟域)
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
# 设置多周期路径
set_multicycle_path -setup 2 -from [get_pins reg_a/Q] -to [get_pins reg_b/D]
这里我重点讲几个容易出错的地方:
- 时钟定义:
period的单位是纳秒(ns)。10.0 表示 100MHz。注意不要写成 10(不带小数点),虽然 OpenROAD 能识别,但有些工具会报 warning。 - 输入/输出延迟:
set_input_delay和set_output_delay的值是相对于时钟边沿的。正值表示信号在时钟边沿之后到达/需要,负值表示在时钟边沿之前。 - 伪路径:跨时钟域的路径如果不做同步处理,必须设为 false path。否则工具会花大量时间尝试优化这些实际上不需要满足的路径。
避坑指南:我曾经接手过一个项目,前端的 SDC 里把 set_false_path 写成了 set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b],但漏掉了 -from 和 -to 之间的双向关系。结果工具只屏蔽了从 clk_a 到 clk_b 的路径,反向路径还在做时序分析,导致 hold 违规一直修不掉。正确的做法是加上 -setup 和 -hold 选项,或者写两条 false path。
3.6 知识体系总览
为了让大家更直观地理解这些输入文件之间的关系,我画了一张图:
3.7 加载顺序与检查清单
在实际项目中,文件的加载顺序是有讲究的。我总结了一个标准流程:
- 先加载 LEF(技术 LEF → 单元 LEF)
- 再加载 Liberty(慢库 → 快库)
- 然后读入 Verilog 网表
- 接着读入 DEF(如果有初始布局)
- 最后加载 SDC 约束
为什么是这个顺序?因为后面的文件可能引用前面的内容。比如 Verilog 网表里引用了标准单元,而这些单元必须在 Liberty 和 LEF 中已经定义好了。
每次加载完一个文件,我建议都做一次快速检查:
| 步骤 | 命令 | 检查内容 |
|---|---|---|
| 加载 LEF 后 | report_objects -type macro |
确认所有宏单元已加载,尺寸正确 |
| 加载 Liberty 后 | report_units |
确认库中单元数量,检查是否有 missing cell |
| 加载 Verilog 后 | check_design |
确认网表无悬空引脚、无未解析引用 |
| 加载 DEF 后 | report_design_area |
确认芯片尺寸、I/O 位置、利用率 |
| 加载 SDC 后 | report_checks -unconstrained |
确认所有路径都有约束,无未约束路径 |
个人经验:我习惯把加载和检查命令写成一个 Tcl 脚本,每次新建项目时直接 source 这个脚本。这样既保证流程一致,又不会漏掉检查步骤。脚本里可以加上 catch 语句,一旦某个步骤出错就停止执行,避免带着错误跑后面的流程。