文件
phonerail/.claude/r2/route_base.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

1267 行
64 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 PCB 布线(M5,**4 层**,D15)。
# ⛔ 计算只在服务器上跑:.claude/r2/remote_route.ps1(它会带 DCDC_ROUTE_SERVER=1 --offline --replan)
/c/Python314/python .claude/r2/route_base.py # 读缓存 + 报告(本机可跑)
/c/Python314/python .claude/r2/route_base.py --push # 下发导线 + 过孔(本机)
/c/Python314/python .claude/r2/route_base.py --clear # 清掉全部布线(本机)
/c/Python314/python .claude/r2/route_base.py --snapshot-vias
2 层版(底层过路费 / D± 构造 / 12V 干道 / 3V3 槽墙 / 车道)整份在 `archive_base2L/route_base.py`。
**引擎仍是刀片那份 `router.py`**(0.25 网格 + 8 邻接 A*,L1/L4 两层可布、内层做平面),这里只换板级常量、加底板的障碍、写主流程。
## 叠层与分工(D15)
| 层 | 内容 | 布线器怎么看 |
|---|---|---|
| L1 | 5 个插槽 + 西侧 `+12V` 大铜(`pcb_base.l1_12v_rects`)| 可布;12V 大铜**只挡导线**(过孔可以落进去,铺铜自己挖反焊盘)|
| L2/L3 | GND 平面 | 不布线;GND 焊盘各打一个过孔下来 |
| L4 | 其余 133 件 | 主力布线层 |
## 主流程(`plan_all`)
1. **Kelvin**:`U13.10→Rs6.1`、`U13.9→Rs6.2` 各一条 0.3 专线,最先布。就近扎进平面的话,
INA226 量到的是「分流电阻 + 一段铜箔」的压降,读数随铜温漂,而 DRC / 连通性全过。
2. **9 A 上 L1**:`Rs6.2` 西侧一块 L4 `+12V` 铜(`V12_PATCH`)+ 15 个过孔,落进 L1 大铜。
3. **D± 七对**:DP 先 A*;DM 再 A*,离 DP 远的格点全收过路费(`HUG_PEN`),于是贴着 DP 走。
⚠ 北去三对(USB1~3)**极性是反的**:U32 北沿 DM 在西、槽上 D+ 在西(x 47.5)。同层两根线
不交叉换不过来 —— 交给 A*,它会让 DM 打一个过孔绕过去。四层薄介质上 0.28 线不耦合时
Zdiff ≈ 100 Ω,仍在 USB 2.0 的 90 Ω ±15% 里,所以局部松耦合可以接受。
4. **平面网扇出**:每只 GND 贴片焊盘一个过孔(通孔焊盘本来就穿到内层,跳过);
两边都 ≥ `EP_MIN` 的 GND 焊盘(U32 散热盘 / 插槽外壳盘 / DAC 底盘 / 模组底盘)在焊盘里打孔阵;
每只 L4 的 `+12V` 焊盘一个过孔,**必须落在 L1 12V 主块里**。
5. 其余:直连 + `router.route_groups` 按线宽分档 A*;`--repair` 拆线重布。
"""
import io
import json
import math
import os
import sys
# `--hash-seed N`:固定字符串哈希(只对 CPython 有意义;PyPy 按插入序遍历,结果与种子无关)。
if '--hash-seed' in sys.argv:
_seed = sys.argv[sys.argv.index('--hash-seed') + 1]
if os.environ.get('PYTHONHASHSEED') != _seed:
os.execve(sys.executable, [sys.executable] + sys.argv,
dict(os.environ, PYTHONHASHSEED=_seed))
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
import pcb_base as B # noqa: E402
import router as RT # noqa: E402
import rules_base as RB # noqa: E402
# 用户明确要求所有布线运算都放到 10.5.5.13。快照、缓存报告和下发仍可在本机做,
# 但任何会启动 A* 的入口必须由 remote_route.ps1 在服务器上显式解锁。
_COMPUTE_FLAGS = ('--replan', '--dp-only')
if any(flag in sys.argv for flag in _COMPUTE_FLAGS) and os.environ.get('DCDC_ROUTE_SERVER') != '1':
sys.exit(u'!! 布线运算禁止在本机执行;请使用 .claude/r2/remote_route.ps1(服务器 10.5.5.13)')
# ── 把 router 的板级常量换成底板的 ────────────────────────────────────────
# `--grid G`:布线网格(默认 0.25)。规则 0.15 / 0.15 的最小线距是 0.3,0.25 网格只能按 0.5 一道排,
# 通道容量浪费约四成(0.5 / 0.65 间距的芯片周围尤其吃亏)。0.125 每轮慢 4~8 倍,只配顺序布线。
if '--grid' in sys.argv:
RT.G = float(sys.argv[sys.argv.index('--grid') + 1])
RT.B = B
RT.PCB = B.PCB
RT.NX = int(round(B.BW / RT.G)) + 1
RT.NY = int(round(B.BH / RT.G)) + 1
RT.PADS = os.path.join(HERE, '_base_pads.json')
RT.FINGER_KEEP_Y = 0.0 # 底板没有金手指区
RT.KELVIN = [] # Kelvin 在这里自己布(router 那份只在它自己的 plan_all 里用)
RT.KELVIN_PADS = set()
RT.CLR = 0.16 # 规则 0.15(四层,同刀片),留一点给网格吸附
RT.W_DEFAULT = 0.15
RT.EDGE_CLR = 0.4
RT.LAYER_DIR = {} # 八方向同价(D± 全是南北向,L1 走廊全是东西向,偏好反而添乱)
# 靠平面成网:焊盘各自一个过孔,铜在几何上本来就是分开的(+12V → L1 大铜,+3V3 → L2 整层)
RT.ISLAND_NETS = ('+12V', '+3V3')
CACHE = os.path.join(HERE, '_base_routes.json')
CACHE_SCHEMA = 8 # v8:4 层(D15)。旧的 2 层缓存(v7)必须被拒
BOARD_VIAS = os.path.join(HERE, '_base_board_vias.json')
PLACE_JSON = os.path.join(HERE, '_base_place.json')
# ⛔ router 里有刀片专用的 `J1` 特判(那边 J1 是金手指:不封顶、不排序、**连通性检查直接跳过**)。
# 底板的 J1 是 DC 座,照原样 VIN_RAW 的入口脚就永远不被检查 —— 断了也报「0 条没搞定」。
# ⇒ 载入焊盘时改名,布线结果里不含位号,下发不受影响。
J1_ALIAS = 'J1dc'
class _NoNotch(object):
u"""`build_grid` 要 `pcb_pour.notch_cut()`(刀片金手指防呆缺口)—— 底板没有。"""
@staticmethod
def notch_cut():
return (-10.0, -10.0, -9.0, -9.0)
RT.P = _NoNotch
def _rule_min(net, auto):
u"""线宽下限查底板自己的 `rules_base`(router 那份会去 import 刀片的 pcb_rules)。"""
cls = RB.NET_TRACK.get(net)
if net in DP_NETS:
return RB.DIFF['strokeWidthMin']
if cls is None:
return 0.15
return RB.TRACK_RULES[cls]['strokeWidthMin']
RT.rule_min = _rule_min
# ── 通孔焊盘(layer 12)──────────────────────────────────────────────────
# 刀片全贴片,router 一句 `if lay not in LAY: continue` 就把它们漏出占用图。拆成两条:
# L4 那条可布(器件在背面),L1 那条带后缀、只当障碍(同网的线仍可从上面过)。
TH_SUFFIX = '#t'
TH_GROW = 0.6 # J1/J23 的孔是长槽,槽比焊盘大
TH_KEYS = set()
def split_th(pads):
out = []
for p in pads:
if p[6] == 12:
# ⛔ 用**改名之后**的位号(J1 → J1dc)。第一版记的是 J1,load_pads 随后改名,
# 于是 J1 的两只通孔地脚没被认成通孔,被当成贴片散热盘在孔里打了孔阵
# (DRC「钻孔到钻孔」4 条,J1_2 / J1_3)。
TH_KEYS.add((J1_ALIAS if p[0] == 'J1' else p[0], str(p[1])))
a = list(p); a[6] = 2; a[4] += TH_GROW; a[5] += TH_GROW
b = list(p); b[6] = 1; b[1] = str(p[1]) + TH_SUFFIX
b[4] += TH_GROW; b[5] += TH_GROW
out += [a, b]
else:
out.append(list(p))
return out
def load_pads():
# ⚠ 丢掉零尺寸的「幽灵焊盘」:多边形焊盘拆条时的残留(J23 的 A1B12 / A4B9 各有两块 0.30×0.00),
# 真焊盘另有一块。留着的话平面网扇出要给它们找落孔点、信号网要去接它们,全报失败。
pads = [p for p in split_th(RT.load_pads()) if p[4] >= 0.05 and p[5] >= 0.05]
for p in pads:
if p[0] == 'J1':
p[0] = J1_ALIAS
return pads
def load_board_vias():
u"""读取板端既有过孔;`--offline` 时使用本地快照供计算服务器复现。"""
if '--offline' in sys.argv:
if not os.path.exists(BOARD_VIAS):
raise RuntimeError(u'离线计算缺少 _base_board_vias.json;先在 EDA 可达机器运行一次预检')
return [tuple(v) for v in json.load(io.open(BOARD_VIAS, encoding='utf-8'))]
vias = RT.load_vias()
io.open(BOARD_VIAS, 'w', encoding='utf-8').write(json.dumps(vias, ensure_ascii=False, indent=2))
return vias
# ── 线宽 ─────────────────────────────────────────────────────────────────
PAIRS = {name: (dp, dm) for name, dp, dm in RB.PAIRS}
DP_NETS = []
for _name, _dp, _dm in RB.PAIRS:
DP_NETS += [_dp, _dm]
DIFF_W = RB.DIFF['strokeWidthDefault'] # 0.28(L4 参考 L3 / L1 参考 L2,~90 Ω)
RT.WIDTH = {
'VIN_RAW': 2.0, 'VIN_FUSED': 2.0, 'VIN_SW': 2.0, # 9 A 由之后的铺铜 + 断面校核保证
'FAN_12V_SW': 0.8,
# +5V 只给每槽刀片上的负载开关(规则 PWR5V 下限 0.2)。0.6 的干线从槽针排里出不来 ——
# +3V3 改平面网那一轮,5 个槽的 10 只 5V 针全部没接上。
'+5V': 0.4, '+3V3': 0.4,
'+3V3_HUB': 0.3, # 要从 U32 北沿夹在 D± 中间的 5/10/13 脚出来,粗了出不来
}
for _n in DP_NETS:
RT.WIDTH[_n] = DIFF_W
RT.WIDTH_AUTO = {}
RT.NO_CAP = tuple(DP_NETS) + ('VIN_RAW', 'VIN_FUSED', 'VIN_SW', '+5V', '+3V3', '+3V3_HUB')
# ⭐ +3V3 是平面网(2026-09-10 用户拍板:L2 整层 +3V3、L3 整层地,D15)。十轮布线信号网卡在 37~38,
# 大头是这棵 47 只脚、跨全板的电源树;改成每脚一孔扎进 L2,不再布线。
PLANE_NETS = ('GND', '+12V', '+3V3')
LATE_PLANE_NETS = ('GND', '+3V3') # 布完信号之后才打孔的平面网(早打会封死 1 mm 针排里的信号脚)
# ── Kelvin 取样 ──────────────────────────────────────────────────────────
KELVIN = [(('U13', '10'), ('Rs6', '1'), 0.3), # IN+ ← 分流电阻上游
(('U13', '9'), ('Rs6', '2'), 0.3)] # IN− ← 分流电阻下游
KELVIN_W = 0.3
# ── 9 A 上 L1:Rs6.2 西侧那块 L4 铜 + 过孔阵 ────────────────────────────────
# 北边是泄放链(R9A/R9B 焊盘下沿 48.29),东边是 C4 / U13,只有西侧 x 5.3~10.8 空着。
# 铜块东端盖住 Rs6.2 焊盘(同网),过孔 5 × 3 = 15 个(0.3 孔 1 oz 每个 ~1 A 保守)。
V12_PATCH = (5.3, 44.4, 14.3, 48.05)
V12_VIA_X = (5.9, 7.0, 8.1, 9.2, 10.25)
V12_VIA_Y = (45.0, 46.1, 47.2)
def v12_vias():
return [(x, y) for x in V12_VIA_X for y in V12_VIA_Y]
# ── 入口链那一条(L4):别的网收过路费,之后 9 A 的铜面要从这里铺 ─────────────
CHAIN_STRIP = (7.5, 0.5, 18.5, 48.0)
CHAIN_PEN = 4
CHAIN_NETS = ('VIN_RAW', 'VIN_FUSED', 'VIN_SW')
# ── D± 贴合 ──────────────────────────────────────────────────────────────
HUG_BAND = 0.9 # DP 铜外 0.9 mm 以内不收费(DM 中心距 0.5 就落在里面)
HUG_PAD = 1.5 # DM 自己的脚周围也不收费,好起步 / 收尾
HUG_PEN = 12
DP_PAD_PEN = 3 # 布 D± 时的焊盘过路费(默认 10 会让它们绕着满板焊盘蜿蜒)
# ⛔ 北去三对(USB1~3)必须**先**布。第一版先布 USB5/USB4(最长),A* 从 U32 北沿横着往西抄近路,
# 把 USB1~3 的出口整排封死(服务器首轮:三对全「没布通」)。先布北去的,它们的竖线就是一道墙,
# 南去的两对只能往东绕 —— 那正是摆位时给它们留的 DP_H 车道。
# ⚠ USB1 必须在 USB2 前面:两对都要往西绕开 USB3 的 U 形,USB2 先布就横着压过 USB1 的出口
# (第三轮:USB1 两只脚都「出不来」)。最西的先走、走得最远。
PAIR_ORDER = ['USB6', 'USBUP'] # USB1~5 全部按构造画(usb3_uturn / dp_construct);这两对从 L1 的孔起步
# 最后一段才布的电源网(它们的脚在前两段一直收过路费)。
# ⛔ 第二轮是按线宽分档、宽的先布:+3V3 / +5V 的干线沿走廊边一铺,正好横在 U35 两排脚、
# U20 西排、风扇那一窝的出口上,于是几十条信号「没布通」。
# +3V3 已是平面网(L2);+5V 挪进信号那一段一起布(最后布时通道早被占完,10 只槽针全落空)
POWER_LATE = ('+3V3_HUB', 'FAN_12V_SW')
# ── 平面网扇出 ────────────────────────────────────────────────────────────
EP_MIN = 1.2 # GND 焊盘两边都 ≥ 这个 ⇒ 焊盘里打孔(散热盘 / 外壳盘)
EP_PITCH = 1.2
EP_EDGE = 0.45 # 孔心离焊盘边
V12_LAND = 0.6 # +12V 过孔离 L1 主块边
def place_pos():
d = json.load(io.open(PLACE_JSON, encoding='utf-8'))
return {k: (v[0], v[1], v[2]) for k, v in d.items()}
def keep_rects():
u"""两层都不许有铜:天线净空 + 板框缺口(外扩板边净距)。"""
import place_base as PL
ant = PL.antenna_keepout(PL.sizes(), place_pos())
n = B.notch_rect()
e = RT.EDGE_CLR
return [ant, (n[0] - e, n[1] - e, n[2] + 1.0, n[3] + e)]
_KEEP = [None]
_orig_build = RT.build_grid
_orig_pen = RT.pen_pads
def _build(pads, halfw, vias=()):
g = _orig_build(pads, halfw, vias)
_no_via_in_pad(g, pads)
extra = max(0.0, RT.VIA_D / 2.0 + RT.CLR - halfw)
for r in B.l1_12v_rects(): # 只打导线图:过孔可以落进大铜
g.block_rect(1, r[0] - halfw, r[1] - halfw, r[2] + halfw, r[3] + halfw, net='+12V')
x0, y0, x1, y1 = V12_PATCH
g.block_both(2, x0 - halfw, y0 - halfw, x1 + halfw, y1 + halfw, extra, net='+12V')
if _KEEP[0] is None:
_KEEP[0] = keep_rects()
for r in _KEEP[0]:
for l in RT.LAY:
g.block_both(l, r[0] - halfw, r[1] - halfw, r[2] + halfw, r[3] + halfw, extra, net='#edge')
return g
# 还没打孔的 GND 贴片焊盘 + 后面阶段才布的网的焊盘:一律收过路费,给它们自己留出口。
GND_PEN_PADS = []
LATER_PEN_PADS = []
def _pen(g, pads, halfw):
_orig_pen(g, list(pads) + GND_PEN_PADS + LATER_PEN_PADS, halfw)
g.penalize(2, CHAIN_STRIP[0], CHAIN_STRIP[1], CHAIN_STRIP[2], CHAIN_STRIP[3], CHAIN_PEN)
# ⛔ **贴片焊盘里不许落过孔**(散热 / 外壳盘除外,它们的孔阵是 plane_vias 专门打的)。
# router 按「网」标归属,同网的过孔可以落进自己的焊盘 —— 插槽是手焊的,孔在焊盘里会吸锡;
# 0402 的盘上打孔也一样。第二轮已见 A* 把 D± 的孔打在槽针焊盘边上。
def _no_via_in_pad(g, pads):
k = g.nid('#pad')
r = RT.VIA_D / 2.0
for p in pads:
if p[6] not in RT.LAY or is_ep(p) or str(p[1]).endswith(TH_SUFFIX) or (p[0], str(p[1])) in TH_KEYS:
continue
# 只打焊盘所在那一层就够:过孔是通孔,free_via 两层都查
g._mark(g.ownv, p[6], p[2] - p[4] / 2 - r, p[3] - p[5] / 2 - r,
p[2] + p[4] / 2 + r, p[3] + p[5] / 2 + r, k, True)
RT.build_grid = _build
RT.pen_pads = _pen
# ── 12V 大铜底下三个区(L1 被 12V 占着,只能走 L4):没有焊盘在这里的网,路过要加价 ─────────
# 第九轮:Buck 反馈分压($2N72 / $2N70)、U13 的 I²C、风扇几个本地网「没布通」—— 它们一两毫米的路
# 被跨半块板的 +3V3 / 风扇信号 / I²C 总线从这里穿过去占了。
UNDER_12V_ZONES = ('A1', 'A2', 'A6')
UNDER_12V_PEN = 6
_orig_route_net = RT.route_net
def _route_net(g, net, pads_of_net, *a, **k):
for z in UNDER_12V_ZONES:
r = B.ZONES[z]
if not any(r[0] <= q[2] <= r[2] and r[1] <= q[3] <= r[3] for q in pads_of_net):
g.penalize(2, r[0], r[1], r[2], r[3], UNDER_12V_PEN)
return _orig_route_net(g, net, pads_of_net, *a, **k)
if '--zone-pen' in sys.argv:
RT.route_net = _route_net
# ── 小工具 ───────────────────────────────────────────────────────────────
def grid_now(pads, halfw, segs, vias, board_vias):
u"""按 halfw 建占用图并打进已布的铜;再建一张「引出脚口径」的图 gt。和 route_groups 同一套判据。"""
g = RT.build_grid(pads, halfw, board_vias)
for (l, a, b, c, d, ww, nn) in segs:
RT.block_track(g, l, a, b, c, d, halfw + ww / 2.0, nn,
extra=max(0.0, RT.VIA_D / 2.0 + RT.CLR - (halfw + ww / 2.0)))
for (vx, vy, nn) in vias:
rv = RT.VIA_D / 2.0 + RT.CLR + halfw
for l in RT.LAY:
g.block_rect(l, vx - rv, vy - rv, vx + rv, vy + rv, net=nn)
gt = RT.Grid()
sh = RT.W_DEFAULT / 2.0
for (l, a, b, c, d, ww, nn) in segs:
RT.block_track(gt, l, a, b, c, d, RT.CLR + sh + ww / 2.0, nn)
for (vx, vy, nn) in vias:
r = RT.VIA_D / 2.0 + RT.CLR + sh
for l in RT.LAY:
gt.block_rect(l, vx - r, vy - r, vx + r, vy + r, net=nn)
others = {1: [RT.pad_rect(p) for p in pads if p[6] == 1],
2: [RT.pad_rect(p) for p in pads if p[6] == 2]}
return g, gt, others
def route_one(net, ps, w, pads, segs, vias, board_vias, logs, pen_pads=(), hug=None, fence=()):
halfw = w / 2.0 + RT.CLR
g, gt, others = grid_now(pads, halfw, segs, vias, board_vias)
for (l, r) in fence: # 只对这一个网的禁区(导线图,按层)
g.block_rect(l, r[0], r[1], r[2], r[3], net='#fence')
RT.pen_pads(g, list(pen_pads), halfw)
if hug:
apply_hug(g, hug)
s, v, st = RT.route_net(g, net, ps, halfw, w, others, logs, gt)
out = list(s)
for (q, node, ww) in st:
out += RT.stub_emit(q, RT.wx(node[1]), RT.wy(node[2]),
[] if RT._is_launch(q) else others.get(q[6], []), ww / 2.0, ww, net, gt)
return out, list(v)
def apply_hug(g, band):
u"""band = {层: [矩形]}:矩形外的格点一律至少收 HUG_PEN。**在 pen_pads 之后调**(它会先清零)。"""
n = RT.NX * RT.NY
for l in RT.LAY:
field = bytearray([HUG_PEN]) * n
for (x0, y0, x1, y1) in band.get(l, []):
i0, i1 = max(0, int(x0 / RT.G)), min(RT.NX - 1, int(math.ceil(x1 / RT.G)))
j0, j1 = max(0, int(y0 / RT.G)), min(RT.NY - 1, int(math.ceil(y1 / RT.G)))
z = bytes(i1 - i0 + 1)
for j in range(j0, j1 + 1):
field[j * RT.NX + i0: j * RT.NX + i1 + 1] = z
g.pen[l] = bytearray(map(max, g.pen[l], field))
def _seg_box(s, m):
return (min(s[1], s[3]) - m, min(s[2], s[4]) - m, max(s[1], s[3]) + m, max(s[2], s[4]) + m)
# ── 各阶段 ───────────────────────────────────────────────────────────────
def kelvin(pads, segs, vias, board_vias, logs):
padmap = {(q[0], str(q[1])): q for q in pads}
out_s, out_v = [], []
for aa, bb, w in KELVIN:
pa, pb = padmap.get(aa), padmap.get(bb)
if not pa or not pb:
logs.append(u'Kelvin: 找不到 %s.%s / %s.%s' % (aa + bb))
continue
halfk = w / 2.0 + RT.CLR
g, gt, others = grid_now(pads, halfk, segs + out_s, vias + out_v, board_vias)
na = RT.pad_nodes(g, pa, halfk, others.get(pa[6], []), w / 2.0, gt)
nb = RT.pad_nodes(g, pb, halfk, others.get(pb[6], []), w / 2.0, gt)
path = RT.astar(g, na[:16], nb[:16], pa[7], halfk)
if not path:
logs.append(u'Kelvin: %s.%s → %s.%s 没布通' % (aa + bb))
continue
sg, vv = RT.to_segments(path)
out_s += [(l, x0, y0, x1, y1, w, pa[7]) for (l, x0, y0, x1, y1) in sg]
out_v += [(x, y, pa[7]) for (x, y) in vv]
out_s += RT.stub_emit(pa, RT.wx(path[0][1]), RT.wy(path[0][2]), others.get(pa[6], []),
w / 2.0, w, pa[7], gt)
out_s += RT.stub_emit(pb, RT.wx(path[-1][1]), RT.wy(path[-1][2]), others.get(pb[6], []),
w / 2.0, w, pb[7], gt)
return out_s, out_v
def v12_handoff(pads):
u"""Rs6.2 西侧 L4 铜块 + 15 个过孔。铜块本身画成若干条同网宽线(铺铜阶段再用正式铜面覆盖)。"""
bad = []
x0, y0, x1, y1 = V12_PATCH
for p in pads:
if p[6] not in (2, 12) or p[7] == '+12V':
continue
r = RT.pad_rect(p)
d = max(r[0] - x1, x0 - r[2], r[1] - y1, y0 - r[3])
if d < RB.MIN_CLEARANCE + 0.05:
bad.append(u'%s.%s(%s) 离 V12_PATCH 只有 %.2f' % (p[0], p[1], p[7], d))
if bad:
raise RuntimeError(u'V12_PATCH 与异网焊盘冲突(摆位改过?):' + u';'.join(bad))
segs = []
h = y1 - y0
segs.append((2, x0 + h / 2.0, (y0 + y1) / 2.0, x1 - h / 2.0, (y0 + y1) / 2.0, h, '+12V'))
vias = [(x, y, '+12V') for (x, y) in v12_vias()]
return segs, vias
# ── J23(Type-C)同名 D± 脚的桥 ─────────────────────────────────────────────
# 一排里是 B6(DP) A7(DM) A6(DP) B7(DM) 交错,0.5 mm 间距。先布的那根把自己两只脚一桥,
# 正好横在另一根内侧那只脚的出口上(首轮:UP_DM「A7 出不来」)。
# ⇒ 两座桥按构造画:DP 从焊盘**南侧**(连接器本体一侧)绕、DM 从**北侧**绕,互不交叉;
# 之后 A* 只接外侧的 B6(DP,西北方向是空的)和 B7(DM,东北方向是空的)。
J23_BRIDGE_W = 0.15
J23_BRIDGE_OUT = 0.6 # 桥的横段离焊盘端多远
def j23_bridges(pads):
pm = {(q[0], str(q[1])): q for q in pads if q[0] == 'J23'}
need = [('B6', 'UP_DP'), ('A6', 'UP_DP'), ('A7', 'UP_DM'), ('B7', 'UP_DM')]
for pin, net in need:
if ('J23', pin) not in pm or pm[('J23', pin)][7] != net:
raise RuntimeError(u'J23.%s 不是 %s(封装 / 朝向改过?)' % (pin, net))
b6, a6, a7, b7 = (pm[('J23', k)] for k in ('B6', 'A6', 'A7', 'B7'))
if not (b6[2] < a7[2] < a6[2] < b7[2]):
raise RuntimeError(u'J23 的 D± 脚顺序不是 B6 A7 A6 B7(西 → 东)')
ys = b6[3] - b6[5] / 2.0 - J23_BRIDGE_OUT # 南
yn = a7[3] + a7[5] / 2.0 + J23_BRIDGE_OUT # 北
w = J23_BRIDGE_W
segs = [(2, b6[2], b6[3], b6[2], ys, w, 'UP_DP'), (2, b6[2], ys, a6[2], ys, w, 'UP_DP'),
(2, a6[2], ys, a6[2], a6[3], w, 'UP_DP'),
(2, a7[2], a7[3], a7[2], yn, w, 'UP_DM'), (2, a7[2], yn, b7[2], yn, w, 'UP_DM'),
(2, b7[2], yn, b7[2], b7[3], w, 'UP_DM')]
bad = []
for s in segs:
for q in pads:
if q[6] != 2 or q[7] == s[6]:
continue
d = _seg_rect_gap(s, RT.pad_rect(q)) - w / 2.0
if d < RB.MIN_CLEARANCE - 1e-6:
bad.append(u'%s 桥离 %s.%s(%s) 只有 %.3f' % (s[6], q[0], q[1], q[7] or 'NC', d))
if bad:
raise RuntimeError(u'J23 桥净距不够:' + u';'.join(sorted(set(bad))))
return segs, {('J23', 'A6'), ('J23', 'A7')}
# ── USB3:U 形掉头(按构造画)───────────────────────────────────────────────
# 北去三对的极性都是反的:U32 北沿西 → 东是 DM DP,槽上南排是 D+(x 47.5) D−(x 48.5)。
# 同层两根线不交叉换不过来,但**掉头一次左右自然对调**。USB3 的目标 J36 就在 U32 正北 1 mm,
# 所以 U32 东移 2.5 mm(place_base.HUB_CX),让它的两只脚落在 J36 D± 的东边:
# L4:两根一起北上,从 J36 南排底下穿过(焊盘在 L1,L4 空着)→ 到两排中间向西 → 南下到过孔
# L1:过孔 → 往南进焊盘的内端(从北边进,不占南排外侧的出口)
# DM 在内圈、DP 在外圈,DP 的过孔在 DM 过孔的西北,两根 L1 引出脚互不跨越。
U3_DMV = 1.2 # DM 过孔离焊盘内端
U3_DMH = 0.4 # DM 横段在自己过孔北边多少
U3_DPH = 0.8 # DP 横段在 DM 横段北边多少(外圈)
U3_DPV = 0.5 # DP 过孔在自己横段南边多少
def usb3_uturn(pads):
pm = {(q[0], str(q[1])): q for q in pads}
dm0, dp0 = pm[('U32', '6')], pm[('U32', '7')]
sdp, sdm = pm[('J36', '46')], pm[('J36', '47')]
for q, net in ((dm0, 'USB3_DM'), (dp0, 'USB3_DP'), (sdp, 'USB3_DP'), (sdm, 'USB3_DM')):
if q[7] != net:
raise RuntimeError(u'%s.%s 是 %s 不是 %s' % (q[0], q[1], q[7], net))
if not (sdp[2] < sdm[2] < dm0[2] < dp0[2]):
raise RuntimeError(u'USB3 掉头的前提不成立:要 J36 D+ < D− < U32.6 < U32.7(西 → 东),'
u'实际 %.2f %.2f %.2f %.2f —— U32 的 x 改过?' % (sdp[2], sdm[2], dm0[2], dp0[2]))
w = DIFF_W
y_in = sdp[3] + sdp[5] / 2.0
y_dmv = y_in + U3_DMV
y_dmh = y_dmv + U3_DMH
y_dph = y_dmh + U3_DPH
y_dpv = y_dph - U3_DPV
dm = [(dm0[2], dm0[3]), (dm0[2], y_dmh), (sdm[2], y_dmh), (sdm[2], y_dmv)]
dp = [(dp0[2], dp0[3]), (dp0[2], y_dph), (sdp[2], y_dph), (sdp[2], y_dpv)]
segs = []
for net, pts in (('USB3_DM', dm), ('USB3_DP', dp)):
segs += [(2, a[0], a[1], b[0], b[1], w, net) for a, b in zip(pts, pts[1:])]
segs += [(1, sdm[2], y_dmv, sdm[2], sdm[3], w, 'USB3_DM'),
(1, sdp[2], y_dpv, sdp[2], sdp[3], w, 'USB3_DP')]
vias = [(sdm[2], y_dmv, 'USB3_DM'), (sdp[2], y_dpv, 'USB3_DP')]
bad = []
c = RB.MIN_CLEARANCE
for s in segs:
for q in pads:
if q[6] not in (s[0], 12) or q[7] == s[6]:
continue
d = _seg_rect_gap(s, RT.pad_rect(q)) - w / 2.0
if d < c - 1e-6:
bad.append(u'%s 离 %s.%s(%s) %.3f' % (s[6], q[0], q[1], q[7] or 'NC', d))
for (vx, vy, net) in vias:
for q in pads:
if q[7] == net:
continue
d = _seg_rect_gap((0, vx, vy, vx, vy), RT.pad_rect(q)) - RT.VIA_D / 2.0
if d < c - 1e-6:
bad.append(u'%s 的过孔离 %s.%s(%s) %.3f' % (net, q[0], q[1], q[7] or 'NC', d))
a = [s for s in segs if s[6] == 'USB3_DP']
b = [s for s in segs if s[6] == 'USB3_DM']
for s in a:
for t in b:
if s[0] == t[0] and _seg_seg(s, t) - w < c - 1e-6:
bad.append(u'DP/DM 对内净距 %.3f' % (_seg_seg(s, t) - w))
for (vx, vy, net) in vias:
for t in segs:
if t[6] != net and _seg_seg((0, vx, vy, vx, vy), t) - RT.VIA_D / 2.0 - w / 2.0 < c - 1e-6:
bad.append(u'%s 的过孔离 %s 的线太近' % (net, t[6]))
if bad:
raise RuntimeError(u'USB3 掉头几何不过:' + u';'.join(sorted(set(bad))))
return segs, vias, {('U32', '6'), ('U32', '7'), ('J36', '46'), ('J36', '47')}
def geom_check(tag, segs, vias, pads, prior_s=(), prior_v=()):
u"""按构造画的铜没有 A* 占用图兜底,净距必须自己查:焊盘(同层 / 通孔)、此前构造的铜、彼此之间。"""
c = RB.MIN_CLEARANCE
bad = []
r = RT.VIA_D / 2.0
for s in segs:
for q in pads:
if q[6] not in (s[0], 12) or q[7] == s[6]:
continue
d = _seg_rect_gap(s, RT.pad_rect(q)) - s[5] / 2.0
if d < c - 1e-6:
bad.append(u'%s 线离 %s.%s(%s) %.3f' % (s[6], q[0], q[1], q[7] or 'NC', d))
for (vx, vy, net) in vias:
for q in pads:
if q[7] == net:
continue
d = _seg_rect_gap((0, vx, vy, vx, vy), RT.pad_rect(q)) - r
if d < c - 1e-6:
bad.append(u'%s 孔离 %s.%s(%s) %.3f' % (net, q[0], q[1], q[7] or 'NC', d))
alls = list(segs) + list(prior_s)
allv = list(vias) + list(prior_v)
for s in segs:
for t in alls:
if t is s or t[6] == s[6] or t[0] != s[0]:
continue
d = _seg_seg(s, t) - s[5] / 2.0 - t[5] / 2.0
if d < c - 1e-6:
bad.append(u'%s 线离 %s 线 %.3f' % (s[6], t[6], d))
for (vx, vy, net) in allv:
if net == s[6]:
continue
d = _seg_seg(s, (0, vx, vy, vx, vy)) - s[5] / 2.0 - r
if d < c - 1e-6:
bad.append(u'%s 线离 %s 孔 %.3f' % (s[6], net, d))
for (vx, vy, net) in vias:
for (wx_, wy_, n2) in allv:
if n2 != net and (wx_, wy_) != (vx, vy) and math.hypot(vx - wx_, vy - wy_) - 2 * r < c - 1e-6:
bad.append(u'%s 孔离 %s 孔 %.3f' % (net, n2, math.hypot(vx - wx_, vy - wy_) - 2 * r))
for t in prior_s:
if t[6] != net and _seg_seg(t, (0, vx, vy, vx, vy)) - t[5] / 2.0 - r < c - 1e-6:
bad.append(u'%s 孔离 %s 线' % (net, t[6]))
if bad:
raise RuntimeError(u'%s 几何不过:%s' % (tag, u';'.join(sorted(set(bad)))))
# ── U32 北沿夹在 D± 中间的三只 3V3_HUB(5 / 10 / 13):按构造画 ────────────────────
# 口排西 → 东:DM1 DP1 DM2 DP2 **3V3(5)** DM3 DP3 DM4 DP4 **3V3(10)** DM5 DP5 **3V3(13)** NC NC NC。
# 五对一出来,这三只脚就被包进口袋(QFN 内圈出不去:角上缝只有 0.2 mm)。
# 13 脚:沿北沿贴着往东(压过三只 NC 脚上方)→ 下东侧 → 接 24 脚(东侧 3V3),东侧打孔上 L1。
# USB4/5 往东绕时从它上面过,它和 24 / VDD18 去耦一起留在绕行环的里面,靠这个孔接出去。
# 5 脚: 北上进 USB3 掉头的口袋,在 J36 两排之间打孔上 L1。
# 10 脚:北上夹在 USB4/USB5 之间,同样在 J36 两排之间打孔上 L1。
# L1:两孔沿 J36 南排内端外 0.5 mm 往东连,到东头外壳盘前打孔回 L4(USB4/5 绕行车道的**外面**)。
U32N_W = 0.2
U32N_TOP = 0.45 # 13 脚那条横线离北排焊盘外沿
U32N_EAST = 0.5 # 东侧竖线离东排焊盘外沿
U32N_V = 0.5 # 5 / 10 脚的孔离 J36 南排内端
def u32_north(pads, prior_s, prior_v):
pm = {(q[0], str(q[1])): q for q in pads}
p5, p10, p13, p24 = (pm[('U32', k)] for k in ('5', '10', '13', '24'))
for q in (p5, p10, p13, p24):
if q[7] != '+3V3_HUB':
raise RuntimeError(u'U32.%s 是 %s 不是 +3V3_HUB' % (q[1], q[7]))
sdp = pm[('J36', '46')]
u32 = [q for q in pads if q[0] == 'U32' and not is_ep(q)]
x_east = max(q[2] + q[4] / 2.0 for q in u32)
y_top = p13[3] + p13[5] / 2.0
shell_e = min(q[2] - q[4] / 2.0 for q in pads if q[0] == 'J36' and q[4] >= 2.5 and q[2] > B.SLOT_CX)
n, w = '+3V3_HUB', U32N_W
t13 = round(y_top + U32N_TOP, 3)
xe = round(x_east + U32N_EAST, 3)
yv = round(sdp[3] + sdp[5] / 2.0 + U32N_V, 3)
v5 = (round(p5[2] - 0.35, 3), yv)
v10 = (round(p10[2] + 0.75, 3), yv)
v13 = (xe, round(p24[3] + 1.5, 3))
vd = (round(shell_e - 1.0, 3), yv)
ya = round(yv - 0.6, 3)
segs = [(2, p13[2], p13[3], p13[2], t13, w, n), (2, p13[2], t13, xe, t13, w, n),
(2, xe, t13, xe, p24[3], w, n), (2, xe, p24[3], p24[2], p24[3], w, n),
(2, p5[2], p5[3], p5[2], ya, w, n), (2, p5[2], ya, v5[0], v5[1], w, n),
(2, p10[2], p10[3], p10[2], ya, w, n), (2, p10[2], ya, v10[0], v10[1], w, n),
(1, v5[0], v5[1], vd[0], vd[1], w, n)]
vias = [(v5[0], v5[1], n), (v10[0], v10[1], n), (v13[0], v13[1], n), (vd[0], vd[1], n)]
geom_check(u'U32 北沿 3V3_HUB', segs, vias, pads, prior_s, prior_v)
virt = [['U32', 'fan:e13', v13[0], v13[1], 0.3, 0.3, 1, n, 'RECT'],
['U32', 'fan:vd', vd[0], vd[1], 0.3, 0.3, 2, n, 'RECT']]
return segs, vias, {('U32', '5'), ('U32', '10'), ('U32', '13'), ('U32', '24')}, virt
# ── 其余 D± 按构造画(2026-09-10 用户选定:构造 D± + 划横穿车道)────────────────────
# A* 逐网布 D± 不认拓扑:DP 抄最短路把自家 DM 的出口封死(第四轮 USB2/4/5/6 的 DM 全「出不来」)。
# 规则只有一条:**槽上 D+ 在西、D− 在东 ⇒ 这一对必须「向南」进焊盘**(掉头 / 绕行都保持 DM 在行进方向左侧)。
# 北去(USB1 → J34、USB2 → J35):出脚 45° 往西让开 USB3 的 U 形,北上到目标槽,在两排之间**向东掉头**,
# 打孔上 L1、从北边进焊盘内端。
# 南去(USB4 → J37、USB5 → J38):出脚向东(USB5 在 13 脚那条线上面、USB4 在 10 脚过孔上面),
# 到 DP_H 车道南下。USB4 在内圈外侧 ⇒ 转回西时必然横过 USB5 —— 用它本来就要的 L4→L1 那一跳:
# 在 J37 两排之间打孔上 L1、在 L1 上向西进焊盘;USB5 在 L4 从它底下继续南下到 J38 再向西打孔。
# 上行 / USB6:在 U32 西侧过孔带(place_base.LANES['U32_WVIA'])各打两颗交错孔上 L1,之后交给 A*(从 L1 起步)。
DPC_JOG_W = 0.25 # 北去四根出脚后的转弯高度步进(45° 斜段互相平行、中心距 ≥ 0.5)
DPC_SHIFT = {'USB1': 3.5, 'USB2': 2.5}
DPC_VIA = 1.0 # 过孔离焊盘内端
DPC_UT = 0.6 # 内圈横段在过孔外侧多少
DPC_UT_OUT = 0.5 # 外圈横段再往外多少
# 离 DP_H 车道西沿(DP, DM)。USB4 两根拉开到 0.7:DM 的 L4 竖线要一直下到自己的孔(在 DP 孔南边),
# 0.5 的话它擦过 DP 的孔只剩 0.06(静态校验实测)。
DPC_LANE = {'USB5': (0.0, 0.5), 'USB4': (1.8, 2.5)}
DPC_Y5 = 0.5 # USB5 DP 横段在 13 脚那条线上方多少
DPC_Y4 = 0.85 # USB4 DP 横段在 10 脚过孔中心上方多少
WVIA_DX = (0.85, 1.45) # USB6/UP 两颗孔离西排焊盘外沿(DM, DP)
def _poly2(layer, pts, w, net):
return [(layer, a[0], a[1], b[0], b[1], w, net) for a, b in zip(pts, pts[1:])
if abs(a[0] - b[0]) + abs(a[1] - b[1]) > 1e-9]
def dp_construct(pads, prior_s, prior_v):
import place_base as PL
pm = {(q[0], str(q[1])): q for q in pads}
u = lambda k: pm[('U32', k)] # noqa: E731
w = DIFF_W
segs, vias, done, virt = [], [], set(), []
top = u('1')[3] + u('1')[5] / 2.0
def slot(i):
ref = B.USB_SLOT_REF[i]
sp, sm = pm[(ref, '46')], pm[(ref, '47')]
done.update({(ref, '46'), (ref, '47')})
return sp, sm
# 北去:USB1 / USB2
order = [('USB1', '1', '2'), ('USB2', '3', '4')]
k = 0
for name, pdm, pdp in order:
dp_net, dm_net = PAIRS[name]
i = int(name[3])
sp, sm = slot(i)
s = DPC_SHIFT[name]
y_in = sp[3] + sp[5] / 2.0
yv = round(y_in + DPC_VIA, 3)
yt = round(yv + DPC_UT, 3)
for net, pk, xt, ytt in ((dm_net, pdm, sm[2], yt + DPC_UT_OUT), (dp_net, pdp, sp[2], yt)):
q = u(pk)
ya = round(top + 0.3 + k * DPC_JOG_W, 3)
k += 1
x1 = round(q[2] - s, 3)
pts = [(q[2], q[3]), (q[2], ya), (x1, round(ya + s, 3)), (x1, ytt), (xt, ytt), (xt, yv)]
segs += _poly2(2, pts, w, net)
segs += _poly2(1, [(xt, yv), (xt, sp[3])], w, net)
vias.append((xt, yv, net))
done.add(('U32', pk))
# 南去:USB5(内圈)/ USB4(外圈)
lane_x = PL.LANES['DP_H'][0]
t13 = [s2 for s2 in prior_s if s2[6] == '+3V3_HUB' and s2[0] == 2 and abs(s2[2] - s2[4]) < 1e-9
and abs(s2[1] - s2[3]) > 2.0]
y13 = max(s2[2] for s2 in t13) # 13 脚那条横线的 y
v10 = [v for v in prior_v if v[2] == '+3V3_HUB']
y10 = max(v[1] for v in v10 if v[1] < 50) # 5 / 10 脚的孔(J36 两排之间)
sp5, sm5 = slot(5)
y_in5 = sp5[3] + sp5[5] / 2.0
yv5 = round(y_in5 + DPC_VIA, 3)
y5dp = round(y13 + DPC_Y5, 3)
xdp5, xdm5 = round(lane_x + DPC_LANE['USB5'][0], 3), round(lane_x + DPC_LANE['USB5'][1], 3)
wdm5, wdp5 = round(yv5 + 0.64, 3), round(yv5 + 1.14, 3)
for net, pk, yh, xl, yw, xt in ((PAIRS['USB5'][0], '12', y5dp, xdp5, wdp5, sp5[2]),
(PAIRS['USB5'][1], '11', y5dp + 0.5, xdm5, wdm5, sm5[2])):
q = u(pk)
pts = [(q[2], q[3]), (q[2], yh), (xl, yh), (xl, yw), (xt, yw), (xt, yv5)]
segs += _poly2(2, pts, w, net)
segs += _poly2(1, [(xt, yv5), (xt, sp5[3])], w, net)
vias.append((xt, yv5, net))
done.add(('U32', pk))
sp4, sm4 = slot(4)
y_in4 = sp4[3] + sp4[5] / 2.0
y4dp = round(y10 + DPC_Y4, 3)
xdp4, xdm4 = round(lane_x + DPC_LANE['USB4'][0], 3), round(lane_x + DPC_LANE['USB4'][1], 3)
vdp4, vdm4 = round(y_in4 + 4.32, 3), round(y_in4 + 3.52, 3) # 交错两颗孔(J37 两排之间)
for net, pk, yh, xl, yvv, xt in ((PAIRS['USB4'][0], '9', y4dp, xdp4, vdp4, sp4[2]),
(PAIRS['USB4'][1], '8', y4dp + 0.5, xdm4, vdm4, sm4[2])):
q = u(pk)
segs += _poly2(2, [(q[2], q[3]), (q[2], yh), (xl, yh), (xl, yvv)], w, net)
segs += _poly2(1, [(xl, yvv), (xt, yvv), (xt, sp4[3])], w, net)
vias.append((xl, yvv, net))
done.add(('U32', pk))
# 上行 / USB6:西侧过孔带里各两颗交错孔,之后从 L1 起步
west = [q for q in pads if q[0] == 'U32' and not is_ep(q) and q[4] > q[5]]
x_left = min(q[2] - q[4] / 2.0 for q in west)
for name, pdm, pdp in (('USB6', '53', '54'), ('USBUP', '58', '59')):
dp_net, dm_net = PAIRS[name]
for net, pk, dx, dy in ((dm_net, pdm, WVIA_DX[0], -0.25), (dp_net, pdp, WVIA_DX[1], 0.0)):
q = u(pk)
vx, vy = round(x_left - dx, 3), round(q[3] + dy, 3)
pts = [(q[2], q[3]), (round(x_left - 0.35, 3), q[3]), (vx, vy)]
segs += _poly2(2, pts, 0.2, net)
vias.append((vx, vy, net))
virt.append(['U32', 'fan:%s' % pk, vx, vy, 0.3, 0.3, 1, net, 'RECT'])
done.add(('U32', pk))
geom_check(u'D± 构造(USB1/2/4/5 + 上行/USB6 西侧孔)', segs, vias, pads, prior_s, prior_v)
return segs, vias, done, virt
# ── 通用扇出:这几颗芯片的每只非地、非 D± 脚先打一个孔上 L1,布线从 L1 的落点起步 ─────────
FANOUT_REFS = ('U35', 'U6', 'U7', 'U20')
FAN_W = 0.15
# 这几个分区里**所有件**的非地信号脚也扇出(第五轮:D± 全通之后剩 37 个网,21 条在 U20 北沿那排上拉 /
# NTC 阻容上 —— 0402 一字排开、两头都在 L4,头顶的 L1 是空的却上不去)。
# ⚠ A4H(U32 那条走廊)**不扇**:那里的晶振 / RBIAS / strap 要和 U32 的脚在 L4 就近连,打孔反而绕远。
# ⚠ U31(LDO,宽电源脚)与 J23(Type-C,0.5 间距,出口带另有安排)不扇。
# ⛔ 第七轮实测**反效果**(信号网 37 → 43、GND 打孔失败 97 → 106):小件挨着排(0.6 mm 装配间距),
# 每只焊盘就近打孔 = 把 L4 上的一簇件变成两层都占的一簇孔,A3 里两头都已在 L1 的脚照样「没布通」。
# 根子在摆位间距,不在扇出。先关掉,留着代码。
FAN_ZONES = ()
FAN_ZONE_SKIP = ('U31', 'J23', 'J33')
def fan_zone_refs():
import place_base as PL
pg = PL.page_of()
return set(r for r in pg if r not in FAN_ZONE_SKIP and PL.zone_of_ref(r, pg) in FAN_ZONES)
# U20 的脚距 1.27:最近的落点排成一列、孔距 1.27 ⇒ 在 L1 上连成一道墙(第四轮 29 条失败在 U20 周围)。
# ⇒ 奇数脚往外多让 FAN_STAGGER,孔错成两排。
FAN_STAGGER = {'U20': 1.0}
def fanout(pads, bypad, segs, vias, board_vias, logs):
u"""原地改 bypad:扇出成功的脚换成 L1 上的虚拟落点 `fan:<脚号>`。返回 (segs, vias, 成功数, 失败数)。"""
halfv = RT.VIA_D / 2.0 + RT.CLR
g, gt, others = grid_now(pads, halfv, segs, vias, board_vias)
sh = FAN_W / 2.0
todo = []
zref = fan_zone_refs()
for net, ps in bypad.items():
if net in DP_NETS or net in PLANE_NETS:
continue
for q in ps:
if (q[0] in FANOUT_REFS or q[0] in zref) and not RT._is_launch(q) and q[6] in RT.LAY \
and (q[0], str(q[1])) not in TH_KEYS:
todo.append(q)
def cands(q):
return [nd for nd in RT.pad_nodes(g, q, halfv, others.get(q[6], []), sh, gt)
if g.free_via(nd[1], nd[2], q[7])]
todo.sort(key=lambda q: (len(cands(q)), q[0], str(q[1])))
out_s, out_v, ok, fail = [], [], 0, 0
for q in todo:
cs = cands(q)
if not cs:
fail += 1
continue
nd = cs[0]
stg = FAN_STAGGER.get(q[0], 0.0)
if stg and str(q[1]).isdigit() and int(q[1]) % 2:
d0 = math.hypot(RT.wx(cs[0][1]) - q[2], RT.wy(cs[0][2]) - q[3])
far = [c2 for c2 in cs if math.hypot(RT.wx(c2[1]) - q[2], RT.wy(c2[2]) - q[3]) >= d0 + stg]
if far:
nd = far[0]
vx, vy = RT.wx(nd[1]), RT.wy(nd[2])
ss = RT.stub_emit(q, vx, vy, others.get(q[6], []), sh, FAN_W, q[7], gt)
out_s += ss
out_v.append((vx, vy, q[7]))
keep = halfv + FAN_W / 2.0
for (l, a, b, c, d, ww, nn) in ss:
RT.block_track(g, l, a, b, c, d, keep, nn)
RT.block_track(gt, l, a, b, c, d, RT.CLR + sh + ww / 2.0, nn)
rr = RT.VIA_D / 2.0 + halfv
rt = RT.VIA_D / 2.0 + RT.CLR + sh
for l in RT.LAY:
g.block_both(l, vx - rr, vy - rr, vx + rr, vy + rr, 0.0, net=q[7])
gt.block_rect(l, vx - rt, vy - rt, vx + rt, vy + rt, net=q[7])
bypad[q[7]] = [o for o in bypad[q[7]] if o is not q]
bypad[q[7]].append([q[0], 'fan:%s' % q[1], vx, vy, 0.3, 0.3, 1, q[7], 'RECT'])
ok += 1
return out_s, out_v, ok, fail
def _p2s(px, py, x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
L2 = dx * dx + dy * dy
t = 0.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L2))
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
def _seg_seg(a, b):
u"""两段中心线之间的最短距离(不相交时)。"""
return min(_p2s(a[1], a[2], b[1], b[2], b[3], b[4]), _p2s(a[3], a[4], b[1], b[2], b[3], b[4]),
_p2s(b[1], b[2], a[1], a[2], a[3], a[4]), _p2s(b[3], b[4], a[1], a[2], a[3], a[4]))
def _seg_rect_gap(s, r):
u"""线段(中心线)到矩形的最短距离(相交为 0)。"""
def p2s(px, py, x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
L2 = dx * dx + dy * dy
t = 0.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L2))
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
x0, y0, x1, y1 = s[1], s[2], s[3], s[4]
n = max(2, int(math.hypot(x1 - x0, y1 - y0) / 0.02))
best = 1e9
for k in range(n + 1):
t = k / float(n)
px, py = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t
dx = max(r[0] - px, px - r[2], 0.0)
dy = max(r[1] - py, py - r[3], 0.0)
best = min(best, math.hypot(dx, dy))
return best
# ⛔ 上行 / USB6 两对的脚在 U32 **西**侧,去处却在东边(J23 / U20)。首轮 A* 从 L4 绕 U32 南边,
# 贴着南沿把 strap 脚(CFGSEL0/1、NONREM0/1、HUB_RST、VBUS_DET)全封死。
# ⇒ 布这两对时在 U32 的南 / 北 / 东三面围一道**只对它们**的 L4 栅栏(离焊盘 3 mm),
# 逼它们在西侧打孔上 L1、从芯片顶上过去(L1 在走廊 2 是空的)。
FENCED_PAIRS = ('USB6', 'USBUP')
FENCE_OUT = 3.0
FENCE_IN = 0.9 # 栅栏西端离西排焊盘外沿多远(给西排脚自己的引出脚留地方)
def u32_fence(pads):
ps = [q for q in pads if q[0] == 'U32' and q[7] != 'GND']
x0 = min(q[2] - q[4] / 2 for q in ps)
y0 = min(q[3] - q[5] / 2 for q in ps)
x1 = max(q[2] + q[4] / 2 for q in ps)
y1 = max(q[3] + q[5] / 2 for q in ps)
xw = x0 + FENCE_IN
return [(2, (xw, y0 - FENCE_OUT, x1 + FENCE_OUT, y0 + FENCE_IN)), # 南
(2, (xw, y1 - FENCE_IN, x1 + FENCE_OUT, y1 + FENCE_OUT)), # 北
(2, (x1 - FENCE_IN, y0 - FENCE_OUT, x1 + FENCE_OUT, y1 + FENCE_OUT))] # 东
def route_pairs(bypad, pads, segs, vias, board_vias, logs):
u"""七对 D±:DP 先布,DM 贴着它布。返回新布的 (segs, vias)。"""
out_s, out_v = [], []
pad_pen = RT.PAD_PEN
RT.PAD_PEN = DP_PAD_PEN
fence = u32_fence(pads)
try:
for k, name in enumerate(PAIR_ORDER):
dp, dm = PAIRS[name]
later = [q for nm in PAIR_ORDER[k + 1:] for n in PAIRS[nm] for q in bypad.get(n, [])]
fc = fence if name in FENCED_PAIRS else ()
# ⛔ 上行 / USB6 的 DP:L4 上除了两端脚附近 3 mm 全收过路费,逼它上 L1 走空走廊。
# 第二轮只有栅栏时,它从栅栏西南角绕出去、贴着 J37 底下往东,照样把 strap 脚南路堵了。
# DM 不用单独处理:它贴着 DP 的带子走。
lp = None
if name in FENCED_PAIRS:
lp = {1: [(0.0, 0.0, B.BW, B.BH)],
2: [RT.pad_rect(q, 3.0) for q in bypad.get(dp, []) + bypad.get(dm, [])]}
s, v = route_one(dp, bypad.get(dp, []), DIFF_W, pads, segs + out_s, vias + out_v,
board_vias, logs, pen_pads=later + bypad.get(dm, []), fence=fc, hug=lp)
out_s += s
out_v += v
band = None
if s:
band = {1: [], 2: []}
for q in s:
band[q[0]].append(_seg_box(q, HUG_BAND))
for (vx, vy, _n) in v:
for l in RT.LAY:
band[l].append((vx - 1.2, vy - 1.2, vx + 1.2, vy + 1.2))
for q in bypad.get(dm, []):
r = RT.pad_rect(q, HUG_PAD)
for l in RT.LAY:
band[l].append(r)
s2, v2 = route_one(dm, bypad.get(dm, []), DIFF_W, pads, segs + out_s, vias + out_v,
board_vias, logs, pen_pads=later, hug=band, fence=fc)
out_s += s2
out_v += v2
finally:
RT.PAD_PEN = pad_pen
return out_s, out_v
def is_ep(p):
return p[7] == 'GND' and min(p[4], p[5]) >= EP_MIN
def plane_vias(pads, segs, vias, board_vias, logs, skip, which, done):
u"""GND / +12V 平面网:每只焊盘一个过孔(大 GND 焊盘在盘里打孔阵)。
`which='early'`:+12V 焊盘 + GND 散热 / 外壳盘(盘内孔阵,不占任何人的出口),布信号之前做。
`which='late'`:其余 GND 贴片焊盘,**布完信号之后**做。
⛔ 首轮是全部放在信号之前:插槽 1 mm 间距的针排里,GND 脚的孔一落就把隔壁信号脚的出口
夹成 0.4 mm(0.15 的线都过不去)—— I²C / VDAC / EFUSE_EN / PRSNT 几十条全报「没布通」。
`done` 记已处理的焊盘(早段孔阵没打上的,晚段按普通焊盘再试)。
"""
halfv = RT.VIA_D / 2.0 + RT.CLR
g, gt, others = grid_now(pads, halfv, segs, vias, board_vias)
m = B.l1_12v_rects()[0]
land = (m[0] + V12_LAND, m[1] + V12_LAND, m[2] - V12_LAND, m[3] - V12_LAND)
out_s, out_v = [], []
cnt = {'GND': 0, '+12V': 0, '+3V3': 0, 'EP': 0}
sh = RT.W_DEFAULT / 2.0
def occupy(ss, vx, vy, net, w):
keep = halfv + w / 2.0
for (l, a, b, c, d, ww, _nn) in ss:
RT.block_track(g, l, a, b, c, d, keep, net)
RT.block_track(gt, l, a, b, c, d, RT.CLR + sh + ww / 2.0, net)
r = RT.VIA_D / 2.0 + halfv
rt = RT.VIA_D / 2.0 + RT.CLR + sh
for l in RT.LAY:
g.block_both(l, vx - r, vy - r, vx + r, vy + r, 0.0, net=net)
gt.block_rect(l, vx - rt, vy - rt, vx + rt, vy + rt, net=net)
for p in pads:
net = p[7]
if net not in PLANE_NETS or p[6] not in RT.LAY:
continue
key = (p[0], str(p[1]))
if str(p[1]).endswith(TH_SUFFIX) or key in TH_KEYS or key in skip:
continue
if net == '+12V' and p[6] == 1:
continue # 槽上的 12V 针本来就在 L1 大铜里
if key in done:
continue
# 早段:+12V、散热盘孔阵、以及几颗扇出芯片自己的 +3V3 脚(它们最容易被信号封住)
early_3v3 = net == '+3V3' and p[0] in FANOUT_REFS
if which == 'early' and not (net == '+12V' or is_ep(p) or early_3v3):
continue
if which == 'late' and net not in LATE_PLANE_NETS:
continue
if which == 'early' and is_ep(p):
x0, y0, x1, y1 = RT.pad_rect(p)
nx = max(1, int((x1 - x0 - 2 * EP_EDGE) / EP_PITCH) + 1)
ny = max(1, int((y1 - y0 - 2 * EP_EDGE) / EP_PITCH) + 1)
nx, ny = min(nx, 3), min(ny, 3)
got = 0
for a in range(nx):
for b in range(ny):
vx = p[2] if nx == 1 else x0 + EP_EDGE + a * (x1 - x0 - 2 * EP_EDGE) / (nx - 1)
vy = p[3] if ny == 1 else y0 + EP_EDGE + b * (y1 - y0 - 2 * EP_EDGE) / (ny - 1)
i, j = RT.gx(vx), RT.gy(vy)
if g.free_via(i, j, 'GND'):
out_v.append((round(vx, 3), round(vy, 3), 'GND'))
occupy([], vx, vy, 'GND', 0.0)
got += 1
if got:
cnt['EP'] += 1
done.add(key)
continue # 没打上的留给晚段按普通焊盘处理
w = max(RT.rule_min(net, {}), min(0.5 if net == '+12V' else 0.3, p[4], p[5]))
# ⛔ 落孔只看**过孔图**(free_via,两层都按过孔口径让开)。第一版还要求两层**导线图**都空,
# 而 L1 的 12V 大铜在导线图里是占着的 —— A1/A2/A6 里 40 多只 GND 脚因此全报「找不到落孔点」。
# GND 孔落进 12V 大铜是合法的:铺铜自己挖反焊盘。
cand = [nd for nd in RT.pad_nodes(g, p, halfv, others.get(p[6], []), w / 2.0, gt)
if g.free_via(nd[1], nd[2], net)
and (net != '+12V' or (land[0] <= RT.wx(nd[1]) <= land[2]
and land[1] <= RT.wy(nd[2]) <= land[3]))]
if not cand:
logs.append(u'%s: %s.%s 找不到落过孔的地方' % (net, p[0], p[1]))
continue
nd = cand[0]
vx, vy = RT.wx(nd[1]), RT.wy(nd[2])
ss = RT.stub_emit(p, vx, vy, others.get(p[6], []), w / 2.0, w, net, gt)
out_s += ss
out_v.append((vx, vy, net))
occupy(ss, vx, vy, net, w)
cnt[net] += 1
done.add(key)
print(u' 平面网扇出(%s):GND %d / +3V3 %d 只脚各一孔、散热/外壳盘 %d 块打孔阵、+12V %d 只脚;共 %d 孔'
% (which, cnt['GND'], cnt['+3V3'], cnt['EP'], cnt['+12V'], len(out_v)))
return out_s, out_v
# ── 主流程 ───────────────────────────────────────────────────────────────
def plan_all(priority=()):
pads = load_pads()
board_vias = load_board_vias()
print(u'板上已有过孔 %d 个;通孔焊盘 %d 只(L4 可布 + L1 障碍)' % (len(board_vias), len(TH_KEYS)))
segs, vias, logs = [], [], []
ks, kv = kelvin(pads, segs, vias, board_vias, logs)
segs += ks
vias += kv
print(u' Kelvin:%d 段 / %d 孔' % (len(ks), len(kv)))
hs, hv = v12_handoff(pads)
segs += hs
vias += hv
print(u' 9 A 上 L1:Rs6.2 西侧铜块 + %d 个过孔' % len(hv))
js, jdone = j23_bridges(pads)
segs += js
print(u' J23 同名 D± 脚两座桥:%d 段(DP 南绕 / DM 北绕)' % len(js))
us, uv, udone = usb3_uturn(pads)
segs += us
vias += uv
jdone |= udone
print(u' USB3 U 形掉头:%d 段 / %d 孔(按构造画,换位不交叉)' % (len(us), len(uv)))
ns, nv, ndone, nvirt = u32_north(pads, segs, vias)
segs += ns
vias += nv
jdone |= ndone
print(u' U32 北沿 3V3_HUB(5/10/13 + 24):%d 段 / %d 孔(按构造画)' % (len(ns), len(nv)))
cs_, cv, cdone, cvirt = dp_construct(pads, segs, vias)
segs += cs_
vias += cv
jdone |= cdone
nvirt = nvirt + cvirt
print(u' D± 构造(USB1/2 北去掉头、USB4/5 东绕、上行/USB6 西侧上 L1):%d 段 / %d 孔' % (len(cs_), len(cv)))
kel = {a for a, _b, _w in KELVIN} # U13.9/.10 已由专线接好
bypad = {}
for p in pads:
if not p[7] or p[7] in PLANE_NETS or str(p[1]).endswith(TH_SUFFIX):
continue
if (p[0], str(p[1])) in kel or (p[0], str(p[1])) in jdone:
continue
bypad.setdefault(p[7], []).append(p)
for v in nvirt:
bypad.setdefault(v[7], []).append(v)
ds, dv = route_pairs(bypad, pads, segs, vias, board_vias, logs)
segs += ds
vias += dv
print(u' D± 七对:%d 段 / %d 孔' % (len(ds), len(dv)))
if '--dp-only' in sys.argv:
return segs, vias, logs
skip = set(kel) | {('Rs6', '2')}
done = set()
ps, pv = plane_vias(pads, segs, vias, board_vias, logs, skip, 'early', done)
segs += ps
vias += pv
GND_PEN_PADS[:] = [q for q in pads if q[7] in LATE_PLANE_NETS and q[6] in RT.LAY
and (q[0], str(q[1])) not in done and (q[0], str(q[1])) not in TH_KEYS
and not str(q[1]).endswith(TH_SUFFIX)]
fs, fv, fok, ffail = fanout(pads, bypad, segs, vias, board_vias, logs)
segs += fs
vias += fv
print(u' 扇出上 L1(%s):%d 只脚打孔,%d 只没落点(留给 A* 直接接)'
% (' '.join(FANOUT_REFS), fok, ffail))
rest = set(bypad) - set(DP_NETS)
RT.AUTO_CACHE.clear()
blocked = [('#ant', r) for r in (_KEEP[0] or keep_rects())] + [('+12V', V12_PATCH)]
direct_nets = rest & set(priority) if priority else rest
dsegs, merged = RT.direct_links(pads, {n: bypad[n] for n in direct_nets}, segs, vias,
blocked_rects=blocked)
segs += dsegs
for net, groups in merged.items():
drop = set()
for group in groups:
drop |= set(list(group)[1:])
bypad[net] = [q for q in bypad[net] if (q[0], q[1]) not in drop]
print(u' 直连:%d 段 / %d 个网络' % (len(dsegs), len(merged)))
# U32 的本地网(晶振 / RBIAS / VDD18PLL / VDD18 / strap / HUB_RST / VBUS_DET):第六轮它们的脚全被
# 先布的长信号在 U32 西侧落的孔、南边拉的线堵住,而要连的件就在一两毫米外 ⇒ 在长信号之前先布。
u32_local = set(n for n in rest if n not in POWER_LATE and n not in CHAIN_NETS
and any(q[0] == 'U32' for q in bypad.get(n, [])))
fixed_s, fixed_v = len(segs), len(vias)
# 四段:输入链(短、宽、有专属条带)→ U32 本地网 → 普通信号 → 电源(最后布,脚一直收过路费)
phases = [set(CHAIN_NETS) & rest, u32_local, set(), set(POWER_LATE) & rest]
phases[2] = rest - phases[0] - phases[1] - phases[3]
if '--pf' in sys.argv:
LATER_PEN_PADS[:] = [q for n in rest - u32_local for q in bypad.get(n, [])]
logs += RT.route_groups(bypad, pads, segs, vias, board_vias, {}, priority=priority,
only=u32_local)
print(u' U32 本地网(%d 个)先布:失败 %d 条' % (len(u32_local), len(logs)))
fixed_s, fixed_v = len(segs), len(vias)
rest = rest - u32_local
# 协商式拥塞布线(router.pf_solve,PathFinder):每轮所有网重布、被多网压着的格点逐轮涨价,
# 直到有人让路。第四轮的失败已经不是「脚出不来」而是贪心抢通道(长网先布、走最短路,
# 走廊 3 和槽体西段 x 36~43 很快被占光)—— 正是 PathFinder 要解决的那一类。
if '--pf-iters' in sys.argv:
RT.PF_ITERS = int(sys.argv[sys.argv.index('--pf-iters') + 1])
LATER_PEN_PADS[:] = []
logs += RT.pf_solve({n: bypad[n] for n in rest if n in bypad}, pads, segs, vias,
board_vias, {}, fixed_s, fixed_v)
print(u' PathFinder:失败 %d 条' % len(logs))
phases = []
for k, ph in enumerate(phases):
later = set().union(*phases[k + 1:]) if k + 1 < len(phases) else set()
LATER_PEN_PADS[:] = [q for n in later for q in bypad.get(n, [])]
n0 = len(logs)
logs += RT.route_groups(bypad, pads, segs, vias, board_vias, {}, priority=priority, only=ph)
print(u' 第 %d 段(%d 个网):失败 %d 条' % (k + 1, len(ph), len(logs) - n0))
LATER_PEN_PADS[:] = []
if logs and '--repair' in sys.argv:
print(u' 首轮 %d 条没搞定,进入拆线重布' % len(logs))
segs, vias, logs = rip_repair(bypad, pads, segs, vias, logs, board_vias, rest, fixed_s, fixed_v)
GND_PEN_PADS[:] = []
ps, pv = plane_vias(pads, segs, vias, board_vias, logs, skip, 'late', done)
segs += ps
vias += pv
gs, nres = gnd_rescue(pads, segs, vias, logs, done, blocked)
segs += gs
print(u' GND 打不上孔的脚:直连到邻近已接地焊盘救回 %d 只(%d 段)' % (nres, len(gs)))
segs, vias = RT.dedupe_vias(segs, vias)
for m in RT.check_connected(check_pads(pads), segs, vias):
if m not in logs:
logs.append(u'[连通性] ' + m)
return segs, vias, logs
GND_RESCUE_R = 2.6 # 找「邻近已接地焊盘」的半径(mm,同层)
def gnd_rescue(pads, segs, vias, logs, done, blocked):
u"""晚段打不上孔的 GND 脚:用 router.direct_links 直连到邻近**已接地**的同层焊盘(原地删掉救回的日志)。
插槽针排里两只相邻的 GND 脚(1 mm 间距、缝 0.3)最常见:隔壁那只有孔,直连一段就接上了。
「已接地」= 已有平面孔(done)或通孔焊盘(本来就穿到内层地)。
"""
tag = u'找不到落过孔的地方'
anchor = set(done) | TH_KEYS
out_s, nok = [], 0
for net in LATE_PLANE_NETS: # GND 和 +3V3 各救各的
head = net + ': '
fl = [m for m in logs if m.startswith(head) and tag in m]
if not fl:
continue
fkeys = set()
for m in fl:
ref, pin = m[len(head):].split(' ')[0].rsplit('.', 1)
fkeys.add((ref, pin))
fpads = [q for q in pads if (q[0], str(q[1])) in fkeys and q[6] in RT.LAY]
near = [q for q in pads if q[7] == net and q[6] in RT.LAY and (q[0], str(q[1])) in anchor
and not str(q[1]).endswith(TH_SUFFIX)
and any(q[6] == f[6] and abs(q[2] - f[2]) < GND_RESCUE_R
and abs(q[3] - f[3]) < GND_RESCUE_R for f in fpads)]
dsegs, merged = RT.direct_links(pads, {net: fpads + near}, segs + out_s, vias,
blocked_rects=blocked)
ok = set()
for grp in merged.get(net, []):
keys = set((k[0], str(k[1])) for k in grp)
if keys & anchor:
ok |= keys & fkeys
logs[:] = [m for m in logs
if not (m in fl and tuple(m[len(head):].split(' ')[0].rsplit('.', 1)) in ok)]
out_s += dsegs
nok += len(ok)
return out_s, nok
def check_pads(pads):
u"""连通性判据用的焊盘:去掉 L1 的通孔副本、L1 大铜里的 12V 针、单脚网。"""
cnt = {}
for q in pads:
cnt[q[7]] = cnt.get(q[7], 0) + 1
return [q for q in pads if not str(q[1]).endswith(TH_SUFFIX)
and not (q[7] == '+12V' and q[6] == 1) and cnt.get(q[7], 0) >= 2]
def rip_repair(bypad, pads, segs, vias, logs, board_vias, rest, fixed_s, fixed_v):
u"""`router.repair` 接到底板上:只交普通信号(D± / 平面网不拆),验收用 check_pads。"""
import time
sub = {n: bypad[n] for n in rest if n in bypad}
chk_pads = [q for q in check_pads(pads) if q[7] in sub]
orig_chk, orig_rg = RT.check_connected, RT.route_groups
t0, n = time.time(), [0]
def chk(_pads, s, v):
return orig_chk(chk_pads, s, v)
def rg(*a, **k):
n[0] += 1
out = orig_rg(*a, **k)
print(u' 拆线重布 #%d:重布 %d 个网,失败 %d 条(累计 %.0f s)'
% (n[0], len(k.get('only') or ()), len(out), time.time() - t0))
sys.stdout.flush()
return out
RT.check_connected, RT.route_groups = chk, rg
try:
return RT.repair(sub, pads, segs, vias, logs, board_vias, {}, fixed_s, fixed_v)
finally:
RT.check_connected, RT.route_groups = orig_chk, orig_rg
def report(segs, vias, logs):
L = lambda s: math.hypot(s[3] - s[1], s[4] - s[2]) # noqa: E731
tot = sum(L(s) for s in segs)
top = sum(L(s) for s in segs if s[0] == 1)
print(u'导线 %d 段 / 总长 %.1f mm(L1 %.1f mm = %.0f%%)/ 过孔 %d(GND %d / +12V %d / 信号 %d)'
% (len(segs), tot, top, 100.0 * top / max(tot, 1e-9), len(vias),
sum(1 for v in vias if v[2] == 'GND'), sum(1 for v in vias if v[2] == '+12V'),
sum(1 for v in vias if v[2] not in PLANE_NETS)))
print(u'差分对实测铜长(含引出脚)/ 过孔数:')
for name, dp, dm in RB.PAIRS:
a = sum(L(s) for s in segs if s[6] == dp)
b = sum(L(s) for s in segs if s[6] == dm)
va = sum(1 for v in vias if v[2] == dp)
vb = sum(1 for v in vias if v[2] == dm)
print(u' %-6s DP %5.1f mm / %d 孔 DM %5.1f mm / %d 孔 差 %.1f mm'
% (name, a, va, b, vb, abs(a - b)))
if logs:
print(u'[!] %d 条没搞定:' % len(logs))
for m in logs[:40]:
print(u' ' + m)
if __name__ == '__main__':
if '--clear' in sys.argv:
RT.clear_mine()
sys.exit(0)
if '--snapshot-vias' in sys.argv:
print(u'底板既有过孔快照:%d 个 -> %s' % (len(load_board_vias()), BOARD_VIAS))
sys.exit(0)
c = json.load(io.open(CACHE, encoding='utf-8')) if os.path.exists(CACHE) else {}
cache_ok = (c.get('schema') == CACHE_SCHEMA and c.get('slot_rot') == B.SLOT_ROT and
c.get('usb_slot_ref') == {str(k): v for k, v in B.USB_SLOT_REF.items()})
if '--replan' not in sys.argv and cache_ok:
segs, vias, logs = [tuple(s) for s in c['segs']], [tuple(v) for v in c['vias']], c['logs']
print(u'(用缓存 _base_routes.json)')
elif '--replan' not in sys.argv:
sys.exit(u'!! 没有 4 层版缓存(schema %d);布线要在服务器上算:.claude/r2/remote_route.ps1'
% CACHE_SCHEMA)
else:
best, prio = None, set()
if '--priority-cache' in sys.argv and cache_ok:
prio = set(RT._msg_net(m) for m in c.get('logs', []))
print(u'(用上轮 %d 条失败记录优先排网)' % len(c.get('logs', [])))
if '--passes' in sys.argv:
RT.PASSES = int(sys.argv[sys.argv.index('--passes') + 1])
for it in range(RT.PASSES):
segs, vias, logs = plan_all(prio)
print(u' 第 %d 轮:%d 条没搞定' % (it + 1, len(logs)))
if best is None or len(logs) < len(best[2]):
best = (segs, vias, logs)
if not logs:
break
prio |= set(RT._msg_net(m) for m in logs)
segs, vias, logs = best
if '--dp-only' not in sys.argv:
io.open(CACHE, 'w', encoding='utf-8').write(json.dumps({
'schema': CACHE_SCHEMA, 'layers': B.LAYERS, 'slot_rot': B.SLOT_ROT,
'usb_slot_ref': B.USB_SLOT_REF, 'v12_patch': V12_PATCH,
'segs': segs, 'vias': vias, 'logs': logs}, ensure_ascii=False))
report(segs, vias, logs)
if '--push' in sys.argv:
# 默认仍拒绝下发不完整路由;`--force` 是 2026-09-10 用户选定的收尾方式:
# 先把最好的一版下发,剩下的在客户端手工补 / 逐条构造。
if logs and '--force' not in sys.argv:
sys.exit(u'!! 仍有 %d 条未布通,拒绝下发不完整路由(确认要先下发再手补,加 --force)' % len(logs))
if logs:
print(u'⚠ --force:带着 %d 条未完成下发' % len(logs))
RT.push(segs, vias)