# -*- coding: utf-8 -*- u"""R2 刀片 **B4-EDGE** 页:PCI-E x4 金手指 + PRSNT# 短接 + D± 退路座。 /c/Python314/python .claude/r2/b4_edge.py # 只算 + 本地自检 /c/Python314/python .claude/r2/b4_edge.py --push # 清页 + 重画(幂等) M2 的最后一页(前四页见 b1_power / b2_ctrl / b3_phone / b3b_usb)。 ## T4 已核(2026-09-08):针位表对着 PCI-E CEM 逐脚核过,方案文档 §5.2 有两处是错的 | 针 | 方案文档(凭记忆写的) | CEM 实际 | 后果 | |---|---|---|---| | **B3** | 「RSVD,拿来做 +5V」 | **+12V**(B1/B2/B3 三针都是) | 刀片误插进真 PC:主板把 12 V 灌进刀片的 +5V 轨,U5 TPS22918(6V abs max)与手机 VBUS 全灭 | | B12 | 「RSVD」 | CEM 1.x = RSVD,2.0 起 = CLKREQ# | 不能当自由针用(真主板上有 3.3V 上拉、真卡上是开漏输出)| ⇒ **+5V 改挂 A19 / A32** —— 这两针在 CEM 1.x~5.0 每一版里都是 RSVD, 真主板上不接、真卡上也不接,是两个方向都安全的仅有选择。 代价:两针都在 A 排,图上是两个同名 `+5V` 端口(见下面 ALLOW_SPLIT)。 其余针位与 §5.2 一致:+12V=B1/B2/B3/A2/A3、+3V3=B8/A9/A10、SCL=B5(SMCLK)、 SDA=B6(SMDAT)、INT#=B11(WAKE#)、EFUSE_EN=A11(PERST#)、D±=B14/B15(PET0,两侧 B13/B16 天然夹地)、 PRSNT#=A1↔B31(x4 全长在位;B17 那个是 x1 用的,本板当地用)。 **VDAC 定在 A16**(PERp0):上下两针 A15 / A17 都是地,是全连接器夹得最干净的一个位置。 方案文档原来写「预留差分对 A16/A17」—— 那对差分在刀片上没有对端,留着也用不了, 让给需要屏蔽的模拟量更划算。 ## 悬空的 6 根针(`b4-nc.json`) `A5~A8`(JTAG TCK/TDI/TDO/TMS)、`B9`(JTAG TRST#)、**`B10`(+3.3Vaux)**。 ⚠ B10 是必须悬空的那一根:真卡插进底板槽时,底板若在这针上给了 5V 会直接打穿它的 aux 域。 其余用不上的针(B12 CLKREQ# / B17 PRSNT#2(x1) / B30 PWRBRK# / 全部 lane 针)一律接地: 两个方向都无害(主板侧要么开漏、要么 AC 耦合),换来 39 根地针的回流面积。 ## 版图:地母线甩到最左/最右,信号端口排在母线与引脚之间 抄 `b3b_usb` 的 J4(50 脚 BM24)那一套 —— 地是长横线一路拉到母线,信号是短横线接端口, 两者 y 不同所以永不交叉。竖线只在同一网络的几针之间用,且都躲在母线之外的空白区。 ## ⚠ ALLOW_SPLIT 放宽了,补一层更硬的判据 连接器页天然靠同名端口/旗成网,不是靠导线:`GND`(三段梳子)、`+12V`(A/B 两排)、 `+3V3`(A/B 两排)、`+5V`(A19/A32 两针)、`USB_DP/DM`(金手指 + 退路座)都是分块的。 放宽 ALLOW_SPLIT 会让「漏连」这条检查失效,所以本脚本额外做一件事: 逐脚断言「这根针踩着的那段导线,声明的网络名 == PIN_NET 里写的」(`check_pin_nets`)。 少接一根、接错一根都会当场报出来。 """ 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, by_rel # noqa: E402 PAGE, NAME = 'c8f10dbd1286853b', 'B4-EDGE' EDGE, WV4 = 'PCIE-X4-EDGE-64P', 'C225106' # ── 版面常量 ───────────────────────────────────────────────────────── J1 = (620, 470) # 金手指符号中心;A 排引脚 x=585,B 排 x=655 AX, BX = J1[0] - 35, J1[0] + 35 ABUS, BBUS = 340, 900 # 左/右地母线 APORT, BPORT = 490, 760 # 信号端口列 J2 = (1000, 700) # D± 退路座(1.25mm 4P),引脚朝左 def row(n): u"""金手指第 n 号针的 y(A/B 两排同高,pitch 10)。""" return J1[1] + 155 - 10 * (n - 1) # ── 64 根针的网络分配(T4 已核,出处见表头)────────────────────────── PIN_NET = {} for _n, _net in [(1, 'PRSNT#'), (2, '+12V'), (3, '+12V'), (4, 'GND'), (5, 'NC'), (6, 'NC'), (7, 'NC'), (8, 'NC'), (9, '+3V3'), (10, '+3V3'), (11, 'EFUSE_EN'), (12, 'GND'), (13, 'GND'), (14, 'GND'), (15, 'GND'), (16, 'VDAC'), (17, 'GND'), (18, 'GND'), (19, '+5V'), (32, '+5V')]: PIN_NET['A%d' % _n] = _net for _n in range(20, 32): # A20~A31 全部接地 PIN_NET['A%d' % _n] = 'GND' for _n, _net in [(1, '+12V'), (2, '+12V'), (3, '+12V'), (4, 'GND'), (5, 'SCL'), (6, 'SDA'), (7, 'GND'), (8, '+3V3'), (9, 'NC'), (10, 'NC'), (11, 'INT#'), (12, 'GND'), (13, 'GND'), (14, 'USB_DP'), (15, 'USB_DM'), (31, 'PRSNT#'), (32, 'GND')]: PIN_NET['B%d' % _n] = _net for _n in range(16, 31): # B16~B30 全部接地 PIN_NET['B%d' % _n] = 'GND' assert len(PIN_NET) == 64, len(PIN_NET) NC = {'J1': [p for p, n in PIN_NET.items() if n == 'NC']} assert sorted(NC['J1']) == sorted(['A5', 'A6', 'A7', 'A8', 'B9', 'B10']), NC['J1'] # 地母线要按「连续成段」切:中间隔着信号针的地方断开,每一段自带一面地旗。 AGND = [n for n in range(1, 33) if PIN_NET['A%d' % n] == 'GND'] BGND_MAIN = [n for n in range(1, 33) if PIN_NET['B%d' % n] == 'GND' and n != 32] # 同名端口/旗成网、导线上天然分块的网络(判据换成 check_pin_nets,见表头) ALLOW_SPLIT = set(['GND', '+12V', '+3V3', '+5V', 'USB_DP', 'USB_DM']) EDGE_REL = {} for _n in range(1, 33): EDGE_REL['A%d' % _n] = (-35, 155 - 10 * (_n - 1)) EDGE_REL['B%d' % _n] = (35, 155 - 10 * (_n - 1)) # A1251WV-S-4P rot 0 实测:1~4 自上而下、5/6 是两个固定脚,全部朝左出 WV4_REL = {'1': (-25, 15), '2': (-25, 5), '3': (-25, -5), '4': (-25, -15), '5': (-25, 35), '6': (-25, -35)} def placement(): return [['J1', EDGE, J1[0], J1[1], 0], ['J2', WV4, J2[0], J2[1], 0]] def expect(): return {'J1': by_rel(J1[0], J1[1], EDGE_REL), 'J2': by_rel(J2[0], J2[1], WV4_REL)} def build(): s = Sheet(PAGE, NAME) # ── 1. 地:左右两把梳子 ────────────────────────────────────────── for n in AGND: s.w('GND', (AX, row(n)), (ABUS, row(n))) s.w('GND', (ABUS, row(AGND[0])), (ABUS, 625)) # 母线往上甩出去挂旗 s.w('GND', (ABUS, row(AGND[0])), (ABUS, row(AGND[-1]))) s.gnd(ABUS, 625, 180) for n in BGND_MAIN: s.w('GND', (BX, row(n)), (BBUS, row(n))) # ⚠ 母线在 B8(+3V3) 那一行故意断开:B9/B10 悬空、这一段是全页唯一 # 能让 +3V3 横穿到母线以东挂电源旗的通道(否则右半边被梳子封死)。 s.w('GND', (BBUS, row(4)), (BBUS, 625)) s.w('GND', (BBUS, row(4)), (BBUS, row(7))) s.gnd(BBUS, 625, 180) s.w('GND', (BBUS, row(12)), (BBUS, row(30))) s.w('GND', (BBUS, row(30)), (BBUS, 310)) s.gnd(BBUS, 310, 0) # B32 单独一根(下面就是 PRSNT# 的过桥,梳子伸不下去) s.w('GND', (BX, row(32)), (BX, 290)) s.gnd(BX, 290, 0) # ── 2. +12V:A2/A3 与 B1/B2/B3,各自并起来出一个端口 ───────────── s.w('+12V', (AX, row(2)), (520, row(2))) s.w('+12V', (AX, row(3)), (520, row(3))) s.w('+12V', (520, 650), (520, row(3))) s.w('+12V', (520, 650), (470, 650)) s.port('OUT', '+12V', 470, 650, 0) for n in (1, 2, 3): s.w('+12V', (BX, row(n)), (720, row(n))) s.w('+12V', (720, row(1)), (720, row(3))) s.w('+12V', (720, row(1)), (720, 660)) s.w('+12V', (720, 660), (790, 660)) s.port('OUT', '+12V', 790, 660, 180) # ── 3. +3V3:A9/A10 一面旗,B8 横穿母线缺口到东边一面旗 ────────── s.w('+3V3', (AX, row(9)), (450, row(9))) s.w('+3V3', (AX, row(10)), (450, row(10))) s.w('+3V3', (450, row(9)), (450, row(10))) s.w('+3V3', (450, row(9)), (450, 570)) s.pwr('+3V3', 450, 570, 0) s.w('+3V3', (BX, row(8)), (1000, row(8))) s.w('+3V3', (1000, row(8)), (1000, 580)) s.pwr('+3V3', 1000, 580, 0) # ── 4. +5V:A19 / A32 两根 RSVD 针,各一个同名端口 ─────────────── for n in (19, 32): s.w('+5V', (AX, row(n)), (APORT, row(n))) s.port('OUT', '+5V', APORT, row(n), 0) # ── 5. A 排信号 ───────────────────────────────────────────────── s.w('EFUSE_EN', (AX, row(11)), (APORT, row(11))) s.port('OUT', 'EFUSE_EN', APORT, row(11), 0) s.w('VDAC', (AX, row(16)), (APORT, row(16))) s.port('OUT', 'VDAC', APORT, row(16), 0) # ── 6. B 排信号 ───────────────────────────────────────────────── for n, net, d in ((5, 'SCL', 'BI'), (6, 'SDA', 'BI'), (11, 'INT#', 'IN'), (14, 'USB_DP', 'BI'), (15, 'USB_DM', 'BI')): s.w(net, (BX, row(n)), (BPORT, row(n))) s.port(d, net, BPORT, row(n), 180) # ── 7. PRSNT#:A1 ↔ B31,从符号上方绕到下方(x4 全长在位)──────── s.w('PRSNT#', (AX, row(1)), (AX, 680)) s.w('PRSNT#', (AX, 680), (290, 680)) s.w('PRSNT#', (290, 680), (290, 245)) s.w('PRSNT#', (290, 245), (700, 245)) s.w('PRSNT#', (700, 245), (700, row(31))) s.w('PRSNT#', (700, row(31)), (BX, row(31))) # 这个端口在刀片上没有对端(PRSNT# 只在金手指内部短接),纯粹为了给网络起名 —— # 不放的话 EasyEDA 会给它一个 $4N... 默认名,PCB 侧脚本按名字就认不出来。 # xpage.py 的 NAMING_ONLY 里已加了它。 s.w('PRSNT#', (620, 245), (620, 215)) s.port('OUT', 'PRSNT#', 620, 215, 0) # ── 8. J2 D± 退路座(1=+5V 2=DP 3=DM 4/5/6=GND)────────────────── jx, jy = J2 for dy, net, d in ((15, '+5V', 'OUT'), (5, 'USB_DP', 'BI'), (-5, 'USB_DM', 'BI')): s.w(net, (jx - 25, jy + dy), (890, jy + dy)) s.port(d, net, 890, jy + dy, 0) for dy in (35, -15, -35): # 4 = 地,5/6 = 固定脚 s.w('GND', (jx - 25, jy + dy), (1030, jy + dy)) s.w('GND', (1030, jy + 35), (1030, jy - 35)) s.w('GND', (1030, jy + 35), (1030, jy + 60)) s.gnd(1030, jy + 60, 180) # ── 9. 文字 ───────────────────────────────────────────────────── s.txt(45, 795, u'【B4-EDGE】PCI-E x4 金手指(只借机械与针位语义,不是 PCIe)+ D± 退路座', 14) s.txt(45, 230, u'⚠ NOT PCIe —— 底板插槽旁必须丝印「PhoneRail blade only」。', 11) s.txt(45, 212, u'T4 已核 CEM 引脚表:B1/B2/B3 都是 +12V(不是 RSVD),', 10) s.txt(45, 194, u' 所以 +5V 挪到 A19 / A32 —— 每一版 CEM 里它俩都是 RSVD。', 10) s.txt(45, 176, u'⚠ B10 = +3.3Vaux 必须悬空;B12/B17/B30 与全部 lane 针接地。', 10) s.txt(45, 158, u'PRSNT# = A1 ↔ B31 短接(x4 全长在位判据)。', 10) s.txt(45, 140, u'J2 = D± 退路座:PCB 上金手指到 J2 之间留割断位(D12)。', 10) return s def check_pin_nets(s): u"""逐脚断言「针踩着的那段导线声明的网络」== PIN_NET。ALLOW_SPLIT 放宽之后靠这条兜底。""" from schcheck import _on errs = [] pins = dict((p, (AX if p[0] == 'A' else BX, row(int(p[1:])))) for p in PIN_NET) for p, xy in sorted(pins.items()): want = PIN_NET[p] got = sorted(set(seg[2] for seg in s.W if _on(xy, seg[0], seg[1]))) if want == 'NC': if got: errs.append(u'%s 应当悬空,却踩在 %s 上' % (p, got)) elif got != [want]: errs.append(u'%s 应当是 %s,实际踩着 %s' % (p, want, got or u'(空)')) return errs def main(): push = '--push' in sys.argv place = placement() exp = expect() s = build() s.assert_grid() pins = dict((d, dict((k, tuple(v)) for k, v in want.items())) for d, want in exp.items()) errs = s.netcheck(pins, NC, ALLOW_SPLIT) + check_pin_nets(s) ngnd = len([1 for p, n in PIN_NET.items() if n == 'GND']) print(u'金手指 64 针:地 %d / 悬空 %d / 有名信号 %d' % (ngnd, len(NC['J1']), 64 - ngnd - len(NC['J1']))) s.report(len(place), errs) with io.open(os.path.join(HERE, 'b4-nc.json'), 'w', encoding='utf-8') as f: f.write(json.dumps(NC, ensure_ascii=False, indent=1)) if not push: return s.clear() s.place(place, exp) s.push_geometry() print(u'接着跑 verify_sch.py --nets --nc .claude/r2/b4-nc.json') if __name__ == '__main__': main()