去年做一颗28nm视频编解码SoC的后端收敛时,我们被功耗指标卡了整整两周。时钟树功耗差不多占了芯片动态功耗的25%,DCT拓扑综合出来的网表一进入APR,时序余量就开始全面恶化,前端觉得是后端CTS没做好,后端觉得是综合阶段完全没考虑物理信息。最后真正把DCT/DCG里的Multi-bit FF优化链路完整打通,时钟网络功耗降了20%左右,CTS阶段buffer用量也肉眼可见地少了一批。这篇文章不绕弯子,直接讲DCG下Multi-bit FF物理优化的完整逻辑、具体做法,以及我们在一套叫SPG的流程里反复踩过的坑。适合正在搭DCG综合flow、或者被时钟功耗和拥塞来回折腾的工程师参考。
1. 为什么一个“合并触发器”的动作,能省出整块时钟功耗
Multi-bit FF这个名字听起来像是把几个触发器简单拼在一起,但真正做物理优化的人都知道,这一步动的其实是时钟网络这根大动脉。要理解它的价值,得先从触发器是怎么在芯片里“偷电”的看起。
1.1 触发器在时钟网络里到底是怎么“偷”电的
动态功耗的公式大家都会背:P = C × V² × f。在芯片里,时钟网络是全世界扇出最大、翻转频率最高、物理距离最长的网络。从时钟源出来,要经过一级又一级的buffer,最后到达每一个触发器的clock pin。这条链条上每一级反相器或buffer的输入电容,加上每颗触发器clock pin的输入电容,全部累加在一起,构成了时钟树的总负载电容。
触发器的内部并不是只有时钟pin这一个电容,它通常还有一级内部反相器,把外部进来的时钟翻转为互补时钟给锁存级用。这颗内部反相器的输入电容,也全部反映在时钟树的负载上。也就是说,一颗普通DFF,在时钟网络上贡献的电容,远不止你在Lib里看到那一个clock pin的电容值。如果一万颗触发器都这么接,时钟树上的容性负载就是一个非常可观的数字。
Multi-bit FF的思路,就是让多颗触发器的内部时钟反相器共用。原来四颗触发器需要四套内部反相电路,现在四bit的MBFF只需要一两套,时钟pin数量减少,内部充放电节点减少,时钟树要驱动的总电容自然就降下来了。
1.2 多bit单元来了之后,内部结构发生了什么变化
从单元内部结构看,MBFF并不是把四个DFF并排放进同一个cell这么简单。以最常见的MB2、MB4单元为例,它通常有一组共享的时钟输入引脚,以及可能共享的异步复位/置位引脚,数据端还是独立的D0、D1、D2、D3,输出端也是独立的Q0、Q1、Q2、Q3。共享的是一些控制类引脚和内部时钟电路,数据路径基本保持独立。
这带来的第一个好处,是标准单元数量变少。同样实现四个D触发器,原来需要四个DFF cell,现在只需要一个MB4 cell。单元数量减少意味着布局对象变少,后端处理起来更干净,面积也通常能缩小一些。第二个好处是时钟树的sink节点变少,每个sink的电容更大但总数更少,CTS阶段要处理的skew group更少,时钟树深度可以压得更浅。
但这种压缩是有条件的,并不是任意四颗DFF都能无脑合并。工具能合并的触发器,必须在时钟域、异步控制信号、扫描链连接方式上都保持一致。举例来说,如果四颗触发器里有两颗的复位信号是同一个,另外两颗是另一个,那就大概率没法合并成一颗MB4。扫描链如果有的在链上、有的不在,也不能直接合并。所以你会发现,网表里MBFF的出现位置,往往天然就是那些“控制信号整齐”的寄存器阵列,比如状态寄存器、配置寄存器、数据通路里的流水寄存器。
1.3 这块“蛋糕”并不是白吃的:面积、时序和可布线性的三角关系
很多人一听MBFF能省功耗、省面积,就想把整个设计全部打开这个开关。实际项目里这么干,大概率会翻车。
先看可布线性。MBFF单元通常比普通单元要宽,有些库里4bit单元的高度是标准单元的两倍。布局阶段,如果一行标准单元里塞进一个MB4,剩下的空隙可能小到无法再放其他单元,形成碎片化空隙。更麻烦的是,一旦行里放不下,工具会尝试把MBFF移到附近行,进而带动一串cell的连锁legalize,最终可能造成局部拥塞不降反升。
再看时序。合并之后,四个bit在版图上是有物理位置的,它们可能分散在单元内部的不同区域。如果D端到Q端的路径和原来单bit单元一样短,那没问题;但如果某个bit的数据输入来自单元另一侧的寄存器,数据路径可能被迫绕更长,时序反而变差。
最后还有dynamic IR drop。多bit单元内部几个bit同时翻转时,瞬时电流明显大于单bit单元。特别是4bit或8bit的单元,在低电源电压下很容易在电源轨上打出明显的局部压降。这也是为什么在很多项目里,2bit和4bit单元混用、8bit单元被dont_use掉的原因。
2. DCG里的Multi-bit FF优化:从库的准备到果实的确认
标题里的DCT/DCG,本质上是Design Compiler在不同阶段的形态。DCT走的是拓扑模式,在综合时用抽象布局信息来估算net长度和拥塞;DCG则是把图形化物理环境和综合结合得更紧密。到了当前主流版本,这套能力基本统一在DCG里,但物理优化逻辑一脉相承。
2.1 没有多bit库单元,所有开关都是空谈
这是最容易被忽略的前置条件。Multi-bit FF优化必须由标准单元库里真实存在的多bit触发器单元来承接。Foundry的标准单元库,通常会有MB2、MB4甚至MB8系列,比如mb2_dffq_xxxx、mb4_dffqn_xxxx这类命名。
但如果你用的是内部定制库、IP周边单元或者某些老工艺库,很可能库里根本没有多bit触发器。或者在.lib里虽然有cell,但Power Model、时序弧、功能视图都只按单bit单元写,工具识别不出来,优化结果也是零。
所以在启用任何开关之前,我先用下面的方式确认库里的MBFF家底:
get_lib_cells */*mb* get_lib_cells */*mb*dff*能搜出来多少,决定了后面可以做到什么程度。如果只有2bit单元,那4bit合并就别想了;如果完全没有,就得回头跟库供应商要带多bit建模的版本,或者自己去评估老库手工编辑.lib的风险——我建议不要手工改,很容易在时序验证里埋雷。
2.2 compile_ultra里的寄存器优化开关怎么开
DCG里做Multi-bit FF的核心开关,在compile_ultra的寄存器优化路径上。我最常用的配置是这样:
set_app_options -name compile.ultra.optimize_register -value true set_app_options -name compile.ultra.multi_bit_register_optimization -value true compile_ultra -optimize_register这里compile_ultra -optimize_register会让DC在综合阶段做寄存器优化,包括合并、重组等操作。multi_bit_register_optimization进一步放开多bit合并的空间。
需要提醒一点:compile_ultra -optimize_register打开后,工具不只是做多bit合并,还可能做寄存器重定时、寄存器复制等其他优化。这些优化会改变寄存器拓扑,后端做formal时如果准备不充分,很容易在compare point上出现意外。所以有些团队会先只开multi_bit_register_optimization,用更保守的选项尝试,确认formal没问题后再逐步放开。
2.3 怎么确认优化真的发生了,而不是纸面数字
综合跑完后,不能只看report_qor里的面积数字变小就直接信。我一般会用两条命令交叉确认优化效果:
get_cells -hier -filter "ref_name =~ *mb*" report_register_optimization -verbose第一条命令可以统计网表里实际例化的MBFF单元数量。把数量记下来,再对照RTL里原本的DFF总量,就能估算出合并比例。第二条命令可以看到工具报告里的寄存器优化细节,比如哪些register被merge成了一个多bit单元,哪些因为条件不满足被保留。
另外一个更“物理”的判断方式,是直接导出APR前的时钟网络功耗估算。把MBFF优化前后的两个网表分别跑一遍同样的功耗check,看clock_network那一栏的变化。我们当时的数据是:优化前时钟网络功耗约占总功耗24.7%,优化后降到19.8%,而这个变化主要就是MBFF贡献的。
2.4 控制合并粒度:不是所有寄存器都适合被合进去
DC往往倾向于把能合并的寄存器尽量合并,但物理工程师得学会给这个“热情”踩刹车。我常用的做法,是把设计里几个最关心的关键路径模块的寄存器设置为不可合并区域:
set_dont_touch [get_cells ctrl_top/reg_array_reg*] true同时,在flow上设置一个开关,专门用来控制哪些模块允许MBFF。比如,高频数据通路和锁相环附近的模拟敏感模块,通常不建议合并。因为高频路径上多bit单元带来的额外内部距离,可能正好把本来能收敛的路径拖挂。
还要注意单元选择。有些库的MB4物理尺寸特别大,在标准单元行里合法化后经常造成空洞。我的经验是:如果库里同时提供MB2和MB4,就先放开MB2,让工具优先用2bit单元;只对寄存器密度低、布线资源充裕的模块放开4bit。8bit单元除非你确认后端能处理,否则建议直接设为dont_use。
3. 物理优化联动:DCG怎么同时看“地图”和“电表”
综合阶段只看逻辑,和物理实现阶段只看版图,都会造成决策短视。DCG的价值在于,它能把物理信息前置到综合阶段,让MBFF这类同时影响逻辑和物理的优化动作有据可依。
3.1 DCT、DCG到底差在哪:抽象线负载和真实布局的距离
老一代综合工具估算时序靠的是wireload model,通过统计平均线长来猜net delay。这种方式在工艺尺寸还比较大的年代够用,但到了28nm以下,net delay受具体布局位置影响太大,同一根net放在左上角和右下角,延迟可能差出一个数量级。
DCT的拓扑模式已经引入了简化的物理引擎,可以粗略预估走线长度和拥塞。DCG则更进一步,它能把floorplan、macro位置、电源规划这些信息真正纳入综合过程。做MBFF合并时,DCG看的不是“这两颗DFF在逻辑上相邻”,而是在抽象版图上距离够不够近、合并后会不会造成新的拥塞。这就好比原来你只是按通讯录给邻居组队,现在你手里拿到了小区地图,知道哪些人真的住隔壁。
3.2 把floorplan信息喂给DCG,MBFF才能“看上眼”
要让DCG在Merge寄存器时参考物理距离,前提是它得先看到物理信息。我习惯在综合的最早期,先拿到APR同学给出的一版初始floorplan:哪些区域放memory、哪些区域放标准单元、哪些地方有hard blockage。把这些信息转成DCG能理解的约束文件,和SDC一起喂进去。
这一步的输入质量直接决定MBFF优化质量。如果floorplan里macro位置和实际后端最终位置差得很远,DCG认为可以合并的两颗寄存器,在后端实际布局里可能隔着一条大bus,合并后的MB4放不下,反而引发连锁移动。后面第4章会详细讲这个坑。
喂入floorplan后,DCG会在合并MBFF的同时做一些简单的物理-aware的重叠检查,避免把不可物理实现的优化结果吐出来。这一步不像APR的legalize那么精确,但足以过滤掉大量“纸面上好看、实际上没法floorplan”的方案。
3.3 从DCG到APR:交接的不只是网表,还有“队形”
很多人以为DCG做完,输出一个干净网表给APR就结束了。但实际上,MBFF优化后的网表对后端来说有一些隐形的“队形要求”。比如,合并后的多bit单元数量变多、单bit单元变少,后端工具在legalize时如果没有意识到这些大单元的存在,可能会把它们拆开重新打散,让DCG的优化成果白白流失。
所以交接时我会在SDC或guidance里明确保留MBFF相关的约束,比如对已经合好的多bit单元加dont_touch,除非后端在legalize时真的发现无可避免的拥塞问题,才允许拆开重做。CTS阶段也要提醒后端,MBFF会让时钟树的sink数量变少,skew spec可以适度收紧,但要注意多bit单元内部各bit之间的延迟差异,不要让工具只盯着外部net delay而忽略单元内部的不匹配。内部时序失配在STA里会反映为multibit cell内部的min/max delay问题,但很多新手会误以为是外部时钟树skew没修好。
4. SPG流程避坑指南:我在这套流程里踩过的五个深坑
先说明一下SPG这个名字。在Synopsys官方文档里,你可能找不到一个标准的“SPG流程”叫法,它更像我们团队对“Structural-to-Physical Guidance”这套流程的简称,核心思路是把综合阶段的逻辑结构优化和后端物理实现的物理引导文件串在一起,保证MBFF这些优化能一路走到版图。下面这些坑,都是我们在实际项目里真金白银踩出来的。
4.1 坑一:dont_use误伤MBFF单元,工具悄悄“放弃治疗”
最典型的情况出现在功耗优化脚本里。很多flow会统一对高阈值单元做控制,比如为了低功耗大量使用HVT单元,但同时会在脚本里写类似这样的语句:
set_dont_use [get_lib_cells */*HVT*] set_dont_use [get_lib_cells */*svt*]如果库里的MBFF单元命名里恰巧包含这些描述,或者你的dont_use写得太强势,比如对某个子库的所有cell都加了dont_use,那MBFF单元即使存在,DCG也一个都别想用。更坑的是,这种问题通常不会报error,工具只在log里低调地打一条warning,然后默默输出一个完全没做MBFF优化的网表。
我当时排查这个问题,整整花了一天。先查RTL里有没有可合并的寄存器,再查compile的log,最后才想起去查dont_use列表。从那以后,我在flow里加了一个checker:综合结束后,立刻统计网表里MBFF数量,如果数量为0且库里有MBFF单元,自动报fatal error而不是warning。这个checker后来救过好几轮项目。
4.2 坑二:MBFF库建模不完整,时序报告一片混乱
Multi-bit FF的Lib模型比普通单bit单元复杂得多。一个MB4单元里,四个bit虽然共享时钟端口,但各自的时序弧、内部寄生、翻转功耗都不一样。如果Lib建模时把四个bit的data-to-Q延迟写成完全一样,或者内部电容没有按bit的实际物理位置设置,综合和STA看到的时序就跟真实芯片对不上。
最明显的问题是,DCG在优化时通常选择“看起来最悲观”的bit来评估这个MBFF单元的时序。如果Lib里有个bit的时序弧缺失,工具可能直接把它当0延迟处理,报出过度乐观的余量;到了后端APR,跑真实RC抽取后,这个bit的delay现出原形,时序一下就翻红了。
遇到这种情况,光在后端修是修不回来的。我的建议是:在采用新库的MBFF单元之前,一定要让库团队对多bit单元做一次Liberty-to-SPICE的比对,确认每个bit的时序弧特别是内部时钟到各bit输出的延迟差异都被正确建模。不要嫌麻烦,这一步能省掉后续三个月的floorplan返工。
4.3 坑三:DCG和后端用的floorplan版本不一致
SPG流程里最需要紧绷神经的就是版本一致性。我们曾经有一版floorplan,为了评估一个memory替换方案临时移动过几个macro位置。DCG拿着这版floorplan做了MBFF合并,但综合网表出来后,后端说memory方案被否了,已经切回旧版布局。结果就是:DCG按新位置想的“邻居”,在后端旧位置里变成了“天涯海角”。
最终体现的问题,是后端legalize时大量MB4单元找不到合法位置,工具被迫把它们拆成单bit单元或四处移动,拥塞从3%直接拉到9%,原本Clock tree的一大半收益也丢了。
后来我们立了一条规矩:进入SPG流程后,floorplan冻结。前端、后端各留一份带版本号的checkpoint,所有参与方只认这一版。凌晨改方案可以,但改完必须重新跑综合,不能拿旧网表配新floorplan交代。这个规矩听起来笨,但真的避免了大量无效加班。
4.4 坑四:Formality把多bit合并当成“设计变更”
单bit DFF被合并成MBFF后,形式上RTL里是四颗D触发器,网表里变成了一颗MB4。Formality这类等价性检查工具,必须能识别出这种“结构不同但逻辑等价”的变化,才可能在compare phase把它们对应起来。
问题往往出在库的functional model上。如果MBFF单元的functional model没有正确描述内部各bit的独立性,或者在RTL的某些控制信号(比如异步复位、scan enable)上处理不当,Formality可能在compare point匹配阶段就把多bit单元判成不等价,或者需要用户手动设置才能通过。
我在SPG流程里加了固定动作:跑Formality之前,先确认两个库里多bit单元的verification model都加载到位,并且把set_constant这类约束和后端脚本保持完全一致。一旦出现multi-bit相关的unmatched point,优先查Lib的function定义,而不是急着改RTL。
4.5 坑五:clock gating和MBFF缠在一起,CTS直接懵圈
RTL里常见的if (enable)寄存逻辑,综合工具往往会推断出ICG单元,或者用普通触发器加EN引脚实现。但当MBFF和ICG共存时,很容易出现一种尴尬局面:工具想把一组由ICG控制时钟的寄存器合并成MBFF,但库里的MBFF单元根本没有集成clock gating的能力,或者ICG和多bit单元的组合建模有问题。
于是DCG可能会做出一颗内部部分bit带gate、部分bit不带gate的“缝合怪”单元,或者把ICG放在MBFF前面、导致时钟门控检查的时序路径变得特别长。CTS阶段碰到这种cell,经常出现clock gating check大量违例,修timing变得异常痛苦。
我的建议是:在综合脚本里明确控制ICG与MBFF的配合方式。如果库里没有专门的multi-bit ICG,那就让综合器优先用单个ICG去驱动一组DFF,而不是把ICG和MBFF深绑在一起。时序和功耗的权衡上,宁可牺牲一点点动态功耗,也要保住CTS阶段的可控性。
5. 一套可以直接抄走的启用手册和收益预期
写到最后,把整个流程沉淀成一份可执行的手册,方便你直接对着操作。
5.1 推荐的总流程
Step 1:确认库里有可用的MBFF单元。用get_lib_cells */*mb*检查,把MB2、MB4、MB8分别列出来,在脚本里记录可用单元清单。
Step 2:检查现有dont_use和dont_touch列表,确保MBFF单元没有被误伤。特别是功耗优化脚本里对HVT/SVT单元的过滤逻辑,要单独确认。
Step 3:把初始floorplan信息整理成DCG可读的物理约束,和SDC一并读入。与后端确认这版floorplan会冻结使用,不随意变更。
Step 4:在DCG脚本里打开compile_ultra -optimize_register,并开启multi_bit相关选项。先放开MB2,视拥塞情况再决定是否放开MB4。
Step 5:综合完成后立刻统计MBFF数量,如果为0或明显偏少,先查log里的warning和dont_use,而不是直接进APR。
Step 6:交网表给后端时,附带SPG guidance文件,明确哪些MBFF单元需要保持完整、哪些区域允许后端在高拥塞情况下拆分。
Step 7:后端的formal阶段单独确认multi-bit点的匹配情况,有问题先查库模型。
Step 8:跑到APR后的第一版版图,马上生成拥塞图和IR drop图,对比MBFF优化前后的变化,决定是否要回调规模和合并策略。
5.2 关键参数速查
| 参数/动作 | 推荐值 | 备注 |
|---|---|---|
| compile.ultra.optimize_register | true | 核心总开关 |
| compile.ultra.multi_bit_register_optimization | true | 多bit合并使能 |
| 可用MBFF粒度 | 优先MB2,按需MB4 | 8bit慎用 |
| MBFF验证检查 | 综合后数量>0 | 防止dont_use误伤 |
| floorplan版本 | 冻结一个checkpoint | 防止前后端失配 |
| 后端dont_touch | 按区域选择性加 | 防legalize拆分 |
| IR drop检查 | 在APR后第一版立刻做 | 防多bit同时翻转打穿电源 |
5.3 收益预期和风险预期
以我们这颗视频SoC模块的数据为例:设计里有约12万颗可用于合并的寄存器,启用MBFF优化后,时钟网络功耗下降约20%,标准单元总面积减少约3.5%,CTS阶段的buffer数量减少了约15%。
代价也客观存在:局部拥塞指数从2.1%升到5.8%,动态IR drop热点多了3处,后段ECO时需要拆开MBFF单元才能改单bit逻辑的操作复杂度明显增加。所以这不是一个“全开就完事”的选项,而是一个需要前后端一起做trade-off的物理优化手段。
最后再分享一个小技巧:在所有模块里,优先让“控制逻辑多、寄存器排列整齐、翻转率不高但数量巨大”的模块使用MBFF,收益最稳定;反而是流水线数据通路上,翻转率高、时序紧张,强行合并容易得不偿失。先挑软柿子捏,跑通一版完整流程后,再逐步扩大范围,比一次性全开要稳得多。