3、RTL设计与综合准备:Verilog/VHDL基础回顾、可综合RTL设计原则、综合库(Liberty/DB)文件解析
好,咱们进入第三章。这一章其实挺关键的,属于「地基」部分。
很多刚入行的朋友,写RTL时天马行空,觉得仿真能过就行。结果一到综合,要么面积爆炸,要么时序一团糟。我见过太多这样的案例了。
说白了,综合工具是个「老实人」。你给它什么,它就给你推什么。但前提是——你得给它「能看懂」的东西。
3.1 Verilog/VHDL基础回顾:别小看这些语法
先聊聊Verilog。我个人习惯用Verilog,但VHDL的逻辑其实一样,只是写法不同。
你想想看,综合器能识别的,其实就那么几种结构:
- 组合逻辑:assign、always @(*) 或 always @(a, b, c)
- 时序逻辑:always @(posedge clk) 或 always @(negedge clk)
- 实例化:模块间的连线
嗯,这里要注意一个坑:always @(posedge clk or negedge rst_n) 这种写法,综合器会把它映射成带异步复位的触发器。但如果你写成 always @(posedge clk) 里面再判断 rst_n,那就是同步复位了。两者面积和时序不一样。
核心原则:写RTL时,脑子里要有一张「电路图」。你写的每一行代码,最终都会变成门、触发器和连线。
举个例子,下面这段代码,综合出来是什么?
always @(posedge clk) begin
if (rst_n)
q <= 1'b0;
else if (en)
q <= d;
end
答案是:一个带同步复位的D触发器。复位信号rst_n为低时清零,使能信号en为高时采样数据d。
我曾经遇到一个同事,写了一大堆 for 循环在里面。仿真跑得飞快,一综合,面积直接炸了。为什么?因为 for 循环在综合时会被展开成并行逻辑,循环次数多了,硬件资源成倍增加。
我的建议:写RTL时,尽量用 case 或 if-else 代替 for。除非你明确知道自己在做什么(比如生成重复结构)。
3.2 可综合RTL设计原则:哪些能写,哪些不能写
这个问题其实很基础,但每次培训我都会强调。因为「仿真通过」和「综合通过」是两码事。
可综合的语法:
- assign、always、initial(仅用于testbench)
- if-else、case、casex、casez
- 逻辑运算符(&, |, ^, ~)
- 算术运算符(+, -, *, / 注意乘除法资源大)
- 移位运算符(<<, >>)
- 三目运算符(? :)
不可综合的语法:
- #delay(延时)——综合器直接忽略或报错
- initial(除了用于生成时钟和复位)
- force、release
- 系统函数($display, $monitor, $finish等)
- 动态数组、队列、关联数组
我记得有一次,一个项目里有人用了 #5 来模拟延时。仿真时波形完美,结果综合时报了上百个warning,最后出来的网表功能完全不对。排查了三天才发现是这个问题。
避坑指南:我曾经在综合脚本里忘了加 -no_initial 选项,结果综合器把initial块里的赋值当成了「默认值」,生成了奇怪的锁存器。从那以后,我写RTL时都会检查一遍:所有寄存器都要有明确的复位值。
另外,锁存器(Latch) 是综合的大忌。怎么避免?很简单:
- 组合逻辑中,所有分支都要赋值
- case语句要写default
- if-else要写全
看这个例子:
always @(*) begin
if (sel)
out = a;
// 缺少 else 分支!
end
综合器会推断出一个锁存器:当sel为0时,out保持原值。这在大多数设计中是不想要的。
3.3 综合库(Liberty/DB)文件解析:工具怎么认识你的工艺
好,RTL写好了,接下来要综合。但综合器怎么知道你的芯片是用什么工艺做的?
答案就是——综合库文件。
常见的格式有两种:
- .lib(Liberty格式):文本文件,人能看懂
- .db(Synopsys DB格式):二进制文件,工具读取更快
说白了,.lib 就是工艺厂给你的一张「菜单」。上面写着:
- 每个标准单元的名字(比如INVX1、NAND2X2、DFFQX1)
- 每个单元的输入输出引脚
- 每个单元的时序参数(上升时间、下降时间、建立时间、保持时间)
- 每个单元的功耗信息(动态功耗、漏电功耗)
- 每个单元的面积
举个例子,一个反相器(INVX1)在.lib里大概长这样:
cell (INVX1) {
area : 0.5;
pin (A) {
direction : input;
capacitance : 0.002;
}
pin (Y) {
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.01, 0.02, 0.03, 0.05, 0.08, 0.15, 0.30", \
...
);
}
}
}
}
看着有点复杂对吧?其实核心就几个参数:
- area:这个单元占多少面积
- capacitance:输入引脚的电容(影响前一级的驱动)
- timing:从输入到输出的延迟,跟输入转换时间和输出负载有关
综合器拿到这个库,就能算出:
- 用哪个单元最快?
- 用哪个单元最省面积?
- 用哪个单元功耗最低?
然后根据你的约束(比如时钟频率、面积目标),自动选择最优的单元组合。
一个小技巧:我习惯在综合前先看一眼.lib文件里的 default_max_transition 和 default_max_capacitance。这两个值决定了你的设计能跑多快。如果工艺库里的max_transition是0.5ns,那你时钟周期就不能小于这个值太多。
至于.db文件,其实就是.lib的二进制版本。工具读起来快,但人没法直接看。所以一般流程是:工艺厂给.lib,你用 read_lib 命令转成.db,然后综合工具直接读.db。
嗯,这里有个坑要注意:不同工艺角(PVT)的库要分开。比如:
- slow.lib(慢工艺角,温度高,电压低)
- fast.lib(快工艺角,温度低,电压高)
- typical.lib(典型工艺角)
综合时一般用slow.lib做setup分析,用fast.lib做hold分析。如果你只用一个库,那时序分析就不准了。
我记得有一次,一个项目只用了typical库做综合。结果流片回来,芯片在高温下跑不到目标频率。后来一查,原来是setup margin不够。从那以后,我每次综合都会指定至少两个工艺角。
小结
这一章的内容,说白了就是「写RTL之前,先想清楚综合器会怎么理解你的代码」。
Verilog语法不难,但可综合的写法需要刻意练习。综合库文件看着复杂,但核心就是面积、时序、功耗三个维度。
下一章,咱们聊聊综合约束怎么写。那才是真正决定芯片能不能跑起来的关键。