在Innovus里做数字后端,最让人头疼的往往不是时序本身,而是“东西没放好”。尤其当设计里的mem/macro多到几十上百个时,摆放顺序和位置基本决定了后面的布线能不能走通。这两天又处理了一个mem特别多的block,趁热把思路和命令整理成一篇笔记,希望对还在跟macro做斗争的朋友有点帮助。
先交代一下背景:这是一个偏SoC的模块,里面大大小小的SRAM、register file加起来接近百颗,逻辑里还有一堆需要靠近存储阵列放的定制单元。最初版本“凭感觉”摆完之后,前几版布线结果非常难看,局部congestion爆红,关键路径绕线严重。后来老老实实按数据流重新做floorplan,整套设计才慢慢收敛。这里说的“数据流”不是玄学,而是真正能对应到网表连接关系的物理分布逻辑。
1. 为什么mem一多,摆放就必须跟着数据流走
1.1 mem太多时“拍脑袋摆放”会引发什么问题
很多人觉得floorplan嘛,无非就是给大模块画几个框,把memory往边上一放,中间留空给标准单元。单个mem这么做没问题,但mem一多,问题就接踵而来。
第一个问题是拥塞。memory本身有大量pin,而且多层走线往往要从它们上方或旁边穿过。如果mem的位置和实际信号连接方向是拧着的,比如源端在左下、负载端在右上,中间却挡了一排memory,后面布线就要花很大力气绕,绕出来就是一片一片的congestion热点。这种热点在CTS之后会变成时序修复的噩梦,修setup要插buffer,插完buffer又引入新的density问题,反复迭代到崩溃。
第二个问题是不可控的长线。mem之间的数据总线通常非常宽,几十上百根线同时从A模块连到B模块。如果两个模块因为mem摆得不合理被拉得很远,这一大捆总线的延迟和绕线资源消耗会直接把时序打趴。而且总线绕线还有对齐要求,绕多了DRC也容易爆。
第三个问题是时钟树和复位树。memory的clock pin数量多、负载大,通常需要单独处理。如果同一时钟域下的一批mem被拆到四个角落,CTS阶段你要么插一堆buffer,要么看着skew头疼。有些mem还有特殊clock gate要求,位置不对后面物理综合都不好做。
所以,mem一多,“拍脑袋”的随机摆放等于给自己挖坑。关键不是把每个mem放“好看”,而是让它们在物理上尽可能靠近它们对应的逻辑,让主要数据流的路程最短、绕行最少。
1.2 数据流到底该怎么读,才算读懂了
“数据流”听起来抽象,但在物理设计阶段,它其实就是一句话:信号从哪个方向进来,经过哪些模块,最后从哪个方向出去。放在网表里,就是instance之间的连接关系。
我的习惯是打开Innovus GUI,把flyline(飞线)显示打开。这时候屏幕上每一根线都代表一个net连接,密密麻麻一片,但仔细看你很快能看出几条“粗管子”。那些连接特别多的区域,就是数据流的主干道。比如一个模块有二十根128bit总线连到某个SRAM,那这个SRAM就别想跑远,它必须贴在对应逻辑边上。
除了看飞线,还要看连接密度矩阵。Innovus有相关命令可以报告instance之间的connectivity,也可以自己在Tcl里用dbGet扫一遍:遍历所有mem,统计每条mem的net连到哪些逻辑模块,按连接数量排序。这样能找出每个mem的“铁哥们”。物理上把这些“铁哥们”放在一起,数据流就顺了。
还有一个容易被忽略的点:要看输入端口和输出端口的位置。Floorplan的IO pin通常决定了数据从哪进、从哪出。数据流的主方向大致就是“输入pin区 -> 处理逻辑 -> 输出pin区”。mem作为中间存储节点,要么靠近输入侧做缓冲,要么靠近处理逻辑做中间结果缓存,要么靠近输出侧做输出缓冲。先定好主方向,再细化每个mem的位置,比东放一个西放一个靠谱得多。
1.3 读懂数据流之后,怎么转化成摆放策略
数据流读完了,心里要有三张图:第一张是整个block的主数据通路,从输入到输出怎么流;第二张是每个mem被谁驱动、又去驱动谁;第三张是大概的拥塞预期,哪些区域肯定要过很多线。
转化策略也很直接。首先是“按模块聚类”:把同一个逻辑域下用到的mem放在那个逻辑域旁边,别跨域乱放。其次是“按端口方向摆放”:每个mem的pin方向要尽量朝向它所连接的那一侧逻辑。比如某个mem的数据输入来自上方模块,数据输出送到下方模块,那它最好放在两者之间,pin朝上下方向,不要横着摆。最后是“按总线宽度预留通道”:有宽总线经过的区域,提前留出走线通道,别让成排的mem把通道堵死。
这一步做完,你其实已经具备手工摆放的全部输入了。后面如果你愿意,可以完全手摆;如果mem太多,也可以交给工具自动摆,然后人工检查修正。下面就说Innovus里怎么落地这些思路。
2. Innovus里常用的mem摆放命令与思路
2.1 先定边界和基本约束,再谈自动化
很多新手一上来就点place_fp_macro,结果工具一通乱摆,看着还挺整齐,实际上完全不管数据流。原因很简单:工具默认的优化目标未必是你心里的数据流。
我习惯先手动把大框架定死,再让工具在框内自由发挥。首先是floorplan边界,命令大概是这样的:
floorPlan -r 0.85 0.7 10 10 10 10这里-r 0.85是长宽比,0.7是core利用率,后面四个10是core到左右上下边界的间距。利用率这个参数很重要,mem多的设计建议先保守一点,0.65到0.75都是常见的,因为后面还要给mem之间的布线通道留空间。
然后是给每个mem或者每组mem加halo和fence。Halo是每个macro四周的禁止区域,相当于给mem和标准单元之间留出隔离带;Fence是把一组instance限制在一个矩形区域内。这两个约束是工具自动摆放和后续place最重要的“边界”。
# 给所有mem加2um halo addHaloToInst -halo {2 2 2 2} [dbGet top.insts.cell.type macro] # 给一组mem创建fence addFence mem_group1 -type hard -area {50 50 300 200}addFence这个命令本质上不是“摆放”,而是告诉工具:这些mem只能在指定区域里动。对于数据流已经明确的分组,我建议直接手动把fence画出来。画fence的时候心里要有数:fence不是越大越好,太大了mem会在里面乱跑,数据流方向又乱了;太小了放不下或者导致mem排成很奇怪的形状。一般先把一组mem估计一个大致面积总和,再按长宽比画一个约1.2到1.5倍的矩形,留一点buffer给自动摆放去调整。
2.2 用place_fp_macro跑一波“数据流自动摆放”
定完fence和halo,就可以上工具自动摆了。Innovus里负责macro摆放的命令主要是place_fp_macro,它支持基于数据流的自动摆放(不同版本叫法略有差异,老EDI里可能有类似选项)。
place_fp_macro -autoDf -halo {2 2 2 2} -instList [allMacros]-autoDf表示让工具根据网表连接关系来优化macro之间的相对位置。工具会计算各个macro之间的连接权重,然后把连接紧密的macro尽量放近。这个思路和手工找“铁哥们”是一样的,只是工具算得更细,能同时考虑几十上百个macro的相互连接。
不过要提醒一点:place_fp_macro -autoDf不等于“一劳永逸”。它对纯数字逻辑之间的mem效果不错,但对那种“模拟约束”、“特殊时序要求”、“多bit总线对齐”的场景,工具不一定知道你的深层需求。举个例子,一组总线宽度很大的mem,工具可能因为连接权重认为它们中间还需要放若干小cell,就把mem拆散了;但在物理设计上,你更希望它们并排摆成整齐的阵列,方便bus routing和shielding。这种时候,工具自动结果只能作为起点。
如果你不想完全自由自动,也可以自己指定初始位置,让工具只做局部调整。比如先用setInstancePlacement给关键mem一个初始位置,再跑place_fp_macro,加-dontRestore之类的保护选项(具体看版本),让工具保留一部分人工约束。
2.3 自动结果只是起点,手工精修查这几项
自动摆放完成后,别急着往下走,先做几项检查。
第一是查PIN方向。选中一个mem,看它的所有pin的平均朝向。如果mem的data pin朝右、但连到它的模块在左边,那这个mem基本是要旋转的,后面绕线一定多。Innovus里可以通过dbGet快速检查mem的orient和bbox,也可以直接在GUI里看pin的飞线聚集方向。
第二是查拥挤度和通道。跑一步快速拥塞预估(early global route或congestion estimate),看mem区域周围有没有大片的红黄色热点。红点通常出现在两个区域之间只有一条细通道的地方,需要判断是不是mem把路堵了,必要时去掉某些halo或把mem往边上挪一挪。
第三是查对齐网格。mem最好对齐到core的row或某条统一网格上,这样后面电源地轨、clock mesh都更好处理。如果工具摆出来的位置乱七八糟,用snapFpSite之类的命令(具体名称看版本)或手动微调把它们对齐。
我个人的经验是:自动摆放 + 手动精修的时间比例大概在3:7。自动帮你搞定70%的mass placement,剩下30%的“数据流关键节点”一定要自己看。比如最大的那几个SRAM、处在关键路径中间的那些register file,必须单独确认它们的位置和方向。
3. 一次完整实操:从数据流分析到mem落位
3.1 第一步:用飞线和连接密度锁定数据流方向
拿一个我最近做的模块举例。顶层有四个子模块:input_ctrl、compute_top、output_ctrl,以及一个central buffer区。mem分布如下:input_ctrl旁边有8颗input SRAM,compute_top里面有40多颗中间结果SRAM和register file,output_ctrl旁边有12颗output SRAM。
一开始我什么都不做,先加载netlist和 floorplan,打开GUI的flyline显示。结果很明显:input端口在左侧,出来的大量总线先连到input SRAM,再从SRAM连到compute_top;compute_top的中间结果又连到中间SRAM阵列和output_ctrl;最后输出在右侧。主数据流就是“左进右出”,中间经过若干存储节点。
再用dbGet扫了一下每个mem的top-5连接逻辑块,把连接数大于阈值的模块列入“强关联”列表。这一步是为了验证GUI飞线的直觉:
# 简单统计当前所有macro与模块的连接关系思路 set allMacros [dbGet top.insts.cell.type macro] foreach m $allMacros { set nets [dbGet $m.nets.name] puts "$m : [llength $nets] nets" }实际用的时候,我会写脚本把每个macro的net连接对象分类统计,例如“这个SRAM有32根net连接到compute_top,有8根连到output_ctrl”,这样就能定量地判断一个mem到底该靠近谁。
3.2 第二步:给mem分组并加fence/halo
数据流方向定了以后,分组几乎是自然的:input SRAM是一组,compute_top里的中间SRAM按子模块分成若干组,output SRAM是一组。我不会把几十个mem全丢在一个大fence里,那样工具很难摆出有序结构。
实际操作中我用类似这样的脚本:
# 按组别依次画fence addFence input_sram_grp -type hard -area {10 100 120 300} addFence comp_sram_grp1 -type hard -area {130 100 300 350} addFence comp_sram_grp2 -type hard -area {310 100 500 350} addFence output_sram_grp -type hard -area {520 100 650 300} # 对关键mem单独加更大的halo addHaloToInst -halo {5 5 5 5} comp_sram_grp1fence画好后,我会把每个fence内存的mem数量、mem总面积、预估标准单元面积大概加一下,确保fence面积没有太离谱。计算方法很简单:把mem的面积从LEF里读出来,加和,再除以目标利用率。比如一组mem总面积是20000um²,如果目标利用率0.7,那么fence面积至少给28500um²,我一般再乘1.2,给到34200um²左右,留出走线通道和多余度。fence画大了不要紧,那部分空隙可以作为布线缓冲,画小了后面肯定要返工。
3.3 第三步:逐个方向校正pin与orient
fence画完,我用place_fp_macro先跑一版自动摆放,然后开始人工检查。这一步最花时间,但也是整个floorplan最值钱的部分。
检查方法是把GUI切换到“只显示macro和它们的前几级连接”视图,然后逐个看mem的pin分布。比如有一颗input SRAM,它的数据输入来自左侧input_ctrl,数据输出去右侧compute_top。理想状态下它的pin应该左右分布,即library里的pin结构是左右出pin的朝向。如果工具给它摆成上下朝向,那肯定不对,我直接旋转:
# 将指定instance旋转为R0方向(具体orient依库而定) setInstancePlacement inst_name -orient R0这里要提一个容易被忽视的点:同一个mem在不同的fence里,最优orient可能不一样。你不能图省事让所有mem都朝一个方向,而是要看每个mem连到外部逻辑的方向。批处理可以做,但最好按组来,比如input组统一朝右、output组统一朝左、中间处理组上下对接,这样整体数据流看起来非常清晰,后面绕线也直。
另外,mem的pin如果分布四周,那就尽量让每条边的pin都平均对应周围的逻辑,不要出现“一堆pin朝着无人的空地”这种尴尬情况。
3.4 第四步:快速验证与迭代
摆完之后不能直接拍板,我一般会做三件事:快速congestion评估、快速时序估算、检查macro之间的间距。
# 检查macro间距是否有violation checkPlacement快速congestion评估我一般只跑early global route,不用全量布线。跑完看热力图,如果某些通道的congestion超过预设阈值(比如大于120%),记下来,回去调整对应区域的mem位置。这个环节通常会迭代两三轮。
一个有用的技巧:每次调整完,把当前的macro位置导出成脚本存档:
writeFpMacroPlacement flow_ok.macro_placement后面改乱了随时可以恢复回这个版本,省得手滑把好不容易调好的位置丢掉。不同版本的Innovus导出命令可能不同,老版本我记得是saveFpMacroPlacement之类,用之前可以查一下help write*。
三到五轮迭代之后,通常会得到一个“看起来舒服、飞线不交叉、拥塞可控”的mem摆放结果。这时候再去做standard cell place,你的起点就比之前高了一大截。
4. 常见问题与排障记录
4.1 局部拥塞爆红,怎么快速定位
最常遇见的坑是:fence画好了,mem也放对了,但某个小区域依然爆红。这种情况先别急着怀疑mem位置,用工具把congestion热点上覆盖的instance列表dump出来,看看里面是buffer多还是组合逻辑多。如果是buffer多,很可能是你的数据流主通道刚好从这里出去,位置本身没错,但缺少足够的绕线资源,这时候给这个区域多加一条走线通道往往就解决了。
如果热点紧贴着mem的边缘,那大概率是mem的halo不够,或者mem的pin方向导致所有信号都从一个角落挤出去。我的处理办法是:在热点侧给mem额外加一层halo,或者把mem旋转180度,让pin分布更均匀。
还有一种隐蔽原因:多个mem的clock pin都被摆到相邻位置,导致CTS buffer在那一小片扎堆。这种看一眼clock pin分布就能发现,需要做的是调整mem的orient,让clock pin朝不同方向分散开,或者把同一个时钟域的mem在fence内做小幅位移避免clock pin叠在一起。
4.2 mem之间走线通道不够,绕得飞起
有时候mem之间的间距确实留了,但布线时发现中间有大量宽总线要通过,通道还是不够。这里要分清“通道面积”和“通道可达性”的区别。面积够,但如果通道两侧被其他mem的halo挡住,入口很窄,总线进不来,一样会绕。检查方法就是在GUI里沿着通道走一遍,看有没有“瓶口”。
解决的办法很直接:把通道两侧的mem向外挪一点,保证通道宽度在整个路径上是均匀的。不要只在中间加宽,两边入口窄,等于白加。还有,通道内部尽量不要放标准单元,因为布线阶段这些位置需要给via留空间。可以用placement blockage挡住:
createPlacementBlockage -area {x1 y1 x2 y2} -type hard4.3 自动摆的mem太散,CTS做不干净
place_fp_macro -autoDf自动出来的结果,有时候会把同一组mem拆得很散,表面上没有congestion问题,但CTS阶段很痛苦。因为同一个时钟域的mem分散后,clock tree的depth和skew非常难控制。
我后来习惯在自动摆放之后,用脚本检查每个fence内的mem是否都还在原fence里,如果工具把某些mem扔到别处去了,我会重新手动把它们放回所属fence,甚至可以加一个更小的硬fence限制它们。CTS敏感的mem组,比如register file、大容量的SRAM array,我甚至不建议用-autoDf全自动,直接手工把它们的相对位置固定好,再让工具只摆无关紧要的小macro。
4.4 顺手收藏:几条高频命令速查
这里把上面提到的常用命令汇总一下,方便直接抄:
| 操作目的 | 参考命令 |
|---|---|
| 设置floorplan边界 | floorPlan -r ratio util left bottom right top |
| 给mem加halo | addHaloToInst -halo {d d d d} inst_list |
| 创建fence | addFence name -type hard -area {x1 y1 x2 y2} |
| 自动摆放macro | place_fp_macro -autoDf |
| 手动设置instance位置朝向 | setInstancePlacement inst -orient R0 |
| 检查placement合法性 | checkPlacement |
| 快速拥塞评估 | reportCongestion -hotspot(按版本确认) |
| 导出当前macro摆放 | writeFpMacroPlacement file |
再补充一个经常被问到的技巧:怎么在Innovus里快速选中某个名字带特定特征的标准单元或pg term。比如有人想选中名叫biasnw的PG term,可以这样:
# 先找到名为biasnw的instance的pgTerms dbGet [dbGet top.insts.name biasnw].pgTerms.name # 如果要模糊匹配所有类似名字的instance dbGet [dbGet top.insts.name *biasnw*].pgTerms.name这条命令在排查PG连接、分析IR drop或者做特殊net查证时非常实用,也算是一个额外小抄。
5. 一些值得养成的实操习惯
5.1 把摆放模板脚本化
同一个项目迭代版本非常多,每次重新读入netlist之后,如果把几十个fence、halo、orient重新手画一遍,既慢又容易出错。我通常会把floorplan阶段的设置全部整理成一个Tcl脚本,里面按组注释好每个区域是干什么的、为什么这么画,每次新版本直接source一遍,再根据具体情况微调。这样做还有个好处:换人接手时,光看脚本就能快速理解整个floorplan的思路。
脚本里我会把每个fence的逻辑作用写清楚,比如# input SRAM buffer region, data flow left->right。等过几个月再回来看,也能很快回忆起当初为什么这么摆。
5.2 不同时钟域或宽总线单独划区
如果设计里有多个时钟域,mem摆放时尽量按时钟域划分物理区域。这个习惯能帮你省掉大量CTS阶段的时间。不同时钟域之间的mem混放,表面上可能没有违例,但跨时钟域的线通常更长,约束也更多,放一起会互相干扰。
宽总线也是个敏感点。比如128bit或256bit的bus,如果可能,把它们的源端、目的端、中间mem摆成一条直线,别让总线里面弯来弯去。这个“直线原则”在布局初期多花五分钟,后面布线阶段能省一天。
5.3 早期评估,别等时序乱成一锅粥再调
有些朋友喜欢所有东西都放完、跑完CTS再回头看问题,那时候改floorplan成本极高。我的建议是:每调整一轮mem摆放,都做一次快速时序评估(哪怕没有完整约束,只做estimate),看看关键路径的趋势。如果某些路径因为摆放变长了,立刻微调,别拖到后期。
早期评估不要求绝对准确,只要趋势一致就行。比如某条关键路径从500ps变成700ps,说明这轮摆放可能把数据流带偏了,回去看飞线,十有八九是某个mem的位置不对。这种“小步快跑”的验证节奏,比一次性把所有东西放到位再检查要高效得多。
最后说说我自己的体会:mem多不是问题,问题是你能不能站在数据流的角度看floorplan。每次摆mem之前,我都会问自己一句:如果我是这捆数据线,从这里走到那里,我希望路上有没有墙?答案越清晰,floorplan就越顺。希望这篇笔记也能帮你找到那种“一眼看去就知道数据在怎么流动”的摆放状态。