☰
calibredrv GDS合并深层机制:cell重名、层号与dbu避坑
2026/10/29 5:54:01 网站建设 项目流程

做版图集成的人几乎都经历过这种场面:两份 GDS 放进同一条命令里跑完,工具不报错,日志干干净净,打开版图一看,某个 IP 宏的内部少了两块 metal,某个本该出现的小模块变成了一个空壳子,或者 top cell 的位置整体偏了几十微米。更麻烦的是,这些现象在当时都不会报错,要等到 LVS 跑出几百条 unmatched,或者等到流片前的最后一道检查才发现,人已经很难回头怀疑是 merge 那一步的问题。

这是这个系列第三篇。前两篇聊的是 calibredrv 里 merge GDS 的基础命令怎么写、常用开关有哪些,属于"能跑起来"的层面。这一篇我想往深一层走,聊命令背后那些默认行为:工具以什么为单位组织数据、遇到同名 cell 时它悄悄替你做了哪个决定、层号在合并过程中是怎么被对齐的、dbu 不一致会带来多少看不见的误差。这些东西在帮助文档里通常只有一两句话,但恰恰是决定 merge 结果对不对的关键。下面这些内容适合已经能跑通基本 merge、但被结果不一致坑过的人看,也适合刚开始接手版图交付集成的同学当作检查清单。

1. 先搞清楚 merge 到底在合什么:GDS 的主键是 cell 名

1.1 cell 名才是 GDS 的真正身份标识

GDS 文件本质上是一个扁平的记录流,里面有 HEADER、BGNLIB、UNITS 这些文件级信息,然后是一串 STRUCTURE 记录。每一个 STRUCTURE 就是我们在版图工具里看到的一个 cell,里面装着 BOUNDARY(多边形)、PATH、TEXT、SREF(单元引用)、AREF(阵列引用)这些元素。关键在于,SREF 和 AREF 记录里保存的不是指针,而是被引用 cell 的名字字符串。

这个设计决定了一件事:合并两份 GDS,本质上不是把两棵几何树拼在一起,而是把两个"名字字典"合并成一个。冲突天然发生在名字层面,而不是几何层面。这就好比把两个文件夹拖进同一个目录,同名文件必然要有一个处理规则,要么问你要不要覆盖,要么自动改名。工具不会替你判断哪个才是"对的",它只按既定策略执行。

很多人第一次 merge 出问题,就是因为脑子里想的是"两个版图叠在一起",而工具执行的是"两份 cell 名清单求并集"。这两个理解之间的差距,就是后面所有坑的来源。

1.2 同名 cell 撞车时,工具替你做的三种选择

在不同的命令组合和不同版本下,calibredrv 面对重名 cell 大致会走到三条路上去,风险差别非常大。我把它们列出来对比一下:

处理方式表现风险等级典型后果
后读入者覆盖先读入者版图打开看起来正常,cell 数不变高cell 内容被静默替换,引用它的所有位置一起变
自动加后缀重命名cell 数量变多,出现xxx_1之类中LVS 里多出 unmatched cell,需要人工认领
合并前主动重命名名字可控,带业务前缀低需要多写几行脚本,但结果可预测

第一种是最阴的。因为 cell 名对、位置对、层次深度也对,唯独里面的多边形换了内容。你在版图上放大看,可能只发现某块区域"颜色好像不太对",但如果这个 cell 是被几百个地方引用的标准单元,你根本没法逐个核对。我个人的习惯是:永远不让工具走到第一条路上,宁可多花两分钟做一次主动重命名。

主动重命名的思路很简单:在把源文件读进来之后、合并之前,把它的所有 cell 名统一加一个业务前缀,把命名空间隔离开。示意脚本大概是这个样子:

# 示意:给一份 GDS 的所有 cell 加前缀后另存 # 具体子命令名请以你所用版本的 help 输出为准 set L [layout create -dt_expand] $L gdsread src_ip.gds foreach c [$L cells] { $L rename cell $c "IP1_$c" } $L gdswrite src_ip_prefixed.gds

加前缀这件事还有额外好处:以后在合并后的大库里,只要看到带IP1_前缀的 cell,你就知道它的来源,排查问题时能顺着名字回溯到具体交付件,比在一堆无意义的名字里瞎猜高效得多。

1.3 合并前先摸清每份文件的 top 和 dbu

在动手 merge 之前,有两件事必须先查清楚,否则后面全是被动救火。

第一件是每份文件到底有几个 top。GDS 格式里并没有一个字段显式标注"我是顶层",工具判断 top 的方式是排除法:把所有 cell 名的集合减去所有被引用过的 cell 名集合,剩下的就是没有被任何人引用的 cell,也就是 top 候选。问题在于,一份从项目里导出的 GDS 经常会带上几个历史遗留的孤立 cell——它们不被任何 cell 引用,也不参与实际版图,但会被算作 top 一起 merge 进来。合并后你发现库里多出一堆莫名其妙的顶层,占空间还干扰 LVS,根因往往在这里。

判断逻辑用脚本表达就是两步集合运算:先收集$L cells的全部名字,再遍历每个 cell 收集它的引用列表,两者做差集。如果差集里不止一个名字,就要确认哪个才是真正的交付顶层,其余的在 merge 前删掉。

第二件是dbu 是否一致。GDS 头部的 UNITS 记录定义了数据库单位和用户单位,常见配置是 0.001 微米(也就是 1nm)或者 0.0005 微米。当两份文件的精度不同时,合并会以目标库的精度为基准做换算,坐标不是整数倍的部分会被取整。举个具体的例子:源文件 dbu 是 0.5nm,某个多边形顶点坐标落在 1.5nm 处,目标库 dbu 是 1nm,这个点只能落到 1nm 或 2nm,误差 0.5nm。在成熟节点上这点误差无所谓,但在先进节点上,一条按 0.5nm 精度设计的间距规则,被取整误差吃掉之后,DRC 就会在那一片区域疯狂报错,而你盯着版图怎么也看不出哪里窄了。

查出 dbu 的办法各版本入口不太一样,比较稳妥的是把两份文件分别用 calibredrv 打开,查看库属性里的数据库精度,或者干脆各转存一次再比较文件头。这个动作花不了一分钟,但能省掉后面几小时的困惑。

2. 层次结构怎么留:wrapper top、局部 flatten 与全平的代价

2.1 别一上来就全 flatten,先把账算清楚

层次结构这东西,平时看不出价值,一旦全平就能体会到什么叫代价。GDS 里的 AREF 记录是极度紧凑的:一个 1000 乘 1000 的阵列,在文件里只占一条记录,描述了它引用的 cell 名、起点、列数、行数、列间距、行间距。而在内存和扁平化后的数据里,它是实打实的一百万份实例展开。

算笔账:假设阵列里每个单元有 20 个多边形,平均每个多边形 8 个顶点。保留层次时,这条 AREF 占用的数据量可以忽略不计;一旦 flatten,就是 2000 万个多边形、1.6 亿个顶点。按每个顶点几十字节的存储开销估算,光这一处就要吃掉好几个 GB 的内存,而整份版图里这样的阵列可能有几十上百个。所以"flatten 之后跑不动"不是工具的问题,是数据量的量级变了。

我把 flatten 的适用场景归成三类:一是需要对几何做布尔运算(比如合并某几层做逻辑推导),二是下游工具或交付格式不支持层次,三是某个子模块的层次组织混乱到影响后续脚本遍历。除了这三类,绝大多数时候都应该保留层次。

保留层次的代价也要说清楚:后续用脚本遍历时会麻烦一些,想找到某个具体位置的几何,需要沿着引用关系一层层跳下去,路径可能长到七八级。但这点麻烦比起内存爆炸,完全是值得的。

2.2 造一个 wrapper top,让多份 GDS 有序同居

最常用的合并方式其实是这种:每份源 GDS 保持自己的层次和顶层不动,在合并后的库里新建一个顶层 cell,然后把各个源顶层作为实例放进这个新顶层里,各自摆放到位。这样做的好处是来源清晰、可回溯、可单独替换某一个模块,也符合版图集成的实际工作方式——顶层本来就是个拼装车间。

思路是三步:读入所有源文件,新建一个空 cell 作为 wrapper,在新 cell 里创建指向各源顶层的引用并指定坐标。示意代码:

# 示意:新建 wrapper top 并放入两个源顶层 set L [layout create -dt_expand] $L gdsread block_a.gds $L gdsread block_b.gds $L create cell CHIP_TOP # 坐标单位是 dbu,不是微米! $L create ref CHIP_TOP A_TOP 0 0 $L create ref CHIP_TOP B_TOP 12000 0 $L gdswrite merged.gds

这里必须重点提醒一个高频错误:坐标参数的单位是 dbu。很多人下意识按微米填,写了1000以为是往右挪 1000 微米,实际在 1nm 精度的库里那是 1000nm,也就是 1 微米。两块版图几乎完全重叠在一起,肉眼看版图时还以为合并成功了,实际上所有东西都糊在一处。正确填法是把微米数除以 dbu 得到整数,比如要偏移 500 微米,1nm 精度下就填 500000。

2.3 局部 flatten 的操作边界:先平还是先合

有时候确实需要 flatten,但只需要 flatten 一小块。典型的场景是:某个 IP 的 pin 层命名和我们自己的规范不一样,需要把它的 pin 层统一掉;或者某个子模块的层次深得离谱,需要压平减少遍历开销。

这里有个顺序问题值得单独说:先 flatten 再 merge,还是先 merge 再 flatten,结果可能不同。

如果先合并再对某个 cell 做 flatten,工具需要对合并后的大库做一次层次遍历和展开,处理的数据量是整个合并结果;如果先对源文件局部 flatten、再参与合并,处理的数据量只有那一份文件。在大版图场景下,后者的耗时可能只有前者的几分之一。所以我的一般原则是:能在源文件里做完的处理,就不要留到合并之后。

另一个边界是 flatten 的粒度。flatten 一个 cell 和 flatten 一个实例是两件完全不同的事。前者是把该 cell 内部的引用关系打散、把子单元的内容提升到该 cell 里;后者是把这个实例替换成实际几何。如果只想消除某处的引用关系又不想影响同名的其他实例,就得先把这个 cell 复制一份、改名,再对副本做处理。这个操作在版图集成里很常见,因为同一个标准单元在不同位置可能有不同的处理需求,不复制就会互相牵连。

3. 层号对齐:merge 里最容易悄无声息出错的一环

3.1 GDS 的层是 (layer, datatype) 二元组

这一点值得反复强调:GDS 里的层不是一个数字,而是一个二元组。BOUNDARY 记录里同时带着 layer 号和 datatype 号,两个数字一起才唯一确定"这属于哪个物理层的哪种用途"。

以常见的配置为例:metal1 的绘制图形在 (10, 0),metal1 的 pin 标识在 (10, 1),metal1 的 label 可能在 (10, 2) 或者走 TEXT 记录,metal1 的填充图形(fill)在 (10, 3)。它们在版图上都落在同一块金属区域内,但在数据意义上承担完全不同的职责——LVS 就是靠 pin 层来识别端口的。

问题在于,某些工具或某些设置下,(layer, datatype) 会被折叠成单一的 layer 名,也就是只看 layer 号、忽略 datatype。一旦折叠发生,drawing 层和 pin 层就混成一团,LVS 找不到 pin,直接报出一堆端口不匹配。而版图打开看是没有区别的,因为几何本来就重叠在一起。这类问题排查起来非常折磨人,因为"看起来什么都没错"。

我的习惯是在创建目标库时就明确指定 datatype 不被折叠的处理方式。calibredrv 的layout create有一个-dt_expand相关的开关(不同版本参数名可能略有差异,用之前先help确认一下),作用是让 datatype 参与层的区分。这个参数加上去,后续所有层操作都会以二元组为单位,安全得多。

3.2 两份 layer map 不一致时的对齐手法

真正麻烦的不是同一家工艺内部的合并,而是跨来源的合并。比如一份 IP 用 4/0 表示 metal1,我们自己的顶层用 10/0 表示 metal1,两边的层号体系完全不同。这种情况下直接 merge,得到的库里同一物理层会有两套编号,DRC 和 LVS 都会疯掉。

处理办法是在合并前做层号重映射。calibredrv 读取 GDS 时可以带一份 layer map 文件,把源文件的层号映射到目标层号上。map 的内容大致是"源 layer/源 datatype → 目标 layer/目标 datatype"的对应关系,实操中我是这样整理对照表的:

来源源 layer/datatype物理含义目标 layer/datatype
外部 IP4/0metal1 drawing10/0
外部 IP4/1metal1 pin10/1
外部 IP5/0via111/0
自有顶层10/0metal1 drawing10/0(保持不变)
自有顶层10/1metal1 pin10/1(保持不变)

整理这张表的动作本身就有价值:它逼着你去确认两边每一个层的语义,而不是想当然地认为"层号一样就是同一层"。我见过不止一次,两个来源的层号碰巧相同,但一个是 poly 另一个是 diffusion,直接合并之后 DRC 报出一堆莫名其妙的短路,查了半天才发现是概念层面的错配。

层号重映射的另一个注意点:要覆盖全部用到的层,包括你平时不太关注的层。text 层、boundary 层、fill 层、dummy 层往往容易被漏掉,漏掉之后它们会保持源编号进入合并库,变成一堆"孤儿层",在后续检查里以"未知层"的形式出现。

3.3 merge 后层漂移的三步自查

做完合并,怎么快速确认层没有漂?我用的是一套三步法,不需要看懂版图,纯靠数字比对:

第一步,合并前统计每份源文件每一层的多边形数量。用脚本遍历每个 cell 的每个层,累加计数,输出一张"层号 → 数量"的表。

第二步,合并后统计整个库的同口径数据。

第三步,比对。合并后的数量应该等于各源文件对应层数量之和,中间可能有层号重映射带来的归并(比如两个源层映射到同一个目标层,那就要相加)。任何一层的差值不为零,就说明这一层出了问题——要么漏了、要么被合并了、要么被别的层吃掉了。

这个方法的优势是定量。版图看半天看不出问题的地方,数字一比对立刻显形。我自己遇到过 datatype 被折叠导致 pin 层多边形数量直接归零的情况,就是靠这张表一眼看出来的。

4. 大文件合并的耗时与内存:分块、增量与引用优化

4.1 内存到底花在哪几个地方

跑大版图 merge 卡死或者被系统杀掉进程,绝大多数时候不是 CPU 慢,而是内存墙。搞清楚内存花在哪,才能对症下药。按我的经验,开销大致集中在这几个方面:

开销来源影响因素优化方向
多边形总数cell 内 BOUNDARY 数量 × 实例层次保留层次、用 AREF
顶点总数每个多边形的顶点数简化高密度多边形、避免无意义折点
TEXT 与属性标注数量、属性条目数合并前清理无用标注
层次深度引用链长度压平过深的链
中间数据结构一次性读入的文件数分批合并

其中顶点总数这一项最容易被低估。一个看起来"多边形不多"的版图,如果每个多边形都是几百个顶点的不规则形状(比如某些模拟模块的保护环、某些 fill 图案),顶点总数可能是多边形数的几十倍。在估算内存需求时,用"多边形数 × 平均顶点数"去算,比只看多边形数准确得多。

另一个经常被忽视的是一次性读入的文件数量。有人习惯把十几个 GDS 一股脑塞进一次合并,觉得这样省事。但工具需要同时持有所有源库的数据结构,峰值内存是叠加的。分成三轮、每轮合并三到四个,峰值内存可能降到三分之一,总耗时反而更短,因为避免了内存交换。

4.2 分块合并与增量写出的脚本骨架

分块合并的套路是:先把源文件分组,每组合并成一个中间文件并写盘,再把中间文件合并成最终结果。写盘这一步很关键,因为它把内存里的数据释放掉了,让下一轮从干净状态开始。

# 示意:分轮合并,避免峰值内存叠加 proc merge_batch {outfile filelist} { set L [layout create -dt_expand] foreach f $filelist { $L gdsread $f puts "loaded $f" } $L gdswrite $outfile puts "written $outfile" # 返回后 L 出作用域,内存释放 } # 第一轮:把十几个源文件压成三个中间件 merge_batch mid_1.gds [list a1.gds a2.gds a3.gds a4.gds] merge_batch mid_2.gds [list b1.gds b2.gds b3.gds] merge_batch mid_3.gds [list c1.gds c2.gds] # 第二轮:合并中间件 merge_batch final.gds [list mid_1.gds mid_2.gds mid_3.gds]

这个骨架有个细节:每次merge_batch返回后,局部变量L就失去了引用,Tcl 的垃圾回收会把这块内存还回去。如果你把所有操作都写在一个大 proc 里,所有 layout 对象会一直活着,分轮的意义就没了。这一点在脚本层面很容易被忽略,但在实际跑大文件时差别非常明显。

中间文件要不要保留,取决于你的调试需求。我的习惯是保留最后两轮的中间件,前面的删掉——万一最终结果有问题,可以从中间件往回定位到是哪一批出了问题。

4.3 用引用代替硬拼,控制多边形数量的思路

还有一种优化比技术手段更根本:在设计层面减少需要合并的几何。

版图里存在大量重复结构:标准单元、存储器的 bitcell 阵列、电源网格的 via 阵列、保护环的重复单元。这些结构如果用引用表达,数据量是常数级的;如果被展平,就变成线性甚至平方级。所以在 merge 前后检查复用率是个好习惯。

复用率的粗略算法是:多边形总数除以 cell 实例总数。如果这个比值高得离谱(比如几万个多边形每个实例),说明要么确实有超复杂的单元,要么某个本该被引用的结构被展平了。拿这个指标和合并前各源文件的数据做对比,能快速定位是不是 flatten 在不该发生的地方发生了。

另一个实操技巧是善用 AREF 而不是 SREF 阵列。如果源文件里一个 100×100 的阵列是用一万个独立 SREF 表达的(有些工具导出时会这样),合并后可以识别出这种规则排列并替换成 AREF,数据量能降两个数量级。这一步需要写脚本做模式识别,但收益巨大。

5. 合完怎么验:三个数字加一套冒烟检查

5.1 cell 数、bbox、多边形数,三量核对

merge 完之后,不管日志多干净,我都会强制自己跑这三个指标:

指标获取方式异常含义
cell 总数遍历库的 cell 列表比预期多说明有重名重命名或孤立 cell;比预期少说明有覆盖
整体 bbox所有 top 的包围盒并集尺寸变化说明有东西位置错了或丢了
多边形总数全库累加差值不为零说明有层被吞或有几何丢失

这三个数字的意义在于,它们都是绝对量,不依赖你去理解版图内容。合并前记下每份源文件的数值,合并后算出理论值,一比对就知道有没有出事。理论值的算法很简单:cell 数按命名空间是否重叠分别处理,bbox 取各源 top 的并集(如果按 wrapper 摆放,要加上偏移量),多边形数直接相加。

bbox 这一项特别值得强调。很多人只核多边形数,觉得数量对上了就没问题。但数量对而位置错的情况太常见了——比如 wrapper 里的偏移量填错、或者某个 AREF 的行列间距被改动。bbox 一旦变化,说明空间的整体占用改了,这时候再去查偏移量、查阵列参数,方向很明确。

5.2 用最轻的 DRC 做冒烟检查

数字核对能抓出量级的错误,但抓不出语义的错误——比如层号重映射表填错了一行,两个本来不同物理层的东西被合并到一起,数量上可能刚好对得上,但物理上完全错了。

这时候跑一遍轻量 DRC 是最划算的。所谓轻量,是指只启用几条最基础、跑得最快的规则:某几层的最小宽度、最小间距、以及层的存在性检查。不需要全套规则,全套可能要跑几小时,轻量版几分钟就出结果。

这一步的价值在于,它能同时验证三件事:层号是对的(层存在性检查通过)、坐标是合理的(宽度间距不违规)、几何是完整的(不出现奇怪的碎片)。如果轻量 DRC 报出的错误集中在某一个小区域,那基本可以锁定是那一块合并时出了问题;如果全图均匀地报错,那多半是层映射整体错了。

5.3 一份可复用的自检脚本

把上面这套检查固化成脚本,每次 merge 后自动跑一遍,是我这些年最值回票价的一个习惯。骨架大概是这样:

# 示意:合并后自检,输出三量 + 分层统计 proc audit_layout {gdsfile} { set L [layout create -dt_expand] $L gdsread $gdsfile set cells [$L cells] puts "CELL_COUNT: [llength $cells]" set total_poly 0 foreach c $cells { # 统计该 cell 自身的多边形数(不含子单元) # 具体子命令名以版本 help 为准 set n [$L polycount $c] incr total_poly $n } puts "POLY_COUNT: $total_poly" foreach c $cells { set bb [$L cellbbox $c] puts "BBOX $c: $bb" } }

脚本关键不在于写得多漂亮,而在于每次都跑、每次都留档。把每次 merge 的输出存成文本文件,出问题时可以直接和上一次成功的版本做 diff,哪一项变了立刻定位。我自己的目录里常年存着几十份这样的审计日志,回头查某个版本是哪天开始不对劲,翻日志比翻版图快一百倍。

6. 几种实际场景下的 merge 组合拳

6.1 外部交付件与自有顶层的拼装

这是最常见的场景:拿到一份外部 IP 的 GDS 加一份 LEF 或者一份端口说明,要把它塞进自己的顶层版图里做集成验证。

这个场景的坑集中在三处。一是命名空间,外部 IP 的 cell 名往往带很长的前缀或者奇怪的字符,直接合并容易和自己的命名撞车,务必先加业务前缀。二是层号体系,外部 IP 用自己工艺的层号,必须做映射,映射表要逐层核对,特别别漏 text 层和 pin 层。三是摆放精度,IP 的 pin 位置通常是按它的 dbu 算好的,如果两边 dbu 不同,pin 落到的实际坐标会有亚纳米级的偏移,接不上自己的金属。所以在集成前,先把两边的 dbu 对齐,是一个不该省的动作。

做完这三步,我一般会专门画一个只包含 pin 层的视图,看一眼两边的 pin 是不是严丝合缝对上了。这个检查比跑 LVS 快得多,也直观得多。

6.2 标准单元库与存储器宏的合并

这个场景的特殊性在于数据量。标准单元库动辄几百个 cell,存储器宏内部有巨大的 bitcell 阵列。合并这两个东西,最容易踩的是阵列展开。

存储器宏里的 bitcell 阵列通常用 AREF 表达,几百万个 bitcell 在文件里就是一条记录。但如果合并过程中因为某种原因触发了 flatten(比如目标库不支持某个层次的表达、或者中间经过了某个不支持层次的格式转换),这几百万个实例瞬间展开,文件从几十 MB 涨到几十 GB,内存直接见底。

防范办法是在合并后立刻查两个数字:cell 数量和实例数量。如果实例数量远小于预期,说明阵列被展开了。这时候别急着重跑,先回头查中间有没有经过格式转换环节——很多时候问题不在 merge 本身,而在中间那一步转换。

另外一点经验:标准单元库合并时,建议按功能分组、分批合并,不要一次性把整个库塞进去。分成 IO、标准逻辑、特殊单元三批,每批独立验证,出问题好定位。

6.3 版本对比:合并之后怎么做差异检查

最后一个场景是版本管理。同一份版图有两个版本,想快速知道改了什么,靠肉眼比对是不现实的。可行的做法是分别合并成两个结构一致的库,然后做几何差分。

差分的思路是分层、分 cell 地做布尔运算:A 减 B 得到"删掉的",B 减 A 得到"新增的",两者合起来就是改动区域。然后把改动区域单独输出成一个 GDS,在版图工具里高亮看。这个方法在检查 ECO 是否改对了位置时特别有用——你能直接看到改动的形状落在哪里,而不是靠坐标数字去脑补。

做差分有个前提:两边的层次结构要尽量一致,否则同一个几何在两边的 cell 归属不同,差分结果会是一堆碎片。所以在做版本对比前,最好先把两边都按同一套规则整理一遍(同样的 flatten 深度、同样的 cell 命名),差分才有意义。

最后分享一个我自己的习惯:每次做 merge,不管多简单的两个文件,我都会在同一个目录下留三个东西——合并脚本、审计日志、以及一份层映射表。半年后回头看,这三个文件能让你在十分钟内重建当时的环境,而不是对着一份不知道从哪来的 GDS 发呆。版图集成这活儿,最贵的从来不是算力,是"当时到底怎么弄的"这个问题的答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询