Step-by-step tutorial · structdevice

分步教程:针卡优先的 padframe

对应公开 gallery 里的 padframe demoexample_template/digital/padframe_pnr_lvs.py)。这是一个反序的硬件流程:探针卡 / pad 环先存在(位置很久前就冻结了),电路必须去迁就它——哪怕卡内部装不下整个器件块。这篇教程把它拆成 6 个阶段,每一步都在 live KLayout 里真的画出来、截图,并给出对应的 klink 代码,最后 card 与 --no-card 两种模式各自跑 live LVS,match=True 才算完。文中所有数据都是合成的(工艺、器件、针卡都是替身),不含任何真实厂商或探针卡型号。

前置条件

  • 安装 klink:pip install klayout-klink(一条命令装好 klink 和它的 Rust 内核)。
  • KLayout 正在运行,并已加载 klink 插件。
  • 这个 demo 是 starterklink init <proj> 脚手架里就有(example_template/digital/padframe_pnr_lvs.py),需要一个运行中的 KLayout 会话。完整命令是:
    python example_template/digital/padframe_pnr_lvs.py --port <会话端口>            # 有卡模式
    python example_template/digital/padframe_pnr_lvs.py --port <会话端口> --no-card  # 无卡模式
    # 仓库克隆里也可以: python -m examples_klink.public.demos.digital.padframe_pnr_lvs --port <会话端口> [--no-card]
这篇教程把 padframe_pnr_lvs.py 内部一次性完成的构建拆成 6 个可观察阶段,方便讲清"卡先存在、电路后迁就"这条反序流每一步在做什么。生产环境里你还是直接跑那一个脚本——分阶段只是教学手段。它复用 拟合器件 demo 的公开 3 层工艺与合成拟合器件库,所以本身零器件几何;电路是一个开源逻辑综合器网出来、再映射到这些合成器件上的 4-bit 加法器(173 器件 / 96 net / 62 gate)。

1合成电路基底:lint + 层数顾问 + 平铺摆放

lint 器件 netlist——在任何几何存在之前,就把手写 netlist 的结构错误(未知实例、终端接到两个 net、实例不在任何组里……)连同修复建议一起报出来。lint 通过后,层数顾问把每个候选层栈的代价摊开:层数越少越好(每多一层都是真实的一道沉积 / 光刻 / 通孔工序),选择权在你。add4 在公开 3 层栈上就能舒服布通,所以我们用它。

候选层栈路由层数信号层core 尺寸
public-3L32V + 1H920 × 1208 µm = 1.111 mm²
example-7L72V + 2H920 × 984 µm = 0.905 mm²

顾问只报算术,候选层栈本身是示例数据(你的 fab 实际能造什么)。这一步的截图里,器件块先按无带约束平铺(8×8 网格,行距 190 µm),用来量出 block 占位、给后面的卡定位/定尺寸:

rep = lint_netlist(nl, device_terms=terms)          # 结构 lint,错误即修复指令
assert rep["ok"]
adv = layer_demand_report(nl, terms, [("public-3L", D.PUBLIC_PROCESS),
                                      ("example-7L", PUBLIC_MULTILAYER)])
rows, cols = derive_grid(len(nl["groups"]))          # 8 x 8
flat = eng.place_grid(nl, rows, cols, profile=P, row_pitch=rp)   # 无带平铺
8 行合成器件按网格平铺,每个 gate 是一列堆叠的器件,尚无卡、无布线
Step 1 · lint 通过、选定 3 层栈后,器件块先按无带约束平铺(place_grid,8×8 网格)。这是量 block 占位、给卡定位的基准,此时还没有卡、没有布线。

2用户的针卡:造替身 → pads_from_gds harvest 回来

这是反序的核心:卡先存在。为了让 demo 自包含,我们先一个替身针卡 GDS(106/0 层上 20 个 pad 方块排成一圈,就当它是从抽屉里翻出来的旧卡),然后用 pads_from_gds 把它 harvest 回来。真实场景里你跳过 fabricate 这一步,直接 harvest 你自己的卡文件:

from klink.routing.grid.pad_harvest import pads_from_gds

# 真实流程从这一行直接开始:pads_from_gds(你的卡文件, 你的cell, 你的层)
pads = pads_from_gds(CARD, "PROBE_CARD_20", "106/0", min_size_um=50.0)
# -> [{"id": "PAD00", "box_um": [...]}, ...]  共 20 个 pad

卡的几何是示例数据:pad 100×100 µm,环内部够宽能容下 block,但高度只有 block 的一半——这正是后面第 ④ 步"一半卡内 / 一半卡下"的由来。harvest 出来的就是一张普通 pad 表 [{"id", "box_um"}, ...],net 分配是之后人/agent 在它上面做的决定。

20 个 pad 方块围成一圈的 pad 环,套在中间的器件块外面,左右各 5 个、上下各 5 个
Step 2 · 从替身 GDS harvest 回来的 20-pad 环(106/0 层),左右各 5 个 + 上下各 5 个。环内部装不下整块器件的全高——注意底部 pad 行会横穿 block 下半部。

3net → pad 分配表

在 harvest 出来的 pad 上叠一张纯表格:哪个 net 走哪个 pad。本例约定(你可自由改):输入走左列 + 顶行,输出走右列,GND 在顶行(它的 tie rail 在 block 上方派生),VDD 在左下角 pad(沿左边留一条干净走廊到 block 下方的 rail)。20 个 pad 里用掉 16 个(全部 14 个主端口 + VDD + GND),剩 4 个冗余 pad 保持未用——这在任何旧卡上都很正常。

def near(x, y):     # 离某个卡上点最近的 harvest pad
    return min(pads, key=lambda p: (p["box_um"][0] - x) ** 2 + (p["box_um"][1] - y) ** 2)

for cy, net in zip(side_y, ["A[0]", "A[1]", "A[2]", "A[3]", "B[0]"]):
    near(ring_x1, cy)["net"] = net          # 左列:5 个输入
for cy, net in zip(side_y, ["S[0]", "S[1]", "S[2]", "S[3]", "COUT"]):
    near(ring_x2 - PS, cy)["net"] = net     # 右列:5 个输出
for cx, net in zip(row_x, ["B[1]", "B[2]", "GND", "B[3]", "CIN"]):
    near(cx, ring_y2 - PS)["net"] = net     # 顶行:输入 + GND
near(row_x[0], ring_y1)["net"] = "VDD"      # 左下角:VDD
每个 pad 上标了 net 名字:左列 A[0..3]/B[0],右列 S[0..3]/COUT,顶行 B/GND/CIN,左下角 VDD,底部四个标 PAD16..19 表示冗余未用
Step 3 · 一张 net→pad 表叠在 pad 上:左列 5 输入、右列 5 输出、顶行输入 + GND、左下角 VDD。底部 PAD16..PAD19 保留原始 id,表示它们是冗余、未分配 net 的 pad。

4一半卡内 / 一半卡下的摆放

卡的底部 pad 行横穿器件块。因为卡是固定的、能屈能伸的是 block——所以我们把那条水平带禁掉place_grid(forbid_y_bands=[band]) 会把任何会撞到这条带的器件行推到带下面去。前 inner_rows 行留在环内,其余继续排在环下,布线在底部 pad 之间穿过。这样一整块器件就"一半在 pad 环内 / 一半在环外"地被劈开,不需要任何新的摆放引擎

band = (ring_y1 - CLR, ring_y1 + PS + CLR)          # 底部 pad 行 + 间隙
placement = eng.place_grid(nl, rows, cols, profile=P, row_pitch=rp,
                           forbid_y_bands=[band])   # 撞带的行被推到带下方
# 实测:85 个器件在环内,80 个在环下
器件块被底部 pad 行劈成上下两半:上半留在 pad 环内部,下半整体下移到环外,中间空出底部 pad 行占的水平带
Step 4 · forbid_y_bands 把底部 pad 行占的水平带禁掉:85 个器件留在环内、80 个被推到环下(对比 Step 3,下半部整体下移,让开了底部 pad 行)。卡不动,是 block 让路。

5信号 P&R + 分区电网,然后成品

最后一次调用 route_and_draw_flexdr 同时干三件事:信号布线(faithful FlexDR,在 track grid 上避开电网)、器件 PCell + 走线 + 通孔、以及把 io_pads 里已分配的 pad 变成该 net 自己的固定金属 + 额外布线终端(未用 pad 变成硬避障)。同一条禁带还会劈开电网pdn_split_bands 每个区域造一张 PDN,再用一条 spine strap 穿过底部 pad 行最宽的无 pad 缝隙把两区桥接——电源像信号一样在卡的 pad 之间穿线。

with KLinkClient(port=PORT).connect() as c:
    ok, info, _ = eng.route_and_draw_flexdr(
        c, "PUB_PADFRAME_ADD4", nl, placement, profile=P, layers=layers,
        vias=P.via_rules(), cut_layer=cut_layer, geom_path=D.GEOM, devices=DEVICES,
        use_rust=True, io_pads=IO, pdn_split_bands=[band])
    # -> routed 94/94, markers 0, 4.1s
完整成品:pad 环内外的器件块全部布线,绿色/橙色走线连到四周的 pad,VDD 从左下角一条竖直走廊下到电网
Step 5 · 成品(view.zoom_fit):routed 94/94 · markers 0。上下两半器件块都布通,信号连到四周已分配的 pad;VDD 从左下角 pad 沿左边走廊接到下方电网,GND 顶行 rail 在上方。
放大左下角:VDD pad 下方一条竖直的黄色 strap 走廊接到电网 tie rail,右侧是 GND rail 和器件
Step 5b · 左下角局部:VDD pad 沿自己中心一条竖直 strap 走廊下到电网 tie rail——power pad 需要一条干净的竖直车道到它 net 的 rail,没有的话 route_and_draw_flexdr 会报一条指明冲突的错误。
放大底部 pad 行区域:走线和一条电网 spine strap 从底部三个冗余 pad 之间的缝隙穿过,连接环内和环下两个区域
Step 5c · 底部 pad 行局部:信号走线和电网 spine strap 都从底部 pad(PAD16..18)之间的无 pad 缝隙穿过,把环内 PDN 和环下 PDN 桥接起来——电源和信号一样在卡的 pad 之间穿线。

6无卡模式:每个端口引成裸线头

--no-card完全没有 pad,也不画任何 pad。每个端口作为一条裸标注线端离开 block——spread_ports 造出线端目标(draw=False:不画盒子,走线自己的金属在那里结束,带上 net 名文字)。输入吸在西边、输出吸在东边,都吸附到器件行之间的路由通道中心(避免线端落在器件行带里逼着水平段挤进邻道)。电源根本不需要线头:PDN tie rail 已被引擎自动标注为 VDD/GND,在 rail 上任意处 bond/probe 即可。

from klink.routing.grid.pad_harvest import spread_ports

IO2 = {"pad_layer": "106/0", "text_size_um": 15.0,
       "pads": (spread_ports(BB, INPUTS,  side="W", size_um=P.wire_width_um,
                             clear_um=120.0, prefix="IN",  snap=chan)     # 输入在西
                + spread_ports(BB, OUTPUTS, side="E", size_um=P.wire_width_um,
                             clear_um=120.0, prefix="OUT", snap=chan))}   # 输出在东
ok2, info2, _ = eng.route_and_draw_flexdr(
    c, "PUB_PADFRAME_ADD4_NOCARD", nl, flat, profile=P, layers=layers, ...,
    io_pads=IO2, pdn_split_bands=None)      # -> routed 94/94, markers 0, 3.6s
无卡版本:中间是器件块,没有 pad 环,输入标签 A/B/CIN 在左边、输出 S/COUT 在右边,都是裸的线端标注
Step 6a · --no-card 成品:routed 94/94 · markers 0。没有 pad 环——输入(A[*]/B[*]/CIN)在西边、输出(S[*]/COUT)在东边,全是裸标注线端;电源在自动标注的 PDN tie rail 上。
放大西边:A[1]、CIN 等 net 名标在走线末端,走线到那里就结束,没有 pad 方块
Step 6b · 西边局部:A[1]CIN 等 net 名标在走线末端——线端就是端口,没有画任何 pad 盒子(draw=False)。这些标签告诉你之后自己的 pad 该接哪里。

验证:两种模式各自的 live LVS

和别的教程一样,截图只是给人看的,不是完成依据。这个 demo 的完成门槛是两种模式各自 live LVS match=True,外加一条正向逐 pad 证明:每个已分配 pad/线头的金属必须和它 net 的器件终端落在同一条抽取网络上;每个未用 pad 必须不碰任何 net。本次实跑(8767 会话,Rust 内核)的真实数字:

模式routedDRC markersLVS okLVS match器件逐 pad 证明用时
card(有卡)94/940TrueTrue17316 connected + 4 隔离4.1s
--no-card94/940TrueTrue17314 connected + 0 隔离3.6s
# card 模式
[PUB_PADFRAME_ADD4]        LVS ok=True match=True devices=173
[PUB_PADFRAME_ADD4]        pad proof ok=True connected=16 isolated=4
# --no-card 模式
[PUB_PADFRAME_ADD4_NOCARD] LVS ok=True match=True devices=173
[PUB_PADFRAME_ADD4_NOCARD] stub proof ok=True connected=14 isolated=0

两种模式的 match 都是 True、DRC markers 都是 0、逐 pad/线头证明全过——这才是"padframe 做完了"的依据。有卡模式 16 个 pad 全 CONNECTED、4 个冗余 pad 全隔离;无卡模式 14 个裸线头全 CONNECTED(电源不占线头,直接在 PDN rail 上)。

下一步

padframe_pnr_lvs.py 复制一份,改里面的pad 表(pad 尺寸、位置、net→pad 约定)就是你自己的卡;改 PUBLIC_PROCESS / PUBLIC_MULTILAYER 就是你自己的层栈——流程不变,klink 只出机制。想先搞清楚器件本身怎么从示例几何拟合成参数化 PCell、再进 P&R,看同批的分步教程:拟合器件 → P&R → LVS。更多公开 demo 的实测数字见示例页 · Probe-card padframe