文件
phonerail/.claude/obsolete/t1_usbtap.py
T
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

363 行
18 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""Board3 / T1-USBTAP:小米8 尾插 J1401 转接小板(第三块板,每台手机一块)。
作用:从手机主板正面的 J1401(Hirose BM24-50DS/2-0.35V)取出 USB,
用 1.25mm 4P 线接到载板的 J17~J21。§13.5 方案 C。
板上内容:
J91 BM24-50DP/2-0.35V(51) 对插 J1401
J92 A1251WV-S-4P 出线到载板(1=VBUS 2=D- 3=D+ 4=GND,和 C3-USB 的 J17 一致)
R901 56k CC1(pin30) -> VBUS 的 Rp(Default USB 档),§13.5 必补项 1
R902 10k pin38 USB_CONN_THERM -> GND,§13.5 必补项 2(口温占位,首台机实测确认)
U91 USBLC6-2SC6 D± + VBUS ESD(原厂尾插小板上的 ESD 没了,补回来)
C901 100nF VBUS 去耦
TP901/902/903 DEBUG_UART TX / RX / GND 测试点(1.8V 电平,默认不接)
CC2(pin32) 悬空 §13.5 必补项 3
用法: /c/Python314/python .claude/t1_usbtap.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
SCH = 'fe54ec63db9be605' # phonerail-usbtap (Board3)
PAGE = '56ed81d66f1b0b83' # T1-USBTAP
LIB = '0819f05c4eef4c71ace90d822a990e87'
P = {
'BM24': '7e3457a7ea314e6e8f2e4c5034e6b8a5', # C424560 BM24-50DP/2-0.35V(51)
'WV4': 'a442828947794d278f7b5933443e57dd', # C225106 A1251WV-S-4P
'R56K': '0d85e42011ec4910b1749e936b037dd7', # C25796 56k 0402
'R10K': 'c3b9baa5ef2e4070a4c0f9e9cd04fe6e', # C25744 10k 0402
'C100N': '87bb635d0b2f489a9f60e7cd225beb3c', # C1525 100nF 0402
'ESD': '1e5e214711544da8ba71fb2137ae703f', # C2687116 USBLC6-2SC6
'TPRED': '17b5ed105c5b4fe180eee3ffb4702963', # C9900007423 红色测试点(引脚在 cx-20)
'TPBLK': 'cf8b789614c04ee491fd17b89f81a8ab', # C9900007426 黑色测试点(引脚在 cx-10)
}
# ---- J1401 引脚用途(§13.4 的 Hirose 编号)----
GND_L = [3, 9, 13, 21] # 左列(符号 1..25)里的地
GND_R = [27, 28, 31, 36, 40, 44, 49, 50] # 右列(符号 26..50)里的地
SIG_R = [(29, 'UART_RX'), (30, 'CC1_P'), (38, 'THERM_P'),
(46, 'USB_DP_P'), (48, 'USB_DM_P')] # 右列的信号
SIG_L = [(24, 'UART_TX')] # 左列的信号
PWR_VBUS = [51, 52] # 一端的电源触点(两个焊盘同一个触点)-> USB_VBUS
PWR_GND = [53, 54] # 另一端的电源触点 -> GND
USED = set(GND_L + GND_R + [k for k, _ in SIG_R] + [k for k, _ in SIG_L])
NC_PINS = [str(k) for k in range(1, 51) if k not in USED] # 含 32(CC2),故意悬空
# ---- 版面常量 ----
J91 = (300, 470) # 左列引脚 x=260(y = 590 - 10*(k-1)),右列 x=340(y = 350 + 10*(k-26))
GNDBUS_L, GNDBUS_R = 200, 500
PORT_R = 400 # 右列信号端口的 x
U91 = (760, 550) # USBLC6:1/6=I/O1@cy+10 2=GND@cy-0 3/4=I/O2@cy-10
J92 = (960, 470) # A1251WV-S-4P,引脚朝上(y = cy+25)
R901, R902 = (700, 760), (700, 380)
C901 = (680, 660)
TP901, TP902, TP903 = (250, 260), (250, 220), (250, 180) # 图纸右下角是标题栏,别往那边放
Y_VBUS = 700 # VBUS 母线
Y_DP, Y_DM = 540, 560 # DP 接 I/O2(低),DM 接 I/O1(高)—— 这样 J92 扇出零交叉
Y_GNDJ = 440
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 placement():
return [
['J91', P['BM24'], J91[0], J91[1], 0],
['J92', P['WV4'], J92[0], J92[1], 0],
['U91', P['ESD'], U91[0], U91[1], 0],
['R901', P['R56K'], R901[0], R901[1], 0],
['R902', P['R10K'], R902[0], R902[1], 0],
['C901', P['C100N'], C901[0], C901[1], 270],
['TP901', P['TPRED'], TP901[0], TP901[1], 0],
['TP902', P['TPRED'], TP902[0], TP902[1], 0],
['TP903', P['TPBLK'], TP903[0], TP903[1], 0],
]
def wiring(pins):
u"""pins: {位号: {引脚号: (x, y)}},全部读回来的真实坐标。"""
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]])
j91 = pins['J91']
# ---------- J91 左列 ----------
ys = sorted(j91[str(k)][1] for k in GND_L)
for k in GND_L:
px, py = j91[str(k)]
w(px, py, GNDBUS_L, py)
w(GNDBUS_L, ys[0], GNDBUS_L, ys[-1] + 40)
F.append(['Ground', 'GND', GNDBUS_L, ys[-1] + 40, 180])
for k, net in SIG_L:
px, py = j91[str(k)]
w(px, py, 160, py)
PT.append(['BI', net, 160, py, 180])
# ---------- J91 右列 ----------
ysr = sorted(j91[str(k)][1] for k in GND_R)
for k in GND_R:
px, py = j91[str(k)]
w(px, py, GNDBUS_R, py)
w(GNDBUS_R, ysr[0], GNDBUS_R, ysr[-1] + 30)
F.append(['Ground', 'GND', GNDBUS_R, ysr[-1] + 30, 180])
for k, net in SIG_R:
px, py = j91[str(k)]
w(px, py, PORT_R, py)
PT.append(['BI', net, PORT_R, py, 0])
# ---------- J91 两个电源触点 ----------
# 51/52 是同一个电源触点的两个焊盘,53/54 是另一端那个。往上/往下各引一条。
xs = sorted(j91[str(k)][0] for k in PWR_VBUS)
yv = j91[str(PWR_VBUS[0])][1]
for k in PWR_VBUS:
px, py = j91[str(k)]
w(px, py, px, yv + 50)
w(xs[0], yv + 50, PORT_R, yv + 50)
PT.append(['BI', 'VBUS_P', PORT_R, yv + 50, 0])
xs2 = sorted(j91[str(k)][0] for k in PWR_GND)
yg = j91[str(PWR_GND[0])][1]
for k in PWR_GND:
px, py = j91[str(k)]
w(px, py, px, yg - 40)
w(xs2[0], yg - 40, 360, yg - 40)
F.append(['Ground', 'GND', 360, yg - 40, 0])
# ---------- U91 USBLC6:D± 直通 + VBUS 钳位 ----------
u = pins['U91']
io1a, io1b = u['1'], u['6'] # I/O1 -> DM
io2a, io2b = u['3'], u['4'] # I/O2 -> DP
ugnd, uvbus = u['2'], u['5']
assert io1a[1] == io1b[1] == Y_DM, u'U91 的 I/O1 不在 y=%d:%s' % (Y_DM, (io1a, io1b))
assert io2a[1] == io2b[1] == Y_DP, u'U91 的 I/O2 不在 y=%d:%s' % (Y_DP, (io2a, io2b))
# ⚠ USBLC6 的 1/6 是同一个 I/O1、3/4 是同一个 I/O2(芯片内部短接),
# 但 EasyEDA 画线时会在元件引脚处把导线截断 —— 想画一条「穿过芯片」的直通线是做不到的
# (实测:create([620,540,860,540]) 回来变成 [620,540→715,540] 和 [860,540→805,540],
# 中间穿过芯片本体那一段直接没了)。所以 D± 天然被切成两段,各自起名:
# USB_DP_P / USB_DM_P = 手机那一侧(J91 -> U91)
# USB_DP_C / USB_DM_C = 载板那一侧(U91 -> J92)
# 这正好逼着 PCB 把走线串在 U91 的焊盘中间,而不是只搭一个脚 —— 是想要的结果。
for nm, yy, pn in ((u'DP', Y_DP, io2a), (u'DP', Y_DP, io2b),
(u'DM', Y_DM, io1a), (u'DM', Y_DM, io1b)):
assert pn[1] == yy, u'U91 的 %s 引脚不在 y=%d:%s' % (nm, yy, pn)
w(620, Y_DP, io2a[0], Y_DP)
PT.append(['BI', 'USB_DP_P', 620, Y_DP, 180])
w(620, Y_DM, io1a[0], Y_DM)
PT.append(['BI', 'USB_DM_P', 620, Y_DM, 180])
# 这两个名字放在拐点上(不是 U91 边上)—— 贴着芯片会把符号盖住
PT.append(['BI', 'USB_DP_C', 860, 465, 180])
PT.append(['BI', 'USB_DM_C', 890, 475, 180])
# U91 的 GND(2) 和 VBUS(5) 都夹在两个 I/O 中间,往哪边走都必然跨过一条 D 线。
# 全页只有这两处交叉,都是纯交叉(没有结点)= 不相连,这是原理图的常规画法。
w(ugnd[0], ugnd[1], 670, ugnd[1], 670, Y_GNDJ)
F.append(['Ground', 'GND', 670, Y_GNDJ, 0])
# ---------- J92 出线座(引脚朝左,竖排一列)----------
j = pins['J92']
p1, p2, p3, p4 = j['1'], j['2'], j['3'], j['4'] # 1=VBUS 2=D- 3=D+ 4=GND
p5, p6 = j['5'], j['6'] # 固定脚
assert p1[0] == p2[0] == p3[0] == p4[0], u'J92 四个信号脚不在同一列:%s' % [p1, p2, p3, p4]
assert p1[1] > p2[1] > p3[1] > p4[1], u'J92 引脚 y 序不是 1>2>3>4:%s' % [p1, p2, p3, p4]
JX = p1[0]
# D+ 走 I/O2(低的那条),D- 走 I/O1(高的那条);D+ 的拐点 x 必须比 D- 的小,
# 否则两条 D 线会互相交叉(J92 上 D+ 也在 D- 下面,顺序一致就零交叉)。
w(io2b[0], Y_DP, 860, Y_DP, 860, p3[1])
w(860, p3[1], JX, p3[1])
w(io1b[0], Y_DM, 890, Y_DM, 890, p2[1])
w(890, p2[1], JX, p2[1])
# VBUS 母线:J91 的电源触点 -> J92 pin1,U91.5 在中途搭上去(和 D- 交叉一次)
w(620, Y_VBUS, 920, Y_VBUS, 920, p1[1])
w(920, p1[1], JX, p1[1])
PT.append(['BI', 'VBUS_P', 620, Y_VBUS, 180])
w(uvbus[0], uvbus[1], 835, uvbus[1], 835, Y_VBUS)
# pin4 和固定脚 6 并到一条地;固定脚 5 单独一个旗(往上走,避开 VBUS 那条竖线)
w(p4[0], p4[1], 900, p4[1], 900, 420)
w(p6[0], p6[1], 900, p6[1])
F.append(['Ground', 'GND', 900, 420, 0])
w(p5[0], p5[1], p5[0], p5[1] + 25)
F.append(['Ground', 'GND', p5[0], p5[1] + 25, 180])
# ---------- C901 VBUS 去耦 ----------
c = pins['C901']
ctop = max(c.values(), key=lambda v: v[1])
cbot = min(c.values(), key=lambda v: v[1])
w(ctop[0], ctop[1], ctop[0], Y_VBUS)
w(cbot[0], cbot[1], cbot[0], cbot[1] - 40)
F.append(['Ground', 'GND', cbot[0], cbot[1] - 40, 0])
# ---------- R901:CC1 -> 56k -> VBUS(§13.5 必补项 1)----------
r = pins['R901']
ra = min(r.values(), key=lambda v: v[0])
rb = max(r.values(), key=lambda v: v[0])
w(620, ra[1], ra[0], ra[1])
PT.append(['BI', 'CC1_P', 620, ra[1], 180])
w(rb[0], rb[1], 780, rb[1], 780, Y_VBUS)
# ---------- R902:pin38 口温 -> 10k -> GND(§13.5 必补项 2)----------
r2 = pins['R902']
r2a = min(r2.values(), key=lambda v: v[0])
r2b = max(r2.values(), key=lambda v: v[0])
w(620, r2a[1], r2a[0], r2a[1])
PT.append(['BI', 'THERM_P', 620, r2a[1], 180])
w(r2b[0], r2b[1], 800, r2b[1])
F.append(['Ground', 'GND', 800, r2b[1], 0])
# ---------- 调试串口测试点 ----------
for des, net in (('TP901', 'UART_TX'), ('TP902', 'UART_RX')):
px, py = pins[des]['1']
w(170, py, px, py)
PT.append(['BI', net, 170, py, 180])
px, py = pins['TP903']['1']
w(px, py, 190, py)
F.append(['Ground', 'GND', 190, py, 0])
TX += [
(45, 806, u'【Board3 / T1-USBTAP】小米8 尾插 J1401 转接小板 —— 每台手机一块,共 5 块', 14),
(45, 784, u'J91 插进主板正面的 J1401(Hirose BM24-50DS/2-0.35V),把 USB 引到 J92,'
u'再用 1.25mm 4P 线接载板的 J17~J21。§13.5 方案 C。', 10),
(45, 120, u'⚠⚠ 打样前必须先解决:立创这颗 BM24 的封装焊盘编号和 Hirose 的真实编号不一致', 12),
(45, 102, u' 立创封装 CONN-SMD_BM24-50DP-2-0.35V51 是 DIP 式:1..25 排一行、26..50 反着排另一行。', 10),
(45, 86, u' 而小米8 图纸上的 J1401 是奇偶式:1,3,5..49 一行,2,4,6..50 另一行。两条硬证据:', 10),
(45, 70, u' ① 44/46/48/50 = GND/D+/D-/GND 在同一行相邻(差分对被地包夹);', 10),
(45, 54, u' ② 奇数行全是音频、偶数行全是数字/电源,33/35/37 与 43/45/47 各是三针并联的喇叭。', 10),
(45, 38, u' 本页按 Hirose 编号连线(和 §13.4 的表一致)。转 PCB 前要自建封装,或先实测确认。', 10),
(600, 300, u'【§13.5 方案 C 的三条必补项】', 11),
(600, 282, u'1. R901 56k:CC1(30) 上拉到 VBUS。整条链路里已经没有 USB-C 连接器了,', 10),
(600, 266, u' 没人提供 Rp,手机看不到 Rp 就只充电不枚举。56k = Default USB 档。', 10),
(600, 250, u'2. R902 10k:pin38 口温。⚠ 阻值是占位,首台机上实测确认(也可能要 100k NTC);', 10),
(600, 234, u' 我们本来就用 input_suspend 关掉了充电,口温读数不准也不影响枚举。', 10),
(600, 218, u'3. CC2(32) 必须悬空 —— 只拉一边 CC,手机才判得出插入方向。', 10),
(45, 754, u'U91 USBLC6-2SC6:原厂尾插小板上的 ESD 随着方案 C 一起没了,在这里补回来。', 10),
(45, 738, u'⚠ 引脚 1/6 是同一个 I/O1、3/4 是同一个 I/O2(芯片内部短接),而 EasyEDA 会在引脚处截断', 10),
(45, 722, u' 导线,所以 D± 在网表里天然是两段:USB_D?_P 手机侧 / USB_D?_C 载板侧。', 10),
(45, 706, u' PCB 上走线必须从一个焊盘进、另一个焊盘出(串在线中),不能只搭一个脚;这颗不许不贴。', 10),
(45, 690, u' 线电容 3.5pF,比 §15.3 想要的 <1pF 大,但这是机箱内 20cm 的短链路,USB2.0 HS 够用。', 10),
(960, 806, u'J92 引脚必须和载板 C3-USB 的 J17~J21 一一对上:', 10),
(960, 790, u'1=VBUS 2=D- 3=D+ 4=GND,5/6 固定脚接地。', 10),
(960, 774, u'TP901/902 是高通调试串口(pin24 TX / pin29 RX),', 10),
(960, 758, u'⚠ 1.8V 电平,不能直接接 ESP32;默认不焊线,', 10),
(960, 742, u'内核 panic 时这是唯一能看到原因的通道(§13.5)。', 10),
(960, 726, u'J91 上没画的引脚(音频/射频/电源轨)全部悬空。', 10),
]
# 下发前的几何断言(skill §6c / §8a-2b)
for a, b, c2, e in W:
assert (a == c2) != (b == e), u'有一段导线不是横平竖直或零长度: %s' % ((a, b, c2, e),)
for v in (a, b, c2, e):
assert v % 5 == 0, u'导线坐标 %d 不是 5 的倍数: %s' % (v, (a, b, c2, e))
for kind, lst in (('flag', F), ('port', PT)):
for it in lst:
assert it[2] % 5 == 0 and it[3] % 5 == 0, \
u'%s %s 坐标 (%d,%d) 不是 5 的倍数' % (kind, it[1], it[2], it[3])
return W, F, PT, TX
def main():
push = '--push' in sys.argv
place = placement()
print(u'元件 %d 个' % len(place))
if not push:
print(u'(未推送,加 --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!=='T1-USBTAP') 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)
print(u'页面已清空')
for i in range(0, len(place), 4):
call("""
for (const [d,uuid,x,y,rot] of %s){
await eda.sch_PrimitiveComponent.create({libraryUuid:'%s', 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 + 4]), LIB))
# BM24 的默认名 'BM24-50DP/2-0.35V(51)' 有 21 个字,压在符号下面很难看,改短名。
# supplierId / manufacturerId 都不动,BOM 照旧。
call("""
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_Designator()==='J91') await c.toAsync().setState_Name('BM24-50DP').done();
}
return 1;
""")
print(u'已放置 %d 个元件' % len(place))
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(), (p.getState_PinName?p.getState_PinName():''),
Math.round(p.getState_X()), Math.round(p.getState_Y())]);
}
return out;
""")
pins = {k: {p: (x, y) for p, _n, x, y in v} for k, v in raw.items()}
pnames = {k: {p: _n for p, _n, x, y in v} for k, v in raw.items()}
bad = []
if len(pins['J91']) != 54:
bad.append(u'J91 引脚数 %d,期望 54(50 信号 + 4 个电源触点焊盘)' % len(pins['J91']))
want = {'1': 'I/O1', '2': 'GND', '3': 'I/O2', '4': 'I/O2', '5': 'VBUS', '6': 'I/O1'}
for k, v in want.items():
got = pnames['U91'].get(k, '')
if got.upper() != v.upper():
bad.append(u'U91.%s 引脚名期望 %s 实测 %s' % (k, v, got))
if bad:
raise SystemExit(u'引脚核对失败:\n ' + '\n '.join(bad))
W, F, PT, TX = wiring(pins)
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'导线 %d 旗 %d 端口 %d 文字 %d' % (len(W), len(F), len(PT), len(TX)))
ncf = os.path.join(HERE, 't1-nets-nc.json')
json.dump({'J91': NC_PINS}, open(ncf, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(u'NC 清单已写入 %s(%d 个悬空脚)' % (ncf, len(NC_PINS)))
if __name__ == '__main__':
main()