初始导入:PhoneRail 五路手机直供电源板
原理图/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
这个提交包含在:
@@ -0,0 +1,219 @@
|
||||
# -*- 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())
|
||||
在新工单中引用
屏蔽一个用户