综合到底做了什么
你的 RTL 是一份*行为描述*:「当这些输入一致时输出为高」「这个值在时钟沿更新」。它完全没说是哪些晶体管在干活。逻辑综合就是把这份行为描述翻译成一张具体的真实门网表的步骤——一份零件清单,外加它们之间的连线。把 RTL 想成用大白话写的菜谱(「翻拌到混匀为止」),把网表想成某位特定厨师会用的那一连串确切手部动作。
关键在于,工具是对着一个目标库来做这件事的——这是一份固定的门目录,里面的门已经有人为你的工艺节点设计好、刻画好了。综合不是在发明晶体管,而是在从目录里选购,再把买来的东西排布好。两样东西进去(你的 RTL 和一个单元库),一样东西出来(一张门级网表),还有第三样东西——你的约束——告诉工具「好」是什么意思。
标准单元库
标准单元库就是综合选购时翻的那本目录。每个单元都是一个小巧的、预先设计好的逻辑门——一个反相器、一个 NAND、一个与或门、一个 D 触发器——晶圆厂把它一直布局到晶体管层级,并做了特性刻画:对每个单元,他们都测过它翻转要花多久、占多少面积、烧多少功耗。这些单元共用一个固定的高度,于是能整整齐齐地拼成一行行,就像一堆同样凸点高度的乐高积木。
下面这点会让新手意外:同一个逻辑功能存在很多变体。一个库里可能放着十几种「驱动强度」的 NAND2——小的那个又慢但只啜一点点功耗和面积,大的那个翻转飞快但两样都吃得凶。真值表一样,肌肉不一样。综合不只挑*哪个*门,还挑*这个门的哪种尺寸*,而面积/时序/功耗的权衡,大半就活在这一个选择里。
RTL → 优化后的门级网表
我们来落到实处。这里有一小段 RTL:一个寄存器,每个时钟沿都把 `d` 装进去,外加一个组合输出。它描述的是行为——*发生了什么*——从头到尾没点名过任何一个门。
module tiny (input clk, input a, input b, input d,
output q, output y);
reg q_r;
always @(posedge clk) q_r <= d; // a register: remembers d each clock edge
assign q = q_r;
assign y = ~(a & b); // combinational: a NAND of a and b
endmodule现在综合把它映射到真实的库单元上。那个时钟驱动的 `reg` 变成一个 DFF 标准单元;`~(a & b)` 塌缩成单独一个 NAND2——正是库里已经备好的那个门,所以一个反相器都没浪费。结果是结构化的:现在每一行都点名一个*物理零件*,并接上它的引脚。
module tiny (clk, a, b, d, q, y); input clk, a, b, d; output q, y; DFF u_q (.CLK(clk), .D(d), .Q(q)); // chosen standard cell NAND2 u_y (.A(a), .B(b), .Y(y)); // chosen standard cell endmodule
约束左右最终结果
没有指引,工具根本不知道你想要这份设计*又小*还是*又快*——而它没法白白两样都要。约束就是你告诉它的方式。最重要的那一条是时钟周期:你声明「这个时钟比如说跑在 1 ns」,这一个数字就成了每个信号在两个寄存器边沿之间必须塞进去的预算。
# A constraint (timing format, not Verilog): the clock budget create_clock -name clk -period 1.0 [get_ports clk] # 1.0 ns -> 1 GHz
把这个周期收紧,工具就会*作出回应*:它会去抓更大、更快的单元,复制逻辑来缩短最长的那条路径,重组运算以省下一级门——拿面积和功耗去换速度。把周期放宽,它就反着来,换上小巧、低功耗的单元,因为它现在时间有富余。约束不只是检查结果,它还主动塑造哪些单元会被选中。工具忙完后一条路径上剩下的那点喘息空间,就是它的时序裕量——为正表示塞得下,为负表示失败了,而最紧的那条路径就是关键路径。
面积/时序/功耗的取舍
综合里的每一个选择都会同时扯动三根线:面积(硅片,也就是钱)、时序(时钟能跑多快)和功耗(发热和电池)。想象一个三角形——使劲往一个角拉,你就从另两个角滑开。想更快?那就买更大的单元:面积涨、功耗涨。想省功耗?那就接受更慢的单元和更松的时钟。没有哪种设置能三样全赢;工具的全部工作,就是找出三角形里那个仍然满足你约束的最佳点。
这正是约束如此要紧的原因:它们告诉工具*你想住在三角形的哪个位置*。手机芯片偏向功耗那个角;高端 CPU 偏向时序,代价是面积和瓦数。综合器会在你卡得最紧的那一项上下最狠的功夫——给它一个激进的时钟,它就乐呵呵地拿面积和功耗去把它达成,不管你是不是真有这个意思。
读懂一张网表
打开一张综合好的网表,乍看像天书——一页页带着古怪名字的单元实例。但它不过是那本目录的字面落地。分三遍来读,它立刻就敞开了。
- 先找寄存器。凡是单元名以 DFF(或 FF、SDFF 等等)开头的实例,都是一块状态——一个触发器。数一数它们,就知道你的设计*记住*了多少东西,而它们正是每一条时序路径的起点和终点。
- 把单元名读成功能加驱动强度。NAND2_X4 的意思是「二输入 NAND,驱动强度 4」。字母是逻辑,末尾的数字是肌肉。一条路径上扎堆出现的大数字,是工具在喊「这条路径当时很紧——我在这儿砸了大单元」。
- 跟着引脚之间的连线走,别跟着行的顺序走。网表是硬件,所以它的行全都同时存在;告诉你什么喂给什么的是 `.A(...) .Y(...)` 这些连接,而不是从上到下的顺序。从一个寄存器的输出,顺着穿过那些门,追到下一个寄存器的输入——这条链就是一条时序路径。