文件
phonerail/.claude/c5_j1401.py
zhou yang和Claude Opus 5 2bfe308b30 初始导入: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
2026-09-06 18:18:14 +08:00

291 行
14 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板 C5-J1401A / C5-J1401B:五颗对插手机尾插排线的 BM24-50DS 座(方案 D,§13.5)。
手机主板 J1401 ──原厂「主板→尾插」排线── 本页的 J71~J75 ──> 载板上的 USB 电路。
一页三个位(A:CH1~3)、一页两个位(B:CH4~5)—— 一个 52 脚符号带 12 个地
再加信号引出,一个位要 350 单位宽,一页塞不下五个。
⚠️⚠️ 本页按 **§13.4 的 Hirose 编号**连线(D+=46 / D−=48 / CC1=30 / 口温=38 /
地=3,9,13,21,27,28,31,36,40,44,49,50 / 两个电源触点 51,52)。
排线两端是不是 1:1 直通**还没实测**,但那只影响封装不影响本页:
这颗座子本来就必须自建封装(立创那个是 DIP 绕圈编号,和 Hirose 对不上,见 §13.4 末尾),
自建时把「焊盘号 = 经排线看过去的 Hirose 脚号」定死即可。
⚠️ `PWR` 里「哪个电源触点是地」同样是待实测项 —— 接反就是 5V 对地短路。
用法: /c/Python314/python .claude/c5_j1401.py [--push]
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
LIB = '0819f05c4eef4c71ace90d822a990e87'
# ⚠ 2026-09-04 改用**自建封装版**器件(个人库),不再用立创原件:
# 立创封装 CONN-SMD_BBM24-50DS-2-0.35V51 是 DIP 绕圈编号,和 Hirose 奇偶行对不上(§13.4)。
# 自建封装 BM24-50DS_HIROSE-PIN 几何来自 Hirose 官方样本,见 .claude/fp_bm24.py。
LIB_BM24 = '543725e8f0454017a5691e7e570aeef6' # 个人库
BM24 = '771dbf002b714bb79cde47cf86aa0fb1' # BM24-50DS/2-0.35V(51) HRS-PIN
BM24_LCSC = 'daa43b90a3484a8ca91a9b599d4254d8' # 作废:C424561 原件(DIP 绕圈封装)
TPRED = '17b5ed105c5b4fe180eee3ffb4702963' # C9900007423 红色测试点(引脚在 cx-20)
PAGES = [('C5-J1401A', '967543b084075b4b', [1, 2, 3]),
('C5-J1401B', '63413775cfc3aafc', [4, 5])]
CX = [270, 620, 970] # 一页里第 1/2/3 个位的中心 x
CY = 560 # 座子中心 y
# ---- J1401 引脚用途(§13.4 的 Hirose 编号)----
J1401_GND = [3, 9, 13, 21, 27, 31, 49, 28, 36, 40, 44, 50]
J1401_SIG = [(30, 'CC1_%d'), (38, 'THERM_%d'), (46, 'USB%d_DPJ'), (48, 'USB%d_DMJ')]
# CC2(32) 必须悬空;调试串口 24/29 故意不引(1.8V,接 ESP32 要电平转换)
def map_pin(n):
u"""J1401 的 Hirose 脚号 -> 载板这颗座子上的脚号(2026-09-04 实测 + 推导,§13.5)。
实测:原厂排线在摊平的平面里是**恒等映射** —— 上方内侧↔下方内侧、上方外侧↔下方外侧,
从左到右一一导通(用户量的)。照片又确认两个公头在**同一面**、相差 180°。
于是在各自的本体坐标里:公头A(某排, 第 m 个) ↔ 公头B(同排, 第 26-m 个)
—— 排不变、序号反过来。母座与插上去的公头引脚一一重合,这条关系原样传给两个母座:
J1401(排, m) ↔ 载板座子(同排, 26-m)
再套 §13.4 已证明的奇偶排编号(奇数一排 / 偶数一排):
奇数 N -> 50-N 偶数 N -> 52-N
校验:1->49, 2->50, 49->1, 50->2,奇偶不变(排没换)✔
**这条推导不需要知道「哪一排是奇数排」**,那个未知量在两边抵消掉了。
"""
return (50 - n) if n % 2 else (52 - n)
GND_PINS = sorted(map_pin(n) for n in J1401_GND)
SIG_PINS = [(map_pin(n), tpl) for n, tpl in J1401_SIG]
GND_L = [k for k in GND_PINS if k <= 25]
GND_R = [k for k in GND_PINS if k > 25]
SIG_L = [(k, t) for k, t in SIG_PINS if k <= 25]
SIG_R = [(k, t) for k, t in SIG_PINS if k > 25]
# 两个电源触点。约定(自建封装时按这个编):
# 符号 pin 51 = Hirose「pin1/pin2 那一端」的电源触点
# 符号 pin 52 = Hirose「pin49/pin50 那一端」的电源触点
# J1401 的 G1(地) 在它的 pin1/2 端;经过上面那个「序号反过来」的映射,
# 落到我们这颗的 pin49/50 端 -> **52 = GND,51 = VBUS**。
# ⚠ 仍需首件实测确认:接反就是 5V 对地短路。
PWR = {'GND': 52, 'VBUS': 51}
USED = set(GND_PINS + [k for k, _ in SIG_PINS])
NC_PINS = [str(k) for k in range(1, 51) if k not in USED]
# 相对 cx 的三档引出长度。左侧现在既有 8 个地又有 4 个信号:
# **地母线必须在信号走廊的外侧**(信号短、地长),否则信号线会横穿地母线 = 短路。
# 端口标签比想象中宽(8 字名字约占 90 单位),所以地母线要留够 —— 拉到 200。
X_GNDL, X_SIG, X_GNDR = -200, 70, 90
Y_VBUS = 730
def call(js, timeout=180):
r = run(js, timeout=timeout)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:900])
return r['result']
def desig(n):
return {'j': 'J%d' % (70 + n)}
def placement(sites):
out = []
for i, n in enumerate(sites):
d, cx = desig(n), CX[i]
out.append([d['j'], BM24, cx, CY, 0, LIB_BM24])
return out
def wiring(sites, pins):
W, F, PT, TX = [], [], [], []
def w(*p):
for i in range(0, len(p) - 2, 2):
W.append([p[i], p[i + 1], p[i + 2], p[i + 3]])
for i, n in enumerate(sites):
d, cx = desig(n), CX[i]
pm = pins[d['j']]
# ---- 左列的地:短线到母线(母线放在信号走廊外侧,两者不会相交)----
ys = sorted(pm[str(k)][1] for k in GND_L)
for k in GND_L:
px, py = pm[str(k)]
w(px, py, cx + X_GNDL, py)
w(cx + X_GNDL, ys[0], cx + X_GNDL, ys[-1] + 40)
F.append(['Ground', 'GND', cx + X_GNDL, ys[-1] + 40, 180])
# ---- 左列信号 -> 端口(走廊比地母线短)----
for k, tpl in SIG_L:
px, py = pm[str(k)]
w(px, py, cx - X_SIG, py)
PT.append(['BI', tpl % n, cx - X_SIG, py, 180])
# ---- 右列的地:同上,母线放得更外,给信号让出走廊 ----
ysr = sorted(pm[str(k)][1] for k in GND_R)
for k in GND_R:
px, py = pm[str(k)]
w(px, py, cx + X_GNDR, py)
w(cx + X_GNDR, ysr[0], cx + X_GNDR, ysr[-1] + 20)
F.append(['Ground', 'GND', cx + X_GNDR, ysr[-1] + 20, 180])
# ---- 右列信号 -> 端口(走廊比地母线短,两者不会相交)----
for k, tpl in SIG_R:
px, py = pm[str(k)]
w(px, py, cx + X_SIG, py)
PT.append(['BI', tpl % n, cx + X_SIG, py, 0])
# ---- 两个电源触点 ----
vx, vy = pm[str(PWR['VBUS'])]
w(vx, vy, vx, Y_VBUS, cx + X_SIG, Y_VBUS)
PT.append(['BI', 'VBUS%d' % n, cx + X_SIG, Y_VBUS, 0])
gx, gy = pm[str(PWR['GND'])]
w(gx, gy, gx, gy - 30)
F.append(['Ground', 'GND', gx, gy - 30, 0])
TX.append((cx - 140, CY + 175, u'CH%d 手机位' % n, 12))
return W, F, PT, TX
def page_text(name, sites):
return [
(45, 806, u'【载板 %s】手机尾插 J1401 对插座 —— CH%s(方案 D,§13.5)'
% (name, '/'.join(str(x) for x in sites)), 14),
(45, 786, u'手机主板 J1401 ──原厂「主板→尾插」排线── 本页的 J7x 座 ──> C3-USB 上的 ESD / Rp / 口温 / VBUS 开关。', 10),
(45, 350, u'引脚映射(2026-09-04 实测 + 推导,§13.5):原厂排线在摊平的平面里是**恒等映射**', 12),
(45, 332, u'(上方内侧↔下方内侧、上方外侧↔下方外侧,从左到右一一导通,用户实测);', 10),
(45, 316, u'两个公头在同一面、相差 180°。于是 J1401(排, m) ↔ 本座子(同排, 26-m):', 10),
(45, 296, u' 奇数脚 N → 50-N 偶数脚 N → 52-N', 12),
(45, 276, u'即 D+ 46→6、D- 48→4、CC1 30→22、口温 38→14;地 12 个 → 1,2,8,12,16,19,23,24,29,37,41,47。', 10),
(45, 260, u'(这条推导不需要知道「哪一排是奇数排」—— 两边都用 Hirose 脚号表达,那个未知量抵消了。)', 10),
(45, 232, u'⚠ 电源触点约定:符号 pin51 = Hirose pin1/2 那一端,pin52 = pin49/50 那一端。', 10),
(45, 216, u' J1401 的 G1(地) 在它的 pin1/2 端,经上面的反序落到我们这颗的 pin49/50 端', 10),
(45, 200, u' → **52 = GND,51 = VBUS**。⚠ 首件仍要实测,接反就是 5V 对地短路。', 10),
(620, 350, u'⚠ 机械:两个公头同一面 → 两个母座都得朝上、大致共面。', 10),
(620, 334, u' 而 J1401 朝天、本座子也朝天、载板在手机下面,所以排线必须走', 10),
(620, 318, u' **台阶式下折**(下 90° 再上 90°,净旋转为零,公头仍朝下),', 10),
(620, 302, u' **不能走 U 形回折**(会把面翻过来,插不进去)。', 10),
(620, 286, u' → 本座子要放在手机轮廓之外、J1401 出线的那一侧,PCB 摆位时定死。', 10),
(620, 258, u'⚠ BM24 只有 10 次插拔寿命,而一根排线上有两个插拔点 —— 插上就别反复拔。', 10),
(620, 234, u'⚠ 没画的引脚(音频 / 射频控制 / 各路 VREG / CC2)全部悬空。', 10),
(620, 218, u' CC2 必须悬空 —— 只拉一边 CC,手机才判得出插入方向。', 10),
(620, 202, u'⚠ 调试串口(pin24 TX / pin29 RX)故意不引:1.8V 电平,接 ESP32 要电平转换。', 10),
]
def build(name, page, sites, push):
if not push:
return
call("""
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1200));
const n=(await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
if (n!=='%s') throw new Error('page='+n);
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()==='sheet') continue;
await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId());
}
for (const w of await eda.sch_PrimitiveWire.getAll()) await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId());
for (const t of await eda.sch_PrimitiveText.getAll()) await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId());
return 1;
""" % (page, name))
place = placement(sites)
for i in range(0, len(place), 3):
call("""
for (const [d,uuid,x,y,rot,lib] of %s){
await eda.sch_PrimitiveComponent.create({libraryUuid:lib, uuid}, x, y, undefined, rot, false, true, true);
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()!=='part') continue;
if (Math.round(c.getState_X())!==x || Math.round(c.getState_Y())!==y) continue;
await c.toAsync().setState_Designator(d).done();
}
}
return 1;
""" % json.dumps(place[i:i + 3]))
# 座子的默认名 'BM24-50DS/2-0.35V(51)' 21 个字,压在符号下面很难看
call("""
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (/^J7[1-5]$/.test(c.getState_Designator())) await c.toAsync().setState_Name('BM24-50DS').done();
}
return 1;
""")
raw = call("""
const out={};
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()!=='part') continue;
out[c.getState_Designator()] = (await c.getAllPins()).map(
p=>[p.getState_PinNumber(), Math.round(p.getState_X()), Math.round(p.getState_Y())]);
}
return out;
""")
pins = {k: {p: (x, y) for p, x, y in v} for k, v in raw.items()}
for n in sites:
d = desig(n)
if len(pins[d['j']]) != 52:
raise SystemExit(u'%s 引脚数 %d,期望 52(50 信号 + 2 个电源触点)'
% (d['j'], len(pins[d['j']])))
W, F, PT, TX = wiring(sites, pins)
TX = TX + page_text(name, sites)
# 下发前的几何断言(skill §6c / §8a-2b)
for a, b, c, e in W:
assert (a == c) != (b == e), u'%s 有一段导线不是横平竖直或零长度: %s' % (name, (a, b, c, e))
for v in (a, b, c, e):
assert v % 5 == 0, u'%s 导线坐标 %d 不是 5 的倍数: %s' % (name, v, (a, b, c, e))
for lst in (F, PT):
for it in lst:
assert it[2] % 5 == 0 and it[3] % 5 == 0, \
u'%s 的 %s 坐标 (%d,%d) 不是 5 的倍数' % (name, it[1], it[2], it[3])
def batch(items, size, js):
for i in range(0, len(items), size):
call(js % json.dumps(items[i:i + size], ensure_ascii=False))
batch(W, 40, "for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;")
batch(PT, 25, "for (const p of %s) await eda.sch_PrimitiveComponent.createNetPort(p[0],p[1],p[2],p[3],p[4],false); return 1;")
batch(F, 25, "for (const f of %s) await eda.sch_PrimitiveComponent.createNetFlag(f[0],f[1],f[2],f[3],f[4],false); return 1;")
batch([list(t) for t in TX], 12,
"for (const t of %s) await eda.sch_PrimitiveText.create(t[0],t[1],t[2],0,null,null,t[3],false,false,false,0); return 1;")
print(u'%s: 元件 %d 导线 %d 旗 %d 端口 %d 文字 %d'
% (name, len(place), len(W), len(F), len(PT), len(TX)))
def main():
push = '--push' in sys.argv
for name, page, sites in PAGES:
if not push:
print(u'%s: %d 个位,元件 %d 个' % (name, len(sites), len(placement(sites))))
continue
build(name, page, sites, True)
if not push:
print(u'(未推送,加 --push)')
return
ncf = os.path.join(HERE, 'c5-nets-nc.json')
json.dump({desig(n)['j']: NC_PINS for n in (1, 2, 3, 4, 5)},
open(ncf, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(u'NC 清单已写入 %s(每颗座子 %d 个悬空脚)' % (ncf, len(NC_PINS)))
if __name__ == '__main__':
main()