style——因为 klink 是机制层,却曾经攥着一堆"某个人对着某台设备调出来的"数字(太阳能量、镜头焦距、束流模糊、页面配色、连晶格图里每个原子的颜色)。那些现在全部外置,klink 一个都不带、也不猜。升级三步:klink update 刷出四份 style 文件 → python sem_style.py 生成 JSON → 调用时加 style=。完整说明见 v0.4.0 release notes。(截面只要 GDS、不加 render 时不需要 style。)这是什么
一份 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)。改成你的工艺就是改这份文件。 |
section_style.pysem_style.pyviewer_style.pyblender_style.py | 四份外观声明,每个出口一份:页面配色、标尺、束流参数、布光、相机、材质配方。klink 自带零个默认值——不给 style 的渲染会被直接拒绝,而不是用别人的审美画一张图给你。每份文件里每个数字都注明了它控制什么、调大调小会怎样。改外观就是改这四份。 |
这篇教程走一遍随包 starter:example_template/imaging/demo_layout.py、demo.pyxs、demo_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,可选)
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
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 帧见上面的胶片):
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 版图出成三样东西:完整模型的查看器、同一模型切开一角的 cutaway 查看器,外加同一个 GLB 换一套配色的 light 页面。这个 3D 模型是版图掩膜的忠实绘图:把 klink_visual_stack_v1 里每一层的多边形在它自己的 z0_um/z1_um 之间拉伸出来,一个圆还是一个光滑的棱柱,不发明任何东西。一次拉伸永远不会包含工艺曲率——LOCOS 的 bird's beak、锥形接触孔、CMP 碟形,这些都不在 GDS 里,自然也不会出现在这个模型里;它们属于 imaging.xsection_run 的 2D 剖面,是引擎精确的。0.5.8 移除了曾经存在的"process 扫描"3D 模式(把 1D 引擎的切片堆成一个体,结果处处都是台阶),换成两个诚实声明、绝不瞎猜的能力:
sidewall_deg(层声明里的字段,默认 0 = 竖直)把一层的墙 loft 成一个平滑的倾角——顶面按thickness*tan(angle)向内收。这个 demo 在 contact 和 metal-1 上都声明了 5 度,跟demo.pyxs刻蚀这两层用的角度一致。窄到撑不住这个倾角的多边形会被画成竖直,并记进结果的warnings。cutaway_um=[x0,y0,x1,y1]把建好的整个模型布尔切开,只保留这个区域(µm):先建完整模型,再切割,所以截面只出现在被切开的那两个面上(需要pip install manifold3d)。
python render3d_demo.py
# ---- 1. the model: a 3D drawing of the masks ---------------------
glb = OUT / "render3d_fast.glb"
r = build_glb_fast(str(GDS), STACK, str(glb), STYLE)
build_viewer_html(str(glb), str(OUT / "render3d_fast.html"),
STYLE, title="demo fast", overwrite=True)
# ---- 2. the same model, cut open ---------------------------------
cut_glb = OUT / "render3d_cutaway.glb"
rc = build_glb_fast(str(GDS), STACK, str(cut_glb), STYLE,
cutaway_um=[-0.8, 1.9, 11.2, 4.7])
build_viewer_html(str(cut_glb), str(OUT / "render3d_cutaway.html"),
STYLE, title="demo cutaway", overwrite=True)
生成的三个 .html 都是完全自含、离线的(查看器 JS + 模型数据都编码进同一个文件),双击直接在浏览器打开,拖动旋转,右侧面板能调每层的颜色/金属度/粗糙度/透明度、导出 PNG。
0.5.2 版按两条真实用户反馈修了两处:GLB 导出现在遵循 glTF 规范的 +Y-up 朝向,查看器还放开了 model-viewer 默认 22.5°–157.5° 的俯仰钳制、变成完整的 0°–180°——旋转不再被墙挡住,模型的每一个面都能转到;查看器面板每种材质还新增一条透明度滑条(A,挨着金属度 M、粗糙度 R),把某一层调淡来突出或弱化它——低于 1 材质会自动切到 BLEND 混合模式,回到 1 恢复它声明的模式,Reset 还原成声明时的样子。下面嵌的就是这份 cutaway 查看器——拖满一整圈试试,也拉一下 A 滑条。
render3d_demo.py 生成的 cutaway 查看器本身,原样嵌在这里:模型先整个建好,再做一次布尔切割,保留 y ≥ 1.9 µm 的一侧,暴露面正好穿过四个栅极;拖动旋转、滚轮缩放,右侧面板调背景/曝光/色调映射/阴影、逐材质改颜色/金属度/透明度,还能导出 PNG。它同样是单文件、离线的——单独打开或拷给同事双击都一样能跑,不连网、不装东西。
render3d_fast.glb 模型被 blender_demo.py 的 mode="die" 重新摆到透明薄膜 + 阴影承接面上渲染出的静态图:悬浮在其声明高度上的金属1梯子,架在横跨浅色有源区条带的红色多晶硅栅极之上,栅极之间是接触塞,全部立在 n-well/衬底板上;contact 和 metal-1 都带着这份 stack 声明的 5 度侧壁倾角。MCP 工具:imaging.render3d——把 klink_visual_stack_v1 声明的每一层拉伸成掩膜实体;旧的 process 扫描相关参数(mode/recipe/exclude 等)在 0.5.8 里已经不存在,换成层声明里的 sidewall_deg(平滑倾角)和调用参数 cutaway_um(先建整模型再布尔切开)。
0.5.8 起,每一次读取 3D 图层都遵守同一条 slit 契约:GDS 里那种0 宽的 keyhole 切割线(画出来的孔存进 GDS 后变成的存储产物)会被静默溶解;宽度 ≥ 1 dbu 的缝隙可能是故意画出来的意图(比如纳米间隙),会被保留并写进结果的 warnings;weld_slits_dbu(默认 0)是显式的焊接开关,用来把确认是假象的窄缝焊死。这条契约同样管着 imaging.xsection_run 和 imaging.blender(figure 模式)读到的每个 3D 图层。新 starter example_template/imaging/keyhole_3d_demo.py 完整走一遍这三条规则。
3SEM 风格顶视
sem_demo.py 把版图渲染成"看起来像"二次电子显微图的顶视图:按 VisualStack 里每层的 sem_grey/edge_glow 出灰阶发射强度和边缘亮边,加光刻圆角、束斑模糊、带种子的胶片颗粒、扫描线、暗角——同一个种子永远出同一张图。
python sem_demo.py
sem_top.png):顶部/底部两条电源轨,中间九根竖条是四个多晶硅栅极和五个金属1接触列交替排列,横跨有源区;背景左半(p 衬底)比右半(n-well)暗——per-layer sem_grey 发射强度决定了这些明暗差。
sem_top_color.png):同一份数据换一种看法——红色是多晶硅栅极,浅蓝灰色是金属1,绿色带是有源区,背景颜色区分 p 衬底(深蓝黑)和 n-well(橄榄棕)。脚本还按 demo_stack.py 的层顺序累积渲染"目前已印出哪些掩膜"的序列——配合配方的 # klink-step: 顺序,就是顶视版的逐步工艺胶片:
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_fast.glb(第②节的产物)重新摆到透明薄膜 + 阴影承接面上,就是上面已经看过的那张 die 渲染图。
blender_figure.png:MoS2 flake 长成真实原子晶格(kind="lattice", motif="mos2"),Au 电极是精确的版图棱柱,衬底是显式的 SiO2/Si slab。两种模式都额外写一份 .blend(blender_figure.blend / blender_die.blend)——在桌面版 Blender 里打开就能手动调相机、灯光、材质再重新渲染,不是一次性黑盒输出。
MCP 工具:imaging.blender——子进程隔离跑 headless bpy;mode="die" 需要先跑过 imaging.render3d(或 render3d_demo.py)产出的 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 是同一份文件。只有剖面(imaging.xsection_run)能看见这个断路,因为它真正跑了配方顺序;render3d 的 3D 现在只拉伸 GDS 里的掩膜多边形,跟配方顺序无关,三种变体会拉伸出同一个模型——这正是 0.5.8 把 3D 和 2D 职责拆干净之后的效果。
顺带说一句诚实话:三种情况下金属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 就是活例子:nwell 用 mask(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.py、demo.pyxs 和 demo_stack.py 拷到你自己的项目里,换成你真实的版图、层号/z 范围/颜色和真实的工艺步骤(沉积厚度、刻蚀深度、掩膜层),四个脚本原样重跑。想看物理对不对,就照着上面「物理对不对?」节的做法,在配方里挪一行代码的顺序(比如把某道刻蚀挪到它该在的沉积之前),对比剖面(imaging.xsection_run)跑出来的材质有没有整个消失——材质凭空消失说明配方在真实生效,不是装饰;3D(imaging.render3d)现在只拉伸 GDS 掩膜多边形,看不出配方顺序的差别。四个出口共用同一套 VisualStack 声明,改一次层颜色,剖面/3D/SEM/Blender 四张图都会跟着换。