# -*- 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()