DesignWare自动优化乘法器时序的完整链路:从RTL到门级关键路径收敛

发布时间:2026/10/7 12:13:41
DesignWare自动优化乘法器时序的完整链路:从RTL到门级关键路径收敛
前段时间做一个数据通路模块16bit乘累加频率目标250MHzRTL里就是一个再普通不过的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_ultrareport_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设计里的时序本质上是指数据在时钟沿之间能不能稳定地从组合逻辑的一端传到另一端。乘法器这类纯组合逻辑单元恰好是最能体现“时序是架构选择出来的不是靠工具硬算出来的”这个道理的模块。