前置条件
- 安装 klink:
pip install klayout-klink(一条命令装好 klink 和它的 Rust 内核,无需第三方科学库)。 - KLayout 正在运行,并已加载 klink 插件(RPC 端口默认
8765)。 - 建一个项目并进入:
klink init nano-demo && cd nano-demo——完整脚本就是python example_template/nanodevice/neural_electrode.py --port <session-port> --elec-rows 4。仓库 checkout 里等价的是python -m examples_klink.public.demos.nanodevice.neural_electrode --port <session-port> --elec-rows 4。
generate_harnesspcell() 内部一次性完成的批量调用拆成 7 个可观察的阶段,方便讲清每一步在画什么、标什么、为什么。生产环境里它就是一次 shape.insert_many 批量写入全部 394 个 shape(见 CLAUDE.md 的批量 RPC 铁律)——分阶段只是教学手段,不是推荐用法。view.zoom_box(bbox_um=[x1,y1,x2,y2]) 和 view.screenshot(bbox_um=..., width_px=..., height_px=...) 的坐标参数都是 bbox_um,单位微米(klink 的标准用户单位)。view.viewport 同时返回 bbox_um(微米)和 bbox_dbu(数据库单位)两种坐标,按需取用。1建 cell + 层规划
这个器件的所有层都是 example-owned(example_template/nanodevice/neural_electrode.py 里硬编码的层号),klink 本身不带任何工艺层。先建 cell,把 Port/Anchor 的标记层指定一遍,再把 7 个工艺层加上 klink 保留的 3 个标记层全部 ensure 出来:
| 层 | 用途 |
|---|---|
1/0 | M1(近场键合 pad 侧走线,net 1-12) |
2/0 | VIA13(M1↔M3 过孔,仅电极远端一侧用到) |
3/0 | M3(远场走线 net 13-24,以及电极自身接线) |
4/0 | VIA35(pad 过孔 + 电极过孔) |
5/0 | Pads(24 个 net 的键合 pad,72 个方块 + 电极阵列 pad) |
6/0 | Frame(探针外壳/本体轮廓) |
7/0 | Protect(保护层,与 Frame 同形状 + 居中填充) |
999/99 | klink 保留的 Port 标记层(KLINK_PORTS,主 Port,带标签) |
999/98 | klink 保留的辅助 Port 层(KLINK_ROUTE_PORTS,供路由用;键合 pad 侧的 24 个显示标签,电极侧的 24 个隐藏标签) |
999/1 | klink 保留的 Anchor 标记层(KLINK_ANCHORS) |
from klink import KLinkClient
with KLinkClient(port=PORT).connect() as c:
c.cell_create(cell_name)
c.call("port.set_layer", {"layer": "999/99"})
c.call("anchor.set_layer", {"layer": "999/1"})
for layer, name in ((1,"M1"),(2,"VIA13"),(3,"M3"),(4,"VIA35"),(5,"Pads"),(6,"Frame"),(7,"Protect")):
c.layer_ensure(layer, 0, name=name)
c.layer_ensure(999, 99, name="KLINK_PORTS")
c.layer_ensure(999, 98, name="KLINK_ROUTE_PORTS")
c.layer_ensure(999, 1, name="KLINK_ANCHORS")
c.show_cell(cell_name, zoom_fit=False)
view.zoom_box(bbox_um=...) 定位)。2画探针本体 / 外壳
探针本体是一圈"匚"字形空心外壳:左边是键合 pad housing(三面墙、右侧开口),中间收窄成一段带圆角过渡的颈部(neck),再分叉成上下两条细长的臂(arm),最后在远端收拢成一个尖端三角形(tip)——这正是真实神经探针"宽底座 + 细长探针轴"的形状。_frame_items() 在 6/0(Frame)和 7/0(Protect)两层各画一份(7/0 多两块居中/尖端填充),一共 22 个 shape,一次 shape_insert_many 写完:
frame_items = _frame_items(spec) # 22 个 box/polygon,覆盖 6/0 + 7/0 两层
c.shape_insert_many(cell_name, frame_items)
6/0/7/0):键合 pad housing + 颈部 + 双臂 + 尖端,22 个 shape 一次写入。全景尺度下双臂之间 130 µm 的间隙已经小到看不出分叉,这也是下面几步要反复面对的"整体 vs. 局部"尺度问题。3画键合 pad + 中继过孔
housing 内部沿 y 方向排布着 24 个 net;每个 net 在 probe_pad_x_um=(-6650,-5450,-4250) 三个 x 位置各留一个 150×150 µm 键合 pad(探针最常用的探针台/引线键合尺寸),一共 3 列 × 24 行 = 72 个 pad 方块,像接力棒一样从最外侧一路排到 route_via_x_um=-3050;其中一半 net(net_num <= pads_per_half)还在这里多留了一对中继过孔——这是给 Stage 6 路由用的"落脚点"。这一步是全篇批量最大的一次写入:288 个 shape,横跨 1/0/2/0/3/0/4/0/5/0 五个层,一次 RPC:
probe_items = items[frame_count:frame_count + probe_count] # 288 个 shape
c.shape_insert_many(cell_name, probe_items)
1/0/2/0/3/0/4/0/5/0),288 个 shape 一次批量写入。要是逐个 shape.insert_box 单发,这一步就是 288 次独立 RPC——CLAUDE.md 的批量 RPC 铁律在这种规模的版图上不是"最佳实践",是"能不能用"的区别。4画电极阵列(由 --elec-rows 参数化)
探针轴远端是真正接触组织的电极阵列:elec_rows 行 × 3 列,两组(elec_left_x_um 靠近颈部、elec_right_x_um 靠近尖端),每个电极 pad 只有 20×21 µm——这就是键合 pad(150 µm)和电极 pad(20 µm)之间接近 8 倍的间距落差,和 CLAUDE.md 里"阵列间距 vs 探针间距不同尺度"是同一个问题,只是这次两端都在同一颗芯片上。--elec-rows 只改一个数字,行数、pad 总数、shape 数量全部联动:
--elec-rows | 每半边键合 pad 数pads_per_half = 3×rows | net 总数net_count = 2×pads_per_half | 电极 shape 数rows×21 |
|---|---|---|---|
| 2 | 6 | 12 | 42 |
| 4(本篇实测) | 12 | 24 | 84 |
| 6 | 18 | 36 | 126 |
--elec-rows 2/6 两行是从同一份 HarnessPCellSpec 公式推出来的(pads_per_half/net_count 是 @property,电极 shape 数是 elec_rows × 21),本篇实际跑的是 --elec-rows 4 那一行,其余两行没有单独重跑截图。
electrode_items = items[frame_count + probe_count:] # elec_rows x 21 = 84 个 shape(本例 rows=4)
c.shape_insert_many(cell_name, electrode_items)
860px 和 1140px 附近,分别是 elec_left/elec_right 两组)就是 84 个电极 shape 的全部视觉痕迹——这就是尺度落差的直观后果。
view.screenshot(bbox_um=[2370,-70,2510,70], width_px=1200, height_px=1200) 的精确裁剪(140×140 µm,无长宽比扩展):elec_rows=4 对应的 4×3=12 个电极 pad 清晰可数,每个 pad 里一个深色小方块是它自己的过孔。5标记 Port + 走廊 Anchor
几何画完后开始标意图。这里有三类 Port:24 个键合 pad Port(electrical,access_mode="edge",可沿 pad 边滑动,一个 net 一个)、24 个中继 Port(供路由用,走辅助层 999/98,同样一个 net 一个)、以及电极侧的 12+24 个 Port——每个 (row, col) 电极位置一个主 Port(elec_left 侧,net 字段同时写 bottom_net 和 top_net 两个 net token)+ 两个辅助 Port(ET{top_net} 在 elec_left 侧、EB{bottom_net} 在 elec_right 侧,各自只认一个 net)——加起来 84 次独立 port.mark 调用。Port 创建走的是 PCell 实例化路径,不在 shape.insert_many 的批量范围内,这 84 次确实是 84 次 RPC,教程如实展示这一点,不掩盖。
Anchor 只有 2 个,但类型和前两篇都不一样:kind="corridor",一条贯穿全芯片 y 方向、宽度由 corridor_width_um()(本例 700 µm)算出的强制走廊——所有键合 pad 侧的走线必须收拢进这条走廊才能穿过颈部,这是"允许穿越窗口"(EBL 教程的 kind="corridor" 窗口)在完全不同场景下的复用:不是避障,而是强制汇聚。
c.call("port.mark", {
"cell": cell_name, "layer": "999/99", "name": "P36", "label": "n1",
"center_um": [x, y], "orientation": 0, "width_um": 30.0,
"port_type": "electrical", "net": "n1", "target_layer": "1/0",
"access_mode": "edge", "slide_allowed": True, "slide_edge": "x0,y0,x1,y1",
})
c.call("anchor.mark", {
"cell": cell_name, "layer": "999/1", "id": "A1_BOTTOM_M1_CORRIDOR",
"kind": "corridor", "center_um": [-1650.0, 0.0], "width_um": 700.0,
"path_points": "0,-1;0,1", "net": "n1,n2,...,n12", "priority": 1, "required": True,
})
n1…n24)和 housing 出口处的走廊标记线在这个尺度下可见;电极侧的 Port 只有 5 µm 宽,在全景截图里是亚像素级的——像素级对比(step-04 与本图逐像素 diff)证实变化只发生在 x∈[118,719] 键合 pad 区域,电极区域 x∈[800,1200] 零像素差异。要看电极侧的 Port 落位,得等 Step 7 的细节裁剪图。6走廊约束下的批量路由
标记完成后,一次调用把 24 个 net 全部路由完:先用 port.list/anchor.list 批量取回所有辅助 Port 和走廊 Anchor,按 route_layer(1/0 一组、3/0 一组)分组,每组各调一次 route_tapered_hybrid_many(客户端 Python 里做可见性图 + Dijkstra 规划,零栅格化)+ commit_tapered_hybrid_many(一次批量 RPC 写回):
from klink.routing.backends.geometric.tapered_segments import (
pair_ports_by_net_tokens, route_tapered_hybrid_many, commit_tapered_hybrid_many,
)
ports = c.call("port.list", {"cell": cell_name, "layer": "999/98"})["ports"]
ports = [p for p in ports if str(p.get("name") or "").startswith(("R", "EB", "ET"))]
anchors = c.call("anchor.list", {"cell": cell_name, "layer": "999/1"})["anchors"]
pairs = pair_ports_by_net_tokens(ports) # 按 net token 配对 source/target
by_layer = {}
for pair in pairs:
by_layer.setdefault(pair.get("route_layer") or "10/0", []).append(pair)
for route_layer, layer_pairs in sorted(by_layer.items()):
planned = route_tapered_hybrid_many(
layer_pairs, anchors=anchors, spacing_um=8.0,
strategy="uniform", angle_mode="manhattan", validate_sibling_overlap=True,
)
commit_tapered_hybrid_many(c, cell_name, planned, route_layer=route_layer, clear=True)
1/0;图上半区紫蓝色 12 条是 net13-24,在 3/0):每条走线在键合 pad 区保持独立通道(CLAUDE.md 的"channel isolation"),到 housing 出口才收拢进 700 µm 宽的走廊 Anchor——多对一收敛,而不是各走各的直线。7收尾:全景 + 电极端细节
routing 已经是最后一次几何写入,Stage 7 只是看结果:
c.zoom_fit()
overview = c.screenshot(mode="base64", width_px=1200)
detail = c.screenshot(mode="base64", bbox_um=[2370.0, -70.0, 2510.0, 70.0],
width_px=1200, height_px=1200)
view.zoom_fit):housing + 24 个 net 的键合 pad 链路 + 24 条走线 + 走廊收拢 + 探针轴 + 电极阵列,全部在一个 cell 里。
elec_right_x_um[2]=2480, y=45 处的电极 pad——它的过孔上标着 EB12(net n12,1/0 层),这条走线从最远的键合 pad 出发、穿过走廊、走完全部近万微米,最终精确落在这个 20×21 µm 的电极上(用 port.list 按坐标核对过,不是目测)。验证,不是截图
和教程首页说的一样,截图只是给人看的,不是完成依据。这次实际运行(--elec-rows 4)的结构化结果:
{
"shape_item_counts": {"frame": 22, "bondpads_and_vias": 288, "electrodes": 84, "total": 394},
"port_count_total": 84,
"anchor_count_total": 2,
"route_result": {
"ok": true,
"port_count": 48,
"anchor_count": 2,
"pair_count": 24,
"groups": [
{"route_layer": "1/0", "route_count": 12, "sibling_overlap_count": 0, "obstacle_hit_count": 0, "inserted": 84},
{"route_layer": "3/0", "route_count": 12, "sibling_overlap_count": 0, "obstacle_hit_count": 0, "inserted": 84}
]
}
}
ok=true,两个 route_layer 分组的 sibling_overlap_count/obstacle_hit_count 全部为 0,说明 24 条走线(12 在 1/0,12 在 3/0)互不重叠、没有相互撞线;pair_count=24 等于 net_count,说明每个 net 都恰好配对成功,没有落单的 Port。这三个数字合在一起,才是"这版参数化 harness 布线正确"的依据,而不是"看起来接上了"。
下一步
把 --elec-rows 改成你自己的行数(比如 2 或 6),对照上面 Stage 4 的公式表核实 shape 数量是否符合预期,再确认 route_result.ok 依旧是 true。如果还没看过前两篇,建议按顺序读:《分步教程:Hall bar 器件》(geometry-first 入门:先标 Port/Anchor 意图再路由)和《分步教程:EBL wraparound 纳米器件》(写场避障:kind="corridor" 窗口第一次出现)——这篇教程的走廊 Anchor 正是从那篇借来的同一个 kind,用在了完全不同的"强制汇聚"场景里。下一篇预告是硅光方向的 gf_mzi_module 教程(python -m examples_klink.public.demos.photonics.gf_mzi_module --port <session-port>),会换到 gdsfactory 桥接和 MZI 器件。Port/Anchor 的完整字段说明见 MCP 参考,批量 RPC 的完整设计边界见仓库 CLAUDE.md 的"Use batch RPCs for generated layouts"一节。