初始导入: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
这个提交包含在:
+290
@@ -0,0 +1,290 @@
|
||||
# -*- 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()
|
||||
在新工单中引用
屏蔽一个用户