- 规则:铜层 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
264 行
12 KiB
Python
264 行
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""摆位体检:外围件有没有摆在**自己那只脚的那一侧**(M7)。
|
||
|
||
/c/Python314/python .claude/r2/place_audit.py # 全板扫一遍
|
||
/c/Python314/python .claude/r2/place_audit.py U2 U1 # 只看这几颗
|
||
/c/Python314/python .claude/r2/place_audit.py --base # R2 底板
|
||
|
||
## 这个脚本在抓什么
|
||
|
||
`U2` 那一窝暴露出来的毛病:它是 WQFN-20,**四条边都有脚**,而外围 12 件全被摆在了
|
||
西边 —— 接东边 `p17~p20` 的四件要**绕整颗芯片**才够得着。这类错:
|
||
|
||
- **DRC 不报**(没有间距问题),
|
||
- **几何自检不报**(没有重叠、没出板框),
|
||
- **布线器只会报「XXX 出不来 / 没布通」**,而那是症状不是病因,
|
||
- 人眼看截图能看出来,但要一颗一颗放大。
|
||
|
||
⇒ 判据写成一条能算的:**从芯片的这只脚,到同网对端焊盘的直线,
|
||
如果从芯片自己的本体里穿过去,这件就是摆错边了。**
|
||
|
||
同一条判据顺手也能抓「被第三个件挡住」(直线穿过别人的本体),
|
||
以及「去耦电容离电源脚太远」。
|
||
|
||
## 判据:绕行代价,不是「穿没穿过」
|
||
|
||
第一版只判「直线穿不穿过本体」,结果 64 处里绝大多数是噪声 —— `U1.14` 到 `U4.14`
|
||
隔着 81 mm,线从 `U4` 自己本体上方过一下,绕行代价才 2 mm,那不是摆错边。
|
||
⇒ 判据改成**绕行代价**:把本体当障碍,算「绕过去的最短路 − 直线」,
|
||
再看它占直线长度的比例。只有
|
||
|
||
· 对端是**小外围件**(≤ `SAT_PINS` 只脚,即 0402/SOT23 这类),
|
||
· 两者相距 < `NEAR`(默认 20 mm),
|
||
· 绕行多走 > `EXTRA_MM` 且占比 > `EXTRA_RATIO`
|
||
|
||
三条同时成立,才算「摆错边」—— 那才是「本可以放对面、白白绕一圈」的情形。
|
||
|
||
平面网(`GND` / `VO_P` / `+12V`)不走线,直接排除;成员多于 `BIG_NET` 的大网
|
||
(`+3V3` 这种)也排除,它们本来就不该按直线看。
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
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)
|
||
if '--base' in sys.argv:
|
||
import place_base as PL # noqa: E402
|
||
PADS = os.path.join(HERE, '_base_pads.json')
|
||
PLANE = ('GND', '+12V', '+5V', '+3V3', '+3V3_HUB',
|
||
'VIN_RAW', 'VIN_FUSED', 'VIN_SW')
|
||
else:
|
||
import pcb_place as PL # noqa: E402
|
||
PADS = os.path.join(HERE, '_blade_pads.json')
|
||
PLANE = ('GND', 'VO_P', '+12V', '+12V_F')
|
||
BIG_NET = 6 # 成员多于这个数的网不判(+3V3 这种大网不该按直线看)
|
||
IC_PINS = 4 # 引脚数 >= 这个数才算「芯片」(两脚件不算)
|
||
MAX_IC_PINS = 24 # 脚多于这个数的当**连接器**,不参与「摆错边」
|
||
SAT_PINS = 3 # 引脚数 <= 这个数才算「小外围件」
|
||
INSET = 0.25 # 本体矩形往里缩多少再判相交(焊盘本来就贴着边)
|
||
NEAR = 20.0 # 只看这个距离内的(远距离网绕一下无所谓)
|
||
EXTRA_MM = 1.5 # 绕行多走多少 mm 才算数
|
||
EXTRA_RATIO = 0.10 # 且要占直线长度的这个比例
|
||
FAR_DECAP = 3.0 # 去耦电容离电源脚超过这个距离就提示(mm)
|
||
|
||
|
||
def load():
|
||
rows = json.load(io.open(PADS, encoding='utf-8'))
|
||
pads, bynet, byref = [], {}, {}
|
||
for r in rows:
|
||
ref, pin, x, y, net = r[0], r[1], r[2], r[3], r[7]
|
||
p = {'ref': ref, 'pin': pin, 'x': x, 'y': y, 'net': net}
|
||
pads.append(p)
|
||
byref.setdefault(ref, []).append(p)
|
||
if net and net not in PLANE:
|
||
bynet.setdefault(net, []).append(p)
|
||
body = {}
|
||
for ref, ps in byref.items():
|
||
xs = [p['x'] for p in ps]
|
||
ys = [p['y'] for p in ps]
|
||
w, h = PL.BODY.get(ref, (0, 0))
|
||
if w: # 焊盘框量不到本体的几件(C60/L1/J4/U4),用真实本体
|
||
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
|
||
body[ref] = (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2)
|
||
else:
|
||
body[ref] = (min(xs), min(ys), max(xs), max(ys))
|
||
return pads, bynet, byref, body
|
||
|
||
|
||
def seg_rect(x1, y1, x2, y2, r, inset=INSET):
|
||
u"""线段和矩形(往里缩 inset)有没有真的相交。Liang–Barsky。"""
|
||
a, b, c, d = r[0] + inset, r[1] + inset, r[2] - inset, r[3] - inset
|
||
if a >= c or b >= d:
|
||
return False
|
||
dx, dy = x2 - x1, y2 - y1
|
||
t0, t1 = 0.0, 1.0
|
||
for p, q in ((-dx, x1 - a), (dx, c - x1), (-dy, y1 - b), (dy, d - y1)):
|
||
if abs(p) < 1e-12:
|
||
if q < 0:
|
||
return False
|
||
else:
|
||
t = q / p
|
||
if p < 0:
|
||
if t > t1:
|
||
return False
|
||
t0 = max(t0, t)
|
||
else:
|
||
if t < t0:
|
||
return False
|
||
t1 = min(t1, t)
|
||
return t0 < t1
|
||
|
||
|
||
def detour(ax, ay, bx, by, r):
|
||
u"""把矩形 r(缩 INSET)当障碍,绕过去的最短路 − 直线 = 多走多少 mm。
|
||
|
||
可见图只有 6 个点(两端 + 四角),Dijkstra 直接跑。不相交就返回 0。
|
||
"""
|
||
if not seg_rect(ax, ay, bx, by, r):
|
||
return 0.0
|
||
a, b, c, d = r[0] + INSET, r[1] + INSET, r[2] - INSET, r[3] - INSET
|
||
pts = [(ax, ay), (bx, by), (a, b), (a, d), (c, b), (c, d)]
|
||
n = len(pts)
|
||
INF = float('inf')
|
||
dist = [INF] * n
|
||
dist[0] = 0.0
|
||
done = [False] * n
|
||
for _ in range(n):
|
||
u = min((k for k in range(n) if not done[k]), key=lambda k: dist[k], default=None)
|
||
if u is None or dist[u] == INF:
|
||
break
|
||
done[u] = True
|
||
for v in range(n):
|
||
if done[v]:
|
||
continue
|
||
if seg_rect(pts[u][0], pts[u][1], pts[v][0], pts[v][1], r):
|
||
continue # 这一段还是穿本体,不可走
|
||
w = math.hypot(pts[u][0] - pts[v][0], pts[u][1] - pts[v][1])
|
||
if dist[u] + w < dist[v]:
|
||
dist[v] = dist[u] + w
|
||
straight = math.hypot(ax - bx, ay - by)
|
||
return 0.0 if dist[1] == INF else max(0.0, dist[1] - straight)
|
||
|
||
|
||
def wrong_side(icpad, sat, r):
|
||
u"""外围件在不在这只脚**朝外的那一侧**;不在的话,绕过去大约要多走多少 mm。
|
||
|
||
⛔ 不用「直线穿不穿本体」判:芯片的**焊盘本身就在本体框里**(甚至框边上),
|
||
线段起点落在障碍内部,绕行最短路直接算成「无解」,返回 0 —— 于是
|
||
`U1.1`(EN,在西边) 配 `R3`(在东边) 这种教科书式的摆错边**一条都报不出来**。
|
||
这是第二版判据交的学费。
|
||
|
||
改判「侧」:脚离哪条边近,那条边的**外法线**就是它的出线方向;
|
||
外围件相对这只脚的方向和外法线反着来(点积 < -0.3),就是摆到对面去了,
|
||
代价约等于「芯片在这个方向上的尺寸 + 两倍净距」(线得绕过去)。
|
||
"""
|
||
x, y = icpad['x'], icpad['y']
|
||
dists = ((x - r[0], (-1.0, 0.0)), (r[2] - x, (1.0, 0.0)),
|
||
(y - r[1], (0.0, -1.0)), (r[3] - y, (0.0, 1.0)))
|
||
_d, nrm = min(dists, key=lambda t: t[0])
|
||
vx, vy = sat['x'] - x, sat['y'] - y
|
||
vlen = math.hypot(vx, vy)
|
||
if vlen < 1e-9:
|
||
return 0.0
|
||
dot = (vx * nrm[0] + vy * nrm[1]) / vlen
|
||
if dot > -0.3:
|
||
return 0.0
|
||
span = (r[2] - r[0]) if nrm[1] == 0 else (r[3] - r[1])
|
||
return span + 2 * INSET
|
||
|
||
|
||
def main():
|
||
only = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||
_pads, bynet, byref, body = load()
|
||
npin = {r: len(ps) for r, ps in byref.items()}
|
||
ics = sorted(r for r in byref if npin[r] >= IC_PINS)
|
||
own, blocked, decap = [], [], []
|
||
for net, ps in sorted(bynet.items()):
|
||
if len(ps) > BIG_NET:
|
||
continue
|
||
for i in range(len(ps)):
|
||
for j in range(i + 1, len(ps)):
|
||
a, b = ps[i], ps[j]
|
||
if a['ref'] == b['ref']:
|
||
continue
|
||
d = math.hypot(a['x'] - b['x'], a['y'] - b['y'])
|
||
if d > NEAR:
|
||
continue
|
||
# 「摆错边」:一端是芯片、另一端是小外围件,而绕自家本体要多走不少
|
||
for (icpad, sat) in ((a, b), (b, a)):
|
||
ic = icpad['ref']
|
||
# ⚠ `J1`(64 脚) / `J4`(50 脚) 这类连接器不判:它们的脚排成一长条,
|
||
# 「离哪条边近」对端头的脚毫无意义(`J4.51` 会被判成朝西出线),
|
||
# 而连接器的出线方向是设计定死的,不是摆位能改的。
|
||
if (ic not in ics or npin[ic] > MAX_IC_PINS
|
||
or npin[sat['ref']] > SAT_PINS):
|
||
continue
|
||
ex = wrong_side(icpad, sat, body[ic])
|
||
if ex > EXTRA_MM and ex / d > EXTRA_RATIO:
|
||
own.append((ex, ex / d, ic, net, a, b))
|
||
for ref, r in body.items():
|
||
if ref in (a['ref'], b['ref']):
|
||
continue
|
||
ex = detour(a['x'], a['y'], b['x'], b['y'], r)
|
||
if ex > EXTRA_MM and ex / d > EXTRA_RATIO:
|
||
blocked.append((ex, ex / d, ref, net, a, b))
|
||
for ref, ps in sorted(byref.items()):
|
||
if len(ps) != 2 or not ref.startswith('C'):
|
||
continue
|
||
if 'GND' not in [p['net'] for p in ps]:
|
||
continue
|
||
pw = [p for p in ps if p['net'] != 'GND']
|
||
if not pw or pw[0]['net'] in PLANE:
|
||
continue
|
||
best = None
|
||
for q in bynet.get(pw[0]['net'], []):
|
||
if q['ref'] == ref or q['ref'] not in ics:
|
||
continue
|
||
dd = math.hypot(pw[0]['x'] - q['x'], pw[0]['y'] - q['y'])
|
||
if best is None or dd < best[0]:
|
||
best = (dd, q)
|
||
if best and best[0] > FAR_DECAP:
|
||
decap.append((best[0], ref, pw[0]['net'], best[1]))
|
||
|
||
def dedup(rows):
|
||
seen, out = set(), []
|
||
for t in sorted(rows, reverse=True):
|
||
k = (t[2], t[4]['ref'], t[4]['pin'], t[5]['ref'], t[5]['pin'])
|
||
if k not in seen:
|
||
seen.add(k)
|
||
out.append(t)
|
||
return out
|
||
|
||
def fmt(t):
|
||
ex, ratio, key, net, a, b = t
|
||
return (u' 绕 %5.1f mm (%3.0f%%) %-5s %-10s %s.%s(%.1f,%.1f) — %s.%s(%.1f,%.1f)'
|
||
% (ex, ratio * 100, key, net, a['ref'], a['pin'], a['x'], a['y'],
|
||
b['ref'], b['pin'], b['x'], b['y']))
|
||
|
||
if only:
|
||
keep = lambda t: t[2] in only or t[4]['ref'] in only or t[5]['ref'] in only
|
||
own, blocked = [t for t in own if keep(t)], [t for t in blocked if keep(t)]
|
||
decap = [t for t in decap if t[1] in only or t[3]['ref'] in only]
|
||
own, blocked = dedup(own), dedup(blocked)
|
||
print(u'⛔ 外围件摆错边(要绕自家芯片本体,多走 > %.1f mm 且 > %.0f%%):%d 处'
|
||
% (EXTRA_MM, EXTRA_RATIO * 100, len(own)))
|
||
for t in own:
|
||
print(fmt(t))
|
||
print(u'\n⚠ 被第三个件挡住(绕得过去,只是提示):%d 处' % len(blocked))
|
||
for t in blocked[:15]:
|
||
print(fmt(t))
|
||
if len(blocked) > 15:
|
||
print(u' ...(还有 %d 处)' % (len(blocked) - 15))
|
||
print(u'\n⚠ 去耦电容离电源脚 > %.1f mm:%d 处' % (FAR_DECAP, len(decap)))
|
||
for d, ref, net, q in sorted(decap, reverse=True):
|
||
print(u' %-5s %-8s 离 %s.%s %.1f mm' % (ref, net, q['ref'], q['pin'], d))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|