文件
phonerail/.claude/r2/b3b_usb.py
T

304 行
14 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 刀片 **B3B-USB** 页:尾插座 + D± ESD + Rp/口温 + VBUS 负载开关。
/c/Python314/python .claude/r2/b3b_usb.py # 只算 + 本地自检
/c/Python314/python .claude/r2/b3b_usb.py --push # 清页 + 重画(幂等)
M2c 的第二页(拆页理由见 `b3_phone.py` 的表头)。R1 把这些铺在
`C5-J1401A`(尾插座)、`C5-AUX`(ESD + Rp + 口温)、`C3-USB`(VBUS 开关)三页上。
| 位号 | 器件 | 出处 |
|---|---|---|
| `J4` | BM24-50DS/2-0.35V,个人库自建封装(**Hirose 奇偶行编号**,立创那个是 DIP 绕圈,错的)| R1 `J71` |
| `U7` | USBLC6-2SC6,D± ESD | R1 `U51` |
| `R64` | 56k `CC1 → VBUS` 的 Rp(Default USB 档)| R1 `R761`,见 §23.3 |
| `R65` | 100k 口温 → GND | R1 `R771` |
| `U5` `C66~C68` `R62` | TPS22918 + 2.2µF CIN + 100nF COUT + 100pF CT + 100k ON 下拉 | R1 `U24/C701/C711/C721/R701`,§18.2 |
## ⚠️ `J4` 的脚号 → 网络**照抄 R1**,不要自己按 §13.4 推
R1 §24 那一轮把「手机 J1401 的 Hirose 脚号」经原厂排线映射到座子焊盘号,
中间隔着一次实测(§24「已实测确认的三件事」)。本页的 `PIN_NET` 表是从
R1 `C5-J1401A` 的 `J71` **原样读回来的**(`_c5j1401a_live.json`),脚本启动时
拿它和源页比对,对不上直接终止 —— 按 §13.4 的表重推一遍必然错位。
## 三条与 R1 相同、这里再记一次的
1. **`U7` 是「1/6 内部同一个 I/O1、3/4 同一个 I/O2」的直通器件**,
原理图上天然是两段(skill §8b):手机侧 `USB_D*J`、上游侧 `USB_D*`。
⇒ PCB 上走线必然串在两个焊盘中间,不会偷偷从旁边绕过去只搭一个脚。
⛔ **2026-09-11 起上游侧接 1/3 脚、手机侧接 6/4 脚**(与 R1 相反,理由见 build() 第 2 节)。
2. **`R64` 这颗 Rp 不能省**:手机的 PMI8998 靠 CC 上看到 Rp 才判定「有 DFP 插入」。
R1 §23.2 的坑是「Rp 藏在 USB-A 转 C 线的 C 头里」,而这里没有那根线。
3. **`R62` 100k 是 `VBUS_EN` 的下拉**(datasheet 明令 ON 脚不得悬空):
PCA9555 复位时高阻,全靠它保证 VBUS 默认断开(D9 复位安全的一半)。
"""
import io
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from schcheck import Sheet, two_pin, by_rel, pins_of # noqa: E402
PAGE, NAME = '4ce5bfe35e90e560', 'B3B-USB'
BM24, ESD, LSW = 'BM24-HRS-PIN', 'C2687116', 'C131941'
R56K, R100K, C2U2, C100N, C100P = 'C25796', 'C25741', 'C377773', 'C1525', 'C1546'
# ── J4 的脚号 -> 网络:从 R1 C5-J1401A 的 J71 原样搬(去掉通道后缀)──────
PIN_NET = {'51': 'VBUS', '4': 'USB_DMJ', '6': 'USB_DPJ',
'14': 'USB_THERM', '22': 'CC1'}
GND_PINS = ['1', '2', '8', '12', '16', '19', '23', '24', '29', '37', '41', '47', '52']
SRC = os.path.join(HERE, '_c5j1401a_live.json')
SRC_MAP = {'VBUS1': 'VBUS', 'USB1_DMJ': 'USB_DMJ', 'USB1_DPJ': 'USB_DPJ',
'THERM_1': 'USB_THERM', 'CC1_1': 'CC1'}
J4 = (940, 580)
U7 = (330, 420)
U5 = (700, 420)
ESD_R270 = {'1': (10, 45), '2': (0, 45), '3': (-10, 45),
'4': (-10, -45), '5': (0, -45), '6': (10, -45)}
# ⚠ skill §6c 又一例:同一个料号(TPS22918 SOT-23-6),R1 的 C3-USB 上
# create(270) 得到「1/2/3 在上排」,本页实测是**在下排**。三种角度都量过:
# rot 0 1(-60,+10) … 6(+60,+10) rot 90 1(+10,+60) …
# rot 270 1(-10,-60) 2(0,-60) 3(+10,-60) / 4(+10,+60) 5(0,+60) 6(-10,+60)
# 所以下面这张表是**本页实测**,不是从 R1 抄的;放完仍然逐脚断言。
LSW_R270 = {'1': (-10, -60), '2': (0, -60), '3': (10, -60),
'4': (10, 60), '5': (0, 60), '6': (-10, 60)}
def bm24_rel(n):
u"""BM24 符号:1~25 左列自上而下,26~50 右列自下而上,51/52 在正中上下。"""
if n == 51:
return (0, 140)
if n == 52:
return (0, -140)
if n <= 25:
return (-30, 120 - 10 * (n - 1))
return (30, -120 + 10 * (n - 26))
BM24_REL = dict((str(n), bm24_rel(n)) for n in range(1, 53))
USED = set(PIN_NET) | set(GND_PINS)
NC = {'J4': [str(n) for n in range(1, 53) if str(n) not in USED]}
ALLOW_SPLIT = set(['GND', 'VBUS', 'CC1', 'USB_THERM',
'USB_DPJ', 'USB_DMJ', 'USB_DP', 'USB_DM'])
LBUS, PORTX, RBUS = J4[0] - 200, J4[0] - 70, J4[0] + 90
def check_source():
u"""和 R1 源页逐脚对账:脚号→网络必须一模一样,对不上就别画。"""
if not os.path.exists(SRC):
print(u'⚠ 找不到 %s,跳过与 R1 的对账(重新 dump_page.py 一次更稳)' % SRC)
return
d = json.load(io.open(SRC, encoding='utf-8'))
pts = d['wires']
segs = [((int(round(pts[i][0])), int(round(pts[i][1]))),
(int(round(pts[i + 1][0])), int(round(pts[i + 1][1]))))
for i in range(0, len(pts), 2)]
segs = [s for s in segs if s[0] != s[1]]
par = {}
def find(a):
par.setdefault(a, a)
while par[a] != a:
par[a] = par[par[a]]
a = par[a]
return a
def uni(a, b):
ra, rb = find(a), find(b)
if ra != rb:
par[ra] = rb
def on(p, s):
(x1, y1), (x2, y2) = s
if x1 == x2:
return p[0] == x1 and min(y1, y2) <= p[1] <= max(y1, y2)
return p[1] == y1 and min(x1, x2) <= p[0] <= max(x1, x2)
for s in segs:
uni(s[0], s[1])
for s in segs:
for t in segs:
if on(s[0], t):
uni(s[0], t[0])
if on(s[1], t):
uni(s[1], t[0])
name = {}
for kind, net, x, y, _rot in d['flags'] + d['ports']:
p = (int(round(x)), int(round(y)))
for s in segs:
if on(p, s):
name.setdefault(find(s[0]), set()).add(
'GND' if kind.startswith('Ground') else net)
break
src = None
for p in d['parts']:
if p[0] == 'J71':
src = p
assert src, u'源页里找不到 J71'
got = {}
for q in src[9]:
pn, x, y = str(q[0]), int(round(q[2])), int(round(q[3]))
for s in segs:
if on((x, y), s):
nm = sorted(name.get(find(s[0]), []))
if nm:
got[pn] = SRC_MAP.get(nm[0], nm[0])
break
want = dict(PIN_NET)
for pn in GND_PINS:
want[pn] = 'GND'
if got != want:
diff = [(k, want.get(k), got.get(k))
for k in sorted(set(want) | set(got), key=int)
if want.get(k) != got.get(k)]
raise SystemExit(u'J4 脚号→网络与 R1 的 J71 对不上:%s' % diff)
print(u'J4 脚号→网络与 R1 的 J71 逐脚一致(%d 个脚)' % len(want))
def placement():
return [['J4', BM24, J4[0], J4[1], 0],
['U7', ESD, U7[0], U7[1], 270],
['U5', LSW, U5[0], U5[1], 270],
['R62', R100K, 760, 295, 270],
['R64', R56K, 920, 300, 270],
['R65', R100K, 1030, 300, 270],
['C66', C2U2, 600, 280, 0], # 2.2µF/0805:rot 0 才是竖的(§8d)
['C67', C100N, 640, 550, 270],
['C68', C100P, 560, 640, 270]]
def expect():
return {'J4': by_rel(J4[0], J4[1], BM24_REL),
'U7': by_rel(U7[0], U7[1], ESD_R270),
'U5': by_rel(U5[0], U5[1], LSW_R270),
'R62': two_pin(760, 295), 'R64': two_pin(920, 300),
'R65': two_pin(1030, 300), 'C66': two_pin(600, 280),
'C67': two_pin(640, 550), 'C68': two_pin(560, 640, 15)}
def build():
s = Sheet(PAGE, NAME)
jx, jy = J4
# ── 1. J4 BM24-50DS(版图照搬 R1 C5-J1401A:地母线甩到最左/最右,
# 信号端口排在母线与引脚之间,两者永不交叉)──────────────────
left = [n for n in GND_PINS if int(n) <= 25 or n == '52']
for n in [x for x in GND_PINS if int(x) <= 25]:
y = jy + bm24_rel(int(n))[1]
s.w('GND', (jx - 30, y), (LBUS, y))
s.w('GND', (LBUS, jy - 110), (LBUS, jy + 160))
s.gnd(LBUS, jy + 160, 180)
for n in [x for x in GND_PINS if 26 <= int(x) <= 50]:
y = jy + bm24_rel(int(n))[1]
s.w('GND', (jx + 30, y), (RBUS, y))
s.w('GND', (RBUS, jy - 90), (RBUS, jy + 110))
s.gnd(RBUS, jy + 110, 180)
s.w('GND', (jx, jy - 140), (jx, jy - 170)) # 52 电源触点 G1
s.gnd(jx, jy - 170, 0)
s.w('VBUS', (jx, jy + 140), (jx, jy + 170), (jx + 70, jy + 170)) # 51 = G2
s.port('BI', 'VBUS', jx + 70, jy + 170, 0)
for n, net in sorted(PIN_NET.items(), key=lambda kv: -int(kv[0])):
if n == '51':
continue
y = jy + bm24_rel(int(n))[1]
s.w(net, (jx - 30, y), (PORTX, y))
s.port('BI', net, PORTX, y, 180)
assert len(left) == len([x for x in GND_PINS if int(x) <= 25]) + 1
# ── 2. U7 USBLC6:中间那个 GND 直着走,两边的信号朝相反方向拐开 ────
# ⛔ 2026-09-11 两侧对调:**1/3 脚(上排)= 金手指侧 USB_DM/DP,6/4 脚(下排)= 手机侧**。
# U7 在 B 面,从 4/6 那排流向 1/3 那排时 DP 永远在前进方向右手边(旋转不改手性),
# 而金手指(DP 在西、向北出线)和 J4(DPJ 在东、从北边进)两头都要 DP 在左手边 ——
# 旧接法前后各要交叉一次(旧版 DP/DM 各 6~7 个孔)。1↔6、3↔4 内部直通,电气等价。
ux, uy = U7
s.w('USB_DM', (ux + 10, uy + 45), (ux + 10, 505), (420, 505))
s.port('BI', 'USB_DM', 420, 505, 0)
s.w('USB_DP', (ux - 10, uy + 45), (ux - 10, 505), (240, 505))
s.port('BI', 'USB_DP', 240, 505, 180)
s.w('GND', (ux, uy + 45), (ux, 545))
s.gnd(ux, 545, 180)
s.w('USB_DMJ', (ux + 10, uy - 45), (ux + 10, 335), (420, 335))
s.port('BI', 'USB_DMJ', 420, 335, 0)
s.w('USB_DPJ', (ux - 10, uy - 45), (ux - 10, 335), (240, 335))
s.port('BI', 'USB_DPJ', 240, 335, 180)
s.w('VBUS', (ux, uy - 45), (ux, 295))
s.port('BI', 'VBUS', ux, 295, 0)
# ── 3. U5 TPS22918(rot270 实测:VIN/GND/ON 在下排,VOUT/QOD/CT 在上排)──
vx, vy = U5
s.w('+5V', (vx - 10, vy - 60), (vx - 10, 320), (560, 320)) # 1 VIN
s.port('IN', '+5V', 560, 320, 180)
s.w('+5V', (600, 320), (600, 300)) # -> C66 上脚
s.w('GND', (600, 260), (600, 235))
s.gnd(600, 235, 0)
s.w('GND', (vx, vy - 60), (vx, 300)) # 2 GND 直着下来
s.gnd(vx, 300, 0)
s.w('VBUS_EN', (vx + 10, vy - 60), (vx + 10, 330), (800, 330)) # 3 ON
s.port('IN', 'VBUS_EN', 800, 330, 180)
s.w('VBUS_EN', (760, 330), (760, 315)) # -> R62 上脚
s.w('GND', (760, 275), (760, 250))
s.gnd(760, 250, 0)
s.w('VBUS', (vx, vy + 60), (vx, 560)) # 5 QOD
s.port('BI', 'VBUS', vx, 560, 0)
s.w('VBUS', (vx - 10, vy + 60), (vx - 10, 520), (vx, 520)) # 6 VOUT -> QOD
s.w('VBUS', (vx, 530), (640, 530)) # -> C67 下脚
s.w('GND', (640, 570), (640, 595))
s.gnd(640, 595, 180)
s.w('CT', (vx + 10, vy + 60), (vx + 10, 620), (560, 620)) # 4 CT
s.w('CT', (560, 620), (560, 625))
s.w('GND', (560, 655), (560, 680))
s.gnd(560, 680, 180)
# ── 4. Rp 与口温 ─────────────────────────────────────────────────
s.w('VBUS', (920, 320), (920, 345))
s.port('BI', 'VBUS', 920, 345, 0)
s.w('CC1', (920, 280), (920, 255))
s.port('BI', 'CC1', 920, 255, 0)
s.w('USB_THERM', (1030, 320), (1030, 345))
s.port('BI', 'USB_THERM', 1030, 345, 0)
s.w('GND', (1030, 280), (1030, 255))
s.gnd(1030, 255, 0)
# ── 5. 文字 ─────────────────────────────────────────────────────
s.txt(45, 795, u'【B3B-USB】尾插座 BM24-50DS + D± ESD + Rp/口温 + VBUS 负载开关', 14)
s.txt(60, 265, u'J4 的脚号→网络照抄 R1 的 J71(隔着一次排线映射实测,§24),', 10)
s.txt(60, 247, u' 脚本每次都和 C5-J1401A 逐脚对账,对不上直接终止。', 10)
s.txt(60, 229, u'⚠ 51/52 是两个大电源触点:51 = G2 = USB_VBUS,52 = G1 = 真地。', 10)
s.txt(60, 211, u' 接反就是 5V 对地短路 —— 打样前用表笔在排线自由端确认一次。', 10)
s.txt(60, 193, u'⚠ R64 56k 是 CC1 上拉到 VBUS 的 Rp:手机靠它判定「有主机插入」。', 10)
s.txt(60, 175, u' R1 里这颗藏在 USB-A 转 C 线的 C 头中,本方案没有那根线,必须自己放。', 10)
s.txt(60, 157, u'⚠ R62 100k:ON 脚不得悬空(datasheet),PCA9555 复位时高阻 ⇒ VBUS 默认断。', 10)
s.txt(60, 139, u'U7 的 1/6、3/4 内部短接:1/3 = 上游 *_DP/DM,6/4 = 手机 *_DPJ/DMJ。', 10)
return s
def main():
push = '--push' in sys.argv
check_source()
place = placement()
exp = expect()
s = build()
s.assert_grid()
errs = s.netcheck(pins_of(exp), NC, ALLOW_SPLIT)
s.report(len(place), errs)
if not push:
return
s.clear()
s.place(place, exp)
s.push_geometry()
json.dump(NC, io.open(os.path.join(HERE, 'b3b-nc.json'), 'w', encoding='utf-8'),
ensure_ascii=False, indent=1)
print(u'接着跑 verify_sch.py --nets --nc .claude/r2/b3b-nc.json')
if __name__ == '__main__':
main()