# -*- coding: utf-8 -*- u"""Inner2 上**没有落脚点**的那些 GND 底填碎片 -> 补缝合孔(接到 L2 的完整地平面)。 /c/Python314/python .claude/pcb1_gnd_stitch.py # 只找位置,不动板 /c/Python314/python .claude/pcb1_gnd_stitch.py --push # 校核过了就下发 ## 这块浮铜是怎么来的(2026-09-06 查清) 板子南沿 y 0.5~9 那一整条 GND 底填(**1312.8 mm²**)在 Inner2 上是孤立的: 南边是板框,北边被 `12V-SW+BR`(y9~34, x0.5~72.6)、`3V3-SE`(y9~41, x73~115)、 `12V-SE+RC`(y9~34, x116~159.5) 三块电源面封死,原本只剩两条缝通到别处 —— x 72.6~73(**0.4 mm**,本来就走不了)与 x 115~116(1.0 mm)。 后者被 `K5_VUP(114.80,28.70)` / `VDAC5(116.70,29.50)` / `EFUSE_EN3(115.00,32.40)` 三个**信号布线打的过孔**的反焊盘掐断了。而这一条里**一个过孔、一个通孔焊盘都没有**, 于是整条铜浮着。 ⚠️ 三道检查全都看不见它: · 铺铜的孤岛判定按**铺铜对象**算,`GND-FILL-L3` 是一个对象铺出十块, 单独某一块有没有落脚点它不管; · 连接性 DRC 只看网络连没连上(GND 靠别处照样连通); · `pcb1_pour.py` 下发前的 `report()` 也看不见 —— 它的栅格**不含别的铺铜**, 算出来 GND 是一整块。**只有下发后 `verify()` 回读实测铜才量得出来。** 而这条浮铜正贴着 ESP32 的天线禁铺区(x 78~98, y 0.8~8.9),160 mm 长的悬空铜条 在 2.4 GHz 附近不是个能忽略的东西。 ## 做法 每块浮铜里挑几个「三层都让得开、钻孔间距也够」的点打 GND 过孔。 过孔是通孔 ⇒ 一个孔就把这块 Inner2 的铜接到 **Inner1 的完整地平面**上。 落点判据直接借 `pcb1_i2c_l3.Field.via_ok()`(外层真净距 + L3 真铜 + 不分网络的钻孔间距)。 """ 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 import pcb1_i2c_l3 as M # noqa: E402 import pcb1_netlist as NL # noqa: E402 import pcb1_route_sig as RS # noqa: E402 #: 想在哪些地方缝。每条是 (说明, x0, y0, x1, y1, 要几个孔)。 #: 南沿那条避开 ESP32 天线禁铺区 x 78~98(`pcb_fanout.ANT`,全层不许有铜)。 ZONES = [ (u'南沿西段', 3.0, 2.0, 74.0, 8.0, 3), (u'南沿东段', 101.0, 2.0, 157.0, 8.0, 3), ] MINGAP = 8.0 # 同一次补的孔彼此至少隔这么远,别扎堆 def pick(zone, f, used): u"""在一个矩形里挑 n 个合法孔位,尽量摊开。""" name, x0, y0, x1, y1, n = zone ok = f.via_ok() out = [] for k in range(n): # 目标位置:把区间等分,然后在目标点附近找最近的合法格 tx = x0 + (x1 - x0) * (k + 0.5) / n ty = (y0 + y1) / 2.0 best = None i0, j0 = f._idx(x0, y0) i1, j1 = f._idx(x1, y1) for j in range(max(0, j0), min(f.ny, j1 + 1)): for i in range(max(0, i0), min(f.nx, i1 + 1)): if not ok[j, i]: continue x, y = f.x0 + i * f.g, f.y0 + j * f.g if any(math.hypot(x - ux, y - uy) < MINGAP for ux, uy in used + out): continue d = math.hypot(x - tx, y - ty) if best is None or d < best[0]: best = (d, round(x, 3), round(y, 3)) if best is None: print(u' ⚠️ %s 第 %d 个孔位没找到(净距或钻孔间距都不够)' % (name, k + 1)) continue out.append((best[1], best[2])) return out def main(): push = '--push' in sys.argv pads, vias, lines = NL.geom(push or '--refresh' in sys.argv) print(u'板上:过孔 %d / 走线 %d' % (len(vias), len(lines))) got, used = [], [] for z in ZONES: f = M.Field((z[1] - 2.0, z[2] - 2.0, z[3] + 2.0, z[4] + 2.0), 'GND', pads, vias, lines, []) pts = pick(z, f, used) used += pts got += pts print(u' %s:%s' % (z[0], ' '.join('(%.2f,%.2f)' % p for p in pts) or u'无')) if not got: sys.exit(u'一个孔位都没挑出来') # 校核:借 pcb1_i2c_l3.verify 的孔判据(路径写成"原地换层",只出一个孔、没有线段) routes = [('GND', [(x, y, M.L3, M.WIRE), (x, y, M.L_TOP, M.WIRE), (x, y, M.L_TOP, M.WIRE)]) for x, y in got] bad = M.verify(routes, pads, vias, lines) for b in bad[:20]: print(u' ✗ ' + b) print(u' 校核:%s' % (u'✅ 全过(%d 个孔)' % len(got) if not bad else u'❌ %d 条' % len(bad))) if bad: sys.exit(1) if not push: print(u'\n(只算不下发。要下发加 --push)') return RS.ROUTES = os.path.join(HERE, '_pcb1_gnd_stitch.json') RS.push({('GND%s%03d' % (chr(0), i)): p for i, (_n, p) in enumerate(routes)}) print(u'⚠️ 新过孔穿过内层平面 —— 现在必须重铺一次:' u'/c/Python314/python .claude/pcb1_pour.py --push --refresh') if __name__ == '__main__': main()