Step-by-step tutorial · imaging

分步教程:从一份 GDS 出五种图

对应 example_template/imaging/ 里的可跑 starter(klink 0.3.3 起随 wheel 打包)。imaging 域是 klink 里专门"出图"的一组出口:process cross-section 剖面、逐步工艺胶片、3D 模型 + 自含网页查看器、SEM 风格顶视、Blender 论文级渲染——全部由你自己的三份文件驱动,klink 机制层零工艺数据。

这是什么

一份 GDS,加上你自己拥有的三份文件,能出五种"图"。驱动它们的只有这三份 EXAMPLE-owned 文件——klink 只提供机制(xsection 引擎绑定、GLB 网格生成、SEM 光栅渲染、Blender 场景搭建),零工艺常数:

你的文件是什么
demo_layout.py示例版图:一整排四晶体管 CMOS 器件(p 衬底两个 NMOS + n-well 两个 PMOS,多晶硅栅极、每个源漏上的接触、金属1加电源轨),并导出切线 CUT_UM改成你的器件就是改这份文件。
demo.pyxs工艺配方——受信 Python,进程内执行;# klink-step: <名字> 注释标记每个工艺步。改成你的流程就是改这份文件。
demo_stack.py一份 klink_visual_stack_v1 声明:每层的 z0_um/z1_um、颜色、SEM 灰阶(sem_grey)、材质类别(solid / lattice / dielectric)、对应的配方符号(recipe_symbol)。改成你的工艺就是改这份文件。

这篇教程走一遍随包 starter:example_template/imaging/demo_layout.pydemo.pyxsdemo_stack.py 和四个 demo 脚本(xsection_demo.py / render3d_demo.py / sem_demo.py / blender_demo.py),全部离线跑,输出落进 _generated/

前置条件

  • 安装 klink:pip install klayout-klink
  • 四个出口各自的依赖是可选的,按需装——少装了什么,klink 的报错会给出精确的 pip 命令:
pip install klayout klayout-pyxs==0.1.13            # 剖面引擎
pip install trimesh shapely mapbox-earcut           # 3D
pip install numpy scipy pillow                      # SEM / 胶片渲染
pip install bpy                                     # Blender(约 300MB,可选)
这条链路不需要 KLayout 在跑——四个 demo 脚本都是纯 Python,离线读写 GDS/GLB/PNG/HTML 文件。连着 KLayout/klink-mcp 时,同样的功能是四个 MCP 工具:imaging.xsection_run / imaging.render3d / imaging.sem_top / imaging.blender,参数和这些脚本一一对应;klink.find_tools domain=imaging 能看到完整用法。

拿到 starter

klink init myproj
cd myproj/example_template/imaging
python xsection_demo.py

升级 klink 之后用 klink update 刷新 starter(新增/修复的文件会回灌,你自己没碰过的旧文件会清理)。

重要:example_template/ 整个目录归 klink 包所有——klink update 会把它刷回随包状态。所以先把 demo_layout.py / demo.pyxs / demo_stack.py 拷贝到项目根目录或你自己的目录再改(和 pdk.py 一个待遇),不要直接改 example_template/ 里的文件。klink update 完全不碰 demo 的 _generated/ 输出目录。

1剖面 + 逐步工艺胶片

xsection_demo.py 先用 demo_layout.py 画一整排四晶体管 CMOS 器件:p 衬底左半两个 NMOS,n-well 右半两个 PMOS,多晶硅栅极横跨有源区,每个源漏上都有接触,金属1覆盖接触并带上下两条电源轨。再沿一条显式切线 CUT_UM = [[-0.8, 1.9], [11.2, 1.9]](贯穿有源行正中间,切过四个栅极、间隔层和阱边界)剖开:一次单帧剖面,一次 steps=True 的逐步工艺胶片。

python xsection_demo.py
十一帧竖排剖面胶片,从裸衬底经 n-well 注入、LOCOS 场氧、多晶硅栅极、LDD、侧墙、源漏注入、层间介质、钨塞接触,一路叠到金属1完成互连
逐步工艺胶片(film_film.png):demo.pyxs 的 11 个 klink-step 各出一帧剖面,从裸衬底一路叠到金属1互连完成。

demo.pyxs# klink-step: <名字> 注释标记工艺步——每个标记命名的是它后面紧跟的那段代码,胶片按标记数量渲染,一步一帧;这版配方有 11 个标记:p-substrate、n-well implant、LOCOS field oxide、gate oxide、poly gate + silicide、LDD implants、spacer、source/drain implants、ILD deposition、contact etch + W plug + CMP、metal-1 damascene。下面是配方的一段摘录(完整 11 步见 demo.pyxs):

# klink-step: p-substrate
pbulk = bulk()

# klink-step: n-well implant
nwell = mask(lnwell).grow(0.55, -0.05, mode='round', into=pbulk)

# klink-step: LOCOS field oxide
mfield = mask(lfield)
_fox_up = mfield.grow(0.22, 0.22, bias=0.12, mode='round')
_fox_dn = mfield.grow(0.22, 0.22, bias=0.12, mode='round',
                     into=[pbulk, nwell])
fox = _fox_up.or_(_fox_dn)

# ... gate oxide / poly gate + silicide / LDD implants / spacer /
# source-drain implants / ILD deposition 在这里(完整代码见 demo.pyxs)...

# klink-step: contact etch + W plug + CMP
mask(lcontact).etch(0.85, into=ild, taper=5)
tungsten = deposit(0.18, 0.18)
planarize(into=[tungsten, ild], less=0.5)

# klink-step: metal-1 damascene
imd = deposit(0.22)
mask(lmetal1).etch(0.32, into=imd, taper=5)
metal1 = deposit(0.22, 0.22)
planarize(into=[metal1], less=0.22)

被赋值给变量的材质会按变量名自动输出,不需要写 output()——但变量名以 _ 开头的例外:上面 _fox_up / _fox_dn 是 LOCOS 场氧上下两半各自生长出的中间结果,按惯例标成下划线开头,只用来 .or_() 合成最终的 fox,本身不会作为一种材质单独出现在图里。

5 帧最有代表性的截取(完整 11 帧见上面的胶片):

第1步剖面:切线下方是一整块均匀的棕褐色 p 衬底,上方是空气,还没有任何结构
步骤 1/11 · p-substrate
第3步剖面:剖面两端各出现一个圆角的蓝灰色场氧化物凸起,中间偏右有一块颜色略浅的 n-well 区域
步骤 3/11 · LOCOS field oxide
第5步剖面:硅表面均匀分布着四个深红色的小方块状多晶硅栅极,场氧化物凸起仍在两端
步骤 5/11 · poly gate + silicide
第10步剖面:浅蓝灰色的层间介质覆盖全表面,若干深灰色梯形钨塞垂直穿过它连到下方绿色和紫色的源漏区
步骤 10/11 · contact etch + W plug + CMP
第11步(最后一步)剖面:在钨塞上方又出现一层灰蓝色金属层,通过下方的钨塞与源漏区相连
步骤 11/11 · metal-1 damascene

MCP 工具:imaging.xsection_run——cut_um 必须显式给(µm),steps=true 配合配方里的 # klink-step: 标记出胶片;z_window_um 给每帧定一个统一的 z 窗口(这里是 (-1.1, 1.5)——engine 的衬底本身有好几微米深,不框一下画面大半都是衬底),axis=true 会在每帧左边画一条带刻度的 z / µm 标尺、右下角配一条水平比例尺(上面每张图左边的刻度和角落的 "1 µm" 线段就是它画的)。输出确定性(GDS 不带时间戳),overwrite=false 是默认值,每次运行都写一份 klink_imaging_result_v1 机读 sidecar。

23D + 自含网页查看器

render3d_demo.py 把同一份四晶体管 CMOS 版图出成两个 GLB + 两个自含网页查看器:

python render3d_demo.py
fast_glb = OUT / "render3d_fast.glb"
build_glb_fast(str(GDS), STACK, str(fast_glb))                    # 拉伸 STACK 声明
build_viewer_html(str(fast_glb), str(OUT / "render3d_fast.html"))

proc_glb = OUT / "render3d_process.glb"
build_glb_process(str(GDS), STACK, str(RECIPE), str(proc_glb),
                  slices=24, fraction=0.6)                        # 扫过真实配方,fraction<1 是切角
build_viewer_html(str(proc_glb), str(OUT / "render3d_process.html"))

fast 模式直接拉伸 demo_stack.py 里的 z0_um/z1_um,快但形状是理想化的层块;process 模式沿着真实的 .pyxs 配方做切片扫描,LOCOS/保形层/CMP 那种真实拓扑都会体现出来——本例还传了 fraction=0.6,做出一个切角(cutaway),暴露面就是一个真实的剖面。生成的 .html完全自含、离线的(查看器 JS + 模型数据都编码进同一个文件),双击直接在浏览器打开,拖动旋转,右侧面板能调每层的颜色/金属度/粗糙度、导出 PNG。

这就是 render3d_demo.py 生成的那个文件本身,原样嵌在这里:拖动旋转、滚轮缩放,右侧面板调背景/曝光/色调映射/阴影、逐材质改颜色与金属度,还能导出 PNG。它是单文件、离线的——查看器 JS 和模型数据都编码在这一个 HTML 里,单独打开或拷给同事双击都一样能跑,不连网、不装东西。
process-cutaway GLB 的渲染:一整条矩形 die,顶面沿长度方向有一道浅槽,均匀分布着四个深红色多晶硅栅极凸起,栅极之间是深灰色的竖直接触/金属列,透明背景带柔和阴影
这不是自含网页查看器的截图(那是一个你要自己双击打开的离线 HTML 页面)——这是同一个 render3d_process.glb 模型被 blender_demo.pymode="die" 重新摆到透明薄膜 + 阴影承接面上渲染出的静态图:能看到顶面沿长度方向的浅槽、四个深红色多晶硅栅极凸起,以及栅极之间深灰色的接触/金属列,用来在文档里表示这个模型长什么样。

MCP 工具:imaging.render3d——mode="fast" 拉伸 klink_visual_stack_v1 声明;mode="process" 扫过 xsection 引擎,配方材质(pbulk/nwell/tungsten 等)通过 recipe_symbol 映射上色,没映射上的材质标灰并报告 unstyled

3SEM 风格顶视

sem_demo.py 把版图渲染成"看起来像"二次电子显微图的顶视图:按 VisualStack 里每层的 sem_grey/edge_glow 出灰阶发射强度和边缘亮边,加光刻圆角、束斑模糊、带种子的胶片颗粒、扫描线、暗角——同一个种子永远出同一张图。

python sem_demo.py
灰度 SEM 顶视图:上下各一条粗的亮灰色电源轨,中间九根亮灰色竖条高度交替排列,横跨一条稍亮的水平有源区带,背景左半接近纯黑、右半是较浅的深灰色,分界线出现在正中间
灰度版(sem_top.png):顶部/底部两条电源轨,中间九根竖条是四个多晶硅栅极和五个金属1接触列交替排列,横跨有源区;背景左半(p 衬底)比右半(n-well)暗——per-layer sem_grey 发射强度决定了这些明暗差。
假彩色 SEM 顶视图:红色竖条(多晶硅栅极)和浅蓝灰色竖条(金属1)交替排列,横跨绿色的有源区带,上下各有一条浅蓝灰色电源轨,背景左侧深蓝黑、右侧橄榄棕色
假彩色版(sem_top_color.png):同一份数据换一种看法——红色是多晶硅栅极,浅蓝灰色是金属1,绿色带是有源区,背景颜色区分 p 衬底(深蓝黑)和 n-well(橄榄棕)。

脚本还按 demo_stack.py 的层顺序累积渲染"目前已印出哪些掩膜"的序列——配合配方的 # klink-step: 顺序,就是顶视版的逐步工艺胶片:

只印过 n-well 掩膜后的顶视:画面几乎是空白的,只能看到一条竖直分界线把左侧较深的区域和右侧略浅的区域分开,没有任何其他可见结构
只印过掩膜 1/0(n-well)之后的顶视(sem_after_1_0.png)——n-well 本身是一次没有自身形貌的注入,这一步画面只是一块被 well 边界分成深浅两块的平坦灰场;有源/多晶/接触/金属这些之后才印的掩膜,此刻完全看不到任何结构。layers= 参数限制到目前已印出的掩膜子集。

MCP 工具:imaging.sem_top——灰度 + 假彩色两个 PNG 一次出,layers=[...] 限制到某一步已印出的掩膜子集,确定性渲染(同种子同结果)。

4Blender 论文级渲染

blender_demo.py 用无头 bpy 出两张图,来自同一批声明:

python blender_demo.py
  • mode="figure":一个二维材料器件,按版图坐标 1:1 摆放——10/0 层是 MoS2 flake,声明 kind="lattice", motif="mos2" 后直接长出真实原子晶格(也支持 motif="graphene");20/0 层是 Au 电极,画成精确的版图棱柱;外加显式的 Si/SiO2 衬底 slab。
  • mode="die":把 render3d_process.glb(第②节的产物)重新摆到透明薄膜 + 阴影承接面上,就是上面已经看过的那张切角芯片图。
MoS2 场效应器件的论文级渲染:中间是不规则六边形的原子晶格薄片,两端各连一个金色矩形电极,下方是灰蓝色的 SiO2/Si 衬底切块
blender_figure.png:MoS2 flake 长成真实原子晶格(kind="lattice", motif="mos2"),Au 电极是精确的版图棱柱,衬底是显式的 SiO2/Si slab。

两种模式都额外写一份 .blendblender_figure.blend / blender_die.blend)——在桌面版 Blender 里打开就能手动调相机、灯光、材质再重新渲染,不是一次性黑盒输出。

MCP 工具:imaging.blender——子进程隔离跑 headless bpymode="die" 需要先跑过 imaging.render3d(或 render3d_demo.py)产出的 process GLB;mode="figure" 直接吃 gds + stack,需要 bpy(外加 trimesh/klayout-pyxs==0.1.13,如果要用 die 模式)。

物理对不对?

这不是营销话术,是一个可核对的结果:同一份版图,把 .pyxs 配方里接触工序的那几行代码换三种写法,钨塞是否真的出现在生成的 3D 模型里就完全不一样——因果关系在配方顺序里,不在版图坐标里。

变体配方改动模型里的 W plug
A(随包 demo.pyxs 就是这个顺序) ild = deposit(...)mask(lcontact).etch(0.85, into=ild, taper=5)tungsten = deposit(...)planarize(...) z = 0.00 – 0.49 µm:钨塞从硅表面一路灌到 CMP 平坦化后的顶面。
B(删掉接触刻蚀那一行) mask(lcontact).etch(...) 那一行整个删掉 模型里完全没有 W plug 这种材质——没有孔可填,CMP 直接把整层钨磨掉了。
C(把接触刻蚀挪到 ILD 沉积之前) 同样的 etch 那一行,挪到 ild = deposit(...) 之前执行 同样完全没有——是在一层还没沉积出来的膜上开孔,相当于在空气里刻蚀。

三种情况下版图逐字节相同,只是配方顺序不同:B 和 C 都是开路——每个晶体管都断在接触层,整个器件不导通。而且版图视图里什么都看不出来:普通的版图检查工具只看 GDS 里的多边形,这三个变体的 GDS 是同一份文件。只有剖面和 process 模式 3D 才能看见这个断路,因为它们真正跑了配方顺序,而不是把层当成理想化的盒子直接叠起来。

顺带说一句诚实话:三种情况下金属1都稳稳地待在 z = 0.45 – 0.93 µm——上面几层看起来完全正常,因为金属1的 damascene 工序根本不关心下面的钨塞在不在。失效只在接触层可见,光看版图或只看金属层视图都发现不了。

常见坑:deposit ≠ patterned

动词语义(最容易踩的坑):带图案的沉积是 mask(l).grow(...)——没有 mask(l).deposit() 这种写法;deposit(...) 永远是整面覆盖(blanket)操作。etch(...) 同理也是整面刻蚀;带图案的刻蚀要写 mask(l).etch(...)demo.pyxs 就是活例子:nwellmask(lnwell).grow(...)(图案化生长),gox/ild/tungsten/imd/metal1 这些用整面的 deposit(...),接触孔和金属1沟槽分别用 mask(lcontact).etch(...)mask(lmetal1).etch(...)(图案化刻蚀)。

确定性 · 产物契约

  • 永不静默覆盖:目标文件已存在时默认拒绝,必须显式传 overwrite=True 才会替换。
  • 每次运行都写机读 sidecar*.klink_imaging.json,格式 klink_imaging_result_v1——里面有输入的 cut_um/参数、每个输出文件的 sha256、GDS/配方输入的 sha256,脚本或 CI 可以直接读这份 JSON 而不是解析人读的日志。
  • GDS 输出字节可复现:不写时间戳,同样的输入两次跑出字节相同的 GDS。

引擎与查看器归属

剖面由 klayout-pyxs 引擎驱动——klink 把它当依赖导入,固定版本 ==0.1.13,从不 vendor 进仓库;3D 查看器里嵌的是 Google 的 <model-viewer>(vendor 了一份打包好的 JS,这样导出的 .html 才能完全离线打开)。klink 自己写的是 GLB 网格生成、SEM 光栅渲染和 Blender 场景搭建这几层胶水,不是剖面引擎或查看器本身。

下一步

demo_layout.pydemo.pyxsdemo_stack.py 拷到你自己的项目里,换成你真实的版图、层号/z 范围/颜色和真实的工艺步骤(沉积厚度、刻蚀深度、掩膜层),四个脚本原样重跑。想看物理对不对,就照着上面「物理对不对?」节的做法,在配方里挪一行代码的顺序(比如把某道刻蚀挪到它该在的沉积之前),对比 process 模式(imaging.render3d mode="process")或剖面(imaging.xsection_run)跑出来的材质有没有整个消失——材质凭空消失说明配方在真实生效,不是装饰。四个出口共用同一套 VisualStack 声明,改一次层颜色,剖面/3D/SEM/Blender 四张图都会跟着换。