- 规则:铜层 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
189 行
8.1 KiB
Python
189 行
8.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""下板 PCB1:**每个还要接的焊盘,能不能走得出去**。
|
||
|
||
/c/Python314/python .claude/pcb1_escape_audit.py # 全部
|
||
/c/Python314/python .claude/pcb1_escape_audit.py --net SDA # 只看这几个网络
|
||
/c/Python314/python .claude/pcb1_escape_audit.py --why U8.3 # 单个焊盘,列出挡路的铜
|
||
|
||
## 为什么要单独有这一支
|
||
|
||
布线器报「布不通」的时候,看不出根因是**这一头出不来**还是**中间那段挤**。
|
||
两者的处理办法完全不同:前者要动扇出过孔 / 摆位(几何上就没有出路,
|
||
换多少种布线策略都一样),后者才是布线器的事。
|
||
|
||
判据:在焊盘周围开一个 8 x 8 mm 的窗,按 0.05 mm 栅格算「0.2 mm 的线能走的格」,
|
||
从出线段末端泛洪,看能不能摸到窗边。摸不到 = **这个焊盘被围死了**。
|
||
(窗开 8 mm 是因为 0.5 脚距 QFN 的死胡同都在 2~3 mm 以内;
|
||
再大就分不清「出不来」和「出得来但走不远」了。)
|
||
"""
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
import numpy as np
|
||
from scipy import ndimage
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
|
||
import pcb1_route_sig as R # noqa: E402
|
||
|
||
E, D = R.E, R.D
|
||
WIN = 4.0 # 窗口半边长(mm)
|
||
G = 0.05
|
||
W = 0.20
|
||
|
||
|
||
def audit(pads, obstacles, tags, verbose_tag=None):
|
||
bytag = {p['tag']: p for p in pads}
|
||
stubs = getattr(audit, 'stubs', {})
|
||
alts = getattr(audit, 'alts', {})
|
||
bad = []
|
||
for tag in sorted(tags):
|
||
q = bytag.get(tag)
|
||
if q is None:
|
||
continue
|
||
# ⚠️ **每一个出线方向都要试。**只试首选方向会把
|
||
# `U13.2` 这种“南边是死胡同、北边其实通”的误报进来 ——
|
||
# `route_net()` 本来就会在失败时换方向重试。
|
||
cands = list(alts.get(tag) or ([stubs[tag]] if tag in stubs else [(None, 0)]))
|
||
best = None
|
||
for st in cands:
|
||
r = _breathe(q, st if st[0] else None, obstacles)
|
||
if r is None:
|
||
best = None
|
||
break
|
||
best = r if best is None else max(best, r)
|
||
if best is None:
|
||
continue
|
||
bad.append((tag, q['net'], u'被围死:%d 个出线方向全试过,'
|
||
u'最好的一个两层加起来只能走 %.2f mm²'
|
||
% (len(cands), best)))
|
||
if tag == verbose_tag:
|
||
st = cands[0]
|
||
sx, sy = st[0] if st[0] else (q['x'], q['y'])
|
||
box = (q['x'] - WIN, q['y'] - WIN, q['x'] + WIN, q['y'] + WIN)
|
||
f = E.Field(obstacles, box, skip_net=q['net'], grid=G)
|
||
_why(f, q, sx, sy, obstacles, q['lay'] if q['lay'] != 12 else E.L_TOP)
|
||
return bad
|
||
|
||
|
||
def _breathe(q, st, obstacles):
|
||
u"""从这个出线方向能不能走到窗边。能 → None;不能 → 能走的面积 mm²。否则"""
|
||
if True:
|
||
sx, sy = st[0] if st else (q['x'], q['y'])
|
||
box = (q['x'] - WIN, q['y'] - WIN, q['x'] + WIN, q['y'] + WIN)
|
||
f = E.Field(obstacles, box, skip_net=q['net'], grid=G)
|
||
f.set_relax([E.sh_rect(cx, cy, hw, hh, R.pad_layers(q))
|
||
for cx, cy, hw, hh in R.pad_rects(q)]
|
||
+ ([E.sh_seg(q['x'], q['y'], sx, sy, st[1], q['lay'])] if st else []))
|
||
lay = q['lay'] if q['lay'] != 12 else E.L_TOP
|
||
# ⚠️ **两层一起泛洪,中间按 `via_ok()` 打通。**
|
||
# 只在焊盘自己那一层上算的话,「出来一步就打孔翻到底层」这种最常见的逃法
|
||
# 会被判成走不通 —— 第一版就是这样,报出 60 多个假阳性
|
||
# (`U23.9` / `U6.*` 明明单独布都能通)。
|
||
ok = {L: f.free(L, W) for L in (E.L_TOP, E.L_BOT)}
|
||
vok = f.via_ok()
|
||
i, j = f._idx(sx, sy)
|
||
if not (0 <= i < f.nx and 0 <= j < f.ny):
|
||
return None
|
||
if not ok[lay][j, i]:
|
||
return 0.0
|
||
seed = {L: np.zeros_like(ok[L]) for L in (E.L_TOP, E.L_BOT)}
|
||
seed[lay][j, i] = True
|
||
oth = {E.L_TOP: E.L_BOT, E.L_BOT: E.L_TOP}
|
||
for _it in range(8):
|
||
grew = False
|
||
for L in (E.L_TOP, E.L_BOT):
|
||
r = ndimage.binary_propagation(seed[L], mask=ok[L])
|
||
if (r != seed[L]).any():
|
||
seed[L] = r
|
||
grew = True
|
||
for L in (E.L_TOP, E.L_BOT):
|
||
cross = seed[L] & vok & ok[oth[L]]
|
||
if (cross & ~seed[oth[L]]).any():
|
||
seed[oth[L]] = seed[oth[L]] | cross
|
||
grew = True
|
||
if not grew:
|
||
break
|
||
# ⚠️ 环到哪一圈才算「出去了」:窗口外那圈墙本身是障碍,
|
||
# 紧贴着它的十几格 `free()` 永远是假的(edt 不够)。
|
||
# 拿自由区最外圈当判据会把每一个焊盘都报成“出不去”。
|
||
nb = max(1, int(round(E.Field.RIM / G)))
|
||
rim = nb + int(round((E.CLR + W / 2.0 + f.qerr) / G)) + 2
|
||
edge = np.zeros(ok[lay].shape, bool)
|
||
edge[rim, :] = True
|
||
edge[-rim - 1, :] = True
|
||
edge[:, rim] = True
|
||
edge[:, -rim - 1] = True
|
||
if any((seed[L] & edge).any() for L in (E.L_TOP, E.L_BOT)):
|
||
return None
|
||
return sum(int(seed[L].sum()) for L in (E.L_TOP, E.L_BOT)) * G * G
|
||
|
||
|
||
def _why(f, q, sx, sy, obstacles, lay):
|
||
u"""把挡在这个焊盘 3 mm 内的异网铜按距离列出来。"""
|
||
print(u' —— %s 周围 3 mm 内的异网铜(按到出线段末端的距离)——' % q['tag'])
|
||
rows = []
|
||
for s, n in obstacles:
|
||
if n == q['net'] or lay not in s[-1]:
|
||
continue
|
||
if s[0] == 'rect':
|
||
d = D.seg_rect((sx, sy), (sx, sy), s[1], s[2], s[3], s[4])
|
||
what = u'焊盘/铺铜 (%.2f,%.2f) %.2fx%.2f' % (s[1], s[2], s[3] * 2, s[4] * 2)
|
||
elif s[0] == 'via':
|
||
d = math.hypot(sx - s[1], sy - s[2]) - s[3]
|
||
what = u'过孔 (%.2f,%.2f)' % (s[1], s[2])
|
||
else:
|
||
d = D.seg_pt(s[1], s[2], s[3], s[4], sx, sy) - s[5]
|
||
what = u'走线 (%.2f,%.2f)-(%.2f,%.2f) w%.2f' % (s[1], s[2], s[3], s[4], s[5] * 2)
|
||
if d < 3.0:
|
||
rows.append((d, n, what))
|
||
for d, n, what in sorted(rows)[:14]:
|
||
print(u' %6.3f %-11s %s' % (d, n or u'(空)', what))
|
||
|
||
|
||
def main():
|
||
argv = sys.argv[1:]
|
||
want = None
|
||
if '--net' in argv:
|
||
want = set(a for a in argv[argv.index('--net') + 1:] if not a.startswith('--'))
|
||
why = None
|
||
if '--why' in argv:
|
||
why = argv[argv.index('--why') + 1]
|
||
|
||
pads, vias, lines = R.board()
|
||
pours = R.pour_rects()
|
||
obstacles = R.obstacles_of(pads, vias, lines, pours)
|
||
js = R.jobs(pads, want, skip=() if want else R.SKIP)
|
||
# ⚠️ 2026-09-06 起 `route_sig` 把出线段也下发了 —— 布不通的焊盘在板上是
|
||
# 「一个焊盘 + 一小截自己的出线段」,不再是光焊盘。只认光焊盘会报「0 个」。
|
||
# 出线段全长 ≤ 1.2 mm、没有过孔的碎片,一样当孤立焊盘审。
|
||
need = set()
|
||
for net, frags in js:
|
||
for fr in frags:
|
||
if len(fr['pads']) != 1 or fr['vias']:
|
||
continue
|
||
q = fr['pads'][0]
|
||
if all(max(math.hypot(l['x0'] - q['x'], l['y0'] - q['y']),
|
||
math.hypot(l['x1'] - q['x'], l['y1'] - q['y'])) <= 1.2 for l in fr['lines']):
|
||
need.add(q['tag'])
|
||
order = [n for n, _f in js]
|
||
stubs, alts = R.plan_stubs(order, pads, obstacles, need)
|
||
audit.stubs = stubs
|
||
audit.alts = alts
|
||
if why:
|
||
need = set([why])
|
||
print(u'要接的孤立焊盘 %d 个,其中留到出线段的 %d 个' % (len(need), len(stubs)))
|
||
bad = audit(pads, obstacles, need, verbose_tag=why)
|
||
print(u'\n**被围死的焊盘 %d 个**:' % len(bad))
|
||
for tag, net, msg in bad:
|
||
print(u' [!!] %-10s %-12s %s' % (tag, net, msg))
|
||
if not bad:
|
||
print(u' (没有)')
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|