Step-by-step tutorial · nanodevice · EBL

分步教程:画一个 EBL wraparound 纳米器件

对应仓库里的 example_template/nanodevice/ebl_wraparound.py(demo 功能/依赖/实测数字见示例 · EBL wraparound)。这是Hall bar 教程之后的第二篇:同样是纯离线合成器件、同样用 Port/Anchor 标记意图,但这次引入了 Hall bar 教程结尾留下的悬念——写场(writefield)避障。电子束光刻按矩形写场分块曝光,拼接处会有对准误差,因此走线只能在预先规划好的"允许穿越窗口"里跨越写场边界,klink 把这个约束表达成一批 keepout 几何和 corridor Anchor,而不是靠算法临时猜。

前置条件

  • 安装 klink:pip install klayout-klink(一条命令装好 klink 和它的 Rust 内核,无需第三方科学库)。
  • KLayout 正在运行,并已加载 klink 插件(RPC 端口默认 8765)。
  • 建一个项目并进入:klink init nano-demo && cd nano-demo——完整脚本就是 python example_template/nanodevice/ebl_wraparound.py --live(想留住生成的 cell 便于查看再加 --keep)。仓库 checkout 里等价的是 python -m examples_klink.public.demos.nanodevice.ebl_wraparound --live --keep
这篇教程把 build_wraparound_demo() 内部一次性完成的批量调用拆成 7 个可观察的阶段。构造顺序是固定的:4 个对准十字(每个 3 个 item)→ flake 多边形 → keepout 框 + 文字 → 8 组接触(每组 7 个 item,顺序固定为 contact box / M1 走线 / via / M2 绕行走线 / 锥形过渡 / pad / 标签)→ 拼接 patch → writefield keepout 框。教程按这个真实顺序对 shape_items 做切片,没有引入教程专属的绘制逻辑。

1建 cell + 层规划

这个器件的所有层都是 example-owned(example_template/nanodevice/ebl_wraparound.py 里的 WRAP_LAYERS),klink 本身不带任何工艺层。真实脚本用一个通用的 _ensure_layers helper 扫一遍 shape_items 里出现过的每个 (layer, datatype),自动建齐——包括 klink 保留的写场 keepout 层 900/0(它是从 writefield 障碍框的 shape item 里自然出现的,不用单独声明):

用途
30/0flake(器件有源区)
10/0M1(近场接触走线)
11/0M2(绕行走线到远端 pad)
40/0via(接触过孔)
20/0探针 pad
6/0标签 + 对准标记 + 局部 keepout 框
113/0写场拼接 patch
900/0klink 保留的写场 keepout 层(自动出现)
999/99klink 保留的 Port 标记层
999/1klink 保留的 Anchor 标记层
from klink import KLinkClient
from klink.domains.nanodevice.devices.wraparound import (
    ANCHOR_LAYER, CELL, PORT_LAYER, build_wraparound_demo,
)

def _ensure_layers(client, items):
    seen = set()
    for item in items:
        key = (int(item["layer"]), int(item.get("datatype", 0)))
        if key not in seen:
            seen.add(key)
            client.layer_ensure(key[0], key[1], name=f"NANODEVICE_{key[0]}_{key[1]}")

with KLinkClient(port=PORT).connect() as c:
    bundle = build_wraparound_demo(WRAP_LAYERS)
    c.cell_create(CELL)
    _ensure_layers(c, bundle["shape_items"])
    c.call("port.set_layer", {"layer": PORT_LAYER})
    c.call("anchor.set_layer", {"layer": ANCHOR_LAYER})
空白 cell,已建好全部图层,画布中还没有任何几何形状
Step 1 · 层规划完成后的空 cell,画面框住整个 460×340 µm 芯片区域,此时还没有任何形状。

2画 flake + 局部 keepout + 对准标记

中心是一块不规则多边形 flake(真正的器件有源区),外面套一个局部 keepout 框([-54,-44,54,44],标签 "flake + local keepout")防止后续走线太靠近;四个角上各放一个对准十字,供 EBL 系统做全局对准:

c.shape_insert_many(CELL, bundle["shape_items"][0:15])  # 12 个对准十字 item + flake 多边形 + keepout 框 + 文字
画面中央是一个不规则多边形 flake,外面套着一个红色 keepout 方框,四角各有一个红色对准十字
Step 2 · flake(30/0)+ 局部 keepout 框(6/0)+ 四角对准十字(6/0)。

3画近场接触 + via

flake 周围伸出 8 条窄 M1 走线(C0C7),从 flake 边缘附近的接触点斜着连到各自的 via——这一段是"近场"布线,尺度和 flake 本身相当:

contact_block = bundle["shape_items"][15:71]  # 8 组 x 7 个 item
stage3 = [it for i, it in enumerate(contact_block) if i % 7 in (0, 1, 2)]  # contact box / M1 走线 / via
c.shape_insert_many(CELL, stage3)
flake 周围放射状伸出八条细窄蓝色走线,每条末端连到一个小方块 via
Step 3 · 8 条 M1 近场接触走线(10/0)+ 8 个 via(40/0),像蜘蛛一样从 keepout 框边缘辐射出去。

4绕行走线到探针 pad

这才是 "wraparound" 名字的由来:每条 via 到 pad 之间不是走直线,而是先水平/垂直拉到芯片边界附近,再拐向远端探针 pad——这样可以让 8 个 pad 均匀分布在芯片四周,同时避开 flake 和相邻走线:

stage4 = [it for i, it in enumerate(contact_block) if i % 7 in (3, 4, 5, 6)]  # M2 绕行走线 / 锥形过渡 / pad / 标签
c.shape_insert_many(CELL, stage4)
八条 M2 走线从每个 via 拐出直角弯道绕到芯片外侧的大探针 pad,pad 标注 C0 到 C7
Step 4 · 8 条 M2 绕行走线(11/0,含锥形过渡)+ 8 个探针 pad(20/0,标注 C0C7)。

5标记 Port + Anchor 意图

和 Hall bar 教程不同:这里几何是先手工画好的,Port/Anchor 是事后补标的,不是路由前的意图声明。原因是写场拼接走廊是硬约束,路径形状必须先按走廊定死,klink 的路由算法不适合在这种强约束下从零求解;Port/Anchor 在这里的作用是记录意图,供后面的验证步骤核对:

for port in bundle["port_marks"]:          # 16 个:8 接触 + 8 pad
    payload = dict(port); payload["cell"] = CELL
    c.call("port.mark", payload)
for anchor in bundle["anchor_marks"][0:8]:  # 8 个 via waypoint anchor
    payload = dict(anchor); payload["cell"] = CELL
    c.call("anchor.mark", payload)
每个接触点和 pad 旁边出现红色 Port 标记方框,每个 via 上出现红色 Anchor 文字标签 wrap_0 到 wrap_7
Step 5 · 16 个 Port(8 接触 + 8 pad,999/99)+ 8 个 via waypoint Anchor(999/1,标签 via (wrap_0)via (wrap_7))。

6writefield 避障规划

这是这篇教程和 Hall bar 教程真正不同的地方。klink.domains.nanodevice.ebl.writefield.plan_writefields() 把 460×340 µm 的芯片按 115 µm 写场尺寸切成 4×4=16 个写场,在每条写场边界上画出 keepout 障碍框(klink 保留层 900/0)——除了 11 个预先规划好的"允许穿越窗口"。只有在这些窗口对应的位置,走线才被允许跨越写场边界;窗口本身用 corridor 类型的 Anchor 标出来:

obstacle_items = bundle["shape_items"][71 + bundle["patch_report"]["patch_count"]:]  # 17 个 900/0 keepout 框
c.shape_insert_many(CELL, obstacle_items)

for anchor in bundle["anchor_marks"][8:]:   # 11 个 corridor 窗口 anchor
    payload = dict(anchor); payload["cell"] = CELL
    c.call("anchor.mark", payload)
芯片被黑色网格线分成四乘四的写场,网格线上有红色条纹状的 keepout 墙,墙上有若干缺口,缺口处标着 WF 开头的红色文字标签,走线正好从缺口穿过
Step 6 · 4×4 写场网格 + 写场边界 keepout 墙(红色条纹,900/0)+ 11 个 corridor 窗口 Anchor(标签 WF_XL_MIDWF_Y0_WIDE 等)——已经画好的走线全部从窗口缺口穿过,不碰墙。

7拼接 patch + 收尾

光刻写场拼接处存在对准误差,generate_wf_patches()(移植自 Klayout-Router 的 Auto-patching 宏)会在每条走线穿越写场边界的地方补一个方形 patch 做误差补偿。最后写入 cell 名标签,看整体和一处细节:

patch_items = bundle["shape_items"][71:71 + bundle["patch_report"]["patch_count"]]
c.shape_insert_many(CELL, patch_items)
完整的 EBL wraparound 版图全景:中央 flake、八条辐射状近场走线、八条绕行走线、写场网格、keepout 墙、走廊窗口和探针 pad
Step 7a · 成品全景(view.zoom_fit):flake + 8 接触 + 8 绕行走线 + 写场网格/keepout/走廊 + 8 pad + 12 个拼接 patch。
放大显示一条走线穿过写场竖直边界的位置,边界上有一个灰色方形拼接 patch,边界墙在窗口范围之外重新出现
Step 7b · 局部放大:C0 网络的 M2 绕行走线穿过 x=-115 写场边界的位置,正好落在 WF_XL_MID 走廊窗口内;灰色方块是拼接 patch,可以看到窗口上方 keepout 墙重新出现。
同一位置,橙色方框和箭头标出拼接 patch 的确切位置
Step 7c · 同一位置橙色高亮:这个 7×7 µm 的灰色方块就是这条走线在写场边界上唯一的一个拼接 patch。

验证,不是截图

教程首页说的一样,截图只是给人看的,不是完成依据。这次实际运行的构建报告:

{
  "shape_items": 100,
  "ports": 16,
  "anchors": 19,
  "patches": 12,
  "wf_obstacles": 17,
  "wf_crossings": 20,
  "wf_crossing_violations": 0,
  "route_centerline_overlaps": 0
}

wf_crossings=20wf_crossing_violations=0,说明这 20 次"走线穿越写场边界"全部落在预先规划的走廊窗口内,没有一次直接撞在 keepout 墙上;route_centerline_overlaps=0 说明 8 条不同网络的走线中心线互不重叠、没有短路风险。这两个数字合在一起,才是"这版图对写场约束合规"的依据,而不是"看起来穿过去了"。

下一步

如果还没看过第一篇教程,建议先读《分步教程:画一个 Hall bar 器件》——它是这篇教程的前置:同样的 Port/Anchor/geometry-first 流程,但走线是 klink 路由算法先标记后求解,而不是像这里手工画好再补标。把 WRAP_LAYERSbuild_wraparound_demo 里的 crossing_windows 改成你自己的写场尺寸、走廊位置,重跑一遍确认 wf_crossing_violations=0;Port/Anchor 的完整字段说明见 MCP 参考,更多离线器件背景见核心思路教程 · 离线器件教程