做版图的同行大概都经历过这种时刻:花了两周时间把一块模块的版图收拾得干干净净,DRC 清了、LVS 过了,打包一并发给对接方,两天后收到一句话——“你这份 GDSII 坐标精度和我们的库不一致,层号表也对不上,麻烦重出。”于是又得回去翻 PDK 文档、翻工艺说明,重新设单位、重导层映射,一整天的活。这类问题几乎全部出在数据交换这一环,而不是设计本身。GDSII 就是这样一份“人人都得用、但很少有人真正读一遍规范”的格式。它诞生在上世纪八十年代,到今天依然是版图数据在设计与制造之间流转的主干道。这篇文章我想把它彻底拆开讲一遍:文件里到底写了什么、那些字段为什么这么设计、用脚本怎么生成和自查、从一份 GDSII 到最终交付给制造方中间还会被改造成什么样、以及我这些年踩过的那些坑。不管你是刚接触版图的学生,还是已经做过几次交付的老手,下面这些内容都能直接拿去用。
1. GDSII 到底是什么:一份活化石级的版图交换协议
1.1 它存的不是“设计”,而是“一堆带层号的几何图形”
先把最容易误解的一点说清楚:GDSII 里没有晶体管、没有网表、没有器件参数,也没有任何设计意图。它只记录了几何信息——某个层号上有一个多边形,顶点坐标是这些整数;某个位置上引用了一个叫某某的子单元,旋转了多少度、放大了多少倍。除此之外,它对图形代表什么一无所知。
这种“无知”恰恰是它能活四十年的根本原因。任何一种数据格式,只要试图承载设计语义,就必然和某一家工具的模型绑定,换个平台就得做语义映射,映射过程必然丢信息。而 GDSII 把自己压缩到最底层:只搬运几何。任何工具都能读写,任何流程都能拼接,代价是你必须自己保证几何的正确性,格式不会帮你。
我常用一个类比:GDSII 就像一张只标注了线宽和层高的建筑平面图,它告诉你墙在哪里、多厚,但不告诉你这间是卧室还是厨房。施工队照着能盖出来,但盖出来是不是你想要的,得靠你自己核对。
1.2 一个文件里同时装着“图”和“图章”
GDSII 是层次化格式,这一点很多人用过但没深想过。它把版图组织成一棵结构树,每个节点叫一个结构(structure,常被叫作 cell),树根是顶层结构,叶子是只含几何的基础单元。上层结构通过引用(reference)把下层结构“盖”到自己身上,可以是单个引用,也可以是一次性排出 m 行 n 列的阵列引用。
这个设计的价值在于复用。一个标准单元在版图里出现几千次,文件里只存一份几何加几千条引用记录,体积能差出三四个数量级。但代价也很明显:任何一次引用变换(平移、旋转、镜像、缩放)都会累积误差,而且不同工具对嵌套引用的变换合成顺序处理不一定完全一致。我在做跨平台数据转换时遇到过顶层整体镜像、下层再有旋转的情况,两边算出来的位置差了半个图形宽度——原因就是变换矩阵的乘法顺序理解不同。
1.3 为什么 OASIS 出来了二十年,GDSII 还在一线
OASIS 在压缩率、坐标范围、重复结构表达、字符串表等方面全面优于 GDSII,一个典型版图转成 OASIS 通常能小五到十倍。按理说早该替换了,但现实是它只替换了一半。
原因很实在:一是生态惯性,参考流程、检查脚本、验收规范全是围绕 GDSII 建的,改一处要牵动一整条链;二是可读性,GDSII 结构简单到可以手写解析器,出问题时能快速定位,OASIS 的变长编码和压缩表让排错成本高不少;三是“最小公约数”效应——你的上下游可能有一方只认 GDSII,那你就得交 GDSII。
下面这张表是我在实际项目里做的取舍参考,不一定适用于所有场景,但方向大体一致:
| 维度 | GDSII | OASIS |
|---|---|---|
| 坐标表示 | 32 位有符号整数 | 变长整数,范围大得多 |
| 典型体积 | 基准 | 约为 GDSII 的 1/5 到 1/10 |
| 重复结构 | 只支持规则阵列 | 支持多种重复方式,表达力强 |
| 字符串 | 定长记录,逐个存 | 字符串表,重复内容只存一次 |
| 解析难度 | 低,可手写 | 中高,需处理压缩表 |
| 工具兼容性 | 事实标准,几乎全支持 | 主流工具都支持,老旧脚本未必 |
| 常见用法 | 交付、跨平台交换 | 内部存储、超大版图归档 |
我自己的习惯是:内部工作副本用 OASIS 存,减少磁盘和加载时间;对外交付前统一转 GDSII,并保留一份转换日志,记录两边的图形数量、层号集合、包围盒,方便对方核对。
1.4 它在整条链路上的位置
一份版图从设计者手里到晶圆上,会经过这几个环节:版图编辑与验证(DRC、LVS、寄生提取、后仿)、交付数据准备、掩模数据生成、掩模制造、光刻。GDSII 主要活跃在前两个环节,但从第三个环节开始,它就会被改造成别的形态。
关键在于,每经过一次交接,数据就可能变形一次。你在编辑器里看到的多边形,到了掩模数据里可能已经被切成了几十个梯形;你标注的层号,到了对方系统里可能被映射成了完全不同的编号。所以理解 GDSII 不能只理解“怎么存”,还要理解“它会被谁、以什么方式读走”。
2. 拆开二进制:文件结构与你真正需要关心的字段
2.1 一切都是记录:四字节头加数据体的极简结构
GDSII 是纯二进制、大端序(高位字节在前)。整个文件由一条条记录首尾相接构成,没有全局索引,没有文件头魔数,解析器就是从头读到尾,遇到 ENDLIB 就停。
每条记录的结构极其规整:前 2 个字节是记录总长度(包含这 2 字节本身),第 3 个字节是记录类型,第 4 个字节是数据类型,后面是数据体。长度字段是 2 字节无符号整数,所以单条记录最长 65535 字节,这也是为什么复杂多边形的顶点必须拆成多条 XY 记录。
数据类型只有六种:0 表示无数据体,1 是位数组(2 字节),2 是 2 字节有符号整数,3 是 4 字节有符号整数,5 是 8 字节实数,6 是 ASCII 字符串。注意 4 号(4 字节实数)在规范里保留但基本不用,遇到的话按 8 字节处理多半会错位。
拿文件第一条记录举例,一个 6.0 版本的 GDSII 开头是这六个字节:
00 06 00 02 02 58拆开看:00 06是长度 6,00是记录类型 0(HEADER),02是数据类型 2(2 字节整数),02 58是值 600,代表版本 6.0。整份文件就是靠这种“自描述”的记录串起来的,一条读错,后面全乱,所以解析器必须严格校验长度字段。
2.2 主要记录一览:真正需要盯的就那么十几个
规范里定义的记录类型有几十种,但日常打交道、需要关心语义的其实不多。我把常用的整理成了下面这张表,编号是我按规范核对的,实际写解析器时建议对着正式文档再确认一遍,因为不同版本的说明文档在个别条目上有出入。
| 记录名 | 编号 | 数据体 | 作用与注意点 |
|---|---|---|---|
| HEADER | 0 | 2 字节整数 | 版本号,600 表示 6.0 |
| BGNLIB | 1 | 12 个 2 字节整数 | 库起始,含修改时间和访问时间各 6 项 |
| LIBNAME | 2 | 字符串 | 库名,规范限制 32 字符 |
| UNITS | 3 | 2 个 8 字节实数 | 数据库精度定义,全文最关键的两条数据 |
| ENDLIB | 4 | 无 | 库结束,缺失基本等于文件被截断 |
| BGNSTR | 5 | 12 个 2 字节整数 | 结构起始,同样带时间戳 |
| STRNAME | 6 | 字符串 | 结构名,规范限制 32 字符 |
| ENDSTR | 7 | 无 | 结构结束 |
| BOUNDARY | 8 | 无 | 多边形开始标志 |
| PATH | 9 | 无 | 路径开始标志 |
| SREF | 10 | 无 | 单元引用开始 |
| AREF | 11 | 无 | 阵列引用开始 |
| TEXT | 12 | 无 | 文本标注开始 |
| LAYER | 13 | 2 字节整数 | 层号 |
| DATATYPE | 14 | 2 字节整数 | 数据类型(同一层内的子分类) |
| WIDTH | 15 | 4 字节整数 | 路径线宽,负数表示绝对宽度 |
| XY | 16 | 4 字节整数序列 | 坐标点,成对出现 |
| ENDEL | 17 | 无 | 元素结束 |
| SREFNAME | 18 | 字符串 | 被引用的结构名 |
| COLROW | 19 | 2 个 2 字节整数 | 阵列的列数、行数 |
| TEXTTYPE | 20 | 2 字节整数 | 文本类型 |
| STRING | 22 | 字符串 | 文本内容 |
| STRANS | 23 | 位数组 | 变换标志,含镜像位和绝对标志位 |
| MAG | 24 | 8 字节实数 | 放大倍率 |
| ANGLE | 25 | 8 字节实数 | 旋转角度,逆时针为正 |
| PATHTYPE | 30 | 2 字节整数 | 路径端点样式,0 方头 1 圆头 2 延伸半宽 4 自定义 |
| PROPATTR | 41 | 2 字节整数 | 自定义属性名 |
| PROPVALUE | 42 | 字符串 | 自定义属性值 |
| BOX | 43 | 无 | 注释框,多数流程直接忽略 |
| BOXTYPE | 44 | 2 字节整数 | 注释框类型 |
看清楚这张表,你会发现一个事实:GDSII 能表达的东西真的很少。没有贝塞尔曲线、没有圆弧、没有布尔运算、没有参数化。所有复杂形状都必须先被离散成折线,所有曲线都必须先被切成短线段。这是它“通用”的代价,也是很多精度问题的源头。
2.3 八字节实数:这份格式里最容易翻车的地方
GDSII 的实数字段不是 IEEE 754,而是一套以 16 为基的浮点表示。八个字节里,第一个字节拆成 1 位符号位加 7 位指数,后七个字节是 56 位尾数。公式是:
值 = (-1)^符号 × 尾数 × 16^(指数 - 64)其中尾数被解释为一个纯小数,取值必须落在 [1/16, 1) 区间内,靠指数钳制来保证。这个“必须落在该区间”就是规格化的含义。
举个具体例子,数值 1.0 的编码是:
41 10 00 00 00 00 00 00推导一下:1.0 需要写成尾数乘 16 的幂,取尾数 0.0625(也就是 1/16),指数就是 1,因为 0.0625 × 16 = 1.0。指数编码 = 64 + 1 = 65 = 二进制 1000001,符号位 0,拼起来第一字节是 0100 0001,即 0x41。尾数 0.0625 的二进制是 0.0001,放进 56 位里就是最高 4 位为 0001,其余全 0,也就是 0x10 后面跟六个 0x00。
这个格式的精度大约相当于 16 位十进制有效数字,表示常见单位值完全够用。但坑在于:它不像 IEEE 754 那样人人熟悉,很多自制解析器会直接用struct.unpack('>d')去读,结果读出来是垃圾数,然后 UNITS 就变成了天文数字,整个版图的坐标全错。我自己早期写过一个转换脚本就犯了这个错,症状是所有图形缩到了原来的千万分之一,看起来像“什么都没了”。
还有一处更隐蔽的坑:浮点表示的基数是 16,相邻可表示数之间的间距是跳跃的,同一个十进制小数在不同工具里写出来,最低字节可能有微小差异。对 UNITS 来说这通常无害,但如果哪天你做了逐字节比对的版本管理,就会出现“内容没改但文件哈希变了”的情况,别慌,是编码层的事。
2.4 UNITS 记录:所有坐标偏移问题的总根源
UNITS 里的两个数,是整个文件里最应该先看的两条数据。它们的含义经常被搞混,我用一句话定义清楚:
- 第一个数:一个用户单位等于多少个数据库单位。用户单位通常就是微米。
- 第二个数:一个数据库单位等于多少米。
典型值(0.001, 1e-9):一个用户单位(1 微米)= 1000 个数据库单位,一个数据库单位 = 1 纳米。也就是说文件里所有坐标的整数 1,代表实际尺寸 1 纳米。
另一种常见的(0.0001, 1e-10):一个数据库单位 = 0.1 纳米。精度高十倍,坐标数值也大十倍。
怎么选?核心矛盾是精度与范围的权衡。GDSII 的坐标是 32 位有符号整数,最大值 2147483647。我算一下不同精度下的物理上限:
| 数据库单位 | 1 个坐标单位对应 | 坐标最大值对应物理长度 |
|---|---|---|
| 1 nm | 0.001 微米 | 约 2147 毫米 |
| 0.5 nm | 0.0005 微米 | 约 1073 毫米 |
| 0.1 nm | 0.0001 微米 | 约 214.7 毫米 |
| 0.05 nm | 0.00005 微米 | 约 107 毫米 |
看起来都很宽裕,但别忘了这是“单点坐标绝对值上限”,不是“芯片尺寸上限”。如果你把版图原点放在芯片左下角,芯片本身 30 毫米见方,那用到 0.1 纳米精度时最大坐标约 3 亿,离上限还有七倍余量,够用。但如果版图里有人把一个辅助标记放在芯片外很远的地方,或者叠了个坐标系原点偏移很大的模块,就可能溢出。溢出不会报错,会直接回绕成负数,图形飞到另一个角落去。
注意:坐标溢出是静默失败,没有任何警告。交付前务必扫一遍所有坐标的绝对值,确认离 2^31 有足够余量。
还有一个实践中的细节:数据库单位必须和工艺的设计栅格对齐。如果工艺要求所有图形落在 5 纳米栅格上,而你的数据库单位是 1 纳米,那坐标只要是 5 的倍数就没问题;但如果你随手画了一个落在 3 纳米处的边,它虽然被合法存储了,却违反了工艺栅格要求,某些检查会报“off-grid”。
2.5 结构体与引用:嵌套、变换和那个容易搞错的阵列定义
BGNSTR 到 ENDSTR 之间是一个结构体。开头是 STRNAME,然后是若干个元素,最后 ENDSTR 收尾。结构之间不能交叉嵌套,只能通过引用建立层次关系,这一点和编程语言里的函数调用很像。
单个引用 SREF 的记录顺序是:SREF、SREFNAME、可选的 STRANS、可选的 MAG、可选的 ANGLE、XY。XY 只有一个点,就是被引用结构的原点落在父结构坐标系里的位置。STRANS 是一个 2 字节位数组,最高位表示镜像,还有两位分别表示“放大倍率是绝对值”和“角度是绝对值”——后两位的意思是,这个变换不再继承上层变换,直接以全局坐标系为准。这两位的处理是不同工具差异最大的地方之一,我建议除非有明确理由,否则不要在嵌套引用里用绝对标志。
阵列引用 AREF 稍微复杂一点。它用 COLROW 记录列数和行数(两个 2 字节整数),然后 XY 里给三个点:第一个是阵列原点,第二个定义列方向的向量,第三个定义行方向的向量。列间距等于第二点减第一点的向量除以列数,行间距同理。这里有几个坑:
一是列数行数用 2 字节有符号整数表示,理论范围是 -32768 到 32767,超过就溢出。有些工具在列数特别大时会偷偷拆成多个 AREF,有些则直接报错。
二是负数列数在部分工具里被解释为“反方向排列”,但这不在规范里,跨工具基本不可移植。我遇到过一次,某工具导出的负行数阵列在另一个工具里被当成 0 行处理,整块图形凭空消失,查了大半天才定位到 COLROW。
三是 AREF 的间距向量必须是精确的整数倍关系,否则会累积舍入误差。如果你的阵列间距本身不是数据库单位的整数倍,那排到第一千个单元时位置可能已经偏了。
2.6 几何元素:多边形、路径、文本各自的门道
BOUNDARY 是最常用的元素,记录序列是 LAYER、DATATYPE、XY。XY 里的点必须首尾闭合,也就是最后一个点等于第一个点,而且至少要有四个点(构成一个三角形)。少一个点很多工具会拒收,某些工具会自动帮你补上,结果就是同一份数据在不同平台读出来图形数量不一样。
关于孔洞(挖空的多边形),GDSII 规范本身没有“带孔多边形”这个概念,实际做法有两种:一种是用外层多边形加内层多边形、靠绕向(顺逆时针)区分,另一种是直接用布尔运算把带孔形状拆成若干个简单多边形。前一种做法在跨工具时风险很高,因为绕向约定并不统一。我的建议是导出前统一做一次绕向规范化,或者干脆拆成简单多边形。
PATH 用于画线,记录序列是 LAYER、DATATYPE、PATHTYPE、WIDTH、XY,可选加扩展信息。PATHTYPE 控制端点样式:0 是平头,1 是圆头,2 是方头(向外延伸半个线宽),4 是自定义延伸量。WIDTH 是 4 字节有符号整数,正数表示线宽会被上层的 MAG 缩放影响,负数表示绝对宽度、不随缩放变化。这个负数语义很容易被忽略,一旦你的层次里存在非 1 的放大倍率,正负号的区别就会让线宽差出几倍。
还有一个实践要点:PATH 和 BOUNDARY 在几何上是等价的(路径可以转换成有宽度的多边形),但在检查工具和掩模数据准备工具里,两者可能走完全不同的代码路径。我就遇到过路径在某个检查工具里被忽略、转换多边形之后才被识别的情况。所以交付前把 PATH 全部转成 BOUNDARY 是一个稳妥做法,代价是文件变大。
TEXT 用于标注,记录里除了位置和内容,还有 PRESENTATION(控制对齐方式)和 TEXTTYPE。绝大多数流程对文本只做展示,不参与几何运算。但有些流程会拿文本做标记层,这时候要注意字体和大小在不同工具里渲染不一致,别指望它精确对齐。
BOX 记录是早期遗留,用于给区域加注释框,现代流程基本直接忽略。千万别用 BOX 去画实际的图形层,某些检查工具看不见它,等到发现时已经晚了。
PROPATTR 和 PROPVALUE 用来挂自定义键值对,很多工具支持,但也有很多工具在读入时静默丢弃。所以关键信息不要只存在属性里,该写进图层就写进图层。
3. 从零生成、回读与自查:一份能落地的实操流程
3.1 工具组合与选型理由
我常用的组合是三件套,各司其职:
第一件是一个开源的跨平台版图查看与编辑工具,用来目视检查、做布尔运算、跑简单的设计规则检查、做两份数据的异或比对。选它的理由是可脚本化、跨平台、启动快,处理几 GB 的文件也不至于卡死。
第二件是一个基于脚本语言的版图生成库,用来程序化生成图形、批量改层号、做结构化自查。选脚本而不是图形界面,是因为交付前的检查项往往是几十条规则,靠手点必漏。
第三件是对接方提供的工艺文件和层映射表,这是唯一权威的数据源,任何疑问都以它为准,不要凭经验猜。
说明:上面提到的工具我用功能代称,不指名具体产品,避免变成广告。你在自己的环境里找对应能力的工具即可,能力对得上就行。
3.2 生成一个带层次和阵列的最小可用样例
下面这段代码演示了完整流程:建库、定义精度、建底层单元、建顶层单元、放单引用和阵列引用、写出文件。库名用了占位符,函数语义和你手头的库基本一一对应,改改名字就能跑。
import layoutapi as la # 库名做了替换,语义与常见版图脚本库一致 # 第一步:建库,同时把数据库精度钉死 lib = la.Library(name="demo_lib", unit=1e-6, precision=1e-9) # unit=1e-6 表示用户单位是 1 微米 # precision=1e-9 表示数据库单位是 1 纳米 # 这两个值最终会被写成 UNITS 记录里的两个实数 # 第二步:建一个基础单元,里面放几条金属线 base = lib.new_cell("METAL_BAR") # 画一个 2 微米 x 0.2 微米的长方形,层号 10,数据类型 0 # 注意所有坐标都要换成数据库单位的整数,这里 1 微米 = 1000 base.add_polygon( [(0, 0), (2000, 0), (2000, 200), (0, 200)], layer=10, datatype=0 ) # 画一条路径,线宽 0.1 微米,端点平头 base.add_path( [(0, 400), (2000, 400)], width=100, # 100 个数据库单位 = 0.1 微米 layer=11, datatype=0, pathtype=0 ) # 第三步:建顶层单元 top = lib.new_cell("TOP") # 单引用:把 base 放在 (10000, 10000) 处,旋转 90 度 ref1 = la.Reference(base, origin=(10000, 10000), rotation=90) top.add(ref1) # 阵列引用:3 列 5 行,列间距 5 微米,行间距 3 微米 aref = la.Reference( base, origin=(20000, 20000), columns=3, rows=5, spacing=(5000, 3000) # 列间距、行间距,单位是数据库单位 ) top.add(aref) # 第四步:写文件 lib.write_gds("demo.gds") print("cell 数量:", len(lib.cells)) print("顶层 cell:", lib.top_level()[0].name)这段代码跑完会得到一个几百字节的小文件,但里面已经包含了 HEADER、BGNLIB、LIBNAME、UNITS、两个 BGNSTR/ENDSTR、BOUNDARY、PATH、SREF、AREF、ENDLIB 几乎全部关键记录。拿十六进制编辑器打开它,对照第 2 节那张记录表一条条看,是理解 GDSII 最快的方式,比读十页文档都管用。
这里有个细节值得单独说:坐标系和阵列参数必须用整数,也就是数据库单位的整数倍。上面写(2000, 0)而不是(2.0, 0),就是因为一旦传了浮点,库内部会帮你做舍入,而舍入方向和时机未必是你能控制的。宁可自己在业务代码里就把单位换算算清楚,也不要依赖库的隐式转换。
3.3 回读自查:交付前必须跑的那几项
生成完不算完,一定要读回来验证。我通常跑下面这几个检查,写成脚本一次执行,输出一份报告,附在交付包里。
第一个检查是结构树完整性:顶层结构有几个、总结构数多少、有没有孤立结构(没被任何结构引用、也不是顶层)。孤立结构通常是删改过程中留下的垃圾,白白占体积,某些严格流程还会报错。
第二个检查是包围盒。算出整个版图的坐标最小值和最大值,换算成毫米,和预期的芯片尺寸对一遍。这一步能抓到绝大多数“整体偏移”和“坐标溢出”问题。如果包围盒比预期大出几十倍,先怀疑单位设错了,再怀疑有没有漏删的辅助图形。
第三个检查是分层统计。按 layer 和 datatype 组合统计多边形数量、路径数量、总面积。这是最有效的丢层检测手段:如果你的金属层应该有上万个多边形,结果统计出来是 0,那基本就是层映射出问题了。
第四个检查是栅格合规性。扫一遍所有坐标,看是否都能被工艺要求的最小栅格整除。不能整除的点就是潜在的 off-grid。
下面这段是回读检查的核心逻辑示例:
import layoutapi as la lib = la.read_gds("demo.gds") # 一次性展平,得到全局坐标下的所有多边形 # 注意:大版图不要全展平,内存扛不住,要按结构逐层取样 flat = lib.flatten() xs = [pt[0] for poly in flat.polygons for pt in poly.points] ys = [pt[1] for poly in flat.polygons for pt in poly.points] dbu = lib.precision # 数据库单位,单位是米 print("X 范围: %.3f ~ %.3f 毫米" % (min(xs)*dbu*1e3, max(xs)*dbu*1e3)) print("Y 范围: %.3f ~ %.3f 毫米" % (min(ys)*dbu*1e3, max(ys)*dbu*1e3)) # 分层统计 from collections import Counter stat = Counter((p.layer, p.datatype) for p in flat.polygons) for key in sorted(stat): print("层 %d/%d: %d 个多边形" % (key[0], key[1], stat[key])) # 栅格检查:假设工艺要求 5 纳米栅格 grid = 5 # 数据库单位是 1 纳米,则 5 表示 5 纳米 bad = [pt for poly in flat.polygons for pt in poly.points if pt[0] % grid or pt[1] % grid] print("非栅格点数量:", len(bad))3.4 层映射表:整条链上最容易丢东西的一环
层映射表定义了设计层名到 GDSII 层号、数据类型对的对应关系。不同工具用的格式不一样,有的是纯文本两列,有的是几列的表格,有的是脚本片段。内容大体是这样:
| 层名 | 用途 | layer | datatype | 备注 |
|---|---|---|---|---|
| NW | N 阱 | 1 | 0 | |
| OD | 有源区 | 2 | 0 | |
| PO | 多晶硅 | 3 | 0 | |
| M1 | 第一层金属 | 10 | 0 | |
| M1_PIN | 第一层金属引脚标注 | 10 | 1 | 同层不同数据类型 |
| M2 | 第二层金属 | 11 | 0 | |
| VIA1 | 通孔 | 20 | 0 | |
| TEXT | 文字标注 | 63 | 0 | 不参与制造 |
| DUMMY | 填充 | 90 | 0 | 交付前需确认是否保留 |
这张表看似简单,坑却最多。第一,它是方向性的:设计层名到层号是一个方向,反过来解析时又是另一个方向,两边必须保持一致,否则你做一次往返转换就会丢层。第二,同层多数据类型很容易漏,比如引脚标注用的 10/1,如果映射表只写了 10/0,那所有引脚标注都会消失,而引脚标注往往影响后续的探针和封装设计。第三,不同工具对“未映射层”的默认行为不同,有的是原样保留,有的是直接丢弃,有的是映射到默认层。丢弃是最危险的,因为不报错。
我的做法是:映射表只维护一份,所有转换脚本都从这一份读,绝不手写第二份;每次转换后对比输入输出文件的层号集合,有差异就直接中止流程。
3.5 交付前自检清单
这份清单我贴在工位上很多年了,条目随着踩坑不断增加,现在大概稳定在这个形态:
| 检查项 | 判断标准 | 出问题的典型后果 |
|---|---|---|
| 数据库单位与工艺一致 | 与工艺文件规定值完全相同 | 整体缩放错误 |
| 坐标范围 | 绝对值远小于 2^31,且与芯片尺寸吻合 | 图形回绕、飞到别处 |
| 顶层结构唯一 | 只有一个顶层,其余都被引用 | 交付方读错顶层 |
| 结构名长度与字符 | 不超过 32 字符,只用字母数字下划线 | 名称截断导致冲突 |
| 层号集合 | 与映射表完全一致,无多余层 | 多余层被当作真实图形 |
| 多边形闭合性 | 首尾点相等,顶点数不少于 4 | 图形被判非法或自动修补 |
| 路径已转多边形 | 交付版本中 PATH 数量为 0 | 检查工具忽略部分图形 |
| 文本层处理 | 明确约定是否保留 | 文字被误当作图形制造 |
| 文件完整性 | 末尾存在 ENDLIB 记录 | 文件被截断 |
| 校验和 | 与随附说明文件一致 | 传输损坏未被发现 |
4. 从 GDSII 到掩模:交付链路上的几个变形点
4.1 图形切割:为什么你的数据到了掩模厂会变大好几倍
掩模写入设备(不管是电子束还是激光)能曝光的形状是有限的,通常是矩形和少量简单梯形。你版图里那些奇形怪状的多边形,包括斜边、圆弧离散出来的短折线,都必须先被切割成设备能识别的简单形状。这个过程叫切割或分片。
切割是个纯粹的数据变换,几何形状理论上保持不变,但实际会带来三个后果。第一是数据量膨胀,一个复杂多边形切成几十个小梯形很常见,整体膨胀三到五倍属于正常,形状复杂的层(比如接触孔阵列、存储阵列)能到十倍。第二是切割算法的差异,不同工具的切割策略不同,切出来的形状边界可能落在略微不同的位置,虽然在容差内,但如果你拿切割前后的数据做严格比对,会看到大量差异。第三是顶点合并,切割工具有时会把共线顶点合并掉,导致多边形的顶点数变化。
这件事对设计者的实际影响是:交付的 GDSII 不一定是你最终看到的样子。如果你的图形里有极窄的缝隙或者极尖的角,切割之后可能出现零宽度的退化图形。所以有些流程要求提交前先做一次“切割预演”,把明显会被切坏的地方修掉。
4.2 光学邻近校正与数据量的二次膨胀
在先进节点上,掩模数据还要经过光学邻近校正处理。简单说,因为光刻时相邻图形会互相影响,需要对图形做“预先变形”,让最终在晶圆上印出来的形状符合设计意图。这一步会把原本规整的矩形改成带各种凸起和凹陷的复杂多边形,数据量再涨几倍。
所以一个完整的链条下来,掩