ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python脚本生成GDSII光刻掩模:从参数化设计到流片前自检

Python脚本生成GDSII光刻掩模:从参数化设计到流片前自检

简介:面向光刻掩模与芯片版图设计场景,gdsCAD 是基于 Python 的 GDSII 布局文件创建与操作工具包,适合科研人员、芯片设计工程师及希望自动化版图流程的开发者。它既能编写脚本批量生成布局,也支持交互式调整,尤其在包含多个增量微调对象的复杂设计中表现出色,并借助 matplotlib 将几何图元到完整布局全部可视化。压缩包共 60 个文件,体积约 452KB,其中 9 个 Python 模块(core、shapes、templates 等)构成核心功能,10 个 RST 文档说明 API 与使用方式,8 张 PNG 图片展示示例效果,另含配置、字体、许可证等辅助文件。目前已有 1933 人学习下载,对入门光刻掩模脚本设计的读者具有较高参考价值。资源内附可运行的对齐标记示例、字形数据与完整目录结构,便于读者观察图形对象、Cell 和 Layout 的分层关系,从而快速上手并扩展出自定义的掩模生成脚本。

1. 写在流片前夜:为什么用Python脚本生成GDSII光刻掩模

某次流片前夜,我把一套MEMS对准标记的间距从5µm改成4.5µm,图形化版图工具里改到第三个,对齐线就全乱了。换成gdsCAD——一个简单但功能强大的Python软件包,专门创建GDSII格式光刻掩模——操作变成改一行变量重跑脚本。它只有Cell、图形、布尔运算、导出几层接口,却能覆盖从单层标记到多层传感器整张掩模。适合不想被大型EDA拖着走的工艺开发者,也适合需要参数化、批量生成版图的人。下面从GDSII原理讲起,按最小脚本、阵列、批量生成到交付前自检,把能直接复现的代码和坑位一次给齐。

2. GDSII格式不是黑匣子:层级结构、记录流与单位模型

很多人第一次用gdsCAD时,把它当成“画图工具”来学,总在找按钮。实际上gdsCAD做的事情是把Python对象翻译成GDSII文件,而GDSII本质上是一串紧凑的二进制记录。不理解这层对应关系,遇到“文件生成了但查看器里是空的”“层号对了但图形跑偏”这类问题时会非常被动。

2.1 GDSII的层级逻辑:LIBRARY、CELL、BOUNDARY与SREF

GDSII文件按记录流组织,常见记录从HEADER开始,中间是BGNLIB、LIBDIR,真正干活的是CELL、BOUNDARY、SREF、AREF、TEXT这些记录。一个CELL可以被多个SREF引用,SREF本身不复制几何数据,只记录引用了哪个Cell、放在哪个坐标、转了多少度。这就是为什么同一单元放一万个位置,文件体积也不会线性膨胀。

gdsCAD的类基本和这些记录一一对应:gc.Cell对应CELL,gc.Rectangle、gc.Polygon、gc.Circle导出为BOUNDARY,gc.CellReference导出为SREF。调试的时候,我会在导出后拿解析工具直接看记录类型,缺哪一层就查是哪一环没生成。比如发现BOUNDARY记录个数为0,那问题一定出在“图形对象没有add进Cell”这一步。

有个容易忽略的点:GDSII里不存在“布尔运算后的复杂对象”,所有几何在文件里都是闭合多边形的顶点坐标列表。你在gdsCAD里做差集、并集,最终导出的仍然是拆解后的边界。所以“多少个矩形”不等于“多少条BOUNDARY”,带孔的图形会被拆成多条边界记录。

2.2 为什么用Python而不是图形界面画掩模

图形化版图工具当然能画,但有两个场景会让人非常难受:一是尺寸批量修改,几十个图形按公式整体缩放,手动改容易漏;二是生成规则阵列或曲线轮廓,比如环形光栅、螺旋电感、渐变宽度导线,手画几乎不可能,脚本几百毫秒就出结果。

脚本化还有一个隐藏收益——可追溯。掩模版本迭代时,只要保存了生成脚本,就能知道上一版到底改了哪个参数。图形化工具里改过的文件,两个版本之间逐个比对几何属性,那份痛苦经历过的都懂。我一般会把每个掩模版本对应的脚本存进同一个目录,脚本用日期命名,后续不管是自己回溯还是交给同事复核,都比翻文件时间戳靠谱。

gdsCAD在这类需求里算轻量且够用的选择。大型EDA的设计套件不是不能用,但启动慢、授权重,只是生成几层掩模的话性价比很低;纯手写GDS二进制又不现实,中间需要一层封装。gdsCAD正好卡在这个位置:接口少而稳定,能出标准GDSII,也能配合shapely这类几何库做复杂计算。

2.3 gdsCAD的单位模型:你其实在跟整数打交道

GDSII规范里所有坐标都是整数,呈现在文件里的是“数据库单位(DBU)”的倍数。导出时有两个参数控制换算:unit是用户单位对应的米数,precision是文件里最小坐标单位对应的米数。gdsCAD的默认组合是unit=1e-6、precision=1e-9,也就是用户单位1µm、DBU 1nm。

实际使用中,DBU决定了你版图能表达的最小尺寸。下面这个表是常见组合:

DBU最小可表达尺寸典型适用场景
1e-91nm大部分成熟工艺,坐标够精细
5e-95nm某些掩模厂的最小网格要求
1e-810nm粗网格设计,文件体积更小

选DBU时不能只看精度,还要看掩模厂的设计规则。有的厂要求坐标是5nm或10nm的整数倍,你用1nm网格画完,导出时数据没问题,但进厂前要被对方“修整”,修整过程可能引入与预期不符的图形改动。建议画图之前先查清楚接收方的最小网格,再决定gdsCAD导出参数。

3. 第一份掩模动手跑:Cell、矩形、布尔运算与导出参数

原理理清之后,直接上手。这一章从最简脚本开始,逐步加入多边形、布尔运算和导出配置。所有代码都可以在Jupyter Notebook或普通Python脚本里直接跑,依赖只有gdsCAD和shapely。

3.1 最小可运行脚本:一个50×20µm的对准标记

import gdsCAD.core as gc # 创建主单元,名字会写进GDSII文件 cell = gc.Cell('ALIGN_TL') # 画一个矩形:两个角坐标,单位为微米 bar = gc.Rectangle((-25, -10), (25, 10), layer=1) cell.add(bar) # 创建库并将单元加入 lib = gc.GdsLibrary() lib.add(cell) # 导出GDSII文件 lib.save_gds('align_marks.gds', unit=1e-6, precision=1e-9)

这段代码的逻辑是三层:先有Cell,再把几何对象add进Cell,最后把Cell挂到Library并保存。gc.Rectangle接收左下角和右上角两个坐标元组,坐标系原点在版图中心还是角落取决于你自己的设计习惯,gdsCAD不会自动调整。layer=1表示该矩形属于1号层,层号是数字,具体对应哪层工艺由掩模厂的映射表决定。

save_gds的两个参数前面说过,unit=1e-6表示用户坐标1单位等于实际1µm,precision=1e-9表示DBU是1nm。如果你希望文件更小,可以把precision放大到1e-8,但前提是工艺网格允许。

3.2 多边形与圆:两条最常用的路径

import gdsCAD.core as gc cell = gc.Cell('SHAPES') # 多边形:顶点列表按顺序排列 pts = [(0, 0), (20, 0), (20, 15), (10, 25), (0, 15)] poly = gc.Polygon(pts, layer=2) cell.add(poly) # 圆:gdsCAD用圆弧段逼近 circle = gc.Circle((0, 0), radius=10, layer=3) cell.add(circle)

gc.Polygon的顶点列表就是GDSII里BOUNDARY的XY记录,顺序要保持一致,否则可能出现自相交的非法多边形。这里有个细节:GDSII不支持真圆,所有圆都是多边形逼近。gc.Circle默认按一段角度生成圆弧,半径越大、逼近段数越多,导出的坐标点就越多。图形化工具里看不出差别,但文件大小和后续仿真加载时间会受影响。

从计算精度角度,我习惯先把顶点算好再传给gc.Polygon,而不是依赖gc.Circle的自动参数。比如要生成一个带120个点的圆环,先用三角函数算出坐标列表,再包成Polygon,这样能精确控制点数,也方便后续做顶点级操作。

3.3 布尔运算:AND、OR、NOT把图形拼出节奏

gdsCAD本身没有把二维布尔运算做成核心功能,这点官方文档也不强调。实际做差集、交集时,我的常见做法是把多边形先交给shapely处理,再把结果转换回gdsCAD对象。这个方法比在版图工具里一点点挖空要稳得多。

import gdsCAD.core as gc from shapely.geometry import Polygon as ShapelyPolygon # 定义两个矩形的shapely几何 a = ShapelyPolygon([(0, 0), (100, 0), (100, 50), (0, 50)]) b = ShapelyPolygon([(40, -10), (140, -10), (140, 60), (40, 60)]) # 差集:用a挖掉b res = a.difference(b) # 把shapely结果转回gdsCAD def add_shapely_polygon(cell, geom, layer): # exterior是闭合环,最后一点和第一点重复,要去掉 pts = list(geom.exterior.coords)[:-1] cell.add(gc.Polygon(pts, layer=layer)) cell = gc.Cell('DIFF_DEMO') add_shapely_polygon(cell, res, layer=5)

shapely返回的多边形坐标顺序是规范的,转回gdsCAD时唯一要处理的是把闭合环的重复点去掉。需要注意:如果布尔运算结果是多个多边形,比如a被b切成两半,res会变成MultiPolygon,这时候直接取exterior会报错。处理办法是遍历res.geoms,逐个转换。这个坑后面避坑章节还会展开。

做完布尔运算后,最好用shapely的buffer(0)处理一次,它能修复部分自相交、退化边问题。这一步在图形化工具里是“自动修复”按钮,在脚本里就要显式调用。

3.4 导出GDS:unit与precision参数不要瞎填

lib = gc.GdsLibrary() lib.add(cell) lib.save_gds('design_out.gds', unit=1e-6, precision=1e-9)

导出参数的选择直接影响掩模厂解析结果。下面是参数组合的含义表:

参数默认值含义建议
unit1e-6用户坐标1单位对应的米数保持1e-6(µm)
precision1e-9DBU对应的米数查清工艺网格后定
layer无图层号按工艺映射文件填

提示:导出前先去掩模厂拿设计规则文件(DRM),确认最小网格和层号映射,再定参数。参数错了轻则坐标放大1000倍,重则整版报废。

4. 参数化生成掩模:用函数、SREF阵列与CSV批量铺满版图

单层图形跑通之后,真正的效率提升来自参数化和批量生成。这一章解决“改一个数,整张掩模跟着变”的需求,也是脚本化版图相比图形工具最明显的优势。

4.1 用函数造PCell:改一个参数重出整个掩模

import gdsCAD.core as gc def alignment_mark(cx, cy, width=50, gap=10, layer=1): """生成一个十字对准标记,返回Cell""" half = width / 2 cell = gc.Cell(f'ALIGN_{cx}_{cy}') # 水平条和垂直条拼出十字 h_bar = gc.Rectangle((cx - half, cy - gap), (cx + half, cy + gap), layer=layer) v_bar = gc.Rectangle((cx - gap, cy - half), (cx + gap, cy + half), layer=layer) cell.add(h_bar) cell.add(v_bar) return cell # 使用示例:在坐标(100, 200)处生成一个默认尺寸的对准标记 cell = alignment_mark(100, 200)

这就是“参数化单元(PCell)”的雏形。函数接收坐标、宽度、间隙、层号,返回一个完整Cell。需要调整标记尺寸时,只改width或gap重跑脚本,整张掩模同步更新,不用再手动逐个拖拽。

参数命名上我建议直接对应物理尺寸,比如width是十字总宽,gap是竖条宽度的一半,这样半年后回来看脚本还能看懂。函数内部用half = width / 2做换算,所有子图形都基于中心点计算,比硬编码绝对坐标更抗改。

4.2 用SREF做阵列:坐标、旋转与镜像

import gdsCAD.core as gc # 先做一个基础单元 unit_cell = gc.Cell('UNIT') unit_cell.add(gc.Rectangle((0, 0), (10, 10), layer=1)) # 顶层单元,引用上面那个基础单元 top = gc.Cell('TOP_2X2') # 平移引用 top.add(gc.CellReference(unit_cell, origin=(20, 0))) # 旋转90度后的引用 top.add(gc.CellReference(unit_cell, origin=(40, 0), rotation=90)) # 带镜像的引用 top.add(gc.CellReference(unit_cell, origin=(60, 0), mirror=True))

gc.CellReference生成的就是GDSII里的SREF记录。好处是重复单元只存一份几何定义,引用本身只占几条记录,文件体积小,修改基础单元时所有引用自动更新。

rotation单位是度,逆时针为正;mirror=True会做镜像翻转。这里提醒一句:SREF的旋转和镜像作用于整个Cell,包括里面包含的文字。具体坑在第5章展开。

4.3 从CSV读坐标批量生成:十秒生成几百个图形

实际项目里,图形位置往往来自仿真结果或外部计算,比如传感器阵列、测试图形布局。这时不再手写坐标,而是从CSV批量读取。

import csv import gdsCAD.core as gc cell = gc.Cell('SENSOR_MATRIX') with open('positions.csv') as f: reader = csv.DictReader(f) for row in reader: cx = float(row['x']) cy = float(row['y']) size = float(row.get('size', 10)) # 以(cx, cy)为中心,生成size×size的矩形 r = gc.Rectangle((cx - size/2, cy - size/2), (cx + size/2, cy + size/2), layer=3) cell.add(r)

CSV的列名和脚本里的字段要对齐,常见错误是DictReader里字段名不匹配,循环跑完却一个图形都没生成。我一般会在循环里加一个计数器,跑完打印“共生成N个图形”,有效避免这类静默错误。

批量生成的另一个注意点是坐标范围。几万个点分布在几毫米见方的区域时还好,如果坐标来自经纬度或者没做归一化,数值可能超出工艺范围几百倍。建议在循环里加最小最大值检查,超范围就直接抛异常,不要带病往下跑。

5. 避坑清单:gdsCAD实战中五个反复翻车的细节

正常流程走通后,这一章写实战里最常翻车的五个问题。每一个都是同事或自己在交付前踩过的,按“现象—原因—解决”的方式记录,方便对着排查。

5.1 现象:图形尺寸在查看器里出现微小跳动

矩形宽度在工具里显示为49.999或50.001,不是预期的整数50。原因是GDSII坐标是整数记录,浮点运算的二进制误差经过导出取整后产生了1nm或几nm的偏差,连续排列的图形之间就会出现不均匀的间距。

解决方法是构建几何时统一按DBU整数运算。比如DBU是1nm,所有坐标先乘以1000再取整,计算完再除以1000传入gdsCAD。更稳妥的做法是脚本开头定义一个GRID = 0.001(单位µm),所有尺寸都用round(value / GRID) * GRID规整。从那以后我画阵列前都会强制跑一遍网格规整。

5.2 现象:布尔运算后的Cell里一个图形都没有

shapely的difference执行后不报错,转到gdsCAD再导出,查看器打开一片空白。原因是差集结果可能是MultiPolygon、带内部孔的多边形,或者退化成一条线。取exterior时拿到的是空对象,转换逻辑没处理这些情况。

解决方法是转换前先判断几何类型:geom.geom_type是Polygon就直接转换,是MultiPolygon就遍历geom.geoms,是LineString说明退化了,应该放弃这条边。另外,做布尔运算前对所有多边形执行buffer(0)修复自相交,能大幅减少这类问题。

5.3 现象:流片回来某层整体不对,金属层莫名镂空

版图里金属连线出现奇怪的缺口,或者某层完全消失。原因是层号映射错误。GDSII的layer只是一个数字,掩模厂按照工艺映射表把它解释成具体的光刻层。你写layer=2,对方可能映射为氧化层刻蚀,而不是你以为的金属层。

解决方法是交付前把图层映射规则写成一张表,放进脚本目录。我用一个固定模板:LAYER_1_META = 1, DATATYPE_MAIN = 0,在脚本里全部用命名常量,不用裸数字。这样映射关系一目了然,也不会不小心改到层号。

5.4 现象:SREF镜像后文字反了

一个包含文字标识的Cell被mirror=True引用后,文字在曝光时变成镜像,流片回来字符是反的。原因是SREF的镜像作用于整个Cell内容,文字也不例外。GDSII本身不会自动对你的文字做“正向补偿”。

解决方法有两种:一是文字单独放一个Cell,不参与镜像;二是必须镜像时,给被引用的Cell整体旋转180度做补偿。检查时在版图查看器里把镜像后的Cell放大看清楚文字方向,不要只看图形轮廓。

5.5 现象:整版图形尺寸差1000倍

导出的GDS在查看器里要么大得离谱要么完全看不见。原因是unit和precision的换算关系搞混了。unit是用户单位定义,precision是DBU大小,两者配合决定坐标值含义。把precision填错几位,整个版图就按错误比例缩放。

解决方法是固定参数组合。如果工艺要求不高,就统一用unit=1e-6, precision=1e-9,必要时把坐标全部换算成目标DBU再导出。交付前再做一个“已知尺寸”的参照图形,比如一个标准的10µm×10µm正方形,在查看器里量一下,数字对不上就说明参数有问题。

6. 交付前自检:读回GDS做几何审计,把问题留在导出前

掩模交付前最重要的一件事,是把导出的GDS读回来看一遍,而不是只看脚本调用结果。我这里用gdstk做读取检查,它和gdsCAD一个管写一个管读,配合起来很稳定。

import gdstk lib = gdstk.read_gds('design_final.gds') for cell in lib.cells: # 跳过名字以$开头的临时单元 if not cell.name.startswith('$'): bbox = cell.bounding_box() area = cell.area() print(cell.name, 'bbox:', bbox, '面积:', area)

这段代码会遍历所有Cell,打印包围盒和面积。拿它跟设计预期比对,能快速发现“坐标偏移”“图形缺失”“单元尺寸不对”的问题。如果某个Cell的包围盒范围超出了预定区域,说明有图形被错误放到了版图角落。

还有一道关键检查是网格精度审计。掩模厂要求坐标落在工艺网格上,否则要额外修整,修整可能改变设计尺寸。下面的脚本检查所有多边形顶点是否落在目标网格上:

import gdstk GRID = 0.005 # 工艺网格:5nm lib = gdstk.read_gds('mask.gds') for cell in lib.cells: for poly in cell.polygons: for point in poly.points: x = float(point[0]) y = float(point[1]) if abs(x / GRID - round(x / GRID)) > 1e-9: print('off-grid:', cell.name, x, y)

gdstk读取后返回的坐标是µm,所以GRID也按µm填。检查到异常后,回到gdsCAD生成端重新规整再导出,而不是在结果文件里硬修。

我现在的交付习惯是固定三条:生成脚本存版本目录、导出后跑网格审计、最后用查看器人工过一遍关键单元。从某次交付因为一个小矩形坐标写错导致整版重做之后,我每次导完GDS都强制走一遍这个流程,不再直接发文件。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进