Step-by-step tutorial · routing

分步教程:五个路由场景,同一个 taper 后端

对应仓库里的 example_template/routing/five_routers.py。klink 的路由后端不需要"五种情况五个路由器"——一条普通两端口直连、一条被强制途经某个 waypoint 的连线、一个可以沿设备边缘滑动的端口、一条要绕开 keepout 障碍物的连线、以及四条需求端口各自挑候选 pad 的扇出,全部走同一个连续多边形 taper 后端(route_tapered / commit_tapered_routes)。五个场景真正不同的是喂给它的输入——Anchor、障碍物 bbox、每条网络的线宽——不是调用了哪个不同的路由函数。这篇教程把这五个场景逐个画出来、路由、截图,并展示 Phase 3 那条诚实的逐路由验证结果:ok=false 从不会被当作"路由完成"。

这是什么

five_routers.py 建五个小场景(各自的设备/pad 几何、Port、Anchor、一个 keepout),逐个路由,再逐个验证。诚实之处在于:五个场景的路由调用完全一样——都是先用 route_with_port_launch_stubs 画一条参考中心线 guide(只给人看,不参与验证),再用 route_tapered 规划出真正的锥形多边形路由、commit_tapered_routes 一次批量写入。场景之间的差异只在 Phase 1 建场景时喂给路由器的输入:要不要 Anchor、Anchor 是什么 kind、端口能不能滑动、障碍物 bbox 是什么。没有为任何一个场景切换到"另一个路由器"。

前置条件

  • 安装 klink:pip install klayout-klink。
  • KLayout 正在运行,并已加载 klink 插件(RPC 端口默认 8765)。
  • 需要一个 live 的 KLayout 会话——五个场景都是直接写入 live 版图的 RPC 调用,不是纯离线合成:python example_template/routing/five_routers.py(--port <端口> 指定 session,默认 8765)。
脚本会自己开一个新 tab(FIVE_ROUTERS_SCRATCH 只是占位脚手架 cell),五个真实场景 cell 在里面各自重建;跑完脚本会自己关掉这个 tab、把你原来的 tab 切回来——不会碰你手工的工作 tab。

涉及的层

层用途
999/99klink 保留的 Port 标记层
999/1klink 保留的 Anchor 标记层
1/0M1_DEVICE_OR_PAD:每个场景自己的设备/pad 几何
900/0KLINK_ROUTE_KEEPOUT:场景 ④ 里要绕开的 keepout 障碍物
996/99KLINK_EXPECTED_ROUTE_GUIDE:route_with_port_launch_stubs 画的参考中心线,只给人核对,不参与验证
11/0KLINK_TAPERED_RESULT:route_tapered → commit_tapered_routes 真正写入的锥形路由多边形
997/99KLINK_EXAMPLE_LABELS:每个场景顶部的说明文字

每个场景的路由调用都是同一个两步模式:

g = route_with_port_launch_stubs(p0, p1, inner_points or None)
guide(c, cell, g["points_um"], width=g["width_um"])          # 996/99 参考线,仅供人眼核对

t = route_tapered(p0, p1, inner_points or None, strategy="uniform", corner_style="miter")
write = commit_tapered_routes(c, cell, [t], route_layer="11/0", clear=False)  # 真正的锥形路由

1直连基线

最简单的场景:两个同网络的端口,中间没有任何 Anchor。端口 A 在 [20, 5]、朝向 0°、线宽 5 µm;端口 B 在 [100, 5]、朝向 180°、线宽 2 µm——路由从 5 µm 平滑收窄到 2 µm,中间没有一次拐弯。

p0 = port(c, cell, "A", [20, 5], 0, net="net_straight", width=5.0)
p1 = port(c, cell, "B", [100, 5], 180, net="net_straight", width=2.0)
# inner_points 为空 -> route_tapered(p0, p1, None, ...)
两个端口之间一条从宽变窄的锥形走线,笔直连接,没有任何拐弯
Step 1 · 直连基线路由完成:一条从 5 µm 平滑收窄到 2 µm 的锥形走线,中间没有一次拐弯——两个端口之间没有任何 Anchor 强制它绕路。

2强制途经点

端口位置不变(分别在 18 µm 和 100 µm 处),但这次中间放了一个 waypoint_region 类型的 Anchor WP1,中心在 [60, 40]、12×10 µm——路由必须穿过这个框,而不是走两个端口之间最短的直线。klink 把它变成一个途经点,塞进 route_tapered 的 inner_points。

anchor(c, cell, "WP1", [60, 40], "waypoint_region", net="net_waypoint",
       label="must_pass", width=12, height=10)
# requests: inner_points=[[60, 40]]
路由走线先从端口出发向上拐,穿过一个矩形 waypoint 标记区域,再拐向另一个端口
Step 2 · 强制途经点路由完成:走线先拐向 WP1 那个 waypoint_region Anchor(图中矩形标记),穿过它之后再拐向对面的端口——不是两端口之间最短的直线。

3边缘滑动端口

A_EDGE 端口的 access_mode="edge"、slide_allowed=True,还带一个 slide_edge 参数——一段用数据库单位(当前默认 1 nm)表示的边缘坐标 "20000,40000,140000,40000",告诉路由器这个端口不是钉死在一个点上,而是可以沿设备顶边(20~140 µm 那一段)滑动,由路由器自己挑一个对布线最有利的出发位置。另一端 B 是普通端口;中间还有一个 EXIT waypoint_region Anchor,强制走线先冲出设备正上方再拐向 B。

# 存成 DBU 是因为当前默认数据库单位是 1 nm;这个示例是刻意把
# 面向路由器的 slide edge 写清楚,方便照抄。
slide_edge = "20000,40000,140000,40000"
p0 = port(c, cell, "A_EDGE", [80, 40], 90, net="net_slide", width=6.0,
          access_mode="edge", slide_allowed=True, slide_edge=slide_edge)
p1 = port(c, cell, "B", [150, 72], 180, net="net_slide", width=3.0)
一个端口沿长方形设备的顶边可以滑动出发,走线从设备上边缘某处出发,先向上穿过一个 waypoint 再拐向右边的端口
Step 3 · 边缘滑动端口路由完成:A_EDGE 没有钉死在一个固定坐标,路由器沿设备顶边选了一个出发点,先穿过 EXIT waypoint_region 再拐向 B。

4绕开障碍物

900/0 层上画着一个 keepout box [52, -18, 86, 28](标着 KEEP_OUT 文字)。路由必须完全绕开它,不能只是擦边而过。Anchor BEND_ABOVE 是 bend_region 类型、priority=10,中心在障碍物正上方 [69, 42]——但只传这一个途经点还不够:单点只保证走线经过那个点,不保证整条走线全程都在障碍物外面。所以脚本给 route_tapered 传了两个 inner_points([48, 42] 和 [90, 42]),让走线整体贴着障碍物上方绕过去,而不是只在锚点处避开、别处又切回障碍物内部。

keepout_bbox = [52.0, -18.0, 86.0, 28.0]
box(c, cell, keepout_bbox, layer=KEEPOUT)
anchor(c, cell, "BEND_ABOVE", [69, 42], "bend_region", net="net_obstacle",
       label="above", radius=6, priority=10)
# 两个途经点,让走线整体绕过去,只经过锚点中心是不够的
requests = [{"source": p0, "target": p1, "inner_points": [[48, 42], [90, 42]]}]
绕障碍物场景在路由之前的输入:两个端口、一个标着 KEEP_OUT 的矩形障碍物、以及障碍物上方的一个 bend_region Anchor 标记,中间还没有任何走线
Step 4a · 路由之前的输入:两个端口、900/0 层上的 KEEP_OUT 障碍物、以及障碍物正上方的 BEND_ABOVE bend_region Anchor——这是路由器要处理的全部约束,此时还没有任何走线。
走线从左侧端口出发,明显向上绕开中间的 KEEP_OUT 障碍物,再拐回右侧端口,与障碍物之间留有清晰间隙
Step 4 · 绕障碍物路由完成:走线整体抬升到障碍物上方绕过去,全程和 KEEP_OUT 保持间隙——这正是 Phase 3 里 obstacle_hit_count 要验证为 0 的地方。

5需求端口扇出

四个需求端口 IN0..IN3(3 µm 宽,net=sig0..sig3)要连到六个候选 pad PAD0..PAD5 中的四个(8 µm 宽,port_type="candidate_sink",net="")——每条走线都从 3 µm 一路放宽到 8 µm。两组信号各自走一条命名 corridor Anchor(LOWER_CORRIDOR 带 net="sig0,sig1"、UPPER_CORRIDOR 带 net="sig2,sig3"),保证同组两条线沿同一条走廊前进,而不是各走各的。同一条走廊里的两条线还需要一个显式的车道偏移(lane_offset)才能互不接触:脚本注释里记录了一个真实教训——最初 ±3 µm 的车道间距不够,因为 route_tapered 中段的宽度已经超过 6 µm,±3 µm 的车道会重叠;改成 ±5 µm 才是这个几何下第一个能让 sibling_overlap_count 归零的间距。

anchor(c, cell, "LOWER_CORRIDOR", [45, 17], "corridor", net="sig0,sig1",
       label="follow_lower", width=8.0, path_points="-15,-1;10,0;15,1")
anchor(c, cell, "UPPER_CORRIDOR", [75, 49.5], "corridor", net="sig2,sig3",
       label="follow_upper", width=8.0, path_points="-9,-0.5;6,0.5;9,-1")

# +-3um 车道不够(route_tapered 中段宽度已超 6um,会重叠);
# +-5um 是这个几何下第一个能让 sibling_overlap_count 归零的车道间距
assignments = [
    (10, 0, lower_corridor, -5.0),
    (24, 14, lower_corridor, 5.0),
    (38, 42, upper_corridor, -5.0),
    (52, 56, upper_corridor, 5.0),
]
四条锥形走线各自从左侧一个窄端口放宽到右侧一个宽 pad,同组两条线共享同一条走廊但彼此保持独立车道,不触碰
Step 5 · 四条扇出走线全部路由完成:每条都从 3 µm 端口放宽到 8 µm pad,同组两条共用一条走廊但各自占一条独立车道,互不接触——四条互不干扰正是这个场景验证的重点。

验证,不是截图

和核心思路说的一样,截图只是给人看的,不是完成依据。five_routers.py 的 Phase 3 对每个场景做同样三件事:统计 commit_tapered_routes 真正写入的多边形数量是否等于这个场景的预期请求数(route_count)、拿每条走线去和障碍物 bbox 做碰撞检测(obstacle_hit_count)、拿每条走线去和同一场景里其它走线做重叠检测(sibling_overlap_count)——三项全部达标才把 ok 记为 True。脚本给每个场景打印一行,格式固定:

  %-22s ok=%-5s route_count=%d/%d obstacle_hit_count=%d sibling_overlap_count=%d

五个场景全部干净时是这样的:

  ROUTE_01_STRAIGHT      ok=True  route_count=1/1 obstacle_hit_count=0 sibling_overlap_count=0
  ROUTE_02_WAYPOINT      ok=True  route_count=1/1 obstacle_hit_count=0 sibling_overlap_count=0
  ROUTE_03_EDGE_SLIDE    ok=True  route_count=1/1 obstacle_hit_count=0 sibling_overlap_count=0
  ROUTE_04_OBSTACLE      ok=True  route_count=1/1 obstacle_hit_count=0 sibling_overlap_count=0
  ROUTE_05_FANOUT        ok=True  route_count=4/4 obstacle_hit_count=0 sibling_overlap_count=0

all five scenarios routed cleanly

只要有一个场景 obstacle_hit_count 或 sibling_overlap_count 不是 0,或者写入的多边形数量对不上预期请求数,那个场景的 ok 就是 False——脚本会把不达标的场景连同数字列出来,退出码非 0,绝不会把它报告成"路由完成"。

下一步

把 example_template/routing/five_routers.py 里的场景数据换成你自己的端口坐标、线宽和障碍物声明,重跑确认五行全部 ok=True。想直接对着 MCP 工具写,完整的 9 个路由工具参数在 MCP 参考 · 路由后端,Port/Anchor 的完整字段说明在 MCP 参考 · Port、Anchor 与 Region。这篇教程里"先标 Port/Anchor 意图、再让路由算法补全布线"的思路和 Hall bar 器件教程的第 ⑤~⑥ 步是同一套 geometry-first 流程,只是这里换成了五种不同的输入组合;扇出场景里走廊 + 车道的批量路由思路,在更大规模上还能看 神经电极阵列 harness 教程。