初始导入: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
这个提交包含在:
zhou yang
2026-09-06 18:18:14 +08:00
共同撰写人 Claude Opus 5
当前提交 2bfe308b30
共修改 526 个文件,包含 80633 行新增和 0 行删除
+243
查看文件
@@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
u"""把 Inner1 上**真正铺出来的铜**读回来栅格化,量连通性与最窄断面。
/c/Python314/python .claude/inner1_verify.py [--grid 0.05]
## 为什么不能只看 `pcb2_pour.py --verify` 打印的「段数」
`getState_PourFills()` 把一块铜面吐成十几个 fill,**那多半是切块不是断开**:
相邻块共边。所以「17 段」既可能是一整块被切碎,也可能真碎成 17 座孤岛,
段数本身分辨不出来。唯一靠得住的判据是**把所有环画进同一张栅格再做连通域**。
`inner1_check.py` 是**下发前**按 ISLANDS 多边形 + 反焊盘自己算的(预测),
本脚本是**下发后**按 EDA 实际吐出来的铜算的(实测)。两个都要过。
⚠️ 环的格式是 `x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...`,
'ARC' 后面第一个数是**角度不是坐标**;这里按起点/终点/张角反解圆心,
再按 <=5° 细分成折线 —— 直接把弧当弦会把反焊盘算大、低估颈宽。
"""
import json, math, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
from pcb2_pour import ISLANDS, PCB, MIL, body_bbox
U10MIL = 3.9370078740157477 # getState_PourFills() 的坐标单位:10 mil
NECK_MIN = 7.0
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) continue;
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
const rings = [];
let spokes = 0;
for (const seg of f) {
const cp = (seg.path && seg.path.complexPolygon) || [];
// fill=false 的 seg 是**热焊盘十字花的辐条**(描边折线,lineWidth 一般 0.254),
// 不是填充区。出现它就说明这块铜面的焊盘连接方式退回了「十字花」。
if (seg.fill === false) { spokes += cp.length; continue; }
for (const r of cp) rings.push(r);
}
out[info[0]] = {net: info[1], layer: info[2], rings: rings, spokes: spokes, nfill: f.length};
}
return out;
""" % PCB
def ring_to_poly(ring):
u"""一个环 -> 折线点列(mm)。ARC 按 <=5° 细分。"""
pts, i = [], 0
cur = None
while i < len(ring):
v = ring[i]
if v == 'L':
i += 1
continue
if v == 'ARC':
i += 1
ang = ring[i]
ex, ey = ring[i + 1], ring[i + 2]
i += 3
x0, y0 = cur
th = math.radians(ang)
mx, my = (x0 + ex) / 2.0, (y0 + ey) / 2.0
dx, dy = ex - x0, ey - y0
c = math.hypot(dx, dy)
if c < 1e-9 or abs(math.sin(th / 2.0)) < 1e-9:
pts.append((ex, ey))
cur = (ex, ey)
continue
R = c / (2.0 * math.sin(th / 2.0)) # 带符号
d = math.sqrt(max(R * R - (c / 2.0) ** 2, 0.0)) * (1 if R > 0 else -1)
ox, oy = -dy / c, dx / c # 左法向
ccx, ccy = mx + ox * d, my + oy * d
a0 = math.atan2(y0 - ccy, x0 - ccx)
n = max(2, int(abs(ang) / 5.0) + 1)
for k in range(1, n + 1):
a = a0 + th * k / n
pts.append((ccx + abs(R) * math.cos(a), ccy + abs(R) * math.sin(a)))
cur = (ex, ey)
continue
x, y = ring[i], ring[i + 1]
i += 2
pts.append((x, y))
cur = (x, y)
return [(x / U10MIL, y / U10MIL) for x, y in pts]
def raster(polys, g):
u"""所有环画进一张栅格,奇偶填充(外环填、内环挖)。"""
xs = [p[0] for q in polys for p in q]
ys = [p[1] for q in polys for p in q]
x0, x1, y0, y1 = min(xs) - g, max(xs) + g, min(ys) - g, max(ys) + g
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 = []
for (ax, ay), (bx, by) in edges:
if (ay > y) != (by > y):
cr.append(ax + (bx - ax) * (y - ay) / (by - ay))
cr.sort()
row = grid[j]
for t in range(0, len(cr) - 1, 2): # 奇偶填充
i0 = max(0, int(math.ceil((cr[t] - x0) / g - 0.5)))
i1 = min(nx - 1, int(math.floor((cr[t + 1] - x0) / g - 0.5)))
for i in range(i0, i1 + 1):
row[i] = 1
return grid, x0, y0, nx, ny
def components(grid, nx, ny):
lab = [[0] * nx for _ in range(ny)]
k, sizes = 0, {}
for j in range(ny):
for i in range(nx):
if not grid[j][i] or lab[j][i]:
continue
k += 1
st = [(j, i)]
lab[j][i] = k
n = 0
while st:
a, b = st.pop()
n += 1
for da, db in ((1, 0), (-1, 0), (0, 1), (0, -1)):
p, q = a + da, b + db
if 0 <= p < ny and 0 <= q < nx and grid[p][q] and not lab[p][q]:
lab[p][q] = k
st.append((p, q))
sizes[k] = n
return lab, sizes
def main():
g = 0.05
if '--grid' in sys.argv:
g = float(sys.argv[sys.argv.index('--grid') + 1])
r = run(JS, timeout=240)
if not r.get('success'):
sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400])
data = r['result']
pads = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
vias = json.load(open(os.path.join(HERE, '_pcb2_vias.json'), encoding='utf-8'))
print(u'Inner1 实测铜面校核(栅格 %.2f mm,颈宽下限 %.1f mm)' % (g, NECK_MIN))
allok = True
for name, net, _m, _n in ISLANDS:
key = u'%s-INNER1' % name
d = data.get(key)
if not d:
print(u' %-12s [!!] 没读到这块铜面' % key)
allok = False
continue
if d.get('spokes'):
print(u' %-12s [!!] 有 %d 条十字花辐条 —— 这块铜面的焊盘连接方式不是「全连接」,'
u'4 A 只能从 0.254 mm 的辐条里过。去 pcb2_rules.py 看 PLANE-DIRECT 有没有指派上。'
% (key, d['spokes']))
allok = False
polys = [p for p in (ring_to_poly(rg) for rg in d['rings']) if len(p) >= 3]
grid, x0, y0, nx, ny = raster(polys, g)
lab, sizes = components(grid, nx, ny)
main_k = max(sizes, key=sizes.get) if sizes else 0
area = sizes.get(main_k, 0) * g * g
tot = sum(sizes.values()) * g * g
bad, own = [], 0
for ref, num, pnet, layer, px, py, shape, hole in pads:
if pnet != net or layer not in (12, 15):
continue
own += 1
px, py = px / MIL, py / MIL
rad = max(shape[1], shape[2] if len(shape) > 2 and isinstance(shape[2], (int, float)) else shape[1])
rr = int((rad / MIL / 2.0 + 0.35) / g) + 1
i, j, hit = int((px - x0) / g), int((py - y0) / g), False
for q in range(max(0, j - rr), min(ny, j + rr + 1)):
for p in range(max(0, i - rr), min(nx, i + rr + 1)):
if lab[q][p] == main_k:
hit = True
break
if hit:
break
if not hit:
bad.append('%s.%s' % (ref, num))
# ⚠️ 焊盘之外,**本网的过孔也必须落在主域里**:贴片电容全靠这一个孔接岛
# (`pcb2_vo_vias.py` 打的 25 个)。只查焊盘的话,孔掉进边角碎片里没人发现。
nvia = 0
for vnet, vx, vy, dia, _hole in vias:
if vnet != net:
continue
nvia += 1
vx, vy = vx / MIL, vy / MIL
rr = int((dia / MIL / 2.0 + 0.35) / g) + 1
i, j, hit = int((vx - x0) / g), int((vy - y0) / g), False
for q in range(max(0, j - rr), min(ny, j + rr + 1)):
for pp in range(max(0, i - rr), min(nx, i + rr + 1)):
if lab[q][pp] == main_k:
hit = True
break
if hit:
break
if not hit:
bad.append(u'过孔(%.2f,%.2f)' % (vx, vy))
# v2:颈宽只在走廊主体里量(穿筋的颈 3 mm 只走纹波电流,§25.16)
rx0, ry0, rx1, ry1 = body_bbox(_m)
# 边界行只剩铺铜内缩后的一丝铜(实测 2.8 = 穿筋的颈),各缩 0.15 再量
rx0, ry0, rx1, ry1 = rx0 + 0.15, ry0 + 0.15, rx1 - 0.15, ry1 - 0.15
rowsY = [t for t in ((sum(1 for i in range(nx) if lab[j][i] == main_k and rx0 <= x0 + (i + 0.5) * g <= rx1) * g,
y0 + (j + 0.5) * g)
for j in range(ny) if ry0 <= y0 + (j + 0.5) * g <= ry1) if t[0] > 0]
colsX = [t for t in ((sum(1 for j in range(ny) if lab[j][i] == main_k and ry0 <= y0 + (j + 0.5) * g <= ry1) * g,
x0 + (i + 0.5) * g)
for i in range(nx) if rx0 <= x0 + (i + 0.5) * g <= rx1) if t[0] > 0]
my, mx = min(rowsY), min(colsX)
ok = (not bad) and my[0] >= NECK_MIN and mx[0] >= NECK_MIN
allok = allok and ok
print(u' %-12s %-7s %d 环 / %d 连通域,主域 %.0f mm²(占本网铜的 %.1f%%)'
% (key, net, len(polys), len(sizes), area, 100.0 * area / tot if tot else 0))
print(u' %d 焊盘 + %d 过孔 %s 最窄横断面 %5.2f @ y=%.2f 最窄纵断面 %5.2f @ x=%.2f %s'
% (own, nvia, u'全在主域 ✔' if not bad else u'✘ 掉队: ' + ' '.join(bad),
my[0], my[1], mx[0], mx[1], u'✅' if ok else u'❌'))
for key in (u'GND-INNER1', u'GND-INNER2'):
d = data.get(key)
if d:
print(u' %-12s %-7s %d 环' % (key, d['net'], len(d['rings'])))
print(u'%s' % (u'实测全部通过。' if allok else u'!! 实测有不达标的岛。'))
return 0 if allok else 1
if __name__ == '__main__':
sys.exit(main())