# -*- 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()