初始导入: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 行删除
+211
查看文件
@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
u"""自建 BM24-50DS/2-0.35V(51) 封装(方案 D 的载板座子 J71~J75)。
为什么要自建:立创的 `CONN-SMD_BBM24-50DS-2-0.35V51` 用的是 **DIP 绕圈编号**
(1..25 一排、26..50 反着排另一排),而小米8 J1401 / Hirose 用的是**奇偶行编号**
(1,3,5..49 一排 / 2,4,6..50 另一排)。照搬会把 50 个脚全接错。见设计文档 §13.4 末尾。
─────────────────────────────────────────────────────────────────────
几何来源:**Hirose BM24 系列官方样本**(2021.12③ / Aug.1.2026 版,
D47680_en,"Receptacle → Recommended PCB layout")。
不是外推、不是抄立创 —— 数值是从样本 PDF 的矢量图里逐段反算的:
· 标定:图上 P=0.35mm 对应 6.706 pt、B=3.15mm 对应 60.355 pt → 19.1603 pt/mm(两处自洽)
· 图画的是 20 芯那一款(C=7.3 / B=3.15 都对得上表)
尺寸表(50 芯):A=12.25 B=8.4 C=12.55
反算出的真实含义(样本里没写全,是量出来的):
· B = 同一排最外侧两个信号焊盘的**中心距** = 24 × 0.35 = 8.4 ✔
· C = 两个电源焊盘的**外缘到外缘** = 12.55 → 电源焊盘中心 x = ±(12.55−0.49)/2 = ±6.03
· 0.49±0.02 是**电源焊盘的宽(x)**,不是信号焊盘长度(尺寸线量的是 95.44→104.83 那一段)
· 信号焊盘长(y) = (2.6 − 1.64)/2 = 0.48 —— 样本没直接标,由外缘 2.6 与内缘 1.64 得出
· 两排中心距 = (2.6+1.64)/2 = 2.12 → 每排 y = ±1.06(和 Header 图上标的 1.06 对得上)
· 禁止布线区 = 两个电源焊盘内缘之间 × 两排焊盘内缘之间 = ±5.785 × ±0.82
· 0.14MAX = 中线上允许穿过禁布区的那一条细缝 —— **我们不用**(G1 是地、G2 是 VBUS,
这两个电源触点绝不能连起来)
─────────────────────────────────────────────────────────────────────
✅ 编号约定(曾经唯一没有实证的一位,**2026-09-05 已实测确认,见设计文档 §24.3**):
ODD_ROW_TOP = True → 奇数脚 1,3,5…49 在 **+Y 那一排**,2,4,6…50 在 −Y 排
1 脚在 **−X 端**(x = −4.20),49/50 在 +X 端
pin 51 = 1/2 脚那一端的电源触点(x = −6.03)
pin 52 = 49/50 脚那一端的电源触点(x = +6.03)
符号侧(`c5_j1401.py` 的 PWR)已经定死 52 = GND、51 = VBUS。
用法:
/c/Python314/python .claude/fp_bm24.py # 只打印将要下发的几何
/c/Python314/python .claude/fp_bm24.py --push # 真的画进封装
/c/Python314/python .claude/fp_bm24.py --verify # 读回校验
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
LIB_PERSONAL = '543725e8f0454017a5691e7e570aeef6'
FP_UUID = '940f093c1578426aa81e91ebf0bfbd76' # BM24-50DS_HIROSE-PIN
FP_NAME = 'BM24-50DS_HIROSE-PIN'
MIL = 39.3700787401575 # 1 mm = 39.37 mil;PCB 域的单位是 1 mil
# ---- Hirose 推荐 PCB 图(50 芯),单位 mm ----
PITCH = 0.35
NCOL = 25
X0 = -(NCOL - 1) * PITCH / 2.0 # = -4.20
ROW_Y = 1.06
SIG_W, SIG_H = 0.18, 0.48
PWR_X = 6.03
PWR_W, PWR_H = 0.49, 0.76
BODY_L, BODY_W = 12.25, 2.30 # A × 2.3,装配层用
PROHIB_X, PROHIB_Y = 5.785, 0.82
# ✅ 2026-09-05 实测定案(设计文档 §24.3):手机 J1401 上**靠板边那一排是奇数排**,
# 而排线是从板边直出、1:1 内侧↔内侧,载板这颗座子的 +Y 排(朝手机那一排)因此也是奇数排。
# ⇒ True 是对的。翻这一位就能整颗镜像重生成(1 脚丝印点会跟着走,见 DOT)。
ODD_ROW_TOP = True
SILK_X, SILK_Y = 6.60, 1.45 # 丝印框(在所有焊盘之外:电源焊盘外缘 6.275、信号焊盘上缘 1.30)
SILK_W = 6.0 # mil
# 1 脚标记:**必须跟着 ODD_ROW_TOP 走** —— 1 脚在 −X 端的「奇数排」那一侧。
# 写死成 +1.75 的话,翻镜像位之后丝印上的 1 脚点会指着 2 脚,
# 而几何验证与 DRC 都看不出来(丝印不参与任何检查)。
DOT = (-7.05, 1.75 if ODD_ROW_TOP else -1.75)
def mil(v):
return round(v * MIL, 4)
def pads():
u"""返回 [(padNumber, x_mm, y_mm, w_mm, h_mm), ...]"""
out = []
top_y = ROW_Y if ODD_ROW_TOP else -ROW_Y
bot_y = -top_y
for k in range(1, NCOL + 1):
x = X0 + (k - 1) * PITCH
out.append((2 * k - 1, x, top_y, SIG_W, SIG_H))
out.append((2 * k, x, bot_y, SIG_W, SIG_H))
out.append((51, -PWR_X, 0.0, PWR_W, PWR_H))
out.append((52, +PWR_X, 0.0, PWR_W, PWR_H))
return out
def build_js():
P = pads()
body = []
for num, x, y, w, h in P:
body.append("await eda.pcb_PrimitivePad.create(1,'%d',%s,%s,0,['RECT',%s,%s,0],undefined,null);"
% (num, mil(x), mil(y), mil(w), mil(h)))
# 丝印框(4 条,全在焊盘之外)
for (x1, y1, x2, y2) in [(-SILK_X, SILK_Y, SILK_X, SILK_Y),
(-SILK_X, -SILK_Y, SILK_X, -SILK_Y),
(-SILK_X, -SILK_Y, -SILK_X, SILK_Y),
(SILK_X, -SILK_Y, SILK_X, SILK_Y)]:
body.append("await eda.pcb_PrimitiveLine.create('',3,%s,%s,%s,%s,%s);"
% (mil(x1), mil(y1), mil(x2), mil(y2), SILK_W))
# 1 脚标记:一个小方块(用短线画的十字,最省事且不依赖 Circle API)
dx, dy = DOT
body.append("await eda.pcb_PrimitiveLine.create('',3,%s,%s,%s,%s,%s);"
% (mil(dx - 0.15), mil(dy), mil(dx + 0.15), mil(dy), 12.0))
# 装配层:本体外形 A × 2.3
hx, hy = BODY_L / 2.0, BODY_W / 2.0
for (x1, y1, x2, y2) in [(-hx, hy, hx, hy), (-hx, -hy, hx, -hy),
(-hx, -hy, -hx, hy), (hx, -hy, hx, hy)]:
body.append("await eda.pcb_PrimitiveLine.create('',9,%s,%s,%s,%s,%s);"
% (mil(x1), mil(y1), mil(x2), mil(y2), 4.0))
# 文档层:Hirose 的「禁止布线区」,提醒布板时别把铜灌进去
for (x1, y1, x2, y2) in [(-PROHIB_X, PROHIB_Y, PROHIB_X, PROHIB_Y),
(-PROHIB_X, -PROHIB_Y, PROHIB_X, -PROHIB_Y),
(-PROHIB_X, -PROHIB_Y, -PROHIB_X, PROHIB_Y),
(PROHIB_X, -PROHIB_Y, PROHIB_X, PROHIB_Y)]:
body.append("await eda.pcb_PrimitiveLine.create('',13,%s,%s,%s,%s,%s);"
% (mil(x1), mil(y1), mil(x2), mil(y2), 4.0))
return ("await eda.lib_Footprint.openInEditor('%s','%s');"
"await new Promise(r=>setTimeout(r,2000));"
"const old = await eda.pcb_PrimitivePad.getAllPrimitiveId();"
"if (old && old.length) await eda.pcb_PrimitivePad.delete(old);"
"const ol = await eda.pcb_PrimitiveLine.getAllPrimitiveId();"
"if (ol && ol.length) await eda.pcb_PrimitiveLine.delete(ol);"
"%s"
"return {pads:(await eda.pcb_PrimitivePad.getAllPrimitiveId()).length,"
" lines:(await eda.pcb_PrimitiveLine.getAllPrimitiveId()).length};"
% (FP_UUID, LIB_PERSONAL, ''.join(body)))
VERIFY_JS = """
await eda.lib_Footprint.openInEditor('%s','%s');
await new Promise(r=>setTimeout(r,2000));
const ps = await eda.pcb_PrimitivePad.getAll();
const out = ps.map(p=>({n:p.padNumber, x:p.x, y:p.y, layer:p.layer,
shape:JSON.stringify(p.pad)}));
const ls = (await eda.pcb_PrimitiveLine.getAll()).map(l=>({layer:l.layer,
s:[l.startX,l.startY], e:[l.endX,l.endY], w:l.lineWidth}));
return {nPad:out.length, pads:out, nLine:ls.length, lines:ls};
""" % (FP_UUID, LIB_PERSONAL)
def check(res):
u"""读回校验:编号齐全、坐标与理论值一致、同排间距严格 0.35。"""
got = {}
for p in res['pads']:
got[int(p['n'])] = (p['x'] / MIL, p['y'] / MIL, p['layer'])
want = {n: (x, y) for n, x, y, _, _ in pads()}
bad = []
if sorted(got) != list(range(1, 53)):
bad.append(u'焊盘编号不是 1..52:缺 %s / 多 %s'
% (sorted(set(range(1, 53)) - set(got)), sorted(set(got) - set(range(1, 53)))))
for n, (wx, wy) in want.items():
if n not in got:
continue
gx, gy, lay = got[n]
if abs(gx - wx) > 0.002 or abs(gy - wy) > 0.002:
bad.append(u'pin%d 位置 (%.4f, %.4f) ≠ 理论 (%.4f, %.4f)' % (n, gx, gy, wx, wy))
if lay != 1:
bad.append(u'pin%d 不在顶层(layer=%s)' % (n, lay))
# 同排相邻间距
for row in (1, 2):
xs = sorted(got[n][0] for n in range(row, 51, 2))
for a, b in zip(xs, xs[1:]):
if abs((b - a) - PITCH) > 0.002:
bad.append(u'第 %d 排相邻间距 %.4f ≠ 0.35' % (row, b - a))
# 奇偶分排
ys_odd = set(round(got[n][1], 3) for n in range(1, 51, 2))
ys_even = set(round(got[n][1], 3) for n in range(2, 51, 2))
if len(ys_odd) != 1 or len(ys_even) != 1 or ys_odd == ys_even:
bad.append(u'奇偶脚没有分成两排:odd y=%s even y=%s' % (ys_odd, ys_even))
# 电源触点
if abs(got[51][0] + PWR_X) > 0.002 or abs(got[52][0] - PWR_X) > 0.002:
bad.append(u'51/52 电源触点位置不对:%.3f / %.3f' % (got[51][0], got[52][0]))
# 焊盘与丝印不能打架
for l in res['lines']:
if l['layer'] != 3:
continue
for ax in (l['s'][0], l['e'][0]):
pass
return bad
if __name__ == '__main__':
if '--push' in sys.argv:
r = run(build_js())
assert r.get('success'), r
print(u'已下发:', r['result'])
if '--push' in sys.argv or '--verify' in sys.argv:
r = run(VERIFY_JS)
assert r.get('success'), r
bad = check(r['result'])
print(u'读回 %d 焊盘 / %d 线' % (r['result']['nPad'], r['result']['nLine']))
if bad:
print(u'❌ 校验不过:')
for b in bad:
print(' -', b)
sys.exit(1)
print(u'✅ 六项几何校验全过(编号 1..52 / 坐标 / 层 / 间距 0.35 / 奇偶分排 / 电源触点)')
if '--push' not in sys.argv and '--verify' not in sys.argv:
for n, x, y, w, h in pads():
print('pin %2d x=%8.4f y=%7.4f %.2f×%.2f mm (%.3f, %.3f mil)'
% (n, x, y, w, h, mil(x), mil(y)))