1875 行
108 KiB
Python
1875 行
108 KiB
Python
# -*- 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))
|