原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
220 行
9.4 KiB
Python
220 行
9.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""§10.1 的参考面检查:顶层那 5 段 `*J` 差分(U5n→J7n)下方的 Inner1 是不是**一整块同网络的铜**。
|
||
|
||
/c/Python314/python .claude/inner1_diffref.py [--grid 0.05] [--margin 1.0]
|
||
/c/Python314/python .claude/inner1_diffref.py --routes # 沿**实际走线**逐点查(长差分对用这个)
|
||
|
||
载板的层角色是镜像的(Inner1 = 分割电源面,Inner2 = 完整地),
|
||
所以**底层**的长距离 D± 参考 Inner2(完整地,没问题),
|
||
而**顶层**这 5 段 5 mm 的短差分参考的是 Inner1 —— 那一层是被五块 `VOn_P` 岛切开的。
|
||
段长只有 ~5 mm、又紧挨 ESD 器件,可以接受,
|
||
但前提是**每段下方不能横穿分割缝**(§10.1)。
|
||
|
||
判据:把两根线的包围盒外扩 `margin`,在这个窗口里
|
||
① 只能有**一个**网络的铜面覆盖;② 覆盖必须是**一个连通域**;③ 覆盖率 100%。
|
||
"""
|
||
import json, os, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
from pcb2_pour import PCB, MIL
|
||
from inner1_verify import ring_to_poly, components
|
||
|
||
L_PWR = 15
|
||
|
||
JS = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r=>setTimeout(r,1800));
|
||
const byId = {};
|
||
for (const p of await eda.pcb_PrimitivePour.getAll())
|
||
byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''), p.getState_Net(), p.getState_Layer()];
|
||
const out = {};
|
||
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
|
||
const info = byId[q.getState_PourPrimitiveId()]; if (!info || info[2] !== %d) continue;
|
||
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
|
||
const rings = [];
|
||
for (const seg of f) {
|
||
if (seg.fill === false) continue;
|
||
for (const r of ((seg.path && seg.path.complexPolygon) || [])) rings.push(r);
|
||
}
|
||
out[info[0]] = {net: info[1], rings: rings};
|
||
}
|
||
return out;
|
||
""" % (PCB, L_PWR)
|
||
|
||
|
||
def cover(polys, x0, y0, x1, y1, g):
|
||
u"""窗口内被这些环覆盖的栅格(奇偶填充,逐环按所属 fill 分组已在调用方做)。"""
|
||
nx = int((x1 - x0) / g) + 1
|
||
ny = int((y1 - y0) / g) + 1
|
||
edges = []
|
||
for q in polys:
|
||
for k in range(len(q)):
|
||
a, b = q[k], q[(k + 1) % len(q)]
|
||
if a[1] != b[1]:
|
||
edges.append((a, b))
|
||
grid = [bytearray(nx) for _ in range(ny)]
|
||
for j in range(ny):
|
||
y = y0 + (j + 0.5) * g
|
||
cr = sorted(ax + (bx - ax) * (y - ay) / (by - ay)
|
||
for (ax, ay), (bx, by) in edges if (ay > y) != (by > y))
|
||
row = grid[j]
|
||
for t in range(0, len(cr) - 1, 2):
|
||
i0 = max(0, int((cr[t] - x0) / g))
|
||
i1 = min(nx - 1, int((cr[t + 1] - x0) / g))
|
||
for i in range(i0, i1 + 1):
|
||
row[i] = 1
|
||
return grid, nx, ny
|
||
|
||
|
||
JS_TOPLINES = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r=>setTimeout(r,1800));
|
||
const out = [];
|
||
for (const o of await eda.pcb_PrimitiveLine.getAll()) {
|
||
if (o.getState_Layer() !== 1) continue;
|
||
const n = o.getState_Net() || '';
|
||
if (!/^(USB[0-9]_D[PM]J?|UP_D[PM])$/.test(n)) continue;
|
||
out.push([n, o.getState_StartX(), o.getState_StartY(), o.getState_EndX(), o.getState_EndY()]);
|
||
}
|
||
return out;
|
||
""" % PCB
|
||
|
||
|
||
def pt_in(polys, x, y):
|
||
u"""奇偶填充的点包含判定(所有环一起算,孔自然被挖掉)。"""
|
||
c = False
|
||
for q in polys:
|
||
for k in range(len(q)):
|
||
(ax, ay), (bx, by) = q[k], q[(k + 1) % len(q)]
|
||
if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay):
|
||
c = not c
|
||
return c
|
||
|
||
|
||
def by_routes(pours, step=0.1):
|
||
u"""沿顶层**实际走线**逐点查参考面。
|
||
|
||
⚠ 判据是「**每一条连通的顶层走线**下方只能有一种铜」,不是「每个网络」——
|
||
一个网络在板上可能有好几段互不相连的顶层走线(比如 pair5:过隘口那一段
|
||
压在 `VO2_P` 岛上,进 U55 焊盘那一小段压在 GND 底填上),
|
||
各自压在各自的一整块铜上就没问题,**没有哪一段横穿了缝**。
|
||
⚠ 「裸露」点不算违规:那是过孔 / 焊盘的反焊盘(Ø≈1.0),线穿过它天经地义。
|
||
"""
|
||
r = run(JS_TOPLINES, timeout=240)
|
||
if not r.get('success'):
|
||
sys.exit(u'读顶层导线失败: ' + json.dumps(r, ensure_ascii=False)[:400])
|
||
segs = {}
|
||
for n, x0, y0, x1, y1 in r['result']:
|
||
segs.setdefault(n, []).append((x0 / MIL, y0 / MIL, x1 / MIL, y1 / MIL))
|
||
|
||
def runs(lst):
|
||
u"""按端点把线段并成若干条连通走线(并查集,端点取 0.01 mm 容差)。"""
|
||
par = list(range(len(lst)))
|
||
def find(i):
|
||
while par[i] != i:
|
||
par[i] = par[par[i]]; i = par[i]
|
||
return i
|
||
key = {}
|
||
for i, (x0, y0, x1, y1) in enumerate(lst):
|
||
for k in ((round(x0, 2), round(y0, 2)), (round(x1, 2), round(y1, 2))):
|
||
if k in key:
|
||
a, b = find(key[k]), find(i)
|
||
if a != b:
|
||
par[a] = b
|
||
else:
|
||
key[k] = i
|
||
out = {}
|
||
for i, seg in enumerate(lst):
|
||
out.setdefault(find(i), []).append(seg)
|
||
return list(out.values())
|
||
|
||
print(u'§10.1 参考面检查(沿实际走线,步长 %.2f mm):顶层每一条连通走线下方的 Inner1' % step)
|
||
allok = True
|
||
for net in sorted(segs):
|
||
for run_i, run_segs in enumerate(runs(segs[net])):
|
||
seen, bare, tot = {}, 0, 0
|
||
xs, ys = [], []
|
||
for x0, y0, x1, y1 in run_segs:
|
||
xs += [x0, x1]; ys += [y0, y1]
|
||
L = ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5
|
||
m = max(2, int(L / step) + 1)
|
||
for i in range(m + 1):
|
||
t = i / float(m)
|
||
x, y = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t
|
||
tot += 1
|
||
hit = [d['net'] for d in pours.values() if pt_in(d['polys'], x, y)]
|
||
if not hit:
|
||
bare += 1
|
||
for h in hit:
|
||
seen[h] = seen.get(h, 0) + 1
|
||
ok = len(seen) <= 1
|
||
allok = allok and ok
|
||
print(u' %-10s #%d x %6.1f~%6.1f y %5.1f~%5.1f %2d 段/%4d 点 -> %-28s 反焊盘 %3d %s'
|
||
% (net, run_i, min(xs), max(xs), min(ys), max(ys), len(run_segs), tot,
|
||
u' + '.join(u'%s %.0f%%' % (k, 100.0 * v / tot) for k, v in sorted(seen.items()))
|
||
or u'(无铜)', bare, u'OK' if ok else u'!! 跨缝'))
|
||
print(u'%s' % (u'每一条顶层走线都只压在一块铜上。' if allok
|
||
else u'!! 有走线横穿了 Inner1 的分割缝 —— 违反 §10.1。'))
|
||
return 0 if allok else 1
|
||
|
||
|
||
def main():
|
||
g, margin = 0.05, 1.0
|
||
if '--grid' in sys.argv:
|
||
g = float(sys.argv[sys.argv.index('--grid') + 1])
|
||
if '--margin' in sys.argv:
|
||
margin = float(sys.argv[sys.argv.index('--margin') + 1])
|
||
pads = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
|
||
|
||
r = run(JS, timeout=240)
|
||
if not r.get('success'):
|
||
sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400])
|
||
pours = dict((k, {'net': v['net'],
|
||
'polys': [p for p in (ring_to_poly(rg) for rg in v['rings']) if len(p) >= 3]})
|
||
for k, v in r['result'].items())
|
||
|
||
if '--routes' in sys.argv:
|
||
return by_routes(pours)
|
||
|
||
print(u'§10.1 参考面检查:顶层 5 段 `*J` 差分下方的 Inner1(窗口 = 包围盒外扩 %.1f mm)' % margin)
|
||
allok = True
|
||
for n in range(1, 6):
|
||
pts = [(p[4] / MIL, p[5] / MIL) for p in pads
|
||
if p[2] in ('USB%d_DPJ' % n, 'USB%d_DMJ' % n) and p[3] == 1]
|
||
if len(pts) != 4:
|
||
print(u' CH%d [!!] 只找到 %d 个焊盘,应为 4' % (n, len(pts)))
|
||
allok = False
|
||
continue
|
||
x0 = min(p[0] for p in pts) - margin
|
||
x1 = max(p[0] for p in pts) + margin
|
||
y0 = min(p[1] for p in pts) - margin
|
||
y1 = max(p[1] for p in pts) + margin
|
||
hits = []
|
||
for name, d in pours.items():
|
||
grid, nx, ny = cover(d['polys'], x0, y0, x1, y1, g)
|
||
cells = sum(sum(row) for row in grid)
|
||
if not cells:
|
||
continue
|
||
lab, sizes = components(grid, nx, ny)
|
||
hits.append((name, d['net'], cells, nx * ny, len(sizes)))
|
||
tot = nx * ny
|
||
if len(hits) == 1 and hits[0][4] == 1 and hits[0][2] == tot:
|
||
print(u' CH%d x %.2f~%.2f y %.2f~%.2f -> %s (%s) 一整块盖满 ✅'
|
||
% (n, x0, x1, y0, y1, hits[0][0], hits[0][1]))
|
||
else:
|
||
allok = False
|
||
print(u' CH%d x %.2f~%.2f y %.2f~%.2f -> ❌ %s'
|
||
% (n, x0, x1, y0, y1,
|
||
u' / '.join(u'%s(%s) 盖 %.1f%% %d 域'
|
||
% (a, b, 100.0 * c / t, k) for a, b, c, t, k in hits)
|
||
or u'下方没有任何 Inner1 铜'))
|
||
print(u'%s' % (u'五段全部落在同一块连续铜上。' if allok
|
||
else u'!! 有段下方是分割缝或多块铜 —— 违反 §10.1,要挪岛或挪线。'))
|
||
return 0 if allok else 1
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|