从逻辑设计到DV得到模拟结果,只是晶元设计过程中的一次迭代。除此之外,还有别的迭代,比如物理设计。从下面的简单例子可以看出,RTL只是代表了逻辑,或者说是描述了电路的行为,并没有明确交代它应该由什么样的逻辑门和寄存器来组成,更没有交代这些基本单元怎么摆放在晶元上(place)以及怎么用金属线把它们连接起来(route)。这就像码工同学写了一个C++程序,要真正运行起来,需要编译成CPU可以执行的指令。与软体的编译过程比起来,从RTL到物理电路这个编译(即物理设计,也叫后端设计)要漫长许多,需要很多人工操作(也许其中一些工作今后会被ML取代)。

module select_ab (
input clock, // Clock driving the registers.
input reset, // Start registers with known values.
input [1:0] a, // Its not an array, but 2 bits.
input [1:0] b, // Ditto.
input select, // Select between a and b.
output reg out // The only output signal of the module.
);
always @(posedge clock or posedge reset) begin
if (reset == 1b1) begin
out <= 1b0; // Initialize out to 0.
end else begin
out <= select ? a[0] & a[1] : b[0] & b[1]; // You know it :)
end
end
endmodule

物理设计拿到RTL之后,先跑一个逻辑综合,生成一个门电路网单,从而可以知道RTL所描述的逻辑具体应该映射到什么样的逻辑门和寄存器,以及它们的大小。这个步骤类似于把foo.cpp编译成汇编语言的foo.s。经过综合,上面那段简单的RTL可能变成这样:

接下来是跑place&route,把网单里的逻辑门和寄存器摆放好,再连接好,就生成了版图。这一步说起来简单,实则非常复杂。一个电路模块里常常是成千上万个逻辑门,每一个逻辑门的摆放位置都是一个二维变数。连接逻辑门到逻辑门的金属线更是在逻辑门上空的三维空间里穿梭,纵横交错,像是无数座立交桥,并且这些立交桥的层数可以达到十几层(实在是比较难用一般的软体来画,因此本篇题图换了个风格,同学们领会一下精神即可??)。所以place&route实际上是在一个维度极高的搜索空间里面做优化,耗时远远长于把my_program.s编译成my_program.o。

以上这两步有比较成熟的EDA软体来做。如果对晶元功耗、性能、面积(power, performance, area, 简称PPA)要求不高,通常可以用EDA软体来完成绝大部分工作。但问题就在于那个「如果」,同学们有听说哪个厂的CEO开发布会说他们设计了一款性能低且功耗高的晶元?为了实现比较高的PPA指标,物理设计攻城狮要耗费不少心血来帮助软体收敛到一个令人满意的结果。

以性能为例,与它挂钩的一个重要指标是时钟频率。时钟频率取决于整个晶元上最慢的那一条timing path上信号的时延(因此,攻城狮们常常在祈祷自己负责的那个模块里不要出现本届晶元项目中排名垫底的timing path)。一条timing path可以简单地理解成信号从一个寄存器的输出端出发,经过若干逻辑门和导线,最后到达另一个寄存的输入端所走的路径,信号必须在一个时钟周期内跑完这条路径。在上面的例子里,a[1:0]、b[1:0]和select通常源自上一级电路的寄存器输出。从a[0]经过三级逻辑门到达寄存器输入端d,是一条路径。从select经过一个逻辑门到达d,是另一条路径。当然,在真正的数字晶元里,绝大多数timing paths都比上面的例子复杂,一条路径上会有十几个到几十个逻辑门。从寄存器到寄存器之间可以是多对多的关系,再加上一个数字晶元里寄存器众多,因而timing paths不计其数。我见到过一块晶元上的一小块电路,跑完一次timing analysis(有专门的EDA软体来做,一次也需要几个小时)之后发现,有几十万条路径同时fail。咱们可以想像一下,攻城狮在读完那个timing报告(一条路径的报告差不多占一页A4纸)之后的心理阴影面积有多大。几个攻城狮花了几个月时间就修好了晶元上的这么一小块。

手工修timing paths的方法有很多。还是拿上面那个例子来说,如果从a[1:0]到d的时延太大,可以考虑把电路变成:

这样,a[1:0]到d只经过两级逻辑门。但是这是以牺牲select所在路径上的时延为代价,所以还得确认select能比a[1:0]更早到达(某些情况下,确实是这样,像a[n-1:0]这样的汇流排,当n太大的时候比较难做,光是route就很头疼)。另一方面,前面那种综合的结果比较适合timing path在空间跨度大的情况,因为它中间有一个驱动力很强的反相器,有利于驱动比较长的金属连线。虽然攻城狮可以根据具体情况来选择具体的物理设计方案,EDA软体有时候却做不到这一点,或者说,它有时候生成了你想要的结果,但是有时候其它地方稍微有点变化之后它的结果又变了。还有一些EDA演算法,为了能够跳出优化问题的局部最优点,引入了随机数,这反而让最后结果变得更加不可预测。为了解决这个问题,攻城狮可以把希望得到的编译结果hard-code到RTL里,再明确告诉软体不要优化这一段代码,比如像这样:

...
// Connections between gates.
wire sel_a;
wire sel_b;
wire d;
// nand3_gate is the type of gate that we want to use, which is like a C++ class.
// stage1_0 is one gate of that type, which is like a C++ object.
nand3_gate stage1_0(
.in0(a[0]),
.in1(a[1]),
.in2(select),
.out(sel_a)
);
nand3_gate stage1_1(
.in0(b[0]),
.in1(b[1]),
.in2(~select),
.out(sel_b)
);
nand2_gate stage2(
.in0(sel_a),
.in1(sel_b),
.out(d)
);
always @(posedge clock or posedge reset) begin
if (reset == 1b1) begin
out <= 1b0;
end else begin
out <= d;
end
end
endmodule

看到上面的RTL,码工同学有没有想起曾经在C代码里看到用__asm__插入的汇编代码?

从上面这个例子还可以看到,修timing经常遇到的一个问题是,修好了这里(a[1:0]),那里(select)的问题又出来了(做ML的同学是不是感觉又找到软硬体的共同点了???)。place&route也是一样。当你写好floorplan,把两个逻辑门摆得更近一些的时候,发现别的路径又太快了(关于timing,严格地说,既不能太慢也不能太快,但是咱们先不讨论那么烧脑的问题)。或者当你为了解决立交桥第七层的拥堵,写好限制条件,把一排汇流排从第七层移到第九层的时候,发现别的信号在第八层得绕道走(七层到九层的连接需要占用八层的一部分资源)。

物理设计还需要考虑别的很多问题。比如功耗太大了,是不是要加一个clock_enable?它的作用相当于在一个模块不需要工作的时候,对它喊一声「时间停止」,然后它里面的寄存器就停止反转。再比如时钟网路的布局能不能均衡地带动所有寄存器,晶元不管是在南极还是在撒哈拉沙漠都要能正常工作,如何抵御杂讯,如何保证晶元用十年也不能有一根金属连线被烧坏,如何当电压突然降低百分之十或者提高百分之十也要扛得住,等等。当种种问题多到一定程度,物理设计攻城狮会去找逻辑设计攻城狮喝茶。喝完茶,逻辑设计改一些代码,然后物理设计把以上工作进行到下一次迭代,同时DV也被迭代一次。

(未完待续)

作者声明:

  1. 题图引用于网路,版权归原作者所有。
  2. 本文为作者个人兴趣之作,仅代表作者个人观点,与任何公司或机构无关。
  3. 欢迎讨论。如需转载,请务必取得作者同意,并注明出处。

推荐阅读:

相关文章