设计输入与准备:Verilog 网表解析、Liberty 库文件格式、LEF/DEF 文件介绍、设计数据准备流程

好了,咱们正式开始动手做后端。第一步不是跑脚本,而是搞清楚你手里到底有什么数据。

我见过不少新手,拿到一个设计压缩包就往工具里塞,结果跑两天报一堆错,回头一看——库文件版本不对,或者网表里有个模块根本没例化。嗯,磨刀不误砍柴工,咱们先把这几种输入文件吃透。

Verilog 网表解析:从 RTL 到门级

前端同事会给你一个 .v 文件,这叫门级网表(Gate-Level Netlist)。它不再是 always @(posedge clk) 那种写法,而是直接例化了标准单元——与门、或门、触发器、MUX 这些。

举个例子,一个简单的 D 触发器网表长这样:

module top (clk, rst_n, d, q);
  input  clk, rst_n, d;
  output q;

  wire n1;

  INV_X1 U1 (.A(d), .ZN(n1));
  DFF_X2 U2 (.D(n1), .CK(clk), .RN(rst_n), .Q(q));
endmodule

这里 INV_X1 和 DFF_X2 就是标准单元。工具需要知道它们的时序、面积、功耗信息——这些信息不在网表里,在 Liberty 库里。

我的习惯:拿到网表后,第一件事用 grep -i "module" *.v 看看顶层模块名对不对。我曾经有一次因为顶层模块名大小写不一致,浪费了半天查错。

Liberty 库文件格式:标准单元的“身份证”

Liberty 文件(.lib)是后端工具最核心的参考书。它描述了每个标准单元在各种条件下的行为。

一个 Liberty 文件的结构大致如下:

library (typical) {
  delay_model : "table_lookup";
  voltage_map (VDD, 1.08);
  voltage_map (VSS, 0.00);

  cell (INV_X1) {
    area : 0.56;
    pin (A) { direction : input; capacitance : 0.003; }
    pin (ZN) {
      direction : output;
      function : "(!A)";
      timing () {
        related_pin : "A";
        timing_sense : "negative_unate";
        cell_rise (delay_template_7x7) {
          index_1 ("0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0");
          index_2 ("0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0");
          values ( \
            "0.012, 0.015, 0.020, 0.028, 0.045, 0.080, 0.150", \
            ...
          );
        }
      }
    }
  }
}

这里有几个关键点:

  • PVT 条件:库名 typical 代表典型工艺角。实际项目中会有 slow、fast 等多个库。
  • 时序查找表:cell_rise 是一个 7x7 的二维表,横轴是输入转换时间,纵轴是输出负载电容。工具通过查表和插值得到延迟。
  • 引脚电容:每个输入引脚都有固定的电容值,这是驱动强度计算的基础。
注意:Liberty 文件里的 voltage_map 必须和你的电源设置一致。我曾经见过有人把 1.08V 的库用在 1.2V 的设计上,时序分析结果完全不准。

LEF/DEF 文件:物理信息的载体

Liberty 管时序,LEF(Library Exchange Format)管物理尺寸和布线规则。DEF(Design Exchange Format)管你的设计实例在芯片上的具体位置。

LEF 文件描述的是标准单元的“外壳”:

MACRO INV_X1
  CLASS CORE ;
  ORIGIN 0 0 ;
  SIZE 0.56 BY 1.4 ;
  SYMMETRY X Y ;
  PIN A
    DIRECTION INPUT ;
    PORT
      LAYER M1 ; RECT 0.1 0.6 0.2 0.8 ;
    END
  END A
  PIN ZN
    DIRECTION OUTPUT ;
    PORT
      LAYER M1 ; RECT 0.36 0.6 0.46 0.8 ;
    END
  END ZN
  OBS
    LAYER M1 ; RECT 0 0 0.56 1.4 ;
  END
END INV_X1

你看,LEF 告诉你这个单元占多大面积(0.56 x 1.4),引脚在哪个金属层、什么位置,以及哪些区域不能布线(OBS)。

DEF 文件则是你的设计在芯片上的“快照”:

COMPONENTS 2 ;
  - U1 INV_X1 + PLACED ( 10 20 ) N ;
  - U2 DFF_X2 + PLACED ( 30 40 ) N ;
END COMPONENTS

NETS 2 ;
  - n1 ( U1 ZN ) ( U2 D ) + ROUTED M1 ( 15 25 ) ( 25 25 ) ;
END NETS

DEF 记录了每个单元放在哪里,以及连线怎么走。布局布线工具最终输出的就是 DEF 文件。

文件类型 扩展名 主要内容 谁生成
门级网表 .v 标准单元例化、连接关系 前端综合工具
Liberty .lib 时序、功耗、面积参数 工艺厂 / IP 供应商
LEF .lef 单元物理尺寸、引脚位置、布线阻挡 工艺厂 / IP 供应商
DEF .def 设计实例位置、连线、电源网络 后端工具(布局布线后)

设计数据准备流程

在 OpenROAD 里跑一个设计,数据准备通常分这几步:

  1. 收集库文件:把工艺厂给的 .lib、.lef 放到项目目录。注意区分典型、最坏、最好三个工艺角。
  2. 检查网表完整性:用 verilator --lint 或 yosys -q 检查有没有悬空引脚、未定义模块。
  3. 确认顶层模块:在 OpenROAD 里用 read_verilog 读入网表,然后用 link_design 确认所有单元都能找到对应的库。
  4. 设置时序约束:写一个 .sdc 文件,告诉工具时钟周期、输入输出延迟。这一步很多人会忘,但 STA 全靠它。
  5. 初始化 floorplan:定义芯片尺寸、IO 位置、电源网络。OpenROAD 里用 initialize_floorplan 命令。

我的经验:数据准备阶段最容易出问题的就是库文件版本不匹配。比如 Liberty 是 2020 年的,LEF 是 2022 年的,两个文件里单元的引脚定义可能不一样。我建议每次拿到新库,先跑一个 check_library 命令做一致性检查。

下面这张图总结了整个数据流:

设计输入数据流 设计输入数据准备流程 门级网表 (.v) 前端综合输出 Liberty 库 (.lib) 时序/功耗参数 LEF 文件 (.lef) 物理尺寸/引脚 数据检查与约束设置 网表完整性检查 → 库一致性检查 → SDC 时序约束 OpenROAD 命令:read_verilog → link_design → read_sdc 初始化 Floorplan 定义芯片尺寸、IO 位置、电源网络 初始 DEF 文件 包含单元位置和电源网络信息 注意:库版本必须一致 否则工具会报错

小技巧:在 OpenROAD 里,你可以用 read_liberty -lib 同时读入多个工艺角的库。工具会自动根据分析模式选择对应的库。比如 setup 分析用 slow 库,hold 分析用 fast 库。

好了,数据准备这一步走完,你的项目目录应该长这样:

project/
├── rtl/              # 前端 RTL(可选,用于形式验证)
├── netlist/          # 门级网表 (.v)
├── lib/              # Liberty 库 (.lib)
├── lef/              # LEF 文件 (.lef)
├── sdc/              # 时序约束 (.sdc)
├── scripts/          # OpenROAD Tcl 脚本
└── output/           # 输出 DEF、GDS 等

我个人习惯把每个工艺角单独建一个子目录,比如 lib/typical、lib/slow、lib/fast。这样后续切换工艺角时,只需要改脚本里的路径变量,不容易搞混。