3. 设计输入:理解 Verilog 网表、SDC 约束、Liberty 库文件
做后端实现,说白了就是三样东西喂给工具:网表、约束、库。缺一个,工具就罢工。我见过不少新手上来就跑 flow,结果跑出来的 timing 一塌糊涂,回头一看——约束没写对,或者 Liberty 库选错了 PVT 条件。嗯,这章咱们就把这三样东西掰开揉碎讲清楚。
3.1 一个简单的计数器设计
先准备一个 8 位计数器,带同步复位和使能。代码很简单,但足够说明问题。
// counter.v
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'b0;
else if (en)
count <= count + 1'b1;
end
endmodule
我个人习惯,写 RTL 时就想着后端。比如复位用异步低有效,这样综合时工具能自动映射到带复位端的 flip-flop,省掉额外的复位树。你想想看,要是写成同步复位,综合出来要多一堆逻辑门,面积和功耗都不划算。
3.2 Verilog 网表:从 RTL 到门级
综合之后,RTL 变成了门级网表。网表里全是标准单元——与门、或门、触发器、MUX……不再有 always 块了。
网表长什么样?
说白了就是一堆 module 实例化,把标准单元连起来。工具读的就是这个。
// counter_synth.v (综合后网表示例)
module counter_synth (
input wire clk,
input wire rst_n,
input wire en,
output wire [7:0] count
);
wire [7:0] count_int;
wire en_sync;
// 一个 D 触发器实例
DFF_X1 count_reg_0_ (
.D (count_int[0]),
.CK (clk),
.Q (count[0]),
.QN ()
);
// ... 其余 7 位类似
endmodule
我在项目中遇到过一件事:综合出来的网表里,有些信号名字被工具改了,比如 _reg_0_ 这种后缀。如果你在 SDC 里用 RTL 里的信号名去约束,工具会报错。所以,约束一定要对着网表写,或者用 get_pins 配合通配符。
避坑指南
我曾经因为网表里一个信号被优化掉了,导致 STA 报了一堆 violation。后来养成习惯:综合完先看一眼网表,确认关键路径上的信号还在不在。
3.3 SDC 约束:告诉工具你的目标
SDC 是 Synopsys Design Constraints 的缩写。工具不知道你的芯片要跑多快,你得告诉它。
最基本的约束就这几条:
| 约束类型 | 命令示例 | 说明 |
|---|---|---|
| 时钟定义 | create_clock -period 10 [get_ports clk] |
周期 10ns,对应 100MHz |
| 输入延迟 | set_input_delay -clock clk 2 [get_ports en] |
数据在时钟沿后 2ns 到达 |
| 输出延迟 | set_output_delay -clock clk 3 [get_ports count] |
数据在时钟沿前 3ns 必须稳定 |
| 伪路径 | set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b] |
跨时钟域路径不做时序检查 |
对于我们的计数器,SDC 文件大概这样:
# counter.sdc
create_clock -name clk -period 10 [get_ports clk]
set_clock_uncertainty -setup 0.2 [get_clocks clk]
set_clock_transition -rise 0.1 [get_clocks clk]
set_clock_transition -fall 0.1 [get_clocks clk]
set_input_delay -clock clk -max 2 [get_ports en]
set_input_delay -clock clk -min 0.5 [get_ports en]
set_output_delay -clock clk -max 3 [get_ports count]
set_output_delay -clock clk -min 1 [get_ports count]
我的习惯
我会在 SDC 里把 set_clock_uncertainty 设得比实际大一点,比如多 0.1ns。这样留点余量,流片回来万一频率差点,还能救。但别太大,否则面积会炸。
3.4 Liberty 库文件:标准单元的“身份证”
Liberty 库(.lib)描述了每个标准单元的时序、功耗、面积信息。工具靠它来计算延迟。
一个典型的 Liberty 条目长这样:
cell (DFF_X1) {
area : 5.6;
pin (CK) {
direction : input;
capacitance : 0.002;
}
pin (Q) {
direction : output;
timing () {
related_pin : "CK";
timing_type : rising_edge;
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.05, 0.06, 0.08, 0.12, 0.18, 0.30, 0.55", \
"0.06, 0.07, 0.09, 0.13, 0.19, 0.31, 0.56", \
...
);
}
}
}
}
你看,cell_rise 是一个 7x7 的查找表,横轴是输入转换时间,纵轴是输出负载电容。工具根据实际值查表、插值,算出延迟。
关键点
- 每个 PVT 条件(工艺角、电压、温度)对应一个 .lib 文件
- 典型组合:WC(最差情况,慢速)、BC(最好情况,快速)、TC(典型情况)
- OpenROAD 里用
read_liberty读入
我刚开始做后端时,总觉得 .lib 是工艺厂给的,不用管。直到有一次,我用了 WC 的库做 setup 分析,结果 timing 过了,但流片回来芯片在低温下跑不动。为什么?因为低温下管子变快,hold 反而出问题了。后来我才明白:setup 看最慢的库,hold 看最快的库。
避坑指南
我曾经在项目中同时用了两个不同 PVT 的 .lib,结果工具报了一堆不匹配的 warning。后来发现是 .lib 里 nom_process、nom_voltage、nom_temperature 字段不一致。所以,多个 .lib 一定要来自同一个工艺版本。
3.5 三者的关系:一张图说清楚
下面这张图展示了网表、SDC、Liberty 库如何协同工作:
你看,网表、SDC、Liberty 库三者缺一不可。网表告诉工具“你有哪些单元”,SDC 告诉工具“你要达到什么目标”,Liberty 库告诉工具“每个单元有多快”。工具把这三样东西揉在一起,才能算出 timing、面积、功耗。
3.6 准备我们的计数器设计
好了,理论讲完了,咱们动手准备一下。对于 OpenROAD,你需要三个文件:
- counter.v — 综合后的门级网表(或者 RTL,让 OpenROAD 自己综合)
- counter.sdc — 时序约束文件
- Nangate45.lib — 工艺库文件(OpenROAD 自带示例库)
小提示
OpenROAD 的 flow 脚本里,默认会从 designs/src 读 RTL,然后调用 yosys 综合。你也可以自己提前综合好,把网表放到 designs/<design_name>/results 下,然后在 config.mk 里设置 VERILOG_NETLIST 指向它。
我个人习惯用 OpenROAD 自带的综合流程,省事。但如果你有特殊的综合选项(比如 retiming、资源 sharing),那就自己跑 yosys,再喂给 OpenROAD。