# -*- 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± 七对**:USB1~5 按构造画(`dp_construct`,每根 1 个孔),U32 北沿 5/10/13/19 脚先按构造画 (`u32_north`);上行 / USB6 从西侧孔起步交给 A*(DP 先、DM 贴着 DP,`HUG_PEN`)。 2026-09-11 起 Hub 走 strap 模式、口 1~3 片内对调,北去三对与槽上「D+ 在西」同序,**不再掉头**。 每处换层旁补一颗 GND 回流孔(`dp_return_vias`),每根 ≤ 4 孔(`dp_via_limit`,超了记未完成)。 3a. **只走 L4 的敏感网**(`NOVIA_NETS`):晶振 / RBIAS / VDD18PLL / VDD18 / 两颗 Buck 的 FB / Kelvin,A* 不许打孔。 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(四层,同刀片),留一点给网格吸附 if '--old-viamap' in sys.argv: # 对照:route_net 途中封过孔图用 2026-09-11 以前的旧口径 RT.VIA_MAP_FIX = False if '--old-stub' in sys.argv: # 对照:route_net 途中引出脚占位用旧行为(主图 g 挑形状,和真正发线的 gt 不一致) RT.STUB_SAME_GRID = False if '--via-launch' in sys.argv: # 坐在孔上的落点孔心两层都算出口(第 29 轮:影子孔 59 → 25,但断开 29 → 32,默认关) RT.VIA_LAUNCH_BOTH = True RT.W_DEFAULT = 0.15 RT.EDGE_CLR = 0.4 # ⭐ 每层优选方向(2026-09-10 用户定):**插槽面 L1 顺槽长走(x,'h')、器件面 L4 横穿槽阵走(y,'v')**, # 逆向每格代价 ×2(软偏好:出脚、绕焊盘这种一两毫米的逆向照样走得通)。 # 以前这里是 `{}`(八方向同价),理由是「D± 全是南北向」—— 现在七对 D± 全按构造画、不走 A*,理由不成立。 # 几何上也顺:L1 x 28~70 被五排 1 mm 针挡死、x<35 是 12V 大铜,南北贯通车道只剩 x 70~74; # 横穿槽阵的长线本来就在 L4 槽体底下走。`--no-dir` 退回八方向同价(对照用)。 if '--no-dir' in sys.argv: RT.LAYER_DIR = {} else: RT.LAYER_DIR = {2: 'v', 1: 'h'} RT.CROSS_COST = 2.0 # 靠平面成网:焊盘各自一个过孔,铜在几何上本来就是分开的(+12V → L1 大铜)。 # ⚠ +3V3 2026-09-10 深夜起回到布线网(L2 改整层 GND),不许再在这里 —— 否则它布断了连通性检查也不报 RT.ISLAND_NETS = ('+12V',) 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') ONBOARD = os.path.join(HERE, '_base_routes.onboard.json') # 最近一次 --push 下发的那份缓存 PLACE_JSON = os.path.join(HERE, '_base_place.json') # ⛔ router 里有刀片专用的 `J1` 特判(那边 J1 是金手指:不封顶、不排序、**连通性检查直接跳过**)。 # 底板的 J1 是取电口(D16 起是 USB-C,与 J23 同料;之前是 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 的孔是长槽,槽比焊盘大 # ⛔ **只有长槽件才放大 0.6**(2026-09-12)。原来是所有通孔盘一律 +0.6,于是 2.54 间距的 # 排针 / 风扇座(圆盘 1.6)在占用图里变成 2.2 × 2.2,相邻两脚之间只剩 0.34 —— 而出口 # 要「半盘 1.10 + 线半宽 + 净距 = 1.33」才落得下第一格 ⇒ **中间那几只脚一根铜都出不来**。 # 症状是布线器报「J26.3 没布通 / 一根铜都没有」,看着像没地方走,其实是脚被自己虚胖的 # 邻盘封死;压线宽、拆线重布、换落位(扫了 500 个候选)全都救不回来。 TH_GROW_REFS = ('J1', J1_ALIAS, 'J23') TH_GROW_SMALL = 0.0 # 圆通孔盘:铜盘本身就是障碍,不用再放 TH_KEYS = set() def th_grow(ref): return TH_GROW if ref in TH_GROW_REFS else TH_GROW_SMALL def split_th(pads): out = [] for p in pads: if p[6] == 12: TH_GROW_NOW = th_grow(p[0]) # ⛔ 用**改名之后**的位号(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_NOW; a[5] += TH_GROW_NOW b = list(p); b[6] = 1; b[1] = str(p[1]) + TH_SUFFIX b[4] += TH_GROW_NOW; b[5] += TH_GROW_NOW 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 u20_remap(pads) return pads # ── 试算:U20(ESP32-S3-WROOM-1)引脚重分配(2026-09-11,用户选「引脚重分配」)────────────── # 只改布线器读到的焊盘网名,**不改原理图**;结果只能看、不能下发(缓存记 `u20_remap`,`--push` 拒绝)。 # 病因:NTC1/NTC2 在北排东头(IO1/IO2)却要去南边(RT601 (85,26) / RT602 (19,8)),风扇那组在南排(IO4~7)却要去西北角 —— # 两组方向全反,风扇线横穿全板成了南北墙(PROGRESS「U20 一簇诊断」)。 # 约束(a3_ctrl.py 的 GPIO 表):NTC 必须 ADC1(IO1~IO10);IO19/20 USB、IO43/44 UART0、IO0/3/45/46 strapping、IO35~37 留 R8 PSRAM —— 都不动。 # 模组脚 → IO:4=IO4 5=IO5 6=IO6 7=IO7 23=IO21 24=IO47 25=IO48 38=IO2 39=IO1。 U20_REMAPS = { # A:NTC ↔ 风扇测速对调(IO4/IO5 也是 ADC1),风扇 PWM/EN 挪到西列北端两颗空脚,UP_VBUS_DET 挪到南排(它去南边) 'A': {'4': 'NTC1', '5': 'NTC2', '39': 'FAN_TACH_OUT', '38': 'FAN_TACH_IN', '24': 'FAN_PWM', '25': 'FAN_EN', '6': 'UP_VBUS_DET', '7': '', '23': ''}, # A2:同 A,只是 FAN_PWM / FAN_EN 放 23 / 24(西列再靠南一格) 'A2': {'4': 'NTC1', '5': 'NTC2', '39': 'FAN_TACH_OUT', '38': 'FAN_TACH_IN', '23': 'FAN_PWM', '24': 'FAN_EN', '6': 'UP_VBUS_DET', '7': '', '25': ''}, # B:第 30 轮 A 把 RXD0/TXD0 弄断了 —— 它们固定在北排 36/37(UART0)要北上 J33,A 的风扇测速在 38/39 西去正好横过。 # ⇒ 北排东头 38/39 空出,风扇整组 4 根集中到西列北段 22~25(IO14/IO21/IO47/IO48)往西北直出; # 原来 22 脚的 PRSNT5#(去南边 J38.63)挪到空出来的南排 7 脚(IO7)。 'B': {'4': 'NTC1', '5': 'NTC2', '6': 'UP_VBUS_DET', '7': 'PRSNT5#', '22': 'FAN_TACH_OUT', '23': 'FAN_TACH_IN', '24': 'FAN_PWM', '25': 'FAN_EN', '38': '', '39': ''}, } def u20_remap_name(): return sys.argv[sys.argv.index('--u20-remap') + 1] if '--u20-remap' in sys.argv else None def u20_remap(pads): name = u20_remap_name() if not name: return m = U20_REMAPS[name] import collections before = collections.Counter(p[7] for p in pads if p[0] == 'U20' and p[7]) for p in pads: if p[0] == 'U20' and str(p[1]) in m: p[7] = m[str(p[1])] after = collections.Counter(p[7] for p in pads if p[0] == 'U20' and p[7]) if before != after: raise RuntimeError(u'U20 重分配 %s 丢 / 多了网:%s' % (name, (before - after) + (after - before))) print(u' ⚠ 试算:U20 引脚重分配 %s(原理图没改,结果不许下发)' % name) 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() # ⛔ 快照只留**既不归布线缓存、也不归缝合**的孔(2026-09-10)。`RT.load_vias()` 是板上全部过孔: # 布线已下发之后再重算,旧的 604 个布线孔(其中一个就扎在插槽定位柱里)和 122 个缝合孔会被当成 # 固定障碍 —— 新布线绕着一堆马上要被 `--clear` 删掉的孔走。布线孔由新缓存重下,缝合孔 `pour_base --stitch` 重算。 import pour_base as PO # ⚠ 按「板上那一版」认,不按当前缓存:2026-09-10 缓存已换成没下发的新结果,按它认的话板上 620 个旧布线孔全成了「固定障碍」 src = ONBOARD if os.path.exists(ONBOARD) else CACHE mine = [] if os.path.exists(src): mine = [v for v in json.load(io.open(src, encoding='utf-8')).get('vias', [])] print(u'(按 %s 认布线孔)' % os.path.basename(src)) off, pitch = PO.INSET + 2.0, PO.STITCH_SUB # 子网格:天线周边加密孔也在上面(第 4 阶段) def on_stitch(v): return v[2] == 'GND' and all(abs((t - off) / pitch - round((t - off) / pitch)) < 1e-3 for t in v[:2]) n_mine = n_st = 0 keep = [] for v in vias: # 0.2 而不是 0.01:USB3 掉头的 USB3_DM 孔在板上比缓存偏了 (−0.127, +0.127) = 正好一格 5 mil, # 按 0.01 认不出来,就被当成固定障碍留下了。同网、0.2 以内的孔只可能是同一个。 if any(m[2] == v[2] and abs(m[0] - v[0]) < 0.2 and abs(m[1] - v[1]) < 0.2 for m in mine): n_mine += 1 elif on_stitch(v): n_st += 1 else: keep.append(v) print(u'板上过孔 %d = 布线缓存 %d + 缝合 %d + 其余 %d(只有「其余」交给布线当障碍)' % (len(vias), n_mine, n_st, len(keep))) io.open(BOARD_VIAS, 'w', encoding='utf-8').write(json.dumps(keep, ensure_ascii=False, indent=2)) return keep # ── 线宽 ───────────────────────────────────────────────────────────────── 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 = { # 9 A 由之后的铺铜 + 断面校核保证。⚠ D16 之后这是整板重布(plan_all)的起手宽;南带局部补线走 # d16_route.py 自己的线宽(0.4~0.5 —— 2.0 落不进 0402 和 U37 的 0.4 mm VIN 脚) 'VIN_RAW': 2.0, 'VIN_FUSED': 2.0, 'VIN_SW': 2.0, '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') # ⭐ 2026-09-10 深夜用户拍板:L2 由整层 +3V3 改**整层 GND**(双地 —— L1 信号参考 L2 地、L4 参考 L3 地, # 换层回流靠旁边的 GND 孔闭合;依据 Ott 叠层 / 乐鑫 ESP32-S3 指南 / TI SPRAAR7)。 # +3V3(47 只脚:15 只槽针每槽 ~3 mA、A3 16 只、A4M 8 只,合计 ≈0.5 A)重新成为**布线网**,宽 0.4。 # 上一次它是布线网时有二十来处没布通(PROGRESS),这回先按普通网布一轮看结果,不行再上构造干线。 # (更早那一版:「+3V3 是平面网,每脚一孔扎进 L2」—— 已作废。) PLANE_NETS = ('GND', '+12V') LATE_PLANE_NETS = ('GND',) # 布完信号之后才打孔的平面网(早打会封死 1 mm 针排里的信号脚) # 诊断开关(2026-09-11,只在服务器对照实验用,结果别下发):量化 +3V3 改布线网之后占了多少地盘。 # --3v3-plane:+3V3 当平面网,每只脚打一个孔(= L2 还是 +3V3 整层那时的做法) # --3v3-skip :+3V3 完全不布(上限) if '--3v3-plane' in sys.argv: PLANE_NETS = PLANE_NETS + ('+3V3',) LATE_PLANE_NETS = LATE_PLANE_NETS + ('+3V3',) # ── Kelvin 取样 ────────────────────────────────────────────────────────── # ⚠ 2026-09-11 起 U13 的 IN± 对调接(A1-INPUT;R1 不变):U13.10 在上、Rs6.2(+12V) 在北, # U13.9 在下、Rs6.1(VIN_SW) 在南 —— 两条只走 L4 的 Kelvin 不再交叉。原来 IN− 为了绕开 IN+ 走了 37 mm, # 把 U13 东列的 SDA / INA_ALERT 圈死。读数因此为负,固件 ina226_set_reversed(0x43) 取反。 KELVIN = [(('U13', '10'), ('Rs6', '2'), 0.3), # IN+ ← 分流电阻下游(+12V) (('U13', '9'), ('Rs6', '1'), 0.3)] # IN− ← 分流电阻上游(VIN_SW) 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 全部按构造画(dp_construct);这两对从 L1 的孔起步 # 最后一段才布的电源网(它们的脚在前两段一直收过路费)。 # ⛔ 第二轮是按线宽分档、宽的先布:+3V3 / +5V 的干线沿走廊边一铺,正好横在 U35 两排脚、 # U20 西排、风扇那一窝的出口上,于是几十条信号「没布通」。 # +3V3 自 2026-09-10 深夜起是普通布线网(L2 改整层 GND),和 +5V 一样在信号那一段一起布 # (最后布时通道早被占完,+5V 那次 10 只槽针全落空) POWER_LATE = ('+3V3_HUB', 'FAN_12V_SW') # 在 U32 本地网之后、普通信号之前单独先布的网(2026-09-11,第 22 轮): # U13 东列 2/5 脚都是 SCL、4 脚 SDA、3 脚 INA_ALERT 夹在中间。SCL 从芯片底下把 2/5 连起来(挡西), # SDA 出脚就斜向东北(挡东)⇒ INA_ALERT 出不来。它是一对一的短网,先布占住东边那条道,SDA 多走半格就行。 # 2026-09-11 第 34 轮起加 RXD0:方案 B 落地后 RXD0(应急 UART)断 —— 它的扇出孔落在 U20 北排焊盘南侧, # J33 在北 18 mm,按普通顺序轮到它时北边已被占满。先布它:离线断开 27 → 26、RXD0 通(再加 PRSNT3# / TXD0 都更差)。 EARLY_NETS = ('INA_ALERT', 'RXD0') # `--early-add A,B`(试算):再加几个网进「普通信号之前先布」那一段。 if '--early-add' in sys.argv: EARLY_NETS = EARLY_NETS + tuple(sys.argv[sys.argv.index('--early-add') + 1].split(',')) # ── 平面网扇出 ──────────────────────────────────────────────────────────── 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()} # 封装自带的非金属化孔:五个插槽各两颗定位柱 Ø1.21(x 44.15 / 67.30,槽心线上)+ J23 两颗 Ø0.61。 # API 读不出来(skill §9u),`npth_base.py --write` 从 L2/L3 反焊盘反解。[x, y, 孔半径, 反焊盘半径]。 # 2026-09-10 以前没有它:USB1 D±、+5V、SCL3 等 12 个网直接从孔上走过去(DRC「挖槽区域 到 导线 0mil」)。 # 禁区按反焊盘半径(孔 + 实测铜面净距 0.353,比 DRC 要的 0.3 严)。缺文件直接报错 —— 没有它就会再压一次孔。 NPTH = json.load(io.open(os.path.join(HERE, '_base_npth.json'), encoding='utf-8')) def keep_rects(): u"""所有铜层都不许有铜:天线净空 + 板框缺口(外扩板边净距)+ 封装自带的 NPTH。""" 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)] + [(h[0] - h[3], h[1] - h[3], h[0] + h[3], h[1] + h[3]) for h in NPTH]) _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) # 只打导线图:过孔可以落进大铜。D16 窗口(pcb_base.PD_L1:CC 两根 + VIN_FUSED 小岛)先从 12V 里扣掉、 # 再按各自的网封 —— ⚠ router 的 _mark 在别的网已占的格子上写,会变成 255(谁都走不了),不能叠加 holes = [r for _n, r in B.PD_L1] for r in B.l1_12v_rects(): for q in B.rect_minus((r[0] - halfw, r[1] - halfw, r[2] + halfw, r[3] + halfw), holes): g.block_rect(1, q[0], q[1], q[2], q[3], net='+12V') for n, r in B.PD_L1: g.block_rect(1, r[0], r[1], r[2], r[3], net=n) 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 # ── 小工具 ─────────────────────────────────────────────────────────────── # ── 敏感网只走 L4(2026-09-11 第 4 阶段,用户选定)───────────────────────────── # 晶振 / RBIAS / VDD18PLL / VDD18、两颗 Buck 的 FB、INA226 的 Kelvin:换层 = 回流换参考面 + 一段孔的电感, # 而这几个网都只有一两毫米。一律不许打孔:A* 的过孔代价抬到 1e9,路径里只要换层就作废(记为没布通,不偷偷放过)。 NOVIA_NAMED = ('XTAL1', 'XTAL2', 'RBIAS', 'VDD18PLL', 'VDD18') NOVIA_PINS = (('U19', '4'), ('U30', '4')) # 两颗 Buck 的 FB 脚(SOT-23-6:4 = FB;U30 那边是自动网名) NOVIA_NETS = set() _orig_astar = RT.astar def _astar_l4(g, starts, goals, net, halfw): s2 = [s for s in starts if s[0] == 2] g2 = [q for q in goals if q[0] == 2] vc = RT.VIA_COST RT.VIA_COST = 1e9 try: path = _orig_astar(g, s2, g2, net, halfw) finally: RT.VIA_COST = vc if path and any(a[0] != 2 for a in path): return None return path def _astar(g, starts, goals, net, halfw): if net in NOVIA_NETS: return _astar_l4(g, starts, goals, net, halfw) return _orig_astar(g, starts, goals, net, halfw) RT.astar = _astar # router 内部按全局名调 astar,换掉模块属性即生效 # 诊断(只在服务器用):`--dbg-nets A,B` —— 这些网在 route_net 里失败时,把那一刻的占用图原样存成 `_dbg_<网>.json`, # 再用 `reach_audit.py --dbg-cmp` 和同一次运行的最终占用图逐格比:找「布线当时封着、最终图却是空的」格点。 # (2026-09-11 第 27 轮:EFUSE_EN4/5、UP_VBUS_DET 在最终图上重放布得通,布线当时却报没布通。) DBG_NETS = set(sys.argv[sys.argv.index('--dbg-nets') + 1].split(',')) if '--dbg-nets' in sys.argv else set() _orig_route_net = RT.route_net def _route_net_dbg(g, net, pads_of_net, halfw, width, others_by_layer, log, gt=None, mutate=True): n0 = len(log) out = _orig_route_net(g, net, pads_of_net, halfw, width, others_by_layer, log, gt, mutate) if net in DBG_NETS and len(log) > n0 and mutate: import base64 b = lambda a: base64.b64encode(bytes(a)).decode('ascii') # noqa: E731 d = {'net': net, 'halfw': halfw, 'width': width, 'NX': RT.NX, 'NY': RT.NY, 'G': RT.G, 'ids': dict(g._id), 'logs': log[n0:], 'pads': [list(p) for p in pads_of_net], 'own1': b(g.own[1]), 'own2': b(g.own[2]), 'ownv1': b(g.ownv[1]), 'ownv2': b(g.ownv[2])} json.dump(d, io.open(os.path.join(HERE, '_dbg_%s.json' % net.replace('#', '_')), 'w', encoding='utf-8')) print(u' [dbg] %s 失败,占用图已存(%s)' % (net, u';'.join(log[n0:]))) return out if DBG_NETS: RT.route_net = _route_net_dbg def grid_now(pads, halfw, segs, vias, board_vias): u"""按 halfw 建占用图并打进已布的铜;再建一张「引出脚口径」的图 gt。和 route_groups 同一套判据。""" g = RT.build_grid(pads, halfw, board_vias) # ⛔ 过孔图的口径(2026-09-11 修,和 build_grid 里焊盘 / 旧孔同一套): # 线:新孔中心离线心 ≥ VIA_D/2 + CLR + ww/2 ⇒ extra = VIA_D/2 + CLR − halfw。 # 原来减的是 (halfw + ww/2),把这条线自己的半宽丢了 —— 0.4 的 +3V3 / +5V 线旁边 0.625 处照样落孔, # 第 17 轮 DRC「线到孔 4.9 mil」15 条全是这个(细线也少 0.075,多半被网格取整盖住)。 # 孔:原来只 block_rect 进走线图、没进过孔图,free_via 看不见已放的孔 —— # USB3 的 GND 回流孔就贴着 USB3_DM 构造孔落下(中心距 0.726,要 VIA_D + CLR = 0.76)。 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), exact_v=True) for (vx, vy, nn) in vias: rv = RT.VIA_D / 2.0 + RT.CLR + halfw ev = max(0.0, RT.VIA_D / 2.0 - halfw) # 过孔图总口径 = max(rv, VIA_D + CLR) for l in RT.LAY: g.block_both(l, vx - rv, vy - rv, vx + rv, vy + rv, ev, net=nn, exact_v=True) # 过孔图用精确模式(孔只落在格点上):保守取整会让细线旁 0.625 这种合法落孔点也没了 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 = _astar_l4(g, na[:16], nb[:16], pa[7], halfk) # Kelvin 只走 L4(第 4 阶段规则) 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 形掉头 `usb3_uturn` 2026-09-11 删除:strap 使能后口 1~3 片内对调,北去三对与槽上同序,不再掉头。) 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])) bad += _npth_bad(segs, vias) if bad: raise RuntimeError(u'%s 几何不过:%s' % (tag, u';'.join(sorted(set(bad))))) def _npth_bad(segs, vias): u"""构造铜 vs 封装自带的 NPTH(按反焊盘半径,所有层 —— 孔是穿透的)。""" bad = [] for (hx, hy, _hr, ar) in NPTH: pt = (0, hx, hy, hx, hy) for s in segs: d = _seg_seg(s, pt) - s[5] / 2.0 - ar if d < 0: bad.append(u'%s 线压 NPTH(%.2f, %.2f) %.3f' % (s[6], hx, hy, d)) for (vx, vy, net) in vias: d = math.hypot(vx - hx, vy - hy) - RT.VIA_D / 2.0 - ar if d < 0: bad.append(u'%s 孔压 NPTH(%.2f, %.2f) %.3f' % (net, hx, hy, d)) return bad # ── U32 北沿 5 / 10 脚(VDDA33)+ 13 / 19 脚(GND):按构造画(2026-09-11 第 4 阶段重画)────────── # strap 使能后北沿西 → 东:DP1 DM1 DP2 DM2 **3V3(5)** DP3 DM3 DM4 DP4 **3V3(10)** DM5 DP5 **GND(13)** NC NC NC。 # 5 / 10 脚夹在 D± 中间,既要去耦也要供电: # 脚 → L4 竖线一路北上 → 半路横接一小段到电容 C750 / C747 的 +3V3_HUB 脚(电容在正北窄带、偏在线的一侧, # place_base.HUB_SAT)→ 到 J36 两排之间打孔上 L1(窄带正上方是 J36 南排焊盘,孔只能落在两排之间) # → L1 沿两排之间东去 → 东头打孔回 L4(虚拟落点 fan:e3v3,交给 A* 接回 +3V3_HUB 那一路)。 # 电容地脚朝北,地孔同样落在两排之间(离 J36 南排焊盘内端 0.55,比供电孔低 0.15 让开 USB4 的横段)。 # 13 / 19 脚改成 GND 之后,从焊盘内端直接接散热盘(EP)—— 散热盘自己有孔阵下平面。 U32N_W = 0.2 # 供电孔(相对脚 x,相对 J36 南排焊盘内端 y):5 脚东边 0.5 是 USB3_DP 竖线,10 脚西边 0.5 是 USB4_DP。 # 5 脚的孔抬到 1.05:C750 的地孔要落在 USB3_DM 的 L1 竖线(x 48.5)以东 ≥ 0.59,只剩 49.1 这一格, # 孔距 ≥ 0.75 ⇒ 供电孔往北让。 U32N_V = {'5': (-0.11, 1.05), '10': (0.20, 0.70)} # 电容地孔(相对电容 x,相对 J36 南排焊盘内端 y) U32N_G = {'C750': (-0.10, 0.50), 'C747': (0.35, 0.55)} U32N_CAP = {'5': 'C750', '10': 'C747'} U32N_EAST = 66.0 # L1 供电线东头落孔 x(USB4 车道 62.0 以东、J36 东定位柱 67.30 以西) U32N_L1Y = 1.60 # L1 供电横线离 J36 南排焊盘内端(两颗供电孔、两颗电容地孔都在它南边) U32N_EP_W = 0.25 def u32_north(pads, prior_s, prior_v): pm = {(q[0], str(q[1])): q for q in pads} for k, net in (('5', '+3V3_HUB'), ('10', '+3V3_HUB'), ('13', 'GND'), ('19', 'GND'), ('65', 'GND')): if pm[('U32', k)][7] != net: raise RuntimeError(u'U32.%s 是 %s 不是 %s(原理图 / 导入改过?)' % (k, pm[('U32', k)][7], net)) n, w = '+3V3_HUB', U32N_W sdp = pm[('J36', '46')] y_in = sdp[3] + sdp[5] / 2.0 # J36 南排焊盘内端(北沿) segs, vias, done = [], [], set() feed = [] for pk in ('5', '10'): p = pm[('U32', pk)] cap = U32N_CAP[pk] cp = [q for q in pads if q[0] == cap and q[7] == n] cg = [q for q in pads if q[0] == cap and q[7] == 'GND'] if len(cp) != 1 or len(cg) != 1: raise RuntimeError(u'%s 的焊盘不是一只 +3V3_HUB + 一只 GND' % cap) cp, cg = cp[0], cg[0] vx = round(p[2] + U32N_V[pk][0], 3) vy = round(y_in + U32N_V[pk][1], 3) segs += _poly2(2, [(p[2], p[3]), (p[2], round(vy - abs(vx - p[2]), 3)), (vx, vy)], w, n) segs += _poly2(2, [(p[2], cp[3]), (cp[2], cp[3])], w, n) gx, gy = round(cg[2] + U32N_G[cap][0], 3), round(y_in + U32N_G[cap][1], 3) segs += _poly2(2, [(cg[2], cg[3]), (gx, gy)], 0.3, 'GND') vias += [(vx, vy, n), (gx, gy, 'GND')] feed.append((vx, vy)) done |= {('U32', pk), (cap, str(cp[1])), (cap, str(cg[1]))} # L1:两颗供电孔各一小段竖线上到横线,横线在两颗电容地孔之北(孔是通孔,L1 的线不能从它上面过) yl = round(y_in + U32N_L1Y, 3) for vx, vy in feed: segs += _poly2(1, [(vx, vy), (vx, yl)], 0.3, n) segs += _poly2(1, [(min(f[0] for f in feed), yl), (U32N_EAST, yl)], 0.3, n) vias.append((U32N_EAST, yl, n)) ep = pm[('U32', '65')] p13, p19 = pm[('U32', '13')], pm[('U32', '19')] segs += [(2, p13[2], p13[3], p13[2], round(ep[3] + ep[5] / 2.0 - 0.3, 3), U32N_EP_W, 'GND'), (2, p19[2], p19[3], round(ep[2] + ep[4] / 2.0 - 0.3, 3), p19[3], U32N_EP_W, 'GND')] done |= {('U32', '13'), ('U32', '19')} geom_check(u'U32 北沿 5/10 供电 + 去耦、13/19 接散热盘', segs, vias, pads, prior_s, prior_v) virt = [['U32', 'fan:e3v3', U32N_EAST, yl, 0.3, 0.3, 2, n, 'VIA']] # 落在 (U32N_EAST, yl) 那颗孔上 return segs, vias, done, virt # ── U32 西沿 60~63 脚(晶振 / VDD18PLL / RBIAS):按构造画(2026-09-11)────────────────────── # 第 16 轮服务器重布这四个网全没通。根因:上行 D± 的 GND 回流孔(dp_return_vias,信号网之前落孔)挑了 # 离换层孔最近的空格 (45.12, 40.62),正好堵死 60/61 脚往西的直出通道(y 40.22~41.02);XTAL2 只好北绕, # 绕出来的线又把 61/62/63 圈在它和芯片之间。先画死,回流孔 / A* 都会让开。全在 L4、不打孔(NOVIA_NETS)。 # XTAL2:60 脚沿脚心线直西进 Y1.3。 # XTAL1:61 脚西去,过了 Y1.3 东沿斜上到 Y1 上下两排焊盘正中(盘距 0.5,0.15 线两边各 ~0.175), # 西到 Y1.1 那一列再竖直进盘。Y1 是对角焊盘、XTAL1 的盘在远端 —— 另一条路是从 Y1 北边绕到 C740,长近一倍。 # VDD18PLL / RBIAS:各一条 45° 折线到 C743.1 / R708.1。 # Y1.1 / Y1.3 留在 A* 名单里:它们还要接各自的负载电容和 R723(A* 只走 L4)。 U32W_W = {'XTAL1': 0.15, 'XTAL2': 0.15, 'VDD18PLL': 0.2, 'RBIAS': 0.2, '+3V3_HUB': 0.2} U32W_XT2_DY = 0.07 # XTAL2 走线比 60 脚脚心线高(让上行 DP 孔) U32W_XT1_DX = 0.5 # XTAL1 开始斜上处离 Y1.3 东沿 U32W_XT1_IN = 0.35 # XTAL1 竖直进 Y1.1 处离盘心(往东,靠两列之间) U32W_STUB = {'62': 0.7, '63': 0.6, '57': 0.6, '52': 0.6} # 出脚先平直西去的长度(让开相邻脚) U32W_57_DY = 0.55 # 57 脚 45° 下到 NC 55/56 那一段的落差(离上行 DM 孔 ≥ 0.72) U32W_57_DX = 0.53 # 57 脚最后 45° 进 C745.1 的斜段水平长度 U32W_RB_DX = 0.17 # RBIAS 竖段离 R708.1 盘心(往东) def u32_west(pads, prior_s, prior_v): pm = {(q[0], str(q[1])): q for q in pads} want = {'60': 'XTAL2', '61': 'XTAL1', '62': 'VDD18PLL', '63': 'RBIAS'} for k, net in want.items(): if pm[('U32', k)][7] != net: raise RuntimeError(u'U32.%s 是 %s 不是 %s(原理图 / 导入改过?)' % (k, pm[('U32', k)][7], net)) def one(ref, net): qs = [q for q in pads if q[0] == ref and q[7] == net] if len(qs) != 1: raise RuntimeError(u'%s 上 %s 的焊盘不是一只' % (ref, net)) return qs[0] y1, y2, y3 = one('Y1', 'XTAL1'), pm[('Y1', '2')], one('Y1', 'XTAL2') c743, r708 = one('C743', 'VDD18PLL'), one('R708', 'RBIAS') p60, p61, p62, p63 = (pm[('U32', k)] for k in ('60', '61', '62', '63')) segs = [] # XTAL2:直西,止于 Y1.3 盘心 x。整条比脚心线高 U32W_XT2_DY:脚心线离上行 DP 孔(45.175, 39.83, # 外径 0.6)只有 0.125,抬 0.07 后 0.195;起止点仍分别在 60 脚 / Y1.3 焊盘里。 yx2 = round(p60[3] + U32W_XT2_DY, 3) if not (y3[3] - y3[5] / 2.0 < yx2 < y3[3] + y3[5] / 2.0 and yx2 < p60[3] + p60[5] / 2.0): raise RuntimeError(u'XTAL2 走线高度出了 60 脚 / Y1.3 焊盘的上下范围(Y1 挪过?)') segs += _poly2(2, [(p60[2], yx2), (y3[2], yx2)], U32W_W['XTAL2'], 'XTAL2') # XTAL1:两排焊盘正中 y gy = round(((y2[3] - y2[5] / 2.0) + (y3[3] + y3[5] / 2.0)) / 2.0, 3) xs = round(y3[2] + y3[4] / 2.0 + U32W_XT1_DX, 3) xin = round(y1[2] + U32W_XT1_IN, 3) segs += _poly2(2, [(p61[2], p61[3]), (xs, p61[3]), (round(xs - (gy - p61[3]), 3), gy), (xin, gy), (xin, y1[3])], U32W_W['XTAL1'], 'XTAL1') # VDD18PLL:平直西去 → 45° 上到电容那一行 → 进盘 x62 = round(p62[2] - U32W_STUB['62'], 3) segs += _poly2(2, [(p62[2], p62[3]), (x62, p62[3]), (round(x62 - (c743[3] - p62[3]), 3), c743[3]), (c743[2], c743[3])], U32W_W['VDD18PLL'], 'VDD18PLL') # RBIAS:平直西去 → 45° 斜上到盘心东 0.17 → 竖直上到电阻那一行 → 进盘 x63 = round(p63[2] - U32W_STUB['63'], 3) xr = round(r708[2] + U32W_RB_DX, 3) segs += _poly2(2, [(p63[2], p63[3]), (x63, p63[3]), (xr, round(p63[3] + (x63 - xr), 3)), (xr, r708[3]), (r708[2], r708[3])], U32W_W['RBIAS'], 'RBIAS') # +3V3_HUB 57 / 52 脚(VDDA33)→ 各自的去耦 C745 / C744:57 脚北边是上行 DM 孔、南边是 USB6 DP 线, # 52 脚正上方是 USB6 DM 孔 —— 各只有一条缝。第 16 轮两只都没通;上面四条画死之后回流孔改落 (45.25, 38.25), # 又正压 57 脚那条缝。所以一起画死;电容的另一头留给 A* 接回 +3V3_HUB 干线。 n = '+3V3_HUB' c745, c744 = one('C745', n), one('C744', n) p57, p52 = pm[('U32', '57')], pm[('U32', '52')] if p57[7] != n or p52[7] != n: raise RuntimeError(u'U32.57 / U32.52 不是 +3V3_HUB(原理图 / 导入改过?)') x57 = round(p57[2] - U32W_STUB['57'], 3) y57 = round(p57[3] - U32W_57_DY, 3) # 下到 NC 55/56 脚那一段再西去 xt = round(c745[2] + U32W_57_DX, 3) segs += _poly2(2, [(p57[2], p57[3]), (x57, p57[3]), (round(x57 - U32W_57_DY, 3), y57), (xt, y57), (c745[2], round(y57 - U32W_57_DX, 3)), (c745[2], c745[3])], U32W_W[n], n) x52 = round(p52[2] - U32W_STUB['52'], 3) segs += _poly2(2, [(p52[2], p52[3]), (x52, p52[3]), (round(x52 - (p52[3] - c744[3]), 3), c744[3]), (c744[2], c744[3])], U32W_W[n], n) geom_check(u'U32 西沿 52/57/60~63 脚(晶振 / VDD18PLL / RBIAS / VDDA33 去耦)', segs, [], pads, prior_s, prior_v) done = ({('U32', k) for k in want} | {('U32', '57'), ('U32', '52')} | {(c743[0], str(c743[1])), (r708[0], str(r708[1]))}) return segs, done # ── U13 东列 3/4 脚(INA_ALERT / SDA)先平直东出一段(2026-09-11,第 22/23 轮)──────────── # 东列 2/5 脚都是 SCL、3 脚 INA_ALERT、4 脚 SDA,三个网抢同一侧出口: # 第 22 轮 SDA 出脚就斜向东北,挡住 INA_ALERT;第 23 轮 INA_ALERT 先布(EARLY_NETS),又把 SDA 挡了。 # 空间其实够:0.5 间距上 0.15 的平行线净空 0.35。两只中间脚各平直东出 U13F_X,末端给 A* 一个落点; # 2/5 两只 SCL 脚照旧(在芯片底下连起来,从两条直出线的南北两侧出)。 U13F_X = 1.2 # 直出长度(离焊盘东端) U13F_W = 0.15 def u13_fan(pads, prior_s, prior_v): pm = {(q[0], str(q[1])): q for q in pads} segs, done, virt = [], set(), [] for pk, net in (('3', 'INA_ALERT'), ('4', 'SDA')): p = pm[('U13', pk)] if p[7] != net: raise RuntimeError(u'U13.%s 是 %s 不是 %s(原理图 / 导入改过?)' % (pk, p[7], net)) x1 = round(p[2] + p[4] / 2.0 + U13F_X, 3) segs += _poly2(2, [(p[2], p[3]), (x1, p[3])], U13F_W, net) virt.append(['U13', 'fan:%s' % pk, x1, p[3], 0.3, 0.3, 2, net, 'RECT']) done.add(('U13', pk)) geom_check(u'U13 东列 3/4 脚直出', segs, [], pads, prior_s, prior_v) return segs, done, virt # ── U13 东列三个网整体构造出线(2026-09-11,接替 u13_fan)───────────────────────────── # u13_fan 只画 1.2 mm 直出,第 24 轮 SDA 从直出末端照样出不去:卡的是出脚之后那一段 —— # U13 东边往北只有 R9E(东沿 26.73)与 J36 外壳盘孔阵西列(28.45)之间一条 1.4 mm 的走廊, # L1 在 x < 27.8 是 12V 主块、J36 南排 12V 指头压着 y 42.4~46.3。三个网谁先布谁把走廊口封掉。 # 第 22 轮 SDA / SCL 通、第 23 轮 INA_ALERT 通,两轮各自的出线拼起来并不冲突 ⇒ 按它们画死,各自交给 A*: # SCL :2/5 脚在芯片底下(x 20.5)相连;5 脚东出 → 45° 下到 y 41.9 → 东去 → 孔 (28.3, 41.9) 上 L1 # (J37 / J36 两个 12V 指头之间那条 L1 横带,第 22 轮 SCL 就是从这里去 U35 的) # SDA :4 脚东出 → 45° 东北 → 在外壳盘孔阵两行(46.95 / 49.0)之间沿 y 48.0 东去 → 孔 (33.0, 48.0) 上 L1 # (J36 两排之间的 L1) # INA_ALERT:3 脚东出 → 45° 东北(与 SDA 平行、中心距 0.354)→ 走廊里 x 27.05 北上到 y 57(R9E.1 以北、C735 以南) # 三条都在 L4;+3V3 仍可从 SCL 之南东去再北上(SDA 横线挡了 x 29.5 那条旧路,要在孔 (33.0, 48.0) 以东北上)。 # 服务器实测(2026-09-11,过孔快照已清掉 131 颗自动布线残孔,离线铜连通口径): # 不构造 31 个网断开(U13 的 SDA 不通);构造、SDA 孔 31.5 → 33 个(U13 三网全通,多断的在 U35 西头 SCL4 / SDA1); # **SDA 孔 33.0 → 29 个、U13 三网全通**(第 26 轮 v3)。INA 北端 53 / 两处不打孔 与 31.5 版结果完全相同; # INA_ALERT 不先布(--no-early)→ 37,INA_ALERT 反而不通。 U13E_W = 0.15 U13E_X0 = 22.8 # 3/4/5 脚平直东出到这里再转 45°(离焊盘东端 0.38:让开 2 脚 / 1 脚) U13E_LINK_X = 20.5 # SCL 2/5 脚在芯片底下相连的竖线(离 3/4 脚西端 0.3、离 8 脚 +12V 孔 1.45) U13E_SCL_Y = 41.9 # SCL 东去 y:孔上 L1 要离 J36 南排 12V 指头(下沿 42.4)≥ 0.15 U13E_SCL_VX = 28.3 # SCL 孔 x:离 L1 12V 主块东沿 27.8 ≥ 0.15 U13E_SDA_Y = 48.0 # SDA 东去 y:外壳盘孔阵 (x 28.45/30.55, y 46.95/49.0) 两行正中偏北 U13E_SDA_VX = 33.0 # SDA 孔 x(外壳盘 L1 东沿 31.0 以东即可;31.5 版多断 4 个网,见上) U13E_INA_X = 27.05 # INA_ALERT 竖线 x:离 R9E 东沿 0.245、离孔阵西列 0.325 U13E_INA_TOP = 57.0 # INA_ALERT 竖线北端(C735.1 下沿 60.39 以南) U13E_SCL_VIA = 1 # 0 = 不打孔,L4 末端直接交给 A*(它自己挑层) U13E_SDA_VIA = 1 def _u13e_opts(): u"""`--u13e NAME=VAL`(可多次):服务器并行试算时改 U13E_* 参数,不用改文件。""" g = globals() for i, a in enumerate(sys.argv[:-1]): if a == '--u13e': k, v = sys.argv[i + 1].split('=') if 'U13E_' + k not in g: raise RuntimeError(u'--u13e 不认识 %s' % k) g['U13E_' + k] = float(v) def u13_esc(pads, prior_s, prior_v): _u13e_opts() pm = {(q[0], str(q[1])): q for q in pads} for pk, net in (('2', 'SCL'), ('3', 'INA_ALERT'), ('4', 'SDA'), ('5', 'SCL')): if pm[('U13', pk)][7] != net: raise RuntimeError(u'U13.%s 是 %s 不是 %s(原理图 / 导入改过?)' % (pk, pm[('U13', pk)][7], net)) p2, p3, p4, p5 = (pm[('U13', k)] for k in ('2', '3', '4', '5')) w, x0 = U13E_W, U13E_X0 segs, vias, virt = [], [], [] # SCL:芯片底下 2 ↔ 5,再从 5 脚东出 segs += _poly2(2, [(p5[2], p5[3]), (U13E_LINK_X, p5[3]), (U13E_LINK_X, p2[3]), (p2[2], p2[3])], w, 'SCL') segs += _poly2(2, [(p5[2], p5[3]), (x0, p5[3]), (round(x0 + (p5[3] - U13E_SCL_Y), 3), U13E_SCL_Y), (U13E_SCL_VX, U13E_SCL_Y)], w, 'SCL') if U13E_SCL_VIA: vias.append((U13E_SCL_VX, U13E_SCL_Y, 'SCL')) virt.append(['U13', 'esc:SCL', U13E_SCL_VX, U13E_SCL_Y, 0.3, 0.3, 1 if U13E_SCL_VIA else 2, 'SCL', 'VIA' if U13E_SCL_VIA else 'RECT']) # SDA segs += _poly2(2, [(p4[2], p4[3]), (x0, p4[3]), (round(x0 + (U13E_SDA_Y - p4[3]), 3), U13E_SDA_Y), (U13E_SDA_VX, U13E_SDA_Y)], w, 'SDA') if U13E_SDA_VIA: vias.append((U13E_SDA_VX, U13E_SDA_Y, 'SDA')) virt.append(['U13', 'esc:SDA', U13E_SDA_VX, U13E_SDA_Y, 0.3, 0.3, 1 if U13E_SDA_VIA else 2, 'SDA', 'VIA' if U13E_SDA_VIA else 'RECT']) # INA_ALERT segs += _poly2(2, [(p3[2], p3[3]), (x0, p3[3]), (U13E_INA_X, round(p3[3] + (U13E_INA_X - x0), 3)), (U13E_INA_X, U13E_INA_TOP)], w, 'INA_ALERT') virt.append(['U13', 'esc:INA', U13E_INA_X, U13E_INA_TOP, 0.3, 0.3, 2, 'INA_ALERT', 'RECT']) geom_check(u'U13 东列三个网构造出线', segs, vias, pads, prior_s, prior_v) bad = [] for (vx, vy, _n) in vias: # 孔在 L1 上要让开 12V 大铜(L1 的线要从孔接出去) for r in B.l1_12v_rects(): d = _seg_rect_gap((0, vx, vy, vx, vy), r) - RT.VIA_D / 2.0 if d < RB.MIN_CLEARANCE - 1e-6: bad.append(u'孔 (%.2f,%.2f) 离 12V 大铜 %.3f' % (vx, vy, d)) if bad: raise RuntimeError(u'U13 构造出线的孔压 12V 大铜:' + u';'.join(bad)) return segs, vias, {('U13', '2'), ('U13', '3'), ('U13', '4'), ('U13', '5')}, virt # ── +3V3 定点构造(2026-09-11,用户选「定点改」)───────────────────────────────────── # 服务器对照实验(第 18 轮为底):+3V3 完全不布 ⇒ 信号网 37 → 29;每脚一孔的平面式 ⇒ 33。实测挡路的两段: # ① J36 ↔ J35 之间绕 U35 西侧 + J36 过孔带里的 L1 跳线(SDA5;U35 西侧出线的 INT# / SCL4 / SDA1); # ② 电源经 x≈29 从东北斜下去喂 U13 / C11 —— 横在 U13 自己的 SDA / INA_ALERT 往东的出线上。 # 两段画死,其余 +3V3 脚交给 A*(这两段上标 WALL 落点 = 多源种子,就近接)。 # A. U35 东侧旁路:J36 北排 9/10 在 L1 并起来 → 槽内孔 J → L4 东到键位缺口列(x 44.0,五个槽这一列两排都没针) # → 北上穿 A4M 走廊(U35 东头 x 42.77 以外是空的)→ 西到槽内孔 K → L1 接 J35 南排 40 与北排 9/10。 # ⚠ 定位柱(NPTH Ø1.21)也在 x 44.15、槽心线上 ⇒ 竖段只在 J36 北排内侧 ~ J35 南排内侧之间,离柱 ≥ 0.56。 # B. U13 从东南进:U13 的 +3V3(6 脚)在西列最下,西边是 Kelvin、北边是 12V / VIN_SW 脚、东列是信号,南边是 VIN_SW 铜。 # C11.1 ↔ U13.6 短接,沿 C11 与 U13 之间 y 41.9 东去(在东列信号脚之南,不挡它们往东 / 往北) # → x 28.5 孔上 L1(J37 / J36 两个 12V 指头之间 y 35.3~42.4 的 L1 是空的)→ L1 y 39.5 东去横穿 I²C 竖向车道(它们在 L4) # → x 39.3 孔回 L4 北上(XTAL1 绕 Y1 西侧的通道在 x ≥ 39.73)→ 槽外孔上 L1 接 J36 南排 40 脚。 V3T_W = 0.4 # 干线线宽(+3V3 规则宽度) V3T_LINK_W = 0.3 # C11.1 ↔ U13.6 短接 V3T_JOIN = 0.25 # 槽脚 L1 并线离焊盘内端(伸进焊盘) V3T_VJ = 0.6 # 孔 J 离 J36 北排内端 V3T_KY = 0.88 # J35 那头 L4 横段 / 孔 K 离 J35 南排内端 V3T_KX = 0.5 # 孔 K 在 J35.40 以东 V3T_BY = 40.45 # B 段 L4 东去的 y:Kelvin IN− 在 y 41.25(线宽 0.3)之下 0.45、C11.2 底 40.98 之下 0.33 V3T_BX = 28.5 # B 段上 L1 的孔 x(L1 12V 主块东沿 27.8) V3T_BL1Y = 39.5 # B 段 L1 东去的 y V3T_BX2 = 38.9 # B 段回 L4 的孔 x(第 19 轮 39.3:孔在走线图上保守封到 x 40.0,XTAL1 绕 Y1 西侧那 2 格通道被堵死) V3T_BTOP = 0.6 # B 段北端孔离 J36 南排外端(槽外) def v3_trunk(pads, prior_s, prior_v): pm = {(q[0], str(q[1])): q for q in pads} n = '+3V3' need = [('J36', '9'), ('J36', '10'), ('J36', '40'), ('J35', '9'), ('J35', '10'), ('J35', '40'), ('J36', '43'), ('J36', '44'), ('U13', '6'), ('C11', '1')] for k in need[:6] + need[8:]: if pm[k][7] != n: raise RuntimeError(u'%s.%s 是 %s 不是 +3V3(原理图 / 导入改过?)' % (k[0], k[1], pm[k][7])) segs, vias, virt = [], [], [] w = V3T_W # ── A. U35 东侧旁路 ── a9, a10, a40 = pm[('J36', '9')], pm[('J36', '10')], pm[('J36', '40')] b9, b10, b40 = pm[('J35', '9')], pm[('J35', '10')], pm[('J35', '40')] xk = round((pm[('J36', '43')][2] + pm[('J36', '44')][2]) / 2.0, 3) # 键位缺口列 a_in = a9[3] - a9[5] / 2.0 # J36 北排内端(南沿) b_in = b40[3] + b40[5] / 2.0 # J35 南排内端(北沿) b_nin = b9[3] - b9[5] / 2.0 # J35 北排内端 yj = round(a_in + V3T_JOIN, 3) jx, jy = round((a9[2] + a10[2]) / 2.0, 3), round(a_in - V3T_VJ, 3) segs += _poly2(1, [(a9[2], yj), (a10[2], yj)], w, n) segs += _poly2(1, [(jx, yj), (jx, jy)], w, n) ky = round(b_in + V3T_KY, 3) kx = round(b40[2] + V3T_KX, 3) segs += _poly2(2, [(jx, jy), (xk, jy), (xk, ky), (kx, ky)], w, n) segs += _poly2(1, [(kx, ky), (b40[2], round(ky - V3T_KX, 3)), (b40[2], round(b_in - V3T_JOIN, 3))], w, n) yb = round(b_nin + V3T_JOIN, 3) segs += _poly2(1, [(kx, ky), (b9[2], round(ky + (b9[2] - kx), 3)), (b9[2], yb), (b10[2], yb)], w, n) vias += [(jx, jy, n), (kx, ky, n)] for ym in (0.35, 0.65): # 旁路竖段在 A4M 走廊里的两处落点(出口多) virt.append(['WALL', 'wall', xk, round(jy + (ky - jy) * ym, 3), 0.6, 0.6, 2, n, 'RECT']) # ── B. U13 从东南进 ── u6, c1 = pm[('U13', '6')], pm[('C11', '1')] # ⚠ Kelvin IN−(+12V,只走 L4,最先布)从 U13.9 往东、从芯片底下穿到南边沿 y 41.25 东去再绕上来 —— # U13.6 / C11 在它外面(西南角),馈线只能从它下面走:C11.1 往下到 V3T_BY 再东去(离那段 0.45)。 # 代价:C11 本来就坐在 VIN_SW 铜的东北角里,这条线在 VIN_SW-2 顶端(x 17.1~19.75)划一道 ~0.7 的缝。 # 在 C11.1 盘心断一下:布线器的连通自检只认「端点落在焊盘里」,穿盘而过会报「C11.1 一根铜都没有」(第 19 轮) segs += _poly2(2, [(c1[2], u6[3]), (c1[2], c1[3]), (c1[2], V3T_BY)], V3T_LINK_W, n) x1 = round(V3T_BX - (V3T_BY - V3T_BL1Y), 3) # 45° 下到孔 segs += _poly2(2, [(c1[2], V3T_BY), (x1, V3T_BY), (V3T_BX, V3T_BL1Y)], w, n) segs += _poly2(1, [(V3T_BX, V3T_BL1Y), (V3T_BX2, V3T_BL1Y)], w, n) a_out = a40[3] - a40[5] / 2.0 # J36 南排外端(南沿) ytop = round(a_out - V3T_BTOP, 3) # L4 竖线不在槽外停:继续北上穿 J36 槽内到孔 J 那一行,东接孔 J —— A、B 两段必须是**一整块铜**。 # 第 19 轮两段各自为政(A 接 J36 北排 9/10、B 接南排 40,同槽两排在板上不相通),两组 WALL 落点坐在两块不相连的铜上, # +3V3 断成 6 块(离线铜连通检查查出来的,布线器自检没报)。 # 在槽外孔 (V3T_BX2, ytop) 处断一下:孔要落在线段端点上(连通自检只认端点) segs += _poly2(2, [(V3T_BX2, V3T_BL1Y), (V3T_BX2, ytop), (V3T_BX2, jy), (jx, jy)], w, n) segs += _poly2(1, [(V3T_BX2, ytop), (a40[2], round(ytop + (a40[2] - V3T_BX2), 3)), (a40[2], round(a_out + V3T_JOIN, 3))], w, n) vias += [(V3T_BX, V3T_BL1Y, n), (V3T_BX2, V3T_BL1Y, n), (V3T_BX2, ytop, n)] virt.append(['WALL', 'wall', round((V3T_BX + V3T_BX2) / 2.0, 3), V3T_BL1Y, 0.6, 0.6, 1, n, 'RECT']) # 焊盘 / 已有构造铜的净距;L1 另查 12V 大铜(铺铜矩形不在焊盘表里) geom_check(u'+3V3 定点构造(U35 东侧旁路 + U13 东南进)', segs, vias, pads, prior_s, prior_v) bad = [] rects12 = B.l1_12v_rects() # 主块 + 各槽 12V 指头([0] 只是主块) for s in segs: if s[0] != 1: continue for r in rects12: d = _seg_rect_gap(s, r) - s[5] / 2.0 if d < RB.MIN_CLEARANCE - 1e-6: bad.append(u'L1 线 (%.2f,%.2f)-(%.2f,%.2f) 离 12V 大铜 %.3f' % (s[1], s[2], s[3], s[4], d)) for (vx, vy, _n) in vias: for r in rects12: d = _seg_rect_gap((0, vx, vy, vx, vy), r) - RT.VIA_D / 2.0 if d < RB.MIN_CLEARANCE - 1e-6: bad.append(u'孔 (%.2f,%.2f) 离 12V 大铜 %.3f' % (vx, vy, d)) if bad: raise RuntimeError(u'+3V3 定点构造压 12V 大铜:' + u';'.join(bad)) done = {('J36', '9'), ('J36', '10'), ('J35', '9'), ('J35', '10'), ('J35', '40'), ('J36', '40'), ('U13', '6'), ('C11', '1')} return segs, vias, done, 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) # 北上竖线 x = 出脚 x − SHIFT。2026-09-10 由 3.5 / 2.5 改:原来 USB1 竖线在 x 44.25 / 44.75, # 正压在 J36/J35/J34 的西定位柱(x 44.15,反焊盘半径 0.958)上。现在四根 45.28 / 45.78 / 46.40 / 46.90: # 西界 = 44.15 + 0.958 + 线半宽;东界 = USB3 掉头在 J36 的过孔(x 47.5)− 孔半径 − 0.15 − 线半宽 = 46.91。 DPC_SHIFT = {'USB1': 2.47, 'USB2': 2.35} DPC_VIA = 1.0 # 南去(USB4/5):过孔离目标焊盘内端 DPC_VIA_S = 0.45 # 北去(USB1/2):过孔离目标焊盘南端(从南边进) DPC_UT = 0.5 # 北去:DP 横段在孔南 0.5、DM 横段在孔南 1.0 DPC_U3 = (1.8, 2.5) # USB3 DP / DM 西拐横段离 J36 南排焊盘内端(在 5 脚供电孔 47.45 / C750 地孔之北) # 离 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.3, 0.8) # USB5 DP / DM 东拐横段离北排焊盘外端(C747 下沿 44.58 在 DM 之上) DPC_Y4 = (1.35, 1.85) # USB4 DP / DM 东拐横段离 J36 南排焊盘内端(10 脚供电孔之北 0.21) 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 def chk(pk, net): if u(pk)[7] != net: raise RuntimeError(u'U32.%s 是 %s 不是 %s(口 1~3 片内对调的端口名换过?)' % (pk, u(pk)[7], net)) # 北去 USB1 / USB2:出脚按脚序阶梯 45° 西去、上车道北上;到目标槽南排外侧东拐到焊盘 x, # 在焊盘南端外打孔上 L1 从南边进(口 1~3 已片内对调,DP 在西 = 槽上 D+ 在西,不用掉头)。 # DP 横段在北(离孔 DPC_UT)、DM 横段在南(2×)—— DM 去的焊盘在东,横段必须在 DP 竖线之下。 k = 0 for name, pdp, pdm in (('USB1', '1', '2'), ('USB2', '3', '4')): dp_net, dm_net = PAIRS[name] chk(pdp, dp_net) chk(pdm, dm_net) sp, sm = slot(int(name[3])) s = DPC_SHIFT[name] yv = round(sp[3] - sp[5] / 2.0 - DPC_VIA_S, 3) for net, pk, t, yt in ((dp_net, pdp, sp, yv - DPC_UT), (dm_net, pdm, sm, yv - 2 * DPC_UT)): 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, round(yt, 3)), (t[2], round(yt, 3)), (t[2], yv)] segs += _poly2(2, pts, w, net) segs += _poly2(1, [(t[2], yv), (t[2], t[3])], w, net) vias.append((t[2], yv, net)) done.add(('U32', pk)) # USB3:两根贴着 C750 东侧直直北上(L4 从 J36 南排底下过),到两排之间西拐到焊盘 x, # 打孔上 L1 从北边进焊盘内端。DP 先拐(低)、DM 后拐(高):DM 的孔在东,横段在 DP 之上。 sp3, sm3 = slot(3) y_in3 = sp3[3] + sp3[5] / 2.0 for net, pk, t, dy in ((PAIRS['USB3'][0], '6', sp3, DPC_U3[0]), (PAIRS['USB3'][1], '7', sm3, DPC_U3[1])): chk(pk, net) q = u(pk) yh = round(y_in3 + dy, 3) segs += _poly2(2, [(q[2], q[3]), (q[2], yh), (t[2], yh)], w, net) segs += _poly2(1, [(t[2], yh), (t[2], t[3])], w, net) vias.append((t[2], yh, net)) done.add(('U32', pk)) # 南去:USB5(内圈,出脚就东拐,C747 压在它上面)/ USB4(外圈,北上越过 5/10 脚的供电孔与电容地孔再东去) lane_x = PL.LANES['DP_H'][0] sp5, sm5 = slot(5) y_in5 = sp5[3] + sp5[5] / 2.0 yv5 = round(y_in5 + DPC_VIA, 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', top + DPC_Y5[0], xdp5, wdp5, sp5[2]), (PAIRS['USB5'][1], '11', top + DPC_Y5[1], xdm5, wdm5, sm5[2])): chk(pk, net) q = u(pk) yh = round(yh, 3) 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 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', y_in3 + DPC_Y4[0], xdp4, vdp4, sp4[2]), (PAIRS['USB4'][1], '8', y_in3 + DPC_Y4[1], xdm4, vdm4, sm4[2])): chk(pk, net) q = u(pk) yh = round(yh, 3) 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])] # 朝目的地落孔(2026-09-11 第 31 轮,`--fan-dir W` 才开):原来只挑「离焊盘最近」的空格点,不管这个网要去哪 —— # U20 重分配的六组试算里 RXD0 全断:它的扇出孔落在北排焊盘南边(模组本体一侧),J33 在北 18 mm。 # 候选按 距离 + W × 背离程度(0 = 朝目的地 / 侧向,1 = 正背离)排;目的地 = 本网其余脚(含落点)的质心。 fan_dir_w = float(sys.argv[sys.argv.index('--fan-dir') + 1]) if '--fan-dir' in sys.argv else 0.0 def toward(q, cs): oth = [o for o in bypad.get(q[7], []) if o is not q] if not fan_dir_w or not oth: return cs ux = sum(o[2] for o in oth) / len(oth) - q[2] uy = sum(o[3] for o in oth) / len(oth) - q[3] L = math.hypot(ux, uy) or 1.0 ux, uy = ux / L, uy / L def key(nd): x, y = RT.wx(nd[1]) - q[2], RT.wy(nd[2]) - q[3] d = math.hypot(x, y) or 1e-9 return d + fan_dir_w * max(0.0, -(x * ux + y * uy) / d) return sorted(cs, key=key) 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 = toward(q, 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], 'VIA']) # 'VIA':孔心两层都是出口 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 # ⛔ D± **不吃层方向偏好**(2026-09-10 实测):开了 ×2 之后 USBUP 长度差 3.5 → 5.7 mm(超 3.81 规则)、 # USB6 / USBUP 过孔每根 2 → 4 个 —— 差分对有自己的拓扑规则(DP 先、DM 贴着),方向偏好只会逼它多换层。 # 这里临时八方向同价,布完恢复;其余网照样按 L1 横 / L4 纵。 layer_dir = RT.LAYER_DIR RT.LAYER_DIR = {} 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 RT.LAYER_DIR = layer_dir return out_s, out_v # ── D± 换层处的 GND 回流孔 / 过孔上限(2026-09-11 第 4 阶段)───────────────────── # 双地叠层:L1 的线参考 L2 地、L4 的线参考 L3 地。D± 一换层,回流也要从 L2 换到 L3 —— 旁边没有地孔, # 回流只能绕到远处的缝合孔,环路面积陡增。⇒ 同一对、1.6 mm 以内的换层孔算一处,就近补一颗 GND 孔。 # 回流孔离**最近**那颗换层孔 ≤ DP_RET_R、离最远那颗 ≤ DP_RET_FAR(中心距)。 # 第一版要求离两颗都 ≤ 1.7:一对并排的孔相距 1.0,而占用图让新孔离每颗 D± 孔 ≥ 0.92(方框), # 空位只在一对的侧面 ⇒ 离远的那颗 ~2 mm,七处有四处落不下(静态校验实测)。 DP_RET_R = 1.3 DP_RET_FAR = 2.6 DP_MAX_VIAS = 4 # 每根 D± 的过孔上限(超了记入未完成) def dp_return_vias(pads, segs, vias, board_vias, logs): halfv = RT.VIA_D / 2.0 + RT.CLR g, _gt, _o = grid_now(pads, halfv, segs, vias, board_vias) out = [] for name, dp, dm in RB.PAIRS: groups = [] for v in [v for v in vias if v[2] in (dp, dm)]: for gr in groups: if any(math.hypot(v[0] - u[0], v[1] - u[1]) < 1.6 for u in gr): gr.append(v) break else: groups.append([v]) for gr in groups: cx = sum(v[0] for v in gr) / len(gr) cy = sum(v[1] for v in gr) / len(gr) # 已经放下的回流孔也满足这一处 ⇒ 共用(上行 / USB6 两组西侧孔就共用一颗, # 各放一颗的话两孔中心距 0.5,钻孔间距不够,dedupe 还会悄悄删掉一颗) if any(min(math.hypot(o[0] - v[0], o[1] - v[1]) for v in gr) <= DP_RET_R and max(math.hypot(o[0] - v[0], o[1] - v[1]) for v in gr) <= DP_RET_FAR for o in out): continue ci, cj = RT.gx(cx), RT.gy(cy) rr = int(math.ceil((DP_RET_FAR + 0.5) / RT.G)) best = None for i in range(max(0, ci - rr), min(RT.NX - 1, ci + rr) + 1): for j in range(max(0, cj - rr), min(RT.NY - 1, cj + rr) + 1): x, y = RT.wx(i), RT.wy(j) ds = [math.hypot(x - v[0], y - v[1]) for v in gr] if min(ds) > DP_RET_R or max(ds) > DP_RET_FAR: continue if not g.free_via(i, j, 'GND'): continue if any(math.hypot(x - o[0], y - o[1]) < RT.VIA_D + 0.15 for o in out): continue key = (min(ds), sum(ds)) if best is None or key < best[0]: best = (key, x, y) if best is None: logs.append(u'%s: 换层处 (%.2f, %.2f) 旁边落不下 GND 回流孔' % (name, cx, cy)) continue _d, x, y = best out.append((round(x, 3), round(y, 3), 'GND')) r = RT.VIA_D / 2.0 + halfv for l in RT.LAY: g.block_both(l, x - r, y - r, x + r, y + r, 0.0, net='GND') return out def dp_via_limit(vias, logs): for _name, dp, dm in RB.PAIRS: for net in (dp, dm): n = sum(1 for v in vias if v[2] == net) if n > DP_MAX_VIAS: logs.append(u'%s: 过孔 %d 个,超过上限 %d' % (net, n, DP_MAX_VIAS)) 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 = [], [], [] pm_net = {(q[0], str(q[1])): q[7] for q in pads} NOVIA_NETS.clear() NOVIA_NETS.update(NOVIA_NAMED) NOVIA_NETS.update(pm_net[k] for k in NOVIA_PINS if pm_net.get(k)) print(u' 只走 L4 的敏感网:%s(+ Kelvin 两条)' % ' '.join(sorted(NOVIA_NETS))) 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)) ns, nv, ndone, nvirt = u32_north(pads, segs, vias) segs += ns vias += nv jdone |= ndone print(u' U32 北沿 5/10 供电 + 去耦、13/19 接散热盘:%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/3 北上直进、USB4/5 东绕、上行/USB6 西侧上 L1):%d 段 / %d 孔' % (len(cs_), len(cv))) ws, wdone = u32_west(pads, segs, vias) # 要在 route_pairs / dp_return_vias 之前:回流孔曾落进晶振通道 segs += ws jdone |= wdone print(u' U32 西沿 60~63 脚(晶振 / VDD18PLL / RBIAS):%d 段(按构造画、只在 L4)' % len(ws)) # ⚠ 默认**不开**(第 24 轮:信号网 35 → 38、预判 DRC 冒出 2 处,SDA 从直出末端照样出不去 —— 卡的不只是出脚) if '--u13-fan' in sys.argv: fs, fdone, fvirt = u13_fan(pads, segs, vias) segs += fs jdone |= fdone nvirt = nvirt + fvirt print(u' U13 东列 3/4 脚(INA_ALERT / SDA)平直东出:%d 段,落点 %d 个' % (len(fs), len(fvirt))) # U13 东列三个网整体构造出线(2026-09-11 第 26 轮起默认开,`--no-u13-esc` 关;接替 --u13-fan) if '--no-u13-esc' not in sys.argv and '--u13-fan' not in sys.argv: es, ev, edone, evirt = u13_esc(pads, segs, vias + [tuple(v[:3]) for v in board_vias]) segs += es vias += ev jdone |= edone nvirt = nvirt + evirt print(u' U13 东列 SCL / SDA / INA_ALERT 构造出线:%d 段 / %d 孔,落点 %d 个' % (len(es), len(ev), len(evirt))) # ⚠ 默认**不开**(2026-09-11 第 21 轮:+3V3 真连通之后信号网 37 → 44,比布线器自己的走法差);`--v3-trunk` 才启用。 # 诊断开关(--3v3-plane / --3v3-skip)下 +3V3 不走线,也别构造。 if '--v3-trunk' in sys.argv and '+3V3' not in PLANE_NETS and '--3v3-skip' not in sys.argv: ts, tv, tdone, tvirt = v3_trunk(pads, segs, vias) segs += ts vias += tv jdone |= tdone nvirt = nvirt + tvirt print(u' +3V3 定点构造(U35 东侧旁路 + U13 东南进):%d 段 / %d 孔,WALL 落点 %d 个' % (len(ts), len(tv), len(tvirt))) 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[7] == '+3V3' and '--3v3-skip' in sys.argv: # 诊断开关,见 LATE_PLANE_NETS 处 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))) rv = dp_return_vias(pads, segs, vias, board_vias, logs) vias += rv print(u' D± 换层处 GND 回流孔:%d 颗' % len(rv)) if '--dp-only' in sys.argv: dp_via_limit(vias, logs) return segs, vias, logs skip = set(kel) | {('Rs6', '2')} | set(jdone) # 构造里已接地的脚(U32.13/19、C750/C747 地脚)不再单打孔 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 本地网 → 普通信号 → 电源(最后布,脚一直收过路费) # 2026-09-11 起多一段 EARLY_NETS(U32 本地网之后、普通信号之前) early = (set(EARLY_NETS) & rest) - u32_local if '--no-early' in sys.argv: # 对照:INA_ALERT 回到普通信号那一段 early = set() phases = [set(CHAIN_NETS) & rest, u32_local, early, set(), set(POWER_LATE) & rest] phases[3] = rest - phases[0] - phases[1] - phases[2] - phases[4] 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) dp_via_limit(vias, logs) 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, 'u20_remap': u20_remap_name(), 'segs': segs, 'vias': vias, 'logs': logs}, ensure_ascii=False)) report(segs, vias, logs) if '--push' in sys.argv: if json.load(io.open(CACHE, encoding='utf-8')).get('u20_remap'): sys.exit(u'!! 这份缓存是 U20 引脚重分配的试算(u20_remap),原理图 / 板上焊盘网名都没改,不许下发') # 默认仍拒绝下发不完整路由;`--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) # 记下「板上现在是哪一版」:过孔快照按它认布线孔(缓存可能已被新的服务器结果换掉、还没下发) io.open(ONBOARD, 'w', encoding='utf-8').write(io.open(CACHE, encoding='utf-8').read()) print(u' 板上版本已记到 %s' % os.path.basename(ONBOARD))