- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
1157 行
54 KiB
Python
1157 行
54 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""下板 PCB1 的 **I²C 干线改走 Inner2(L3)** —— 三层(Top / Bottom / Inner2)迷宫布线。
|
||
|
||
/c/Python314/python .claude/pcb1_i2c_l3.py # 算 + 校核,不动板(默认)
|
||
/c/Python314/python .claude/pcb1_i2c_l3.py --map # 只打 L3 的可走区域图
|
||
/c/Python314/python .claude/pcb1_i2c_l3.py --push # 拆旧长干线 -> 重算 -> 校核 -> 下发
|
||
/c/Python314/python .claude/pcb1_i2c_l3.py --net SDA # 只做一个网络
|
||
|
||
## 为什么单写一个文件,而不是给 `pcb1_route_sig.py` 加一层
|
||
|
||
引擎(`pcb2_route_sig.py`)的层数是写死的 2:`route_grid` 里 `LAY = [L_TOP, L_BOT]`、
|
||
图是 `2*N` 个节点、`Field.viafree = min(edt[TOP], edt[BOT])`。加第三层要动的不止层数 ——
|
||
**`viafree` 那一行会连带把内层平面算成"过孔不许打"**:贴片焊盘对内层透明、
|
||
过孔穿平面本来就有反焊盘,可 `viafree` 是「所有层 EDT 的最小值」,
|
||
把 `+12V` 平面塞进 `edt[16]` 之后,整块 +12V 底下一个孔都打不了。
|
||
⇒ 这里的场把 **L3 分成两张 EDT**:`edt[16]`(含平面,给走线用)与
|
||
`cu16`(只含真铜:通孔、过孔、别的网的 L3 走线,给**打孔**用)。
|
||
引擎那一版没有这个区分,硬加会踩这一条。
|
||
|
||
## 拓扑:为什么只有中桥那 13 mm 还在外层(§10.1 的 Inner2 分块图)
|
||
|
||
Inner2 上东西两半之间**只有一条隔断是不能割的**:`12V-SW+BR` 的中桥
|
||
(x 61.5~72.6,11.1 mm,喂南西块 3.6 A,`pcb1_pour.CUTS` 里三条断面都盯着它)。
|
||
横着割一刀 = 南西块断电,而 I²C 要过去就必须横着割 ⇒ **中桥只能外层跳过去**。
|
||
(上一轮 `pcb1_route_sig` 自己也撞出了同一个结论:板上现成的 `SDA` 就是
|
||
过孔 (61.00,49.30) ↔ 底层 ↔ 过孔 (73.90,51.20) 这么跳的。)
|
||
|
||
除此之外内层随便走:
|
||
- `+3V3` 两大块(`3V3-SW` 57.7×17.5、`3V3-SE` 更大)**只有 0.5 A**,
|
||
IPC-2221 内层 0.5 oz 下 0.40 mm 就够 —— 开一条 1 mm 的槽是「切掉 6%」而不是「切断」。
|
||
这里给它 `PEN_3V3` 的代价(不是障碍),布线器能绕就绕,绕不开就穿。
|
||
- `GND-FILL-L3` 是底填,L2 才是「不许割断」的那张完整地平面(§10.1)。
|
||
- `+12V` 三块一律**硬障碍**,一格都不进。
|
||
|
||
⚠️ 所以下发之后**必须重铺铜**:新的 L3 走线会让 `+3V3` / GND 底填按净距退让,
|
||
而 `pcb1_pour.py --verify` 是**回读实测铜**量断面的,能量出真的被啃窄了多少。
|
||
|
||
## 器件簇:哪些铜留着不动
|
||
|
||
"只挪跨板长干线"(用户 2026-09-06 拍板)。做法不是按长度删线段 —— 那样会把
|
||
簇内那些短接线也切碎 —— 而是划 `CLUSTERS` 里那几个框:**框内的 SDA/SCL 铜原样保留,
|
||
框外的一律拆掉**,然后按剩下的碎片重连。控制块内部(U20 ↔ U6 ↔ U7 ↔ R604/605 ↔ U23)
|
||
那一坨短线因此一根都不用重布。
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
|
||
import numpy as np # noqa: E402
|
||
from scipy import ndimage # noqa: E402
|
||
from scipy.sparse import coo_matrix # noqa: E402
|
||
from scipy.sparse.csgraph import dijkstra # noqa: E402
|
||
|
||
import pcb1_netlist as NL # noqa: E402
|
||
import pcb1_pour as PO # noqa: E402
|
||
import pcb1_route_sig as RS # noqa: E402
|
||
|
||
E = RS.E # 引擎(polish / _l_shapes / board_mask 从这里借)
|
||
|
||
#: 要挪进内层的网络。判据是「**跨板长跑**」:焊盘散在整块板上、在外层要横穿好几条通道。
|
||
#: `INA_ALERT` 的脚就挨着 I²C(六个 INA226 的 ALERT),形状一模一样。
|
||
#: ⚠️ `USB_DP`/`USB_DM` **不许进这个表** —— 90 Ω 差分以 L2 完整地平面为参考(§10.1),
|
||
#: 换到 Inner2 参考面就没了,阻抗模型整个作废。
|
||
#: ⚠️ `INA_ALERT` 形状和 I²C 一模一样(8 个焊盘跨 135 mm,脚就挨着 INA226 的 SDA/SCL),
|
||
#: **但 2026-09-06 试着加进来没跑通**:它板上的铜整片都在顶层、且集中在控制块那一带,
|
||
#: 六条连线里有四条报「L2 源0 / L16 源0」—— 主碎片周围一个合法孔位都没有,扎不下去。
|
||
#: 要挪它得先把控制块那一带的顶层拆开,不是改这一行的事。
|
||
NETS = ('SCL', 'SDA', 'BUCK_PG')
|
||
# ── 侧键模拟线 POGO3_PWR / POGO3_VDN / POGO4_VDN / POGO5_PWR / POGO5_VUP(第二十六轮加)
|
||
# 2026-09-06 随 74HC595 + 按键 MOS 整窝挪到载板(§26.23)之后,下板上不再有 POGO* 网络,
|
||
# 已从候选表拿掉;它们在 Inner2 上的铜由 pcb_prune_nets.py 一并清掉了。
|
||
L_TOP, L_BOT, L3 = 1, 2, 16
|
||
LAYS = (L_TOP, L_BOT, L3)
|
||
|
||
GRID = 0.1
|
||
CLR = 0.20
|
||
WIRE = 0.20 # I²C 线宽(`pcb1_rules` 的 SIG 类默认)
|
||
VIA_D, VIA_H = 0.61, 0.305
|
||
HOLE_CLR = 0.30
|
||
VIA_COST = 2.5 # 换一次层折算成多少 mm 路程
|
||
BW, BH = 160.0, 125.0
|
||
BBOX_PAD = 14.0
|
||
VERBOSE = ('--v' in sys.argv)
|
||
QERR = GRID * math.sqrt(2) / 2
|
||
|
||
#: 层代价。外层贵 3 倍 —— 短跳线(中桥那 13 mm,代价 39)仍然比绕远划算,
|
||
#: 而几十毫米的长跑一定落到 L3 上。改这个数就是改「多长才值得挪进内层」。
|
||
LAY_COST = {L_TOP: 3.0, L_BOT: 3.0, L3: 1.0}
|
||
#: L3 上穿 `+3V3` 平面的代价倍数。**这个数决定的不是长度,是平面会不会被切出孤岛**:
|
||
#: 1.8 的时候 SCL 沿 y=50 在 `3V3-SW` 里横跑了 12.7 mm、SDA 又从东边界斜插进来,
|
||
#: 两根线和平面东边界围出一块孤岛(里面有 R415.2 的 +3V3 焊盘,见 `plane_report`)。
|
||
#: 4.0 之后干线改走北边的 GND 底填,只在器件附近**扎一个盲端**进平面 ——
|
||
#: 盲端不切平面,连通域数不变。堵孤岛重绕是不收敛的(越堵越大,实测四轮)。
|
||
PEN_3V3 = 4.0
|
||
|
||
#: 上一轮把电源面切出孤岛的那些格(L3 上当硬障碍)。`main()` 每轮往里加,见 `plane_report`。
|
||
BLOCK = []
|
||
|
||
#: 器件簇:框内的旧 I²C 铜保留,框外拆掉重来。
|
||
CLUSTERS = {
|
||
'CTRL': (76.0, 14.0, 116.0, 60.0), # U20 / U6 / U7 / R604,R605 / U23
|
||
'U8': (11.5, 45.5, 18.5, 52.5), # CH1 INA226
|
||
'U11': (43.5, 45.5, 50.5, 53.5), # CH4 INA226
|
||
'U9': (49.5, 73.5, 56.5, 80.0), # CH2 INA226
|
||
'U10': (137.5, 71.5, 145.0, 80.0), # CH3 INA226
|
||
'U12': (121.5, 45.5, 129.5, 52.5), # CH5 INA226
|
||
'U13': (3.0, 90.5, 11.0, 100.0), # 输入侧 INA226
|
||
'J24': (30.0, 60.5, 38.5, 67.0), # 排针(通孔针,L3 上直接落脚)
|
||
# 五路 Buck 控制器:`BUCK_PG` 在 U*.14 上。**不划框的话整条出线短线也被拆掉**,
|
||
# 而那几个脚在 0.5 mm 脚距的 QFN 上,`entry_stub` 重建不回来 ⇒ 整个网络断成 6 片
|
||
# (2026-09-06 实测,连通性保险当场拦下)。框只罩住芯片本体 + 0.6 mm。
|
||
'U1': (14.4, 25.1, 19.2, 29.9),
|
||
'U2': (48.7, 95.6, 53.5, 100.4),
|
||
'U3': (137.1, 95.6, 141.9, 100.4),
|
||
'U4': (46.1, 25.1, 50.9, 29.9),
|
||
'U5': (125.6, 25.1, 130.4, 29.9),
|
||
}
|
||
|
||
|
||
# ══════════════════ 几何小工具 ══════════════════
|
||
def poly_mask(poly, XX, YY):
|
||
u"""格心在多边形内(偶奇判据,矢量化)。`PO.inside` 的 numpy 版 ——
|
||
+12V 三块加起来 200 万个格心,逐点调 `PO.inside` 要几十秒。"""
|
||
inside = np.zeros(XX.shape, bool)
|
||
n = len(poly)
|
||
for i in range(n):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % n]
|
||
if y1 == y2:
|
||
continue
|
||
cond = (y1 > YY) != (y2 > YY)
|
||
with np.errstate(invalid='ignore', divide='ignore'):
|
||
xint = (x2 - x1) * (YY - y1) / (y2 - y1) + x1
|
||
inside ^= cond & (XX < xint)
|
||
return inside
|
||
|
||
|
||
def seg_dist2(XX, YY, x0, y0, x1, y1):
|
||
u"""格心到线段的距离平方。"""
|
||
dx, dy = x1 - x0, y1 - y0
|
||
L2 = dx * dx + dy * dy
|
||
if L2 < 1e-12:
|
||
return (XX - x0) ** 2 + (YY - y0) ** 2
|
||
t = np.clip(((XX - x0) * dx + (YY - y0) * dy) / L2, 0.0, 1.0)
|
||
return (XX - x0 - t * dx) ** 2 + (YY - y0 - t * dy) ** 2
|
||
|
||
|
||
def pad_layers(p):
|
||
return LAYS if p['th'] else (p['plane'],)
|
||
|
||
|
||
def in_box(box, x, y):
|
||
return box[0] <= x <= box[2] and box[1] <= y <= box[3]
|
||
|
||
|
||
# ══════════════════ 板上的铜:哪些留、哪些拆 ══════════════════
|
||
def split_old(pads, vias, lines):
|
||
u"""把 I²C 的旧铜分成「簇内保留」与「簇外拆掉」两堆。
|
||
|
||
判据是**整条线段/整个过孔都落在同一个簇框里**才留 —— 一端在框内一端在框外的
|
||
就是要拆的干线。
|
||
"""
|
||
keep_l, drop_l, keep_v, drop_v = [], [], [], []
|
||
for i, l in enumerate(lines):
|
||
if l['net'] not in NETS:
|
||
continue
|
||
# L3 上的 I²C 铜一律拆:板上原来一根都没有,有的全是本脚本上一轮画的,
|
||
# 不拆的话重跑不幂等(簇框里会一轮一轮攒下旧干线的尾巴)。
|
||
hit = (l['lay'] != L3) and [k for k, b in CLUSTERS.items()
|
||
if in_box(b, l['x0'], l['y0']) and in_box(b, l['x1'], l['y1'])]
|
||
(keep_l if hit else drop_l).append(i)
|
||
for i, v in enumerate(vias):
|
||
if v['net'] not in NETS:
|
||
continue
|
||
hit = [k for k, b in CLUSTERS.items() if in_box(b, v['x'], v['y'])]
|
||
(keep_v if hit else drop_v).append(i)
|
||
return keep_l, drop_l, keep_v, drop_v
|
||
|
||
|
||
# ══════════════════ 碎片(同一个网络里已经连在一起的铜) ══════════════════
|
||
class DSU(object):
|
||
def __init__(self, n):
|
||
self.p = list(range(n))
|
||
|
||
def find(self, a):
|
||
while self.p[a] != a:
|
||
self.p[a] = self.p[self.p[a]]
|
||
a = self.p[a]
|
||
return a
|
||
|
||
def union(self, a, b):
|
||
ra, rb = self.find(a), self.find(b)
|
||
if ra != rb:
|
||
self.p[ra] = rb
|
||
|
||
|
||
def _shape(kind, obj):
|
||
u"""统一成 ('rect'|'seg'|'via', ..., 半径, 层元组),方便算距离。"""
|
||
if kind == 'pad':
|
||
return ('rect', obj['x'], obj['y'], obj['hw'], obj['hh'], 0.0,
|
||
pad_layers(obj))
|
||
if kind == 'via':
|
||
return ('via', obj['x'], obj['y'], 0.0, 0.0, obj['r'], LAYS)
|
||
return ('seg', obj['x0'], obj['y0'], obj['x1'], obj['y1'], obj['w'] / 2.0,
|
||
(obj['lay'],))
|
||
|
||
|
||
def _near(a, b, tol=1e-6):
|
||
u"""两个形状在**共同的层**上是否相碰。"""
|
||
if not (set(a[6]) & set(b[6])):
|
||
return False
|
||
def as_seg(s):
|
||
if s[0] == 'seg':
|
||
return (s[1], s[2]), (s[3], s[4]), s[5]
|
||
return (s[1], s[2]), (s[1], s[2]), s[5]
|
||
if a[0] == 'rect' or b[0] == 'rect':
|
||
r, o = (a, b) if a[0] == 'rect' else (b, a)
|
||
p, q, rad = as_seg(o)
|
||
return RS.D.seg_rect(p, q, r[1], r[2], r[3], r[4]) <= rad + tol
|
||
(p0, p1, ra), (q0, q1, rb) = as_seg(a), as_seg(b)
|
||
return RS.D.seg_seg(p0, p1, q0, q1) <= ra + rb + tol
|
||
|
||
|
||
def frags(net, pads, vias, lines):
|
||
u"""该网络现有的铜 -> 若干连通碎片。返回 [[形状, ...], ...]。"""
|
||
items = [_shape('pad', p) for p in pads if p['net'] == net]
|
||
items += [_shape('via', v) for v in vias if v['net'] == net]
|
||
items += [_shape('seg', l) for l in lines if l['net'] == net]
|
||
d = DSU(len(items))
|
||
for i in range(len(items)):
|
||
for j in range(i + 1, len(items)):
|
||
if _near(items[i], items[j]):
|
||
d.union(i, j)
|
||
out = {}
|
||
for i, s in enumerate(items):
|
||
out.setdefault(d.find(i), []).append(s)
|
||
return list(out.values())
|
||
|
||
|
||
# ══════════════════ 三层障碍场 ══════════════════
|
||
class Field(object):
|
||
u"""一个搜索窗口里的三层障碍距离场。
|
||
|
||
`edt[L]` = 到最近**异网铜**的距离。L3 那一层额外带一张 `cu16`:
|
||
只含真铜、不含铺铜平面,**给打孔判据用**(见文件头)。
|
||
"""
|
||
|
||
RIM = 1.4
|
||
|
||
def __init__(self, bbox, net, pads, vias, lines, extra=()):
|
||
x0, y0, x1, y1 = bbox
|
||
r = self.RIM
|
||
self.g = GRID
|
||
self.qerr = QERR
|
||
self.x0 = math.floor((x0 - r) / GRID) * GRID
|
||
self.y0 = math.floor((y0 - r) / GRID) * GRID
|
||
self.nx = int(round((x1 + r - self.x0) / GRID)) + 2
|
||
self.ny = int(round((y1 + r - self.y0) / GRID)) + 2
|
||
xs = self.x0 + np.arange(self.nx) * GRID
|
||
ys = self.y0 + np.arange(self.ny) * GRID
|
||
XX, YY = np.meshgrid(xs, ys)
|
||
self.XX, self.YY = XX, YY
|
||
|
||
occ = {L: np.zeros(XX.shape, bool) for L in LAYS}
|
||
cu16 = np.zeros(XX.shape, bool) # L3 上的真铜(不含平面)
|
||
hole = np.zeros(XX.shape, bool)
|
||
own = {L: np.zeros(XX.shape, bool) for L in LAYS} # 本网铜:放行
|
||
|
||
# ⚠️ **每个图元只在自己的包围盒里算**。第一版对着整个窗口做 numpy 运算,
|
||
# 5000 个图元 × 30 万格 = 十几亿次浮点,一条连线要几分钟;
|
||
# 切窗口之后同样的场是几十毫秒。
|
||
def _win(xa, ya, xb, yb):
|
||
i0 = int(math.floor((xa - self.x0) / GRID))
|
||
i1 = int(math.ceil((xb - self.x0) / GRID))
|
||
j0 = int(math.floor((ya - self.y0) / GRID))
|
||
j1 = int(math.ceil((yb - self.y0) / GRID))
|
||
i0 = max(0, i0); j0 = max(0, j0)
|
||
i1 = min(self.nx - 1, i1); j1 = min(self.ny - 1, j1)
|
||
if i1 < i0 or j1 < j0:
|
||
return None
|
||
return i0, i1, j0, j1
|
||
|
||
def mark(arr, kind, a, b, c, d_, rad):
|
||
if kind == 'rect':
|
||
w = _win(a - c - rad, b - d_ - rad, a + c + rad, b + d_ + rad)
|
||
if w is None:
|
||
return
|
||
i0, i1, j0, j1 = w
|
||
sx, sy = XX[j0:j1 + 1, i0:i1 + 1], YY[j0:j1 + 1, i0:i1 + 1]
|
||
m = (np.abs(sx - a) <= c + rad) & (np.abs(sy - b) <= d_ + rad)
|
||
elif kind == 'disc':
|
||
w = _win(a - rad, b - rad, a + rad, b + rad)
|
||
if w is None:
|
||
return
|
||
i0, i1, j0, j1 = w
|
||
sx, sy = XX[j0:j1 + 1, i0:i1 + 1], YY[j0:j1 + 1, i0:i1 + 1]
|
||
m = (sx - a) ** 2 + (sy - b) ** 2 <= rad * rad
|
||
else:
|
||
w = _win(min(a, c) - rad, min(b, d_) - rad,
|
||
max(a, c) + rad, max(b, d_) + rad)
|
||
if w is None:
|
||
return
|
||
i0, i1, j0, j1 = w
|
||
sx, sy = XX[j0:j1 + 1, i0:i1 + 1], YY[j0:j1 + 1, i0:i1 + 1]
|
||
m = seg_dist2(sx, sy, a, b, c, d_) <= rad * rad
|
||
arr[j0:j1 + 1, i0:i1 + 1] |= m
|
||
|
||
for p in pads:
|
||
tgt = own if p['net'] == net else occ
|
||
for L in pad_layers(p):
|
||
if p['net'] == net:
|
||
mark(own[L], 'rect', p['x'], p['y'],
|
||
max(0.0, p['hw'] - WIRE / 2.0),
|
||
max(0.0, p['hh'] - WIRE / 2.0), GRID / 2.0)
|
||
else:
|
||
mark(occ[L], 'rect', p['x'], p['y'], p['hw'], p['hh'], QERR)
|
||
if p['th'] and p['net'] != net:
|
||
mark(cu16, 'rect', p['x'], p['y'], p['hw'], p['hh'], QERR)
|
||
mark(hole, 'disc', p['x'], p['y'], 0, 0,
|
||
max(p['hw'], p['hh']) + QERR)
|
||
for v in vias:
|
||
tgt = own if v['net'] == net else occ
|
||
for L in LAYS:
|
||
mark(tgt[L], 'disc', v['x'], v['y'], 0, 0,
|
||
(v['r'] - WIRE / 2.0 + GRID / 2.0) if v['net'] == net
|
||
else (v['r'] + QERR))
|
||
if v['net'] != net:
|
||
mark(cu16, 'disc', v['x'], v['y'], 0, 0, v['r'] + QERR)
|
||
mark(hole, 'disc', v['x'], v['y'], 0, 0, VIA_H / 2.0 + QERR)
|
||
for l in lines:
|
||
tgt = own if l['net'] == net else occ
|
||
mark(tgt[l['lay']], 'seg', l['x0'], l['y0'], l['x1'], l['y1'],
|
||
(max(0.0, l['w'] / 2.0 - WIRE / 2.0) + GRID / 2.0)
|
||
if l['net'] == net else (l['w'] / 2.0 + QERR))
|
||
if l['lay'] == L3 and l['net'] != net:
|
||
mark(cu16, 'seg', l['x0'], l['y0'], l['x1'], l['y1'],
|
||
l['w'] / 2.0 + QERR)
|
||
# ⚠️ `extra` 里**必须带网络**。第一版不带,于是同一个网络自己刚布好的
|
||
# 前几条路径被当成障碍标进 `occ`,下一条连线的**源掩膜整个变成空的**
|
||
# (实测「L1 源0/汇36 L16 源0/汇0」,看上去像"布不通",其实是把自己挡死了)。
|
||
for kind, args, enet in extra:
|
||
same = (enet == net)
|
||
if kind == 'seg':
|
||
x0_, y0_, x1_, y1_, w_, lay_ = args
|
||
if same:
|
||
mark(own[lay_], 'seg', x0_, y0_, x1_, y1_,
|
||
max(0.0, w_ / 2.0 - WIRE / 2.0) + GRID / 2.0)
|
||
else:
|
||
mark(occ[lay_], 'seg', x0_, y0_, x1_, y1_, w_ / 2.0 + QERR)
|
||
if lay_ == L3:
|
||
mark(cu16, 'seg', x0_, y0_, x1_, y1_, w_ / 2.0 + QERR)
|
||
else:
|
||
vx, vy = args
|
||
for L in LAYS:
|
||
mark(own[L] if same else occ[L], 'disc', vx, vy, 0, 0,
|
||
(VIA_D / 2.0 - WIRE / 2.0 + GRID / 2.0) if same
|
||
else (VIA_D / 2.0 + QERR))
|
||
if not same:
|
||
mark(cu16, 'disc', vx, vy, 0, 0, VIA_D / 2.0 + QERR)
|
||
mark(hole, 'disc', vx, vy, 0, 0, VIA_H / 2.0 + QERR)
|
||
|
||
def _hits(poly):
|
||
u"""多边形包围盒和本窗口有没有交集(不交就整块跳过)。"""
|
||
px = [q[0] for q in poly]; py = [q[1] for q in poly]
|
||
return not (max(px) < xs[0] or min(px) > xs[-1]
|
||
or max(py) < ys[0] or min(py) > ys[-1])
|
||
|
||
# ── 外层铺铜(顶层 8 A 宽铜 / 底层 VOn_P 小岛)──
|
||
for rr in PO.REGIONS:
|
||
if rr['layer'] in (L_TOP, L_BOT) and rr['net'] != net and _hits(rr['poly']):
|
||
occ[rr['layer']] |= poly_mask(rr['poly'], XX, YY)
|
||
# ── Inner2:+12V 三块是硬障碍;+3V3 只加代价 ──
|
||
pen = np.ones(XX.shape, np.float32)
|
||
for rr in PO.REGIONS:
|
||
if rr['layer'] != PO.L_PWR or not _hits(rr['poly']):
|
||
continue
|
||
if rr['net'] == '+12V':
|
||
occ[L3] |= poly_mask(rr['poly'], XX, YY)
|
||
elif rr['net'] == '+3V3':
|
||
pen[poly_mask(rr['poly'], XX, YY)] = PEN_3V3
|
||
self.pen = pen
|
||
|
||
# ── 上一轮把电源面切出孤岛的那些格:这一轮 L3 上当硬障碍(`repair()`)──
|
||
for bx0, by0, bx1, by1 in BLOCK:
|
||
mark(occ[L3], 'rect', (bx0 + bx1) / 2.0, (by0 + by1) / 2.0,
|
||
(bx1 - bx0) / 2.0, (by1 - by0) / 2.0, QERR)
|
||
|
||
# ── 板外 + 天线区 + 窗口外圈墙 ──
|
||
out = ~E.board_mask(XX, YY)
|
||
ax0, ay0, ax1, ay1 = RS.ANT
|
||
out |= (XX >= ax0 - CLR) & (XX <= ax1 + CLR) & \
|
||
(YY >= ay0 - CLR) & (YY <= ay1 + CLR)
|
||
nb = max(1, int(round(r / GRID)))
|
||
wall = np.zeros(XX.shape, bool)
|
||
wall[:nb, :] = True; wall[-nb:, :] = True
|
||
wall[:, :nb] = True; wall[:, -nb:] = True
|
||
for L in LAYS:
|
||
occ[L] |= out | wall
|
||
own[L] &= ~(out | wall)
|
||
cu16 |= out | wall
|
||
hole |= out | wall
|
||
|
||
# ⚠️ **本网铜的放行不能按「格心落在本网铜里」判**。第一版是
|
||
# `free = edt >= need | own`,于是 0.2 mm 的线可以贴着自家焊盘的**边**走:
|
||
# U8.5 的焊盘只有 0.28 宽,线心压在它西边缘 ⇒ 线的西侧探出去 0.1,
|
||
# 离邻脚 U8.4 只剩 0.06。真正的判据是「本网铜**完全盖住**这根线」,
|
||
# 所以 `own` 是**按半线宽内缩之后**标的(上面每一处 `- WIRE/2`),
|
||
# 留 `GRID/2` 是格心对不上中心线的容差。
|
||
# 过孔不吃这个放行 —— `edt` 本来就不含本网铜,`via_ok` 直接按真净距判。
|
||
self.own = own
|
||
self.edt = {L: ndimage.distance_transform_edt(
|
||
~occ[L], sampling=GRID).astype(np.float32) for L in LAYS}
|
||
self.cu16 = ndimage.distance_transform_edt(
|
||
~cu16, sampling=GRID).astype(np.float32)
|
||
self.holefree = ndimage.distance_transform_edt(
|
||
~hole, sampling=GRID).astype(np.float32)
|
||
|
||
# ── 判据 ──
|
||
def free(self, lay, w=WIRE):
|
||
return (self.edt[lay] >= CLR + w / 2.0 + self.qerr) | self.own[lay]
|
||
|
||
def via_ok(self):
|
||
u"""过孔位:外层两层要让开异网铜,L3 只看**真铜**(平面自己会挖反焊盘),
|
||
再加不分网络的钻孔间距。"""
|
||
# ⚠️ **过孔不吃 `own` 那条放行**。`edt` 里本来就不含本网铜,所以这就是真净距。
|
||
# 第一版写成 `(edt >= need) | own`,于是 Φ0.61 的孔被按在 0.28 宽的
|
||
# INA226 焊盘上 —— "在 L3 上走到焊盘正上方再落下去"正是最短路,
|
||
# 实测「孔-焊盘 SDA(53.00,75.10) vs U9.5 净距 −0.033」,一口气 30 条。
|
||
need = CLR + VIA_D / 2.0 + self.qerr
|
||
m = (self.edt[L_TOP] >= need) & (self.edt[L_BOT] >= need) \
|
||
& (self.cu16 >= need)
|
||
return m & (self.holefree >= VIA_H / 2.0 + HOLE_CLR + self.qerr)
|
||
|
||
def _idx(self, x, y):
|
||
return (int(round((x - self.x0) / self.g)),
|
||
int(round((y - self.y0) / self.g)))
|
||
|
||
def clear_seg(self, x0, y0, x1, y1, lay, w):
|
||
need = CLR + w / 2.0 + self.qerr
|
||
L = math.hypot(x1 - x0, y1 - y0)
|
||
n = max(2, int(L / (self.g / 2.0)) + 1)
|
||
need += (L / (n - 1)) / 2.0 if n > 1 else 0.0
|
||
for k in range(n):
|
||
t = k / float(n - 1)
|
||
i, j = self._idx(x0 + t * (x1 - x0), y0 + t * (y1 - y0))
|
||
if not (0 <= i < self.nx and 0 <= j < self.ny):
|
||
return False
|
||
if self.edt[lay][j, i] < need and not self.own[lay][j, i]:
|
||
return False
|
||
return True
|
||
|
||
def mask_of(self, shapes, w=WIRE):
|
||
u"""一组形状 -> {层: 布尔数组}(该形状盖住的、且可走的格)。"""
|
||
m = {L: np.zeros((self.ny, self.nx), bool) for L in LAYS}
|
||
for s in shapes:
|
||
if s[0] == 'rect':
|
||
hit = (np.abs(self.XX - s[1]) <= s[3]) & \
|
||
(np.abs(self.YY - s[2]) <= s[4])
|
||
elif s[0] == 'via':
|
||
hit = (self.XX - s[1]) ** 2 + (self.YY - s[2]) ** 2 <= s[5] ** 2
|
||
else:
|
||
hit = seg_dist2(self.XX, self.YY, s[1], s[2], s[3], s[4]) \
|
||
<= s[5] ** 2
|
||
for L in s[6]:
|
||
m[L] |= hit
|
||
for L in LAYS:
|
||
m[L] &= self.free(L, w)
|
||
return m
|
||
|
||
|
||
# ══════════════════ 三层 Dijkstra ══════════════════
|
||
def route(f, src, tgt, w=WIRE):
|
||
u"""多源多汇最短路。返回 [(i, j, 层), ...] 或 None。
|
||
|
||
代价 = 路程 × 层代价 × L3 上的 `+3V3` 惩罚 + 每次换层 `VIA_COST`。
|
||
"""
|
||
nx, ny, g = f.nx, f.ny, f.g
|
||
N = nx * ny
|
||
ok = [f.free(L, w) for L in LAYS]
|
||
rows, cols, vals = [], [], []
|
||
for li, L in enumerate(LAYS):
|
||
base = (f.pen if L == L3 else np.ones_like(f.pen)) * LAY_COST[L]
|
||
flat_ok = ok[li]
|
||
for dx, dy in ((1, 0), (0, 1), (1, 1), (1, -1)):
|
||
a = flat_ok[max(0, -dy):ny - max(0, dy),
|
||
max(0, -dx):nx - max(0, dx)]
|
||
b = flat_ok[max(0, dy):ny - max(0, -dy),
|
||
max(0, dx):nx - max(0, -dx)]
|
||
good = a & b
|
||
if not good.any():
|
||
continue
|
||
jj, ii = np.nonzero(good)
|
||
j0 = jj + max(0, -dy); i0 = ii + max(0, -dx)
|
||
j1 = jj + max(0, dy); i1 = ii + max(0, dx)
|
||
ca = base[j0, i0]; cb = base[j1, i1]
|
||
d = g * (math.sqrt(2) if dx and dy else 1.0)
|
||
rows.append(j0.astype(np.int64) * nx + i0 + li * N)
|
||
cols.append(j1.astype(np.int64) * nx + i1 + li * N)
|
||
vals.append((d * (ca + cb) / 2.0).astype(np.float32))
|
||
# ⚠️ **不能写成 `via_ok() & ok[0] & ok[1] & ok[2]`**。`ok[2]` 是「L3 上能不能走线」,
|
||
# 里面含 +12V 平面 —— 那样整块 +12V 底下一个孔都打不了,而穿平面的孔本来就有反焊盘。
|
||
# 落脚点合法与否由 `via_ok()`(外层铜 + L3 真铜 + 钻孔间距)单独判;
|
||
# 孔落在不能走线的格上也没关系,那个格没有任何移动边,路径在那儿待不住。
|
||
vm = f.via_ok()
|
||
jj, ii = np.nonzero(vm)
|
||
if len(jj):
|
||
idx = jj.astype(np.int64) * nx + ii
|
||
for a in range(len(LAYS)):
|
||
for b in range(a + 1, len(LAYS)):
|
||
rows.append(idx + a * N)
|
||
cols.append(idx + b * N)
|
||
vals.append(np.full(len(idx), VIA_COST, np.float32))
|
||
if not rows:
|
||
return None
|
||
G = coo_matrix((np.concatenate(vals),
|
||
(np.concatenate(rows), np.concatenate(cols))),
|
||
shape=(len(LAYS) * N, len(LAYS) * N)).tocsr()
|
||
|
||
def flat(mask):
|
||
out = []
|
||
for li, L in enumerate(LAYS):
|
||
jj_, ii_ = np.nonzero(mask[L] & ok[li])
|
||
out.append(jj_.astype(np.int64) * nx + ii_ + li * N)
|
||
return np.concatenate(out) if out else np.array([], np.int64)
|
||
|
||
si, ti = flat(src), flat(tgt)
|
||
if not len(si) or not len(ti):
|
||
return None
|
||
D, pred, _s = dijkstra(G, directed=False, indices=si, min_only=True,
|
||
return_predecessors=True)
|
||
d = D[ti]
|
||
k = int(np.argmin(d))
|
||
if not np.isfinite(d[k]):
|
||
return None
|
||
path, cur = [], int(ti[k])
|
||
while cur >= 0:
|
||
li, rem = divmod(cur, N)
|
||
j, i = divmod(rem, nx)
|
||
path.append((i, j, LAYS[li]))
|
||
cur = int(pred[cur])
|
||
path.reverse()
|
||
return path
|
||
|
||
|
||
# ══════════════════ 细间距焊盘的直出段 ══════════════════
|
||
def stub_lines(net, pads, vias, lines):
|
||
u"""给**还没有铜接着**的 I²C 焊盘补一段「直出焊盘外」的短线,路由器从这一截接手。
|
||
|
||
INA226 是 0.5 mm 脚距:U8.4/U8.5 中心只隔 0.5、焊盘边缘之间约 0.22 mm。
|
||
直接把 0.2 mm 的线或 Φ0.61 的过孔按到焊盘上,**对邻脚必然违规**
|
||
(实测「孔-焊盘 SCL(15.10,49.90) vs U8.4 净距 −0.145」)。
|
||
直出段由引擎的 `entry_stub` 定方向(长轴优先、否则背离元件形心)。
|
||
"""
|
||
epads = [dict(p, lay=(12 if p['th'] else p['plane'])) for p in pads]
|
||
obst = E.shapes_of(epads, vias, lines) + RS.pour_rects()
|
||
mine_seg = [_shape('seg', l) for l in lines if l['net'] == net]
|
||
mine_via = [_shape('via', v) for v in vias if v['net'] == net]
|
||
out = []
|
||
for p, q in zip(pads, epads):
|
||
if p['net'] != net or p['th']:
|
||
continue
|
||
sp = _shape('pad', p)
|
||
if any(_near(sp, o) for o in mine_seg + mine_via):
|
||
continue # 簇内那些还留着的铜已经接上了
|
||
r = E.entry_stub(q, epads, obst, net, WIRE)
|
||
if not r:
|
||
continue
|
||
(ex, ey), ws = r
|
||
out.append(dict(net=net, lay=p['plane'], x0=p['x'], y0=p['y'],
|
||
x1=ex, y1=ey, w=ws))
|
||
return out
|
||
|
||
|
||
# ══════════════════ 规划:碎片 -> Prim -> 路径 ══════════════════
|
||
def _bbox(shapes):
|
||
xs, ys = [], []
|
||
for s in shapes:
|
||
if s[0] == 'rect':
|
||
xs += [s[1] - s[3], s[1] + s[3]]; ys += [s[2] - s[4], s[2] + s[4]]
|
||
elif s[0] == 'via':
|
||
xs += [s[1] - s[5], s[1] + s[5]]; ys += [s[2] - s[5], s[2] + s[5]]
|
||
else:
|
||
xs += [s[1] - s[5], s[3] + s[5]]; ys += [s[2] - s[5], s[4] + s[5]]
|
||
return min(xs), min(ys), max(xs), max(ys)
|
||
|
||
|
||
def _cen(s):
|
||
return ((s[1] + s[3]) / 2.0, (s[2] + s[4]) / 2.0) if s[0] == 'seg' \
|
||
else (s[1], s[2])
|
||
|
||
|
||
def _pair_dist(fa, fb):
|
||
u"""两个碎片之间最近的一对形状(按形状中心的直线距离,够用)。"""
|
||
best = (1e9, None, None)
|
||
for a in fa:
|
||
ax, ay = _cen(a)
|
||
for b in fb:
|
||
bx, by = _cen(b)
|
||
d = math.hypot(ax - bx, ay - by)
|
||
if d < best[0]:
|
||
best = (d, a, b)
|
||
return best
|
||
|
||
|
||
def plan_net(net, pads, vias, lines, extra):
|
||
u"""把一个网络的碎片连成一片。返回 ([路径, ...], 剩余碎片数)。"""
|
||
fr = frags(net, pads, vias, lines)
|
||
print(u' %s:%d 个碎片,要补 %d 条' % (net, len(fr), len(fr) - 1))
|
||
if len(fr) <= 1:
|
||
return [], 0
|
||
fr.sort(key=len, reverse=True)
|
||
tree, rest = list(fr[0]), fr[1:]
|
||
paths, extra = [], list(extra)
|
||
while rest:
|
||
best = (1e9, None, None, None)
|
||
for k, f_ in enumerate(rest):
|
||
d, sa, sb = _pair_dist(tree, f_)
|
||
if d < best[0]:
|
||
best = (d, k, sa, sb)
|
||
_d, k, sa, sb = best
|
||
tgt_frag = rest[k]
|
||
x0, y0, x1, y1 = _bbox([sa, sb])
|
||
for s in tgt_frag:
|
||
bx0, by0, bx1, by1 = _bbox([s])
|
||
x0 = min(x0, bx0); y0 = min(y0, by0)
|
||
x1 = max(x1, bx1); y1 = max(y1, by1)
|
||
bbox = (max(0.0, x0 - BBOX_PAD), max(0.0, y0 - BBOX_PAD),
|
||
min(BW, x1 + BBOX_PAD), min(BH, y1 + BBOX_PAD))
|
||
f = Field(bbox, net, pads, vias, lines, extra)
|
||
near_tree = [s for s in tree
|
||
if not (_bbox([s])[2] < bbox[0] or _bbox([s])[0] > bbox[2]
|
||
or _bbox([s])[3] < bbox[1] or _bbox([s])[1] > bbox[3])]
|
||
cells = route(f, f.mask_of(near_tree), f.mask_of(tgt_frag))
|
||
if cells is None:
|
||
sm, tm = f.mask_of(near_tree), f.mask_of(tgt_frag)
|
||
det = u' '.join(u'L%d 源%d/汇%d' % (L, int(sm[L].sum()),
|
||
int(tm[L].sum())) for L in LAYS)
|
||
print(u' ✗ %s: (%.2f,%.2f) -> (%.2f,%.2f) 布不通'
|
||
u'(窗口 %.1f,%.1f - %.1f,%.1f;%s)'
|
||
% ((net,) + _cen(sa) + _cen(sb)
|
||
+ (bbox[0], bbox[1], bbox[2], bbox[3], det)))
|
||
rest.pop(k)
|
||
continue
|
||
chunks = E.polish(f, cells, WIRE)
|
||
path = []
|
||
for lay, xy in chunks:
|
||
for x, y in xy:
|
||
path.append((round(x, 4), round(y, 4), lay, WIRE))
|
||
# ⚠️ **最后一点上的过孔必须补一个重复点**,否则 `expand()` 会静默丢掉它
|
||
# (引擎 `_end_via` 的注释:净距校核和 DRC 都抓不到)。这里的路径几乎都是
|
||
# 「L3 走到焊盘正上方再落下去」,最后一段是单点 chunk,全都踩这一条:
|
||
# 实测末段被画在 L3 上、过孔没有,`frags` 报「U8.4 还是单独一片」。
|
||
path = E._end_via(path)
|
||
paths.append(path)
|
||
if VERBOSE:
|
||
L = sum(math.hypot(path[i + 1][0] - path[i][0],
|
||
path[i + 1][1] - path[i][1])
|
||
for i in range(len(path) - 1))
|
||
lays = sorted(set(q[2] for q in path))
|
||
print(u' ✓ (%.2f,%.2f) -> (%.2f,%.2f) %.1f mm 层%s'
|
||
% (path[0][0], path[0][1], path[-1][0], path[-1][1], L, lays))
|
||
segs, vs = E.expand(path)
|
||
for x0_, y0_, x1_, y1_, lay, w in segs:
|
||
extra.append(('seg', (x0_, y0_, x1_, y1_, w, lay), net))
|
||
for vx, vy in vs:
|
||
extra.append(('via', (vx, vy), net))
|
||
tree += tgt_frag + [('seg', s[0], s[1], s[2], s[3], s[5] / 2.0, (s[4],))
|
||
for s in segs]
|
||
tree += [('via', vx, vy, 0.0, 0.0, VIA_D / 2.0, LAYS) for vx, vy in vs]
|
||
rest.pop(k)
|
||
return paths, 0
|
||
|
||
|
||
# ══════════════════ 45° 化(全板不许直角走线) ══════════════════
|
||
def l3_pwr_pads():
|
||
u"""Inner2 上的 `+12V` 三块 -> 一串"假焊盘",好让引擎的 `chamfer_routes`
|
||
也知道那里不能切进去(`seg_slack` 只认焊盘 / 过孔 / 走线三样,不认铺铜)。
|
||
|
||
三块都是**直角多边形**,所以按 y 扫描线切成横条是精确的,不是栅格近似。
|
||
"""
|
||
out = []
|
||
for rr in PO.REGIONS:
|
||
if rr['layer'] != PO.L_PWR or rr['net'] != '+12V':
|
||
continue
|
||
poly = rr['poly']
|
||
ys = sorted(set(q[1] for q in poly))
|
||
for ya, yb in zip(ys, ys[1:]):
|
||
ymid = (ya + yb) / 2.0
|
||
xs = []
|
||
for i in range(len(poly)):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % len(poly)]
|
||
if (y1 > ymid) != (y2 > ymid):
|
||
xs.append((x2 - x1) * (ymid - y1) / (y2 - y1) + x1)
|
||
xs.sort()
|
||
for xa, xb in zip(xs[0::2], xs[1::2]):
|
||
out.append(dict(net=rr['net'], x=(xa + xb) / 2.0,
|
||
y=(ya + yb) / 2.0, hw=(xb - xa) / 2.0,
|
||
hh=(yb - ya) / 2.0, lay=PO.L_PWR,
|
||
ref='POUR', num=rr['name'],
|
||
tag='POUR.' + rr['name']))
|
||
return out
|
||
|
||
|
||
def chamfer(routes, pads, vias, lines):
|
||
u"""切 45° 拐角:全板不许直角走线(`pcb1_route_sig` §③b,用户 2026-09-06 拍板)。
|
||
|
||
⚠️ **这里用 `smooth=False`**,和 `pcb1_route_sig` 不一样。`smooth=True` 会
|
||
**挪动**路径(抹微台阶,最多 0.15 mm)而不只是拿掉铜,而 `seg_slack` 拿的是切之前的
|
||
邻线 ⇒ 两条本轮新画的线会被各自抹到对方身上(那边实测压到 −0.199,
|
||
要靠"整条退回未切版本"再核四轮才收得住)。只切拐角的话切掉的铜是原来两段的**子集**,
|
||
不可能切出新的违规,`chamfer45` 自己的 `ok()` 又逐段核过 —— 一轮就够。
|
||
直线段本来就少(`polish` 已经并过),抹台阶带来的收益不值这个风险。
|
||
"""
|
||
chk = [dict(p, lay=(12 if p['th'] else p['plane'])) for p in pads] + RS.pour_pads(RS.pour_rects()) + l3_pwr_pads()
|
||
rd = {u'%s%s%03d' % (net, chr(0), i): path
|
||
for i, (net, path) in enumerate(routes)}
|
||
cut = RS.D.chamfer_routes(rd, chk, vias, lines, smooth=False)
|
||
return [(k.split(chr(0))[0], cut[k]) for k in sorted(cut)]
|
||
|
||
|
||
# ══════════════════ 校核(下发前后同一段代码) ══════════════════
|
||
def _poly_gap(poly, a, b):
|
||
u"""线段到多边形的距离;线段任一端在多边形里返回 -1。"""
|
||
if PO.inside(poly, a[0], a[1]) or PO.inside(poly, b[0], b[1]):
|
||
return -1.0
|
||
m = 1e9
|
||
for i in range(len(poly)):
|
||
p, q = poly[i], poly[(i + 1) % len(poly)]
|
||
m = min(m, RS.D.seg_seg(a, b, p, q))
|
||
return m
|
||
|
||
|
||
def verify(routes, pads, vias, lines):
|
||
u"""返回 [问题, ...]。空表 = 全过。"""
|
||
bad = []
|
||
new_segs, new_vias = [], []
|
||
for net, path in routes:
|
||
s_, v_ = E.expand(path)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
if math.hypot(x1 - x0, y1 - y0) <= 5e-4:
|
||
continue
|
||
new_segs.append((net, lay, (x0, y0), (x1, y1), w))
|
||
for x, y in v_:
|
||
new_vias.append((net, x, y))
|
||
|
||
# ① 新线 vs 板上旧铜
|
||
for net, lay, a, b, w in new_segs:
|
||
need = CLR + w / 2.0
|
||
for p in pads:
|
||
if p['net'] == net or lay not in pad_layers(p):
|
||
continue
|
||
g = RS.D.seg_rect(a, b, p['x'], p['y'], p['hw'], p['hh']) - w / 2.0
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'线-焊盘 %s vs %s 净距 %.3f' % (net, p['tag'], g))
|
||
for v in vias:
|
||
if v['net'] == net:
|
||
continue
|
||
g = RS.D.seg_seg(a, b, (v['x'], v['y']), (v['x'], v['y'])) \
|
||
- w / 2.0 - v['r']
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'线-过孔 %s vs %s(%.2f,%.2f) 净距 %.3f'
|
||
% (net, v['net'], v['x'], v['y'], g))
|
||
for l in lines:
|
||
if l['net'] == net or l['lay'] != lay:
|
||
continue
|
||
g = RS.D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) \
|
||
- w / 2.0 - l['w'] / 2.0
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'线-线 %s vs %s @(%.2f,%.2f) 净距 %.3f'
|
||
% (net, l['net'], l['x0'], l['y0'], g))
|
||
for rr in PO.REGIONS:
|
||
if rr['net'] == net:
|
||
continue
|
||
if rr['layer'] != lay and not (lay == L3 and rr['layer'] == PO.L_PWR
|
||
and rr['net'] == '+12V'):
|
||
continue
|
||
if rr['layer'] == PO.L_PWR and rr['net'] != '+12V':
|
||
continue # +3V3 / GND 底填:铺铜自己退让,不算侵入
|
||
g = _poly_gap(rr['poly'], a, b) - w / 2.0
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'线-铺铜 %s vs %s 净距 %.3f' % (net, rr['name'], g))
|
||
|
||
# ② 新线之间(两个网络互相)
|
||
for i in range(len(new_segs)):
|
||
ni, li_, ai, bi, wi = new_segs[i]
|
||
for j in range(i + 1, len(new_segs)):
|
||
nj, lj, aj, bj, wj = new_segs[j]
|
||
if ni == nj or li_ != lj:
|
||
continue
|
||
g = RS.D.seg_seg(ai, bi, aj, bj) - wi / 2.0 - wj / 2.0
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'新线-新线 %s vs %s @(%.2f,%.2f) 净距 %.3f'
|
||
% (ni, nj, ai[0], ai[1], g))
|
||
|
||
# ③ 新孔:铜净距(外层 + L3 真铜)与钻孔间距(不分网络)
|
||
for net, x, y in new_vias:
|
||
pt = (x, y)
|
||
for p in pads:
|
||
if p['net'] == net:
|
||
continue
|
||
lays = pad_layers(p)
|
||
if not (set(lays) & set(LAYS)):
|
||
continue
|
||
if not p['th'] and p['plane'] == L3:
|
||
continue
|
||
g = RS.D.seg_rect(pt, pt, p['x'], p['y'], p['hw'], p['hh']) \
|
||
- VIA_D / 2.0
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'孔-焊盘 %s(%.2f,%.2f) vs %s 净距 %.3f'
|
||
% (net, x, y, p['tag'], g))
|
||
if p['th']:
|
||
d = math.hypot(x - p['x'], y - p['y']) \
|
||
- VIA_H / 2.0 - max(p['hw'], p['hh'])
|
||
if d < HOLE_CLR - 1e-6:
|
||
bad.append(u'钻孔-通孔 %s(%.2f,%.2f) vs %s 间距 %.3f'
|
||
% (net, x, y, p['tag'], d))
|
||
for v in vias:
|
||
d = math.hypot(x - v['x'], y - v['y'])
|
||
if v['net'] != net and d - VIA_D / 2.0 - v['r'] < CLR - 1e-6:
|
||
bad.append(u'孔-孔铜 %s(%.2f,%.2f) vs %s 净距 %.3f'
|
||
% (net, x, y, v['net'], d - VIA_D / 2.0 - v['r']))
|
||
if d - VIA_H < HOLE_CLR - 1e-6:
|
||
bad.append(u'钻孔-钻孔 %s(%.2f,%.2f) vs %s(%.2f,%.2f) 间距 %.3f'
|
||
% (net, x, y, v['net'], v['x'], v['y'], d - VIA_H))
|
||
for l in lines:
|
||
if l['net'] == net or l['lay'] not in LAYS:
|
||
continue
|
||
g = RS.D.seg_seg(pt, pt, (l['x0'], l['y0']), (l['x1'], l['y1'])) \
|
||
- VIA_D / 2.0 - l['w'] / 2.0
|
||
if g < CLR - 1e-6:
|
||
bad.append(u'孔-线 %s(%.2f,%.2f) vs %s 净距 %.3f'
|
||
% (net, x, y, l['net'], g))
|
||
for i in range(len(new_vias)):
|
||
for j in range(i + 1, len(new_vias)):
|
||
ni, xi, yi = new_vias[i]
|
||
nj, xj, yj = new_vias[j]
|
||
d = math.hypot(xi - xj, yi - yj)
|
||
if d < 1e-6:
|
||
continue
|
||
if ni != nj and d - VIA_D < CLR - 1e-6:
|
||
bad.append(u'新孔-新孔铜 %s vs %s @(%.2f,%.2f) 净距 %.3f'
|
||
% (ni, nj, xi, yi, d - VIA_D))
|
||
if d - VIA_H < HOLE_CLR - 1e-6:
|
||
bad.append(u'新孔-新孔钻 %s vs %s @(%.2f,%.2f) 间距 %.3f'
|
||
% (ni, nj, xi, yi, d - VIA_H))
|
||
|
||
# ④ 拐角 > 46°(全板不许直角走线)
|
||
for net, path in routes:
|
||
pts = [(p[0], p[1], p[2]) for p in path]
|
||
for k in range(1, len(pts) - 1):
|
||
a, b, c = pts[k - 1], pts[k], pts[k + 1]
|
||
if a[2] != b[2] or b[2] != c[2]:
|
||
continue
|
||
v1 = (b[0] - a[0], b[1] - a[1])
|
||
v2 = (c[0] - b[0], c[1] - b[1])
|
||
n1 = math.hypot(*v1); n2 = math.hypot(*v2)
|
||
if n1 < 1e-9 or n2 < 1e-9:
|
||
continue
|
||
cs = max(-1.0, min(1.0, (v1[0] * v2[0] + v1[1] * v2[1]) / (n1 * n2)))
|
||
ang = math.degrees(math.acos(cs))
|
||
if ang > 46.0:
|
||
bad.append(u'拐角 %s @(%.2f,%.2f) %.1f°' % (net, b[0], b[1], ang))
|
||
return bad
|
||
|
||
|
||
|
||
def _dropped(o, lines, drop_l, vias, drop_v):
|
||
u"""这一条板上铜是不是本轮要拆掉的 I²C 干线(按包围盒对齐,`obstacles()` 也是包围盒)。"""
|
||
for i in drop_l:
|
||
l = lines[i]
|
||
h = l['w'] / 2.0
|
||
if (o[1] == l['lay']
|
||
and abs(o[2] - (min(l['x0'], l['x1']) - h)) < 1e-6
|
||
and abs(o[3] - (min(l['y0'], l['y1']) - h)) < 1e-6):
|
||
return True
|
||
for i in drop_v:
|
||
v = vias[i]
|
||
if o[1] is None and abs(o[2] - (v['x'] - v['r'])) < 1e-6 and abs(o[3] - (v['y'] - v['r'])) < 1e-6:
|
||
return True
|
||
return False
|
||
|
||
|
||
# ══════════════════ 报告 / 主流程 ══════════════════
|
||
def summarize(routes):
|
||
by = {L: 0.0 for L in LAYS}
|
||
nv = 0
|
||
for net, path in routes:
|
||
s_, v_ = E.expand(path)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
by[lay] += math.hypot(x1 - x0, y1 - y0)
|
||
nv += len(v_)
|
||
tot = sum(by.values())
|
||
print(u' 新铜:%.1f mm —— Inner2 %.1f (%.0f%%) / 顶层 %.1f / 底层 %.1f,'
|
||
u'新过孔 %d'
|
||
% (tot, by[L3], 100.0 * by[L3] / tot if tot else 0,
|
||
by[L_TOP], by[L_BOT], nv))
|
||
return by
|
||
|
||
|
||
def virtual(pads, vias, lines, drop_l, drop_v):
|
||
dl, dv = set(drop_l), set(drop_v)
|
||
return ([l for i, l in enumerate(lines) if i not in dl],
|
||
[v for i, v in enumerate(vias) if i not in dv])
|
||
|
||
|
||
def run_plan(pads, vias, lines, nets):
|
||
u"""⚠️ **两个网络的直出段要先全部留好,再开始布线**(引擎 `plan_stubs` 那一条):
|
||
先布的网络会把后布的那个「出焊盘的 0.6 mm」占掉 —— U8/U9/U11/U12 的
|
||
SDA/SCL 就是紧挨着的两个脚。"""
|
||
lines = list(lines)
|
||
routes, extra = [], []
|
||
for net in nets:
|
||
for l in stub_lines(net, pads, vias, lines):
|
||
lines.append(l)
|
||
routes.append((net, [(l['x0'], l['y0'], l['lay'], l['w']),
|
||
(l['x1'], l['y1'], l['lay'], l['w'])]))
|
||
if routes:
|
||
print(u' 直出段:%d 条' % len(routes))
|
||
for net in nets:
|
||
ps, _n = plan_net(net, pads, vias, lines, extra)
|
||
for pth in ps:
|
||
routes.append((net, pth))
|
||
s_, v_ = E.expand(pth)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
extra.append(('seg', (x0, y0, x1, y1, w, lay), net))
|
||
lines.append(dict(net=net, lay=lay, x0=x0, y0=y0,
|
||
x1=x1, y1=y1, w=w))
|
||
for vx, vy in v_:
|
||
extra.append(('via', (vx, vy), net))
|
||
vias = list(vias) + [dict(net=net, x=vx, y=vy, r=VIA_D / 2.0)]
|
||
return routes
|
||
|
||
|
||
def connectivity(routes, pads, vias, lines):
|
||
ln = list(lines)
|
||
vi = list(vias)
|
||
for net, path in routes:
|
||
s_, v_ = E.expand(path)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
if math.hypot(x1 - x0, y1 - y0) <= 5e-4:
|
||
continue
|
||
ln.append(dict(net=net, lay=lay, x0=x0, y0=y0, x1=x1, y1=y1, w=w))
|
||
for x, y in v_:
|
||
vi.append(dict(net=net, x=x, y=y, r=VIA_D / 2.0))
|
||
out = []
|
||
for net in NETS:
|
||
fr = frags(net, pads, vi, ln)
|
||
groups = []
|
||
for f in sorted(fr, key=len, reverse=True):
|
||
names = []
|
||
for p in pads:
|
||
if p['net'] != net:
|
||
continue
|
||
if any(s[0] == 'rect' and abs(s[1] - p['x']) < 1e-6
|
||
and abs(s[2] - p['y']) < 1e-6 for s in f):
|
||
names.append(p['tag'])
|
||
groups.append(u'{%s}' % (u' '.join(names) or u'裸铜×%d' % len(f)))
|
||
out.append((net, len(fr), u' '.join(groups)))
|
||
return out
|
||
|
||
|
||
def main():
|
||
args = sys.argv[1:]
|
||
push = '--push' in args
|
||
refresh = '--refresh' in args or push
|
||
nets = list(NETS)
|
||
if '--net' in args:
|
||
nets = [a for a in args[args.index('--net') + 1:] if not a.startswith('-')]
|
||
|
||
pads, vias, lines = NL.geom(refresh)
|
||
print(u'板上:焊盘 %d / 过孔 %d / 走线 %d' % (len(pads), len(vias), len(lines)))
|
||
# ⚠️ 只拆**这一轮要动的**网络(`--net` 限定时尤其重要)
|
||
globals()['NETS'] = tuple(nets)
|
||
keep_l, drop_l, keep_v, drop_v = split_old(pads, vias, lines)
|
||
dlen = sum(math.hypot(lines[i]['x1'] - lines[i]['x0'],
|
||
lines[i]['y1'] - lines[i]['y0']) for i in drop_l)
|
||
klen = sum(math.hypot(lines[i]['x1'] - lines[i]['x0'],
|
||
lines[i]['y1'] - lines[i]['y0']) for i in keep_l)
|
||
print(u'旧 I²C 铜:簇内保留 %d 段 %.1f mm / %d 孔;'
|
||
u'簇外拆掉 %d 段 %.1f mm / %d 孔'
|
||
% (len(keep_l), klen, len(keep_v), len(drop_l), dlen, len(drop_v)))
|
||
|
||
vlines, vvias = virtual(pads, vias, lines, drop_l, drop_v)
|
||
print(u'── 规划(拆完之后的碎片重连)──')
|
||
pads_raw, _nth, _nthb = PO.load_pads(False)
|
||
obs_board = [o for o in PO.obstacles(False)
|
||
if not (o[0] in NETS and _dropped(o, lines, drop_l, vias, drop_v))]
|
||
routes = None
|
||
for rnd in range(4):
|
||
routes = run_plan(pads, vvias, vlines, nets)
|
||
if not routes:
|
||
sys.exit(u'一条也没算出来')
|
||
routes = chamfer(routes, pads, vvias, vlines)
|
||
msgs, rects = plane_report(routes, pads_raw, obs_board)
|
||
if not msgs:
|
||
print(u' 平面完整性:✅ Inner2 上 +12V 1 块 / +3V3 2 块,没切出孤岛')
|
||
break
|
||
for m in msgs:
|
||
print(u' ⚠️ ' + m)
|
||
if rnd == 3:
|
||
print(u' ❌ 四轮都还有孤岛 —— 不下发')
|
||
sys.exit(1)
|
||
print(u' → 把这 %d 格钉成 L3 硬障碍,重绕一轮' % len(rects))
|
||
BLOCK.extend(rects)
|
||
summarize(routes)
|
||
bad = verify(routes, pads, vvias, vlines)
|
||
for b in bad[:40]:
|
||
print(u' ✗ ' + b)
|
||
print(u' 校核:%s' % (u'✅ 全过' if not bad else u'❌ %d 条' % len(bad)))
|
||
worse = []
|
||
for net, n, det in connectivity(routes, pads, vvias, vlines):
|
||
was = len(frags(net, pads, vias, lines)) # 动板之前,板上实际的碎片数
|
||
flag = u''
|
||
if n > was:
|
||
flag = u' ⛔ 比改动前差了(原来 %d 片)' % was
|
||
worse.append(net)
|
||
elif n < was:
|
||
flag = u' ↑ 比改动前少 %d 片' % (was - n)
|
||
print(u' 连通性 %s:%d 个碎片 %s%s'
|
||
% (net, n, det if n > 1 else u'✅', flag))
|
||
if bad:
|
||
sys.exit(1)
|
||
if worse:
|
||
# ⚠️ 这条保险是必需的:本脚本会把这些网络在簇框外的铜**全部拆掉**重来。
|
||
# 拆得动、重连不上的话,板子会比动手之前更差,而净距校核照样全绿。
|
||
#
|
||
# 但**不要因此把整轮毙掉** —— `NETS` 是一张「候选表」,里面难免有挪不动的
|
||
# (2026-09-06 实测 `BUCK_PG`:五个分支要去板子四角,而 L3 上 y<34 往东
|
||
# 只剩 x115~116 那条 1.0 mm 缝,`+12V` 把别的通道全占了)。
|
||
# 把变差的那几个**踢出去重算一遍**,剩下的照常下发。
|
||
print(u' → %s 挪进内层会变差,踢出候选表重算一轮' % ' / '.join(worse))
|
||
keep = [n for n in nets if n not in worse]
|
||
if not keep:
|
||
sys.exit(u'⛔ 候选表里一个都留不下 —— 不下发')
|
||
# ⚠️ **拆除清单必须跟着重算**。第一版只换了 `run_plan` 的网络表,
|
||
# 而 `drop_l/drop_v` 还是按完整候选表算的 ⇒ `--push` 会把被踢掉那个网络的
|
||
# 铜删掉、又不给它重布,板子直接变差(净距校核照样全绿)。
|
||
globals()['NETS'] = tuple(keep)
|
||
keep_l, drop_l, keep_v, drop_v = split_old(pads, vias, lines)
|
||
vlines, vvias = virtual(pads, vias, lines, drop_l, drop_v)
|
||
print(u' 重算后要拆:%d 段 / %d 孔(%d 个网络)'
|
||
% (len(drop_l), len(drop_v), len(keep)))
|
||
del BLOCK[:]
|
||
routes = run_plan(pads, vvias, vlines, keep)
|
||
routes = chamfer(routes, pads, vvias, vlines)
|
||
msgs, _r = plane_report(routes, pads_raw, obs_board)
|
||
for m in msgs:
|
||
print(u' ⚠️ ' + m)
|
||
if msgs:
|
||
sys.exit(u'⛔ 重算之后仍有孤岛 —— 不下发')
|
||
summarize(routes)
|
||
bad = verify(routes, pads, vvias, vlines)
|
||
for b in bad[:20]:
|
||
print(u' ✗ ' + b)
|
||
if bad:
|
||
sys.exit(u'⛔ 重算之后校核不过 —— 不下发')
|
||
nets = keep
|
||
worse2 = []
|
||
for net, n, det in connectivity(routes, pads, vvias, vlines):
|
||
was = len(frags(net, pads, vias, lines))
|
||
print(u' 连通性 %s:%d 个碎片 %s%s'
|
||
% (net, n, det if n > 1 else u'✅',
|
||
u'' if n <= was else u' ⛔ 仍然变差'))
|
||
if n > was and net in keep:
|
||
worse2.append(net)
|
||
if worse2:
|
||
sys.exit(u'⛔ %s 重算之后仍然变差 —— 不下发' % ' / '.join(worse2))
|
||
if not push:
|
||
print(u'\n(只算不下发。要下发加 --push)')
|
||
return
|
||
|
||
print(u'── 下发 ──')
|
||
RS.drop(lines, vias, drop_l, drop_v, u'I²C 干线改走 Inner2')
|
||
pads2, vias2, lines2 = NL.geom(True)
|
||
bad2 = verify(routes, pads2, vias2, lines2)
|
||
if bad2:
|
||
for b in bad2[:40]:
|
||
print(u' ✗ ' + b)
|
||
sys.exit(u'拆完之后按实测铜重核不过,没下发')
|
||
# ⚠️ `RS.push` 会把下发的东西写进模块全局 `ROUTES`(`_pcb1_sig_routes.json`)——
|
||
# 那是 `pcb1_route_sig` 自己"我画过哪些铜"的记录,覆盖掉的话它的 `--clean`
|
||
# 会去删本脚本的线、而放过它自己的。换一个文件名。
|
||
RS.ROUTES = os.path.join(HERE, '_pcb1_i2c_l3_routes.json')
|
||
RS.push({('%s%s%03d' % (net, chr(0), i)): path
|
||
for i, (net, path) in enumerate(routes)})
|
||
print(u'⚠️ 新的 L3 走线会让平面退让 —— 现在必须跑:')
|
||
print(u' /c/Python314/python .claude/pcb1_pour.py --push')
|
||
print(u' /c/Python314/python .claude/pcb1_pour.py --verify')
|
||
print(u' /c/Python314/python .claude/drc_read.py')
|
||
|
||
|
||
|
||
|
||
# ══════════════════ 平面完整性:干线不许把电源面切出孤岛 ══════════════════
|
||
#: Inner2 上每个电源网络**应该**有几个连通域(和 `pcb1_pour.grid_checks` 同一套判据)。
|
||
PLANE_EXPECT = (('+12V', 1), ('+3V3', 2))
|
||
|
||
|
||
def plane_report(routes, pads_raw, obs):
|
||
u"""干线画上去之后,Inner2 的电源面还是不是原来那么多块。
|
||
|
||
⚠️ **这是本轮抓到的唯一一条真缺陷,而且是静默的**:`3V3-SW` 被 SDA 的斜线、
|
||
SCL 的 y=50 横线和平面东边界围出一块 7.2 mm² 的孤岛,里面正好有 `R415.2`
|
||
的 +3V3 焊盘和它的过孔 —— 于是
|
||
· 铺铜的「浮铜」检查不响(那块铜有本网落脚点);
|
||
· 「焊盘覆盖」检查不响(焊盘确实落在本网铜上);
|
||
· 连接性 DRC 不响(过孔把它接到顶层去了)。
|
||
只有**数连通域**看得见。所以这一条必须在规划阶段就跑,不能等铺铜。
|
||
|
||
栅格用 `pcb1_pour.Grid`(G=0.25、斜线按包围盒挖),和 `pcb1_pour.py` 是同一段代码 ——
|
||
两套实现各自自洽证明不了板上那块铜就是计划里那块铜。
|
||
"""
|
||
obs2 = list(obs)
|
||
for net, path in routes:
|
||
s_, v_ = E.expand(path)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
h = w / 2.0
|
||
obs2.append((net, lay, min(x0, x1) - h, min(y0, y1) - h,
|
||
max(x0, x1) + h, max(y0, y1) + h))
|
||
for vx, vy in v_:
|
||
r = VIA_D / 2.0
|
||
obs2.append((net, None, vx - r, vy - r, vx + r, vy + r))
|
||
msgs, rects = [], []
|
||
for net, expect in PLANE_EXPECT:
|
||
regs = [r for r in PO.REGIONS
|
||
if r['net'] == net and r['layer'] == PO.L_PWR]
|
||
g = PO.Grid(net, PO.L_PWR, regs, pads_raw, obs2)
|
||
if g.ncomp == expect:
|
||
continue
|
||
keep = set(lab for lab, _n in
|
||
sorted(g.sizes.items(), key=lambda kv: -kv[1])[:expect])
|
||
for lab, n in sorted(g.sizes.items(), key=lambda kv: -kv[1]):
|
||
if lab in keep:
|
||
continue
|
||
cells = [(i, j) for j in range(g.ny) for i in range(g.nx)
|
||
if g.lab[j][i] == lab]
|
||
xs = [i for i, _j in cells]
|
||
ys = [j for _i, j in cells]
|
||
msgs.append(u'%s 在 Inner2 上多出一块孤岛 %.1f mm²'
|
||
u'(x %.1f~%.1f y %.1f~%.1f)'
|
||
% (net, n * PO.G * PO.G, min(xs) * PO.G,
|
||
(max(xs) + 1) * PO.G, min(ys) * PO.G,
|
||
(max(ys) + 1) * PO.G))
|
||
for i, j in cells:
|
||
rects.append((i * PO.G, j * PO.G,
|
||
(i + 1) * PO.G, (j + 1) * PO.G))
|
||
return msgs, rects
|
||
|
||
if __name__ == '__main__':
|
||
main()
|