文件
phonerail/.claude/pcb2_clear.py
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 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
2026-09-10 21:02:10 +08:00

184 行
8.3 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板的**焊盘级净间距**实测检查。
/c/Python314/python .claude/pcb2_clear.py [--min 0.2] [--top 40]
## 为什么要单独有这一条
`pcb2_place.py` 的本地检查用的是**元件焊盘包围盒**,判据是「不重叠」。
§25.3 那个错误(立创给顶针的 Ø2.5 焊盘 vs 侧键触点最近 2.217 mm)
正好卡在这两者之间:包围盒不重叠、但两个圆焊盘的净间距是 **负的**。
所以摆位过了不等于能打样,**必须再按焊盘真实形状量一次净间距**。
这里不依赖 EasyEDA 的 DRC —— 没布线时 DRC 会刷出 1200+ 条「连接性错误」,
真正的间距错误埋在里面看不见(成因见 `drc_read.py` 的注释)。
## 算法
按层分桶(多层焊盘 layer=12 进所有铜层),网格哈希做邻域筛,
两两算**形状到形状**的最短距离:
· RECT / OVAL 都按「圆角矩形」处理,退化成两个轴对齐矩形的间距,
ELLIPSE 按圆算 —— 保守方向(矩形算出来的间距 <= 真实带圆角的间距)。
· 同网络的跳过(同网络挨着是正常的)。
"""
import json, math, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '942ed1560d81c1e7'
MIL = 39.3700787401575
COPPER = (1, 15, 16, 2)
def fetch():
r = run("""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1500));
const out=[];
for (const p of await eda.pcb_PrimitivePad.getAll())
out.push([p.getState_Layer(), p.getState_X(), p.getState_Y(), p.getState_Rotation(),
p.getState_Pad(), p.getState_Net(), p.getState_PadNumber()]);
// ⚠⚠ 焊盘归属哪个元件,两条看着能用的路都是错的:
// ① 坐标匹配 getAllPinsByPrimitiveId 的 x/y —— 有浮点尾巴,且 POLYGON 焊盘的
// x/y 根本不是形心,会漏一大片;
// ② `getState_ParentComponentPrimitiveId()` —— **pcb_PrimitivePad.getAll() 回来的
// 对象上没有这个方法**(TypeError: not a function)。写 try/catch 包起来的话
// 它会静默地把**所有**焊盘都当成自由焊盘,看不出错。
// 真正的判据:**焊盘的 primitiveId 以父元件的 primitiveId 为前缀**。
// 元件 C701 = "356da09e850d14fa",它 1 脚 = "356da09e850d14fae16"。
const name = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll())
name[c.getState_PrimitiveId()] = c.getState_Designator();
const ids = Object.keys(name).sort((a, b) => b.length - a.length); // 长的优先,避免前缀套前缀
const own = [];
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const pid = p.getState_PrimitiveId();
let hit = '';
for (const id of ids) if (pid.length > id.length && pid.indexOf(id) === 0) { hit = name[id]; break; }
own.push(hit);
}
return {pads: out, own};
""" % PCB, timeout=300)
if not r.get('success'):
sys.exit(json.dumps(r, ensure_ascii=False)[:600])
return r['result']
def deg(rot):
u"""⚠⚠ **焊盘的 rotation 是弧度,元件的 rotation 是角度。**
`pcb_PrimitivePad.getState_Rotation()` 回来的是 `1.5707963267948963`(= π/2),
而 `pcb_PrimitiveComponent.getState_Rotation()` 回来的是 `90`。
按角度去判 `rot % 180 == 90`,弧度值永远不等于 90 —— 于是**所有旋转过的
矩形焊盘长短边都会被认反**。J23 那 12 个 0.30 × 1.30 的信号焊盘因此被
误判成两两重叠 0.30 mm(真实净间距 +0.20 mm),13 条假报。
这类错误最阴的地方是**方向对了一半**:不旋转的焊盘全对,只有旋转过的错,
所以整体看上去像是「某个封装有问题」,而不像是「换算错了」。
"""
return rot * 180.0 / math.pi if abs(rot) <= 2 * math.pi + 1e-6 else rot
def half(shape, rot, x, y):
u"""焊盘的中心 + 轴对齐半宽/半高(mm)。圆/椭圆额外返回半径,用来算真圆间距。
⚠️ `POLYGON` 型焊盘(J23 的 4 个电源/屏蔽脚就是)**点表是绝对坐标**,
不是相对焊盘原点的 —— 直接拿 x/y 当中心会算到十万八千里去。
这里改用点表自己的包围盒,忽略 x/y。
"""
kind = shape[0]
if kind == 'POLYGON':
pts = [v for v in shape[1] if isinstance(v, (int, float))]
xs, ys = pts[0::2], pts[1::2]
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
return (cx / MIL, cy / MIL,
(max(xs) - min(xs)) / 2.0 / MIL, (max(ys) - min(ys)) / 2.0 / MIL, None)
w, h = float(shape[1]) / MIL, float(shape[2]) / MIL
if round(deg(rot)) % 180 == 90:
w, h = h, w
r = w / 2.0 if (kind == 'ELLIPSE' and abs(w - h) < 1e-6) else None
return x / MIL, y / MIL, w / 2.0, h / 2.0, r
def gap(a, b):
u"""两个焊盘的净间距(mm)。两个真圆用圆心距,否则用轴对齐矩形。"""
dx, dy = abs(a['x'] - b['x']), abs(a['y'] - b['y'])
if a['r'] is not None and b['r'] is not None:
return math.hypot(dx, dy) - a['r'] - b['r']
return max(dx - a['hw'] - b['hw'], dy - a['hh'] - b['hh'])
def main():
minclr = 0.2 # 跨元件判据
intra = 0.0 # 同封装内只报**负间距**(真重叠)
topn = 40
if '--min' in sys.argv:
minclr = float(sys.argv[sys.argv.index('--min') + 1])
if '--top' in sys.argv:
topn = int(sys.argv[sys.argv.index('--top') + 1])
raw = fetch()
own = raw['own']
pads = []
for lay, x, y, rot, shape, net, num in raw['pads']:
cx, cy, hw, hh, rr = half(shape, rot, x, y)
ref = own[len(pads)]
pads.append({'lay': lay, 'x': cx, 'y': cy, 'hw': hw, 'hh': hh,
'r': rr, 'net': net or '', 'num': num, 'ref': ref})
print(u'焊盘 %d 个,铜层 %s,判据「不同网络净间距 >= %.2f mm」'
% (len(pads), COPPER, minclr))
worst, inner = [], []
for lay in COPPER:
cur = [p for p in pads if p['lay'] == lay or p['lay'] == 12]
cell = 3.0
grid = {}
for i, p in enumerate(cur):
grid.setdefault((int(p['x'] // cell), int(p['y'] // cell)), []).append(i)
seen = set()
for (gx, gy), idxs in grid.items():
near = []
for ax in (-1, 0, 1):
for ay in (-1, 0, 1):
near += grid.get((gx + ax, gy + ay), [])
for i in idxs:
for j in near:
if j <= i or (i, j) in seen:
continue
seen.add((i, j))
a, b = cur[i], cur[j]
if a['net'] and a['net'] == b['net']:
continue
g = gap(a, b)
same = bool(a['ref']) and a['ref'] == b['ref']
if g < (intra if same else minclr):
(inner if same else worst).append((g, lay, a, b))
worst.sort(key=lambda t: t[0])
inner.sort(key=lambda t: t[0])
if inner:
print(u'\n同一封装内**负间距**(焊盘真重叠)%d 对 —— 这是封装自身的几何,'
u'不是摆位问题,但出现就说明封装本身要复核:' % len(inner))
for g, lay, a, b in inner[:12]:
print(u' L%-2d %7.3f mm %s.%s(%s) x %s.%s(%s)'
% (lay, g, a['ref'], a['num'], a['net'] or u'悬空',
b['ref'], b['num'], b['net'] or u'悬空'))
if len(inner) > 12:
print(u' ...还有 %d 对' % (len(inner) - 12))
if not worst:
print(u'\n通过:**跨元件**的不同网络焊盘对,净间距全部 >= %.2f mm' % minclr)
return 1 if inner else 0
print(u'\n!! 跨元件 %d 对低于 %.2f mm(由小到大):' % (len(worst), minclr))
for g, lay, a, b in worst[:topn]:
print(u' L%-2d %7.3f mm %s.%s(%s) @(%.2f,%.2f) x %s.%s(%s) @(%.2f,%.2f)'
% (lay, g, a['ref'] or '?', a['num'], a['net'] or '悬空', a['x'], a['y'],
b['ref'] or '?', b['num'], b['net'] or '悬空', b['x'], b['y']))
if len(worst) > topn:
print(u' ...还有 %d 对' % (len(worst) - topn))
return 1
if __name__ == '__main__':
sys.exit(main())