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时,尽量用 caseif-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_transitiondefault_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语法不难,但可综合的写法需要刻意练习。综合库文件看着复杂,但核心就是面积、时序、功耗三个维度。

下一章,咱们聊聊综合约束怎么写。那才是真正决定芯片能不能跑起来的关键。