阅读此系列文章需要有数字电路基础和Verilog基础,如果完全没有请先自行学习
本篇文章给大家继 续讲解逻辑综合向关的内容 。上一篇文章我们只讲了编译和Library相关的东西。并没有深入到综合本身。这一篇文章我们将一起学习逻辑综合的细节。
课程大纲如下所示:
在上一篇文章我们已经讲了,如何将我们的设计放入综合器,以及如何将标准的Cell Library和IP放入综合器。这就相当于厨师做菜,原材料都已经准备好了。该如何做菜呢?我们来深入探讨一下。
1、Boolean Minimization
在该准备的东西都准备好以后。我们需要进行Elaboration和Binding。所谓的Elaboration实际上就是将RTL转换为布尔数据结构。其包括解析HDL代码、识别模块、解析模块的输入和输出信号,以及创建相应的布尔表达式来表示模块的行为。
在绑定(binding)阶段,综合工具将非布尔模块(如算术运算、寄存器、触发器等)绑定到库中的具体单元(leaf cells)。这些单元通常是标准单元库中的基本逻辑门和存储单元。绑定阶段涉及将HDL代码中的信号连接到库中的单元,确保电路的各个部分正确地连接在一起。
在逻辑综合过程中,所谓的非布尔模块(non-Boolean modules)是指那些不能被完全表示为布尔表达式的模块。这些模块的行为超出了简单的布尔逻辑,需要更复杂的处理和综合技术来正确地映射到实际的硬件资源上。
听上去可能稍微优点抽象,不过大家好好理解一下应该能清楚大概,我们接着往下看。
比如下图,就是一个实际的Elaboration展示。其将一个状态机(这里画出来方便大家理解,实际上应该是RTL代码),转换为了布尔数据结构。(这里对应图里面的门电路),其还包括了推理寄存器的过程。
这里我们对Elaboration稍微做一个总结:在Elaboration阶段,综合工具通常会进行以下操作:
解析HDL代码:识别模块、信号、端口、参数等。
创建布尔表达式:对于组合逻辑部分,创建布尔表达式来表示模块的行为。
识别和推断寄存器:对于时序逻辑部分,综合工具会识别触发器(flip-flops)和寄存器(registers),并根据HDL代码中的描述推断它们的时序特性,如时钟信号、复位信号、时钟沿等。
创建数据结构:将上述信息组织成一个布尔数据结构,如布尔表达式树(BDD)、布尔表达式图(BDDG)或网表(netlist)。
由此我们可以看到,Verilog代码风格是很重要的。按照我Verilog那篇文章的介绍。合理规划代码风格,这样虽然不说有多好,但是能对生成的电路有个大概估计。那些些一堆if/else嵌套的,生成的电路是什么样,就很难把握了。
我们再来看一下什么是两级逻辑,两级逻辑,那自然肯定是两级。比如下图这些例子,都是非常典型的两级逻辑。一堆东西与起来再或。这就是所谓的SOP,即Sum of Product。与之对应的还有POS。很多研究都是针对两级逻辑进行优化,因此其是我们需要掌握的重点。
我们来看一下优化思路,最典型的就是卡诺图。这里我认为大家都学过数电,我就不介绍什么是卡诺图了。大家以前学数电的时候,应该都在草稿纸上画过卡诺图,然后看怎么可以优化表达式。但是这实际上不是那么好用的。为什么?
首先,卡诺图的自动化处理非常困难,因为问题属于NP完全(NP-complete,NPC)类别,这意味着没有已知的多项式时间算法可以解决所有问题实例。此外格子的数量是指数级的,因此手动解决变得非常复杂。大家之前做题的时候,碰到最多最多也就四个变量吧?
作为替代方案,Quine-McCluskey方法提供了一种计算上更简单的方法来简化布尔表达式。这种方法基于二进制数表示,通过合并具有相同因子的项来减少表达式的复杂性。伯克利的学生在此基础上提出了Espresso算法。
我们来看一下这个Espresso算法是干嘛的。首先这个算法输入的是一个Sum Of Product
的表达式。其需要执行以下几步,分别是:
Expand步骤:用来增加每个项的大小,以便覆盖更多的真值组合。该操作会增加变量的数量;
Irredundant步骤:这一步涉及去除冗余的项。如果一个项可以被另一个更大的项完全覆盖,那么它就可以被删除,因为较大的项已经包含了较小的项的信息;
Reduce步骤:在减少步骤中,对覆盖集合中的项进行缩减;
通常情况下,新的布尔表达式会比之前的布尔表达式更小。当然,大家听到这里可能完全没听懂在干什么,我们接着往下看。
我们看一下这个算法,它输入的是一个SoP
表达式,输入以后第一步进行Reduce操作。在REDUCE操作中,需要尝试通过减少每个项中的变量数量来简化表达式。比如下图,有两项从三个变量变成了两个变量。这一步相信大家都能看懂,我们接着往下看。
进行完Reduce操作以后,执行Expand操作。Expand操作实际上和Reduce相反。很明显其把那个框框展开,但是展开以后的样子和最开始的样子不一样。为什么我们要这么做呢?看上去是做了无用功。我们接着往下看。
在执行完Expand操作以后,我们需要执行Irredudant操作,经过这个操作我们从四个框框变成了三个框框。为什么?因为有一个框框,一半在某一个框,另一半在另一个框。这个框框就可以省掉了。一开始我们并没有这样的框框。通过上面的操作,我们得到了这样的框框。成功将f表达式从8个变量变成了6个变量。
这个例子非常简单,实际的算法会很复杂。感性的可以看看下面这个开源项目,当然我没有去研究这些。对于芯片设计人员而言,了解大概流程就足够了。如果你想做EDA,可以好好研究这一块。
https://github.com/classabbyamp/espresso-logic
我们再来看一下多级逻辑。前面我们讨论的都是两级逻辑,但是一般电路表示形式则是更为灵活的多级形式(Multi-Level)。对于这些Multi-Level的直接优化课程并没有介绍。但你需要逻辑表达式,多级逻辑是更为常见的。
我们来看个实际的例子,下图优化多级逻辑。从17个基本数据优化到了13个数据量。
我们再来看一个新的概念,二元决策图,BDD。其本质上基于DAG(Directed Acyclic Graph,有向无环图)的,它们将布尔函数的真值表表示为一种树形结构。
我们看下图,BDD的每一个节点代表一个布尔变量,比如x1 x2 x3。树的根节点是布尔表达式的最高阶变量。树的分支表示变量有两个可能值(真和假)。我们直接找那些叶子节点是1的点,比如最左边这条路径。可以看到x1取0,x2取0,x3也取0。因此就对应了~x1x2x3。剩下的相信大家自己可以分析,最终就得到了f表达式。
我们看一下BDD这个概念是怎么来的。其源于香农展开式。香农大家都知道吧,数字电路的祖师爷。什么是想弄展开呢?给定一个布尔表达式。我们可以将其分为两部分。一部分是某一个变量为1,另外一部分为该变量为0。我们都知道~a+a=1
。所以这样做不改变原来表达式的值。
基于此思想扩展出了BDD框图。
BDD有什么用呢?前面提到了其基于有向无环图,所以很多图论的东西可以用在上面,对其进行优化。从而优化最终的表达式。就比如下面这个例子。
我们的叶子节点实际的值其实就两个,1和0。压根不用8个,因此我们可以将左边的BDD简化成右边。
上面那种优化方式大家都能看懂,我们看一个复杂点的。Merge同构的节点。什么是同构的节点呢?这就问得好了。当两个节点,其往左和往右的结果都一样的时候。我们就认为其是同构的节点。比如下图,中间两个x3就可以合并。
我们理解一下为什么,BDD本质上是从根节点到叶子结点。这就有点像从一个目的地到另外一个目的地。在最开始的BDD中,我们认为有8条独立的路线,其是完全独立的。但后面我们发现,这八个目的地实际上是两个,即1和0,这是第一步可以优化的。
紧接着我们又发现,有两条路线虽然一开始的路线不一样,但它们交汇到了同一点,我们一开始默认这是两个点,但其实可以将其简化成一个点,这就是同构节点。
还可以优化吗?当然可以。比如某一个点,你向左走到达另外一个点。这个另外一个点只能通往目的地1。所以你可以认为这个点向左走,只能通往目的地1。我们就可以把中间那个节点省略掉。相信大家也可以理解。
我们来看看BDD有什么好处:
检查一个布尔表达式是否为恒为1是一个简单的操作。如果一个BDD中只有一个节点,那么这个BDD表示恒为1。
其取反非常方便,给定一个函数 f
的BDD,可以通过交换终端节点(根节点)来得到 f'
的BDD。这是因为BDD的结构反映了布尔函数的真值表,通过交换终端节点可以反映函数的取反。
如果两个函数f
和g
的BDD在相同的变量序下相同,那么这两个函数是等价的。这种等价性检查(Equivalence Checking)可以用于验证两个表达式是否等价,可以利用基于动态规划的ITE算法可以在多项式时间内比较他们是否同构。其往往用于形式化验证(Formal Verification)中。
2、Constraint Definition
在综合的一开始,我们已经提到了。综合是在满足约束的条件下进行优化,这个约束条件就非常重要。现在我们只要了解有这么一回事,其细节我们后面的文章再讲。
3、Technology Mapping
我们来看一下Technology Mapping即工艺映射操作。在前面的流程中我们已经有了优化过的门级电路。我们现在要将这些门级电路映射到具体的工艺上。
为什么已经有了门级电路,涉及到具体的工艺以后,会有不一样呢?我举个非常简单的例子,之前不考虑具体工艺,我可以用一个6输入的与门,实现F=abcdef
功能。但现在拿工艺库过来以后,发现没有6输入与门,最多只有四输入,怎么办?那就只能针对具体的工艺器件,进行相应的映射了。此时就把开始的逻辑网表,转换为了实际的“门”级网表。
这里我们看一个实际的例子,下图分别是模板,工艺映射之前的网表,工艺映射之后的网表。这样映射的目标通常是优化PPA指标(如总面积、路径延迟),实现这一目标通常是把问题转化为有向无环图覆盖问题。对于复杂的设计网表,这个问题同样是NP-hard。不过如果模板和设计网表都是树,则该问题可以通过动态规划(主要是记忆化搜索的形式)高效解决,所以一种解决方案就是把图动态划分成森林、同时结合基于树的算法,来解决相对复杂图的映射。
我们接着往下看。上述基于树的算法,可以简单分成三步。分别是:
将网表和工艺库映射称为简单的门
将网表和工艺库中的门映射到仅包含NAND2(两个输入的NAND门)和NOT(非门)的简单门集合;
工艺库中的门通常以这些基本门为基础,通过布尔表达式和逻辑运算来构建,并为其分配一个成本,成本可以是面积、延迟、功耗等参数,用于评估和选择最佳门;
树化输入网表
将网表转换为一棵树,其中每个节点代表一个逻辑表达式,每个边代表一个逻辑门的输出连接到另一个逻辑门的输入;
在所有扇出大于2的位置分割树;
最小化生成树匹配
对于树中的每个节点,递归地查找该节点的最小成本目标模式;
我们一步一步来看一下上述的内容,到底在讲什么。首先是简单的门映射。假设我们有一个比较复杂的表达式,如下面的F
。我们可以使用德摩根定律,将其转换为与非门和非门的集合。如下图所示。
然后我们准备好相应的标准工艺库。当涉及到将逻辑网络映射到工艺库中的门时,需要定义与工艺库中的门具有相同NAND2/NOT集合的门。这里的“NAND2/NOT集合”指的是工艺库中的门可以通过组合NAND2(两个输入的NAND门)和NOT(非门)来表示。
这样我们原材料准备好了。第二步是进行树化输入网表。我们需要把左边这个网表拆散,使得任意的节点的fanout不能够超过2。这是因为树覆盖算法只能应用于树形结构,而扇出大于2的节点意味着存在循环或环路。通过分割,我们可以将逻辑网络转换为树形结构,其中每个节点的扇出最多为2。这样才可以用树的方式去处理它。我们基于此得到了三个树。
然后我们就可以对树进行优化。我们采用一个递归算法来最小化开销。从图的output开始,对于每一个节点,找到所有和它匹配的目标模式。然后想办法让cost最小。
我们做一个简化的表达方式,将输入画成box,与非门画成full circle。非门画成empty circle。
我们看这个实际的例子,我们从底向上,便可以得到对应的最小cost。当然这个例子是超级简化版的。想要理解透彻,得去学习一下相应的图论算法才行。
4、Verilog for Synthesis
看了不少EDA算法的东西,我们来回顾一下Verilog。这一小节会教大家一些比较好的Verilog写法。
我们已经大概明白综合是怎么样的一个流程了。我们现在看一个42编码器的例子。其将一个独热码转换为2bit的数据。我们很有可能这么写Verilog。如下所示,也就是用if-else
语句。这样好吗?这种方式叫做优先逻辑,也就是某些编码方式的优先级会较高,电路会先判断这个条件。这也会导致电路会是一个串行的结构,如下图右所示。
如果大家想写这样的逻辑。建议大家写成case语句。这样会让电路成为并行架构。其延迟自然而然就变低了。大家一定要好好思考,该方式和上述的方式,区别到底在哪里。
对于上述的第一种写法。如果我们传的的数据不是独热码,则输出会传播X态。但是如果我们能够保证输入是独热编码的话。我们其实可以改变我们的写法,如下图所示。其整体逻辑自然简单不少,但其仍然是串行的逻辑。实际上这种电路叫做优先译码器(最低bit获得了高优先级)。该电路常常用在固定优先级仲裁器设计上。
我们再来看一下一些运算符,会综合成为什么样的电路。
逻辑运算符:会综合成为门级网表原语,比如与门,非门,或门,这个非常好理解吧。
算术运算符:会综合成为加法器,减法器,乘法器等。注意使用乘法要慎重,不要使用除法和求余运算符。
关系运算符:会综合成为比较器,比如大于,小于。
移位常数:不会综合成为移位器,实际上会综合成为固定的连线,和常数的连接之类的。同理乘以一个常数,也不会生成乘法器。(但加上一个常数会生成加法器)
移位可变数:综合成为移位器。
条件判断:有可能生成mux,或者其它简单的比较器等。
希望大家写Verilog的时候。能有上述的认识,对生成的电路从数字逻辑级别有个大概的认知。
我们来看个复杂点的,数据路径综合。直接写乘法会生成什么?可能会生成不同的乘法器。通常这种我们会直接用Designware的IP。而不是手动写乘法。当然位宽很短的情况直接写乘法运算符,也没什么关系。时序都能过的。位宽宽的时候需要调用IP,并指定级数。
我们再来看一下时钟门控。时钟是同步电路中,一个非常非常重要的概念。时钟和复位设计的好不好,大概占电路设计一半的重要程度。时钟也是电路的主要功耗来源,因为时钟一直上上下下。但很多时候,电路不需要工作,可以把时钟停掉。
为了节省功率,设计者会尝试在不需要时关闭时钟信号,从而减少动态功率的消耗。这种方法被称为时钟门控(Clock Gating),它是降低动态功率消耗的一种常见技术。
全局时钟门控(Block-level Clock Gating)是一种时钟门控技术,用于在特定操作模式下节省功耗。这种技术在不需要整个模块或组件时,通过在RTL中定义时钟门控来关闭时钟信号。我们直接看图可以看到,时钟信号和enable信号与在一起。如果enable拉低。则输入进去的时钟信号为0。整个系统自然也就不会工作了。
寄存器级时钟门控(Register-level Clock Gating)是一种更细粒度的时钟门控技术,它可以在寄存器(flip-flops)级别上节省功耗。这种技术可以在寄存器输出不变时关闭时钟信号,从而减少由于时钟信号切换而产生的内部功耗
我们来看一下如何实现上述的时钟门控。最好的办法就是调用现成的模块,这也是我之前说的,不要自己写always块。大家以后进公司,会发现寄存器都封装好了,直接调用就可以了。大家也可以自己写点库,多调用,少写always块。就跟写软件编程语言一样。多调用函数,少写从头到尾的语句。
我们来看一下毛刺问题。如果en在时钟高电平的时候,乱跳怎么办?这样输出的时钟信号就是一段毛刺,这自然会影响我们的设计。
我们可以通过latch解决这一问题。当clk为高的时候,其取反自然而然就是低了。此时latch的输出不会变,输出的enable信号自然也不会变。这样输入的enable毛刺就屏蔽掉了。再和clk与起来就行了。我们直接看时序图,相信大家可以理解。
我们还可以合并enable信号,这个没啥好说的。大家直接看图就能看懂。
时钟门控非常好理解,那什么是数据门控呢?其实和时钟门控很类似,用于在数据信号不被使用时关闭这些信号,以节省功耗。这种情况类似于时钟门控,其中数据信号的切换也会导致功耗的产生,即使这些信号在某些时刻没有被使用。
我们看个实际的例子,我们将基于shift_add
信号是1还是0,选择移位器的输出或者是加法器的输出。如果shift_add
为0,我们用的实际上是加法器的输出。这个时候完全没必要移位的,移位意味着0到1和1到0,自然会产生动态功耗。此时我们可以将输入与上shift_add
。如果其为0,则移位器的输出就是两个0。其输出也固定位0,不会发生比特翻转。从而节省了功耗了。
当然上述的设计,用在功耗比较敏感的场合,很多时候我们写代码,不会考虑的这么细。大家知道有数据门控这回事,并且在适合用的地方能用上就可以了。不一定要每个场合都去思考,可不可以进行数据门控。
此外我们还需要进行代码的LINT检查,通常用的是spyglass工具。这里就不细说了。B站就有不少视频,大家可以搜来看看。
5、Post-Synthesis Optimization
我们来看一下综合工具在完成工艺映射以后,是如何优化你的时序的。下图是一个最简单的例子。下面的文字也讲了很多优化思路。我们来一一解释一下。
我们在电路中,会碰到驱动能力不够的情况。比如多扇出,就有可能驱动能力不够。此时怎么解决呢?可以通过插入Buffer的方式,也可以例化多份器件。还可以Resize Cell的大小(回顾一下逻辑努力的概念)。
我们还可以用时序换驱动能力,这个大家直接看图,就能理解。
还可以将复杂的门变成简单的门(比如运用德摩根定律),也可以交换Pin角(前提是不改变逻辑功能)。
还可以采用retiming的方式。该方式非常常用,因为我们的流水线每一级,其组合逻辑长度不一定平衡,采用retiming交换组合逻辑的位置,可以尽可能让每一级流水线的延迟一样。这样可以让时序更快(木桶效应)。
参考资料如下所示:
系列文章入口——
【芯片验证】sva_assertion: 15道助力飞升的断言练习 |
【芯片验证】可能是RTL定向验证的巅峰之作 |
【芯片验证】RTL仿真中X态行为的传播 —— 从xprop说起 |
【芯片验证】年轻人的第一个systemVerilog验证环境全工程与解析 |
【芯片设计】verilog中有符号数和无符号数的本质探究 |
【芯片设计】论RTL中always语法的消失术 |
【芯片设计】代码即注释,注释即代码 |
【芯片设计】700行代码的risc处理器你确实不能要求太多了 |
入职芯片开发部门后,每天摸鱼之外的时间我们要做些什么呢 |
如何计算系统的outstanding 和 burst length? |
芯片搬砖日常·逼死强迫症的关键词不对齐事件 |
熟人社会里,一群没有社会价值的局外人 |