前段时间做一个数据通路模块,16bit乘累加,频率目标250MHz,RTL里就是一个再普通不过的acc <= acc + a * b;。综合结果出来,关键路径稳稳落在乘法器上,slack -0.43ns。我盯着report_timing看了半天,发现最大的占比不是加法,而是乘法器内部的加法树和那一大串进位链。后来把DesignWare的实现方式换了一下,配合retiming,时序才真正收敛。Design Compiler里那个常被当成“黑盒子”的DesignWare库,实际上决定了乘法器这种算术单元在综合之后到底是快还是慢,是面积小还是功耗低。这篇文章把DesignWare自动优化乘法器时序的完整链路拆一遍,讲清楚工具在背后做了哪些决策,以及我们怎么反过来控制它。
1. 先把概念盘清楚:综合库、DesignWare、乘法器各自扮演什么角色
1.1 一个很容易被低估的事实:综合库不等于DesignWare库
很多刚接触DC的工程师会把“综合库”理解成Foundry给的stdcell库文件,比如SMIC、TSMC的typical.db、slow.db。这个理解不算错,但不完整。Foundry的标准单元库只提供最基础的门级单元,例如AND、OR、INV、DFF、MUX这一层的东西。你写RTL时用了一个*,DC不可能为了这一个乘法器凭空变出一个高效的加法树架构,它只能靠DesignWare库来补位。
DesignWare是Synopsys随DC一起发布的可综合IP库,行业里管这层叫synthetic library,也就是“综合库里的逻辑功能库”。它和物理库的区别在于:物理库是已经画好版图、有准确延迟信息的单元;DesignWare则是一些预先设计好、参数化、未映射到工艺的电路结构,比如乘法器、除法器、移位器、FIFO、同步器等。
这种区别在工程上的实际影响非常大。如果DC配置里漏掉了DesignWare相关的synthetic_library路径,工具遇到乘法运算符时也可以硬综合,但结果通常是一堆门级逻辑拼出来的组合乘积项,速度和面积都很难看。所以我每次搭综合环境时都要确认两件事:变量synthetic_library指向的DesignWare库路径是否正确,以及综合生成的网表里有没有出现DesignWare的例化名,比如DW02_mult_inst。如果一份网表从头到尾没有DW开头的例化名,而设计里又确实写了大位宽乘法,那大概率是DesignWare的配置出了问题。
1.2 DC综合乘法的决策链路:从“*”到“DW02_mult”再到门级网表
RTL里的a * b在被DC吃进去之后,并不是直接变成一个乘法器。HDL Compiler这一层会把RTL转成一个叫GTECH的通用网表,里面所有算术运算符都会被替换成DesignWare组件。典型例子就是乘法变成DW02_mult,加法变成DW01_add,比较器变成DW01_cmp,整数除法变成DW02_div这一类。
等到compile或compile_ultra阶段,DC会根据你给出的时钟约束、面积约束、优化级别,从DesignWare组件内部挑选一个最合适的实现架构,再把这些架构映射成Foundry标准单元的网表。这个过程是两层决策:DesignWare负责“用什么电路结构”,标准单元映射负责“用什么门去搭”。
我见过不少人把DesignWare理解成一个“大黑盒”,觉得只要RTL里写了乘法器,工具就会自动给你最优解。实际情况不是这样。DesignWare组件内部往往有多个候选实现,比如乘法器可以有Booth编码+Wallace树的高速版,也可以有移位相加的紧凑面积版。DC会根据约束条件自动选,但选出来的结果不一定符合你的真实需求,因为它只能按成本函数办事,不知道你面积是否已经到了极限、功耗是否要压、布线拥塞是否严重。所以后面会专门讲到怎么干预这个选择。
1.3 为什么乘法器这么特殊,值得被“特殊照顾”
一个AND门和一个加法器的综合都很简单,标准单元库天然支持。但N位二进制乘法器是个组合复杂度极高的结构体,完全靠综合工具临时拼门,几乎不可能得到有竞争力的结果。乘法器的本质是“部分积生成 + 部分积压缩 + 最终进位传播加法器”三个阶段,如何生成部分积、用多少级压缩树、最终加法器用并行前缀还是行波进位,这些是架构设计层面的选择。
Foundry库不会给你预置一个乘法器单元(就算有也只是很小的位宽),所以这个架构决策必须发生在比门级更高的抽象层次。DesignWare正好承担了这个角色:它把业内几十年的乘法器架构经验封装成可参数化的组件,DC在综合时按需实例化。说白了,DesignWare就是Synopsys替你把“乘法器应该长什么样”这个问题提前回答好了,我们只需要通过综合约束告诉它:我现在重时序,面积可以妥协。
这个“提前回答”的价值在工程上很直接。拿16bit乘法器来说,如果工具从头开始布局逻辑,可能需要大量迭代才能找到一个可以满足时序的拓扑。而DesignWare直接给出一组可选的成熟拓扑,DC只需要在这组拓扑之间做选择,然后映射到具体工艺库。这也是为什么同样的RTL,用DC加DesignWare综合,和用别的方式硬综合,乘法器部分的性能和面积差距可能达到30%到50%。
2. 乘法器为什么总挂在关键路径上:从RTL到门级加法树的真相
2.1 你写的“*”在逻辑综合眼里是什么样的
如果你只在RTL里写了一句assign p = a * b;,DC看到的是一个纯组合乘法器。要生成p的每一位,需要把a和b的各个bit按照二进制乘法规则做“与”操作产生部分积,然后把所有部分积按位相加。16bit乘法会有16个部分积,32bit乘法有32个部分积,每个部分积的宽度在16或32bit左右。综合工具首先要把这一堆与门和加法操作组织起来,然后才能谈优化。
真正让关键路径变长的,不是部分积生成的与门,而是压缩这些部分积的加法树,以及最后那级把两个部分和合并成最终结果的进位传播加法器。你可以把乘法器想象成把几十个数同时相加的过程:想快,就得让这些加法的树形结构尽可能矮,每一级之间尽可能少地传播进位。
很多RTL代码会把乘法和其他操作连在一起写,比如acc <= acc + a * b。这看起来只是乘法和加法串联,但DC在优化时会把乘加结构融合成一个整体,可能生成乘加器(MAF),也就是把累加项作为部分积的一部分提前送进加法树。从面积上看这是好事,但有时会让关键路径上的逻辑级数变多,因为累加器的反馈路径也被拖进了乘法树的压缩过程。工程上这是关键路径频繁出现的位置,后续会详细处理。
2.2 进位链、部分积和加法树:乘法器时序瓶颈的三个来源
乘法器时序瓶颈主要来自三个地方。
第一是部分积数量。位宽每增加一位,部分积数量就增加一位。16bit乘法如果有Booth编码,部分积数量可以从16个降到8个左右,但Booth编码电路本身也有延迟。所以这里有个权衡:不用Booth,部分积多、树的级数可能更多;用了Booth,部分积少但编码逻辑引入额外路径。
第二是加法树的深度。把N个部分积压缩到两个数(部分和与进位),用3-2压缩器(全加器)组成的Wallace树或者Dadda树,级数大约是log以1.5为底的N级别。N越多,树的深度增长其实不快,但每一层的走线和扇出会在物理实现阶段放大延迟。我以前做过一个24bit乘法器,从报告上看加法树只有4到5级,但加上互连延迟之后,关键路径里的互连占比超过了35%,这时候光改逻辑结构已经不够了。
第三是最终进位传播加法器。最后必须把“部分和”和“进位”两个数相加得到最终乘积。这一级如果用了行波进位加法器,进位会从最低位一路传到最高位,延迟非常可观。DesignWare在高速实现里通常会选并行前缀加法器,比如Kogge-Stone或Brent-Kung,用更多硬件换更短的进位链,这就是为什么同一乘法器在不同实现下延迟天差地别的原因之一。
2.3 位宽翻倍,时序代价不是线性涨
一个很常见的误解是“32bit乘法器比16bit乘法器慢一倍”。实际经验是,乘法器位宽增加带来的延迟增量没有想象中那么可怕,但它带来的布线拥塞和面积膨胀往往更让人头疼。
从逻辑级数来看,16bit乘法和32bit乘法在部分积压缩树上的深度差异可能只有一到两级加法器,但部分积数量从16个变成32个,意味着有更多部分积信号需要被安排进压缩树,走线长度、扇出都会增加。在做Floorplan之前的逻辑综合阶段,DC只按线负载模型估算,这个误差还不大;但到了后端布局布线阶段,乘法器区域的拥塞可能突然引爆时序。
所以我在做较大位宽乘法器时,从来不只盯着综合报告的逻辑级数看。会提前在后端长跑一次,看看乘法器放置区域的拥塞情况。如果一个乘法器占据了太大的面积,即使逻辑延迟达标,布线延迟也可能把它重新推上关键路径。这也是为什么后续要手动干预DesignWare选择,甚至考虑在RTL里插入流水线寄存器。
3. DesignWare的“自动优化”到底是怎样运作的
3.1 自动推断架构:速度优先还是面积优先的权衡
DC在综合DesignWare乘法器时,会先读取设计约束,然后在组件内部的多个实现里做权衡。这个“多个实现”确实是真实的,不是同一个电路换汤不换药。以DW02_mult为例,它内部可能同时有面积优先的二进制乘法实现和速度优化的树型乘法实现。
我个人的理解是,DC在自动选择时大致遵循这个思路:先看你的时钟周期bar有多紧。如果bar很宽松,它会倾向于选面积小的实现,因为面积小通常也意味着功耗低、布线方便。如果bar很紧,它会选速度快的实现,用更多硬件并行换更短关键路径。如果两种实现都满足不了时序,它会在日志里打出时序违例,把最接近满足的那个版本输出给你。
这个自动选择过程也会在综合日志里体现。你可以搜索 “Inferred implementation” 或 “Implementation” 这类字样,看看工具在乘法器例化名上做了什么决策。如果日志里没有相关信息,也可以直接看网表本身,用report_implementation查看当前设计里各DesignWare组件正在使用哪种实现。
3.2 乘法器相关组件:DW02_mult、DW02_tree 与 Booth 编码
DesignWare Foundation库里最常碰到的乘法器组件是DW02_mult,它实现标准的二输入乘法操作P = A * B。参数是位宽A_width和B_width,输出P_width通常是两者之和。当DC识别到RTL里的乘法运算符时,就自动实例化这个组件。
除了DW02_mult,还有一个容易被忽略的组件是DW02_tree,它专门用来把多个输入数压缩成两个数。这种“数位压缩器”在实现乘加器、FIR滤波器这类多操作数求和场景时特别有用。如果你写RTL时把多个乘积相加,DC可能不只是用多个DW02_mult再加一堆DW01_add,而是可能直接利用DW02_tree把部分积合并处理,延迟更低。
DesignWare里还体现了Booth编码的选择。Booth编码在电路层面对部分积数量进行压缩,但编码器和选择器会引入额外逻辑。对应到时序上:部分积少了,加法树深度可能降低;但编码器新增的路径可能落在数据通路的起点上。所以“是否启用Booth编码”应该由综合工具在实现选择时判断,而不是在RTL里强行指定。如果你不在RTL里干预,DC通常能选一个比较合理的方案。
3.3 compile_ultra与DesignWare的配合逻辑
普通的compile和compile_ultra在DesignWare的使用深度上差别很大。compile_ultra会启动Datapath Optimization,也就是把设计中的算术逻辑放在一个更大的视野里看,尝试跨运算符优化。例如把多个相邻的乘法器、加法器、比较器重新组合,把共享的部分积生成逻辑合并,或者把一个大的乘法器拆成更利于时序的形态。
我遇到过一种典型场景:设计里有两个16bit乘法器共享同一个输入信号。用compile综合,两个乘法器各自独立生成部分积;换compile_ultra之后,工具识别到共享输入,部分积生成阶段做了共享处理,面积下降,同时因为整体布局更紧凑,时序也略有改善。
另一个要注意的是,compile_ultra会自动启用DesignWare的多个备选实现做“试综合”,最后挑一个成本最低的。所以如果你在工程里用了compile_ultra,但对DesignWare行为感到困惑,建议先打开综合日志里的DW相关消息,它能告诉你工具在实现选择上做过哪些尝试。有些人喜欢在compile_ultra之外再叠加-timing选项,这会让工具在面积上更激进地妥协,换取极端时序。对高速乘法器场景,这个选项确实能救急,但网表面积可能会明显上涨。
4. 实操里真正有用的控制手段:让乘法器时序往你想要的方向走
4.1 hdlin_auto_simplify和implementation selection,综合前就要设好的变量
DesignWare自动优化并不等于放手不管。在综合脚本里,有几个变量会影响最终乘法器形态,我建议每次综合前都检查一遍。
第一个是hdlin_auto_simplify。这个变量控制HDL Compiler在进行RTL分析时是否自动简化电路的冗余逻辑,比如常数乘法、与自身相乘、乘以2的幂等。默认值通常是always,它在大多数情况下是好事。比如a * 8如果被识别成移位,就直接硬连线,不会生成乘法器,节省大量面积。但如果你希望这个乘法器变成真正的硬件乘法器(比如为了应对后期ECO时改用其他值),就要小心自动简化带来的“意外结果”。综合后抽网表检查时,如果发现某个想象中的乘法器不见了,第一反应应该是看hdlin_auto_simplify是否把常数乘法简化掉了。
第二个是DesignWare组件内部的implementation selection机制。DC允许你通过set_implementation命令直接指定DW组件用哪种实现方式。比如想强制乘法器使用速度优先的实现,可以用类似set_implementation DW02_mult -speed的写法(不同DC版本命令格式略有差异,用report_implementation -cmd查一下当前支持的具体参数)。想面积优先,就换成-area。工程上我更倾向于先用DC的自动选择跑一版,再把自动选择的结果通过report_implementation打出来,最后再根据实际时序情况用set_implementation做定向调整。
4.2 用set_implementation干预架构选择:什么时候该出手
不是所有设计都需要手动干预DesignWare的实现选择。如果综合结果里乘法器路径的slack为正,而且面积也在预算内,那自动选择的结果就是可接受的,不要碰。手动指定实现方式只适用于以下几种情况:自动选择的结果仍然有时序违例;面积超出预期;或者你清楚的知道自己的设计目标不是“通用最优”,而是偏向某个极端。
我在乘法器时序收敛项目里经常做的一件事是:先用默认选项跑一次综合,report_timing看到乘法器是主要瓶颈后,用report_implementation检查当前选中的实现。如果当前是面积优先版本,我会先试set_implementation切换到速度优先版本,重新跑综合,看slack怎么变。这一步通常能带来几个百分点到十几个百分点的延迟改善,但面积会同步增加。如果速度优先版本还是不够,那就不是“选型”能解决的问题,必须考虑流水线或者重定时。
需要特别注意:set_implementation的使用范围不是无限制的。DesignWare组件本身必须支持这种实现切换,而且多个实例要分开控制时,需要准确指定instance路径。我在脚本里一般会对组件的例化路径做正则匹配,比如current_design下的所有DW02_mult_*统一设置,而不是在每个实例上手写命令,这样既避免遗漏,也方便后面对比。
4.3 时钟约束怎么写才不会“骗”你
很多乘法器“时序违例”其实是约束不合理造成的假象。DC做实现选择时完全依赖你给出的时钟周期、输入延迟、输出延迟、时钟不确定性。如果这些数值没有一个真实的物理基础,工具就会被带偏。
举个例子:如果set_clock_uncertainty设得过大,DC会认为时钟沿的不确定性占了很大预算,留给组合逻辑的周期变短,于是倾向于选择面积庞大的高速乘法器实现,结果时序满足了,但面积和功耗暴涨。反过来,如果uncertainty设得过小,DC可能选择一个刚好满足约束但余量不足的实现,到了后端还会因为时钟树偏差和片上偏差导致时序失败。
所以约束必须和物理设计保持一致。我一般会在综合前就把后端需要用到的时钟参数、输入输出延迟写进约束文件,然后跑一版快速综合,观察乘法器相关的WNS(最差负slack)和TNS(总负slack)分布。如果乘法器路径上的slack一直很差,用report_constraint -all_violators看看到底是哪个约束在起作用,再决定是改约束还是改设计。这里最容易犯的错是一开始就想着放宽约束,等你后端发现真实时序根本不行时,已经晚了。
4.4 关键报告怎么读:report_timing、report_implementation、report_resources
综合之后,我习惯同时看三份报告,判断乘法器相关路径的真实情况。
第一份是report_timing。要看路径的起点和终点,再看路径上经过的逻辑单元列表。如果路径穿过DW02_mult_*,而且该单元内部多个加法器级联的延迟占比很大,那基本可以判断乘法器是瓶颈。还可以用report_timing -through [get_pins -hier *mul*]这类手段只看穿过乘法器的路径。
第二份是report_implementation。它会列出当前设计里每个DesignWare组件例化到了哪种实现,以及还有哪些可用实现。比如乘加器显示用的是CSA的某个变体,旁边还列着CLA等其他选项。这种报告是判断工具选型是否合理的最直接依据。
第三份是report_resources。它显示RTL运算符和DesignWare组件之间的映射关系。有时候你会发现一个乘法器被DC用多个较小的乘法器替代了,这通常发生在输入信号某些bit为常数的情况下。知道这些资源映射情况,才能解释网表里为什么出现多个乘法器例化,也才能在后端做针对性约束。
5. 一个16x16乘法器时序违例的完整排查与收敛过程
5.1 现场还原:违例情况与初步定位
之前提过的那次16bit乘累加,设定是250MHz,周期4ns。第一版综合使用默认compile_ultra,report_timing显示关键路径slack为-0.43ns,终点是累加寄存器的数据端。路径的起点是乘数寄存器,中间穿过了DesignWare推断出的乘加器组件,逻辑级数大约是19级组合逻辑。
先不急着改RTL,我做的第一件事是检查约束。把时钟周期、uncertainty、input_delay都确认了一遍,排除约束过紧的因素。然后打开综合日志,搜索DesignWare相关信息,发现乘加器被推断成了某个默认实现。接着跑report_implementation,查出当前使用的是偏向面积优化的一组结构。
这里有个很重要的判断:4ns周期,0.43ns违例,大约10%的差距。如果直接把实现切换到速度优先版本,也许能弥补一部分,但未必能完全收敛。可我还是先试了这个方向,因为改动最小,而且在数据上能清晰看到设计空间的边界。如果连速度优先版本都收敛不了,再考虑插流水线和retiming也不迟。
5.2 沿着report_timing往下查的完整链路
我用report_timing打了一份详细路径,逐段看延迟构成。路径大概分了三段:第一段是输入寄存器到乘法器部分积生成,约0.5ns;第二段是部分积压缩树,约1.5ns;第三段是最终进位传播加法器加累加器输入,约1.9ns。此外还有从寄存器到组合逻辑路径的时钟延迟、setup时间等固定开销。
这里最有价值的信息是:第二段和第三段占了总延迟的大头。也就是说,单纯靠换一种乘法器实现,优化空间只集中在“部分积压缩树”和“最终进位加法器”这两个环节。如果DC当前已经选了较快的变体,那再切换实现方式带来的收益就有限。
我还用report_timing -through专门拦了穿过乘加器输出的路径,确认关键路径并不是因为RTL里那个累加器产生了很长的反馈链。结果很清晰:问题几乎都在乘法器内部的进位链和组合级联上。到这里,我基本可以确定下一步该做什么了。
5.3 三个方向的修复尝试与实测结果
方向一:强制切换DesignWare实现。用set_implementation把乘加器切换到速度优先级最高的版本,重新综合。slack从-0.43ns变成了-0.21ns,有改善但不能收敛。面积增加了约22%,功耗也小幅上升。这说明当前工艺库和时钟目标下,单靠组合逻辑重新配置已经逼近上限。
方向二:RTL里手动插入流水线寄存器,把乘法器拆成两段。第一级寄存部分积压缩后的部分和与进位,再进一个加法器得到最终结果。这下组合逻辑级数被砍掉一半,重新综合后slack变成+0.15ns,时序收敛。代价是输出延迟多了一个周期,面积也进一步增加,但可以在一个时钟周期内继续做累加,功能上可以接受。
方向三:用compile_ultra -retime让工具自己把寄存器往前移动,也就是自动retiming。这个方案不改变RTL,工具把靠近乘法器输出端的寄存器往回推,把部分积压缩树“夹”在两个寄存器组之间。实际跑下来效果也不错,slack在+0.08ns附近,面积增加比手动插流水线的方案略小。但retiming会改变触发器的层次路径,给后端等价性检查带来额外工作,团队里如果对ECO和DFT有严格要求,需要提前评估。
最终我选了方向二,因为它是RTL层面明确的流水线结构,综合和后端的行为都可预测,网表和RTL的对应关系也清晰。方向三虽然省事,但在复杂设计里,retiming后功能验证和时序签核可能花费额外精力。这个取舍没有绝对正确,但工程上稳定性优先。
5.4 收敛后的验证:功能仿真、等价性检查和功耗估计
换完流水线结构后,不能只看时序报告就收工。首先要做RTL仿真,确认多出来的一个周期流水级在数据通路控制逻辑里被正确对齐。我的设计里有一个valid信号跟着数据一起走,需要额外打一拍,否则数据通道和控制通道错位,功能直接出错。
然后要跑带DesignWare仿真模型的功能仿真。DesignWare各组件带有自己的仿真模型,在仿真库里实例化后行为应该和RTL一致。流水线化之后需要重新跑一遍回归用例,尤其把边界值、饱和值、溢出场景覆盖到。
之后才是综合层面的后仿检查。我会比较综合后的网表和原RTL在关键边界条件下的仿真结果,确认DesignWare例化没有出现仿真不匹配的情况。最后看综合报告里的面积和功耗变化,和项目预算做对比。如果超标严重,再考虑折中方案,比如只对乘法器的部分级数做流水化,而不是把整个乘加器切到底。
6. 一些容易踩的坑和我的实际体会
6.1 不要迷信“自动”两个字
DesignWare名字听起来像是个自动化的黑科技,但“自动优化”不等于“自动最优”。DC的自动选择只按约束和成本函数做局部最优,它不知道你的项目还有布线拥塞、功耗预算、DFT可测性设计、ECO策略这些额外诉求。所以一定要在综合后认真读报告,确认工具选择了什么实现,而不是闭眼签收。
我试过最典型的反例是:同一个乘法器模块,在A项目里DC自动选了面积优先版本,时序刚好满足;换到B项目,时钟频率提高,DC自动改为速度优先版本,但面积膨胀导致后端布局放不下,逼得我又回头改RTL,把乘法器手动拆分。如果我在综合前就知道B项目的面积是硬性约束,就不会让DC在那里自由发挥了。
6.2 DesignWare库版本和target library不匹配
DesignWare库和Foundry工艺库之间没有直接依赖,但DC版本、DesignWare库版本之间却可能有兼容性问题。遇到乘法器例化后某些实现不可用、或者综合报出奇怪的warning时,先确认当前环境的DesignWare库是否比你早期版本新或旧太多。尤其多个项目共用服务器上的同一个DC安装目录时,很容易出现版本漂移。
排查方法很简单:用report_implementation查看可用实现列表,如果某个速度优化实现没有出现,或者出现了但后面标注不支持,那就先检查环境变量里的synthetic_library路径,再检查DC版本和DesignWare版本的匹配情况。Synopsys安装包里一般会有一个兼容性列表,换版之前花十分钟扫一眼能省后面很多沟通成本。
6.3 一个后续扩展思路:乘法器优化只是DesignWare的一部分
DesignWare库里像乘法器这样的组件还有很多,除法器、开方器、FIFO、同步器、格雷码转换器全都提供多种实现方式。如果项目里碰到除法器路径上的时序问题,基本也可以用同样的思路分析:先report_implementation,再set_implementation切换实现,不行就RTL插流水线,最后再考虑多周期路径。这套方法论复用的价值比我刚开始接触DesignWare时预想的大得多。
另外多说一句,网上那些i2c时序图、SMBus时序、ARM总线时序、时序预测这些概念,和本文说的“时序”完全是两码事,别混在一起理解。IC设计里的时序,本质上是指数据在时钟沿之间能不能稳定地从组合逻辑的一端传到另一端。乘法器这类纯组合逻辑单元,恰好是最能体现“时序是架构选择出来的,不是靠工具硬算出来的”这个道理的模块。