文件
phonerail/.claude/r2/a5_slots.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

458 行
24 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 **A5-SLOTS** 页:PCI-E x4 插槽阵(槽 1~3)。
/c/Python314/python .claude/r2/a5_slots.py # 只算 + 本地自检
/c/Python314/python .claude/r2/a5_slots.py --push # 清页 + 重画(幂等,只有主代理能跑)
配套的 `a5b_slots.py` 画槽 4/5 + 5 个 D± 退路座(J39~J43)。**为什么要拆两页见下面 §版面**。
本页是 `b4_edge.py`(刀片金手指)的**对端**:64 针的语义完全由 `b4_edge.PIN_NET` 定,
本脚本直接 `import` 它,不再抄一份,改针位表只能改那一处。
## ⚠️ 唯一一处**故意不镜像**刀片:`PRSNT#`
刀片(`b4_edge.py`)把 `A1` 和 `B31` **短接在一起**,而且那个网络在刀片上没有别的对端 ——
它就是一段悬空的跳线(PCI-E CEM 里 add-in card 的标准做法)。
所以在底板(= 主板侧)上这两针**必须是两个不同的节点**,才构成在位检测回路:
| | `A1`(PRSNT1#) | `B31`(PRSNT2#) |
|---|---|---|
| 真 PCIe 主板 | 接地 | 上拉 + 被读 |
| **本页(底板)** | **`GND`** | **`PRSNT{n}#`** → A3 的 ESP32 输入(上拉也在 A3)|
| 刀片 B4-EDGE | ←── 两针短接(插上才把 B31 拉到地)──→ |
⛔ **如果底板也照字面「镜像」、把 A1/B31 并成同一个 `PRSNT{n}#`**,
整条回路里就一个对地通路都没有:ESP32 靠上拉永远读到高,
**插不插刀片都报「未插入」,而且 DRC/ERC/几何验证一道都不会响**。
`ROLE_OVERRIDE` 里就这一条,改它之前先把上面这张表看完。
## 版面:5 个槽一页放不下,拆成 A5 + A5B(先算账再动手)
每槽 64 针两排同高、pitch 10 ⇒ 引脚跨度 **310**,两排引脚横跨 70。
按「地梳齿 + 母线 + 信号端口排在母线与引脚之间」(抄 `b4_edge`)压到最紧:
```
cx-170 ── 地旗体 ─┐
cx-160 ── A 排地母线(19 根梳齿从 cx-35 拉过来)
cx-140 ── A 排信号端口列(标签朝内伸,符号也朝内,左边不占地)
cx-80 / cx-65 ── A 排 +12V / +3V3 汇流竖线
cx-35 / cx+35 ── 引脚列
cx+80 ── B 排 +12V 汇流竖线
cx+140 ── B 排信号端口列
cx+160 ── B 排地母线
cx+175 ── B8 的 +3V3 东出通道(母线在这一行断开)
cx+185 ── 旗体右边界
```
⇒ 每槽净宽 **355**、槽距 ≥ 365。一行 3 槽 = 1085 ≤ 页面可用 1125 ✅;
4 槽 = 1450 ❌;5 槽 = 1815 ❌。竖着排两行也不行:一行连旗带引脚约 400,
两行 820 > 802,而且下面那一行必然压进标题栏(`x>455 且 y<192`)。
**⇒ 结论:A5-SLOTS 放槽 1~3,新开 A5B-SLOTS 放槽 4~5 + 全部 5 个 D± 退路座。**
退路座整组放在 A5B 是因为 A5 上一寸空白都不剩了,而 A5B 空出一大片;
它们是同一件事(D12 的退路),放一起也好找。
## 交叉点 0 是设计出来的,不是碰巧
母线在这种页面上是一堵墙,**任何信号都别想横穿**。所以:
- A 排:所有信号支线止步于 `cx-140`、电源汇流止步于 `cx-65`,母线在 `cx-160` ——
母线可以从 A4 一路连到 A31 **不用断**,全 A 排只要一面地旗。
- B 排:母线只在 **B8 那一行断开**(B9/B10 本来就悬空),
这是 `+3V3` 横穿到母线以东挂旗的唯一通道,两段各自带一面地旗(抄 `b4_edge` §17.2)。
- `A1` 的地不往西走(会和 A2/A3 的 +12V 汇流竖线打架),改成从引脚**竖直往上引 25 挂旗**。
## ALLOW_SPLIT 放宽了哪些,凭什么
`GND` `+12V` `+3V3` `+5V` 是靠同名旗/端口成网的,天然分块。
另外放行 **`INT#`**:五个槽的 B11 是 wire-OR 并联,**唯一一颗上拉在 A3**,
每槽出一个同名 OUT 端口是这种总线的标准画法(而且它还跨两页,本来就连不成一块)。
`PRSNT{n}#` / `SDA{n}` / `SCL{n}` / `VDAC{n}` / `EFUSE_EN{n}` **都不放行**,一块就是一块。
放宽之后靠 `check_pin_nets()` 兜底:**5 槽 × 64 针逐脚**断言
「这根针踩着的那段导线,声明的网络名 == 期望名」,少接一根、接错一根当场报出来。
"""
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, _on # noqa: E402
from b4_edge import PIN_NET, EDGE_REL, WV4_REL # noqa: E402
PAGE, NAME = '2c73c5bf77f34053', 'A5-SLOTS'
EDGE, WV4 = 'C19188869', 'C225106' # PCIE-64P11L(SOFNG)/ A1251WV-S-4P
# ── ⚠ 2026-09-08 换料结掉 T8:SAMTEC PCIE-064-02-F-D-TH → SOFNG PCIE-64P11L ─────
# 用户要 PC 标准的便宜料。要注意的是**两颗的脚号体系不一样**:
#
# 刀片 `PCIE-X4-EDGE-64P`(金手指,符号借自 Samtec) 脚号 = `A1`~`A32` / `B1`~`B32`
# 底板 `PCIE-64P11L`(真插槽) 脚号 = `1`~`64`,**脚名**才是 A1~B32
#
# 换算:`A(n) → 脚号 n`、`B(n) → 脚号 32+n`。
# 引脚**相对坐标两颗完全一致**(A 排 x=-35 / B 排 x=+35,y = 155-10*(n-1)),
# 所以版图、母线、端口位置一个字都不用改 —— 只有 `expect()` / `nc_table()` 换脚号。
# ⚠ 它比金手指**多一根 `65` 脚 = 外壳/屏蔽,在 (0,+175)**(符号顶端正中),本页接 GND;
# 不接的话 `verify_sch` 会报悬空引脚。
# ⛔ 它是**贴片立式**(原来那颗是直插):保持力只靠焊盘剪切。五片刀片各驮一块手机主板,
# M4 布板时必须另加固定件(梳齿 / 压条),不能只靠插槽 —— 已写进页面文字与 eda-parts role。
SHIELD_PIN, SHIELD_REL = '65', (0, 175)
def slot_pin(cem):
u"""CEM 针名(`A1`~`A32` / `B1`~`B32`)→ `PCIE-64P11L` 符号的脚号(`'1'`~`'64'`)。"""
i = int(cem[1:])
assert 1 <= i <= 32 and cem[0] in 'AB', cem
return str(i if cem[0] == 'A' else 32 + i)
SLOT_REL = dict((slot_pin(k), v) for k, v in EDGE_REL.items())
SLOT_REL[SHIELD_PIN] = SHIELD_REL
assert len(SLOT_REL) == 65 and SLOT_REL['1'] == (-35, 155) and SLOT_REL['33'] == (35, 155)
assert SLOT_REL['64'] == (35, -155), SLOT_REL['64']
SLOT_REF = {1: 'J34', 2: 'J35', 3: 'J36', 4: 'J37', 5: 'J38'}
WV_REF = {1: 'J39', 2: 'J40', 3: 'J41', 4: 'J42', 5: 'J43'}
# ── 本页的槽位(A5B 画槽 4/5,坐标在 a5b_slots.py)────────────────────
SLOT_XY = {1: (200, 470), 2: (565, 470), 3: (930, 470)} # 槽距 365
# ── 针 → 网络:刀片的 PIN_NET 加槽号后缀,只有 A1 例外(见表头)──────
ROLE_OVERRIDE = {'A1': 'GND'} # ⚠ 底板 = 主板侧,A1 接地;改前先读表头
GLOBAL = set(['GND', '+12V', '+3V3', '+5V', 'INT#', 'NC']) # 不加槽号的
def base_net(pin, n):
u"""刀片侧针名 → 底板侧网络名(n = 槽号)。"""
role = ROLE_OVERRIDE.get(pin, PIN_NET[pin])
if role in GLOBAL:
return role
if role == 'PRSNT#':
return 'PRSNT%d#' % n
if role.startswith('USB_'): # USB_DP / USB_DM
return 'USB%d_%s' % (n, role[4:])
return '%s%d' % (role, n) # SCL / SDA / VDAC / EFUSE_EN
# 逐脚自检:镜像关系必须是「除了 A1 全都对得上」
assert len(PIN_NET) == 64, len(PIN_NET)
assert PIN_NET['A1'] == 'PRSNT#' and PIN_NET['B31'] == 'PRSNT#', u'刀片侧 PRSNT# 针位变了'
assert base_net('A1', 1) == 'GND' and base_net('B31', 1) == 'PRSNT1#'
assert [p for p in PIN_NET if PIN_NET[p] != base_net(p, 1).rstrip('1')
and base_net(p, 1) not in ('USB1_DP', 'USB1_DM', 'PRSNT1#')] == ['A1'], \
u'除了 A1 之外还有针没有镜像刀片 —— 要么 PIN_NET 变了,要么 base_net 写漏了'
NCPINS = sorted([p for p, r in PIN_NET.items() if r == 'NC'])
assert NCPINS == sorted(['A5', 'A6', 'A7', 'A8', 'B9', 'B10']), NCPINS
AGND = [n for n in range(1, 33) if base_net('A%d' % n, 1) == 'GND']
BGND = [n for n in range(1, 33) if base_net('B%d' % n, 1) == 'GND']
assert AGND[0] == 1 and len(AGND) == 20, AGND # A1 也归了地
assert len(BGND) == 20, BGND
BGND_TOP = [n for n in BGND if n <= 7] # B4 / B7,母线上段
BGND_BOT = [n for n in BGND if n >= 12] # B12 以下,母线下段
assert BGND_TOP == [4, 7] and BGND_BOT[0] == 12 and BGND_BOT[-1] == 32
# A 排信号(脚号, 端口方向);A1 已经归地,不在这里
A_SIG = [(11, 'IN'), (16, 'IN'), (19, 'IN'), (32, 'IN')]
B_SIG = [(5, 'BI'), (6, 'BI'), (11, 'OUT'), (14, 'BI'), (15, 'BI'), (31, 'OUT')]
# D± 退路座(A1251WV-S-4P):逐槽镜像刀片 J2 的 1=+5V 2=DP 3=DM 4/5/6=GND
WV_NET = {'1': '+5V', '2': 'USB%d_DP', '3': 'USB%d_DM',
'4': 'GND', '5': 'GND', '6': 'GND'}
# ── 一个槽的版图 ──────────────────────────────────────────────────────
def geom(cx, cy):
return dict(AX=cx - 35, BX=cx + 35, ABUS=cx - 160, BBUS=cx + 160,
APORT=cx - 140, BPORT=cx + 140,
A12=cx - 80, A33=cx - 65, B12=cx + 80, B33=cx + 175)
def rowy(cy, n):
u"""第 n 号针的 y(A/B 两排同高,pitch 10,A1/B1 在 cy+155)。"""
return cy + 155 - 10 * (n - 1)
def draw_slot(s, n, cx, cy):
g = geom(cx, cy)
AX, BX = g['AX'], g['BX']
def r(k):
return rowy(cy, k)
def net(pin):
return base_net(pin, n)
# ── A 排:地 ────────────────────────────────────────────────────
# A1 单独往上挂旗:它往西走会和 A2/A3 的 +12V 汇流竖线在 (A12, r1) 撞成交叉
s.w('GND', (AX, r(1)), (AX, r(1) + 25))
s.gnd(AX, r(1) + 25, 180)
# 65 脚 = 连接器外壳/屏蔽(符号顶端正中 (0,+175)),接地。金手指那颗没有这根针。
# ⚠ 不要往**上**引再挂一面地旗:cy+195 那一带是页眉文字的地盘(A5B 上就撞了)。
# 往左 35 正好落在 A1 那根地短线 (AX, r1)~(AX, r1+25) 的中段上 —— T 型相连,
# 不新增旗、不占用连接器上方的任何空间。
s.w('GND', (cx, cy + 175), (AX, cy + 175))
for k in AGND[1:]:
s.w('GND', (AX, r(k)), (g['ABUS'], r(k)))
# 母线连续(A4 → A31):信号止步于 cx-140、电源止步于 cx-65,没人横穿
s.w('GND', (g['ABUS'], r(4)), (g['ABUS'], r(31) - 20))
s.gnd(g['ABUS'], r(31) - 20, 0)
# ── A 排:+12V(A2/A3)与 +3V3(A9/A10),各自一面 Power 旗 ──────
for k in (2, 3):
s.w('+12V', (AX, r(k)), (g['A12'], r(k)))
s.w('+12V', (g['A12'], r(3)), (g['A12'], r(1) + 10))
s.pwr('+12V', g['A12'], r(1) + 10, 0)
for k in (9, 10):
s.w('+3V3', (AX, r(k)), (g['A33'], r(k)))
# 往上挂到 A5~A8 那段悬空针留出来的空白里(旗体上沿离 A4 的梳齿还有 10)
s.w('+3V3', (g['A33'], r(10)), (g['A33'], r(9) + 20))
s.pwr('+3V3', g['A33'], r(9) + 20, 0)
# ── A 排:信号端口(+5V ×2 / EFUSE_ENn / VDACn)──────────────────
for k, d in A_SIG:
nm = net('A%d' % k)
s.w(nm, (AX, r(k)), (g['APORT'], r(k)))
s.port(d, nm, g['APORT'], r(k), 180) # IN + rot180 = 符号/标签朝内
# ── B 排:地(母线在 B8 那一行断开,给 +3V3 让出东出通道)────────
for k in BGND_TOP:
s.w('GND', (BX, r(k)), (g['BBUS'], r(k)))
s.w('GND', (g['BBUS'], r(4) + 20), (g['BBUS'], r(7)))
s.gnd(g['BBUS'], r(4) + 20, 180)
for k in BGND_BOT:
s.w('GND', (BX, r(k)), (g['BBUS'], r(k)))
s.w('GND', (g['BBUS'], r(12)), (g['BBUS'], r(32) - 20))
s.gnd(g['BBUS'], r(32) - 20, 0)
# ── B 排:+12V(B1/B2/B3)与 +3V3(B8,穿母线缺口)───────────────
for k in (1, 2, 3):
s.w('+12V', (BX, r(k)), (g['B12'], r(k)))
s.w('+12V', (g['B12'], r(3)), (g['B12'], r(1) + 20))
s.pwr('+12V', g['B12'], r(1) + 20, 0)
s.w('+3V3', (BX, r(8)), (g['B33'], r(8)))
s.w('+3V3', (g['B33'], r(8)), (g['B33'], r(8) - 20))
s.pwr('+3V3', g['B33'], r(8) - 20, 180)
# ── B 排:信号端口(SCLn/SDAn/INT#/USBn_DP/DM/PRSNTn#)───────────
for k, d in B_SIG:
nm = net('B%d' % k)
s.w(nm, (BX, r(k)), (g['BPORT'], r(k)))
s.port(d, nm, g['BPORT'], r(k), 180) # BI/OUT + rot180 = 标签朝内
def draw_wv(s, n, jx, jy):
u"""D± 退路座(D12):1=+5V 2=USBn_DP 3=USBn_DM 4/5/6=GND,引脚全部朝左。"""
px, gx, ptx = jx - 25, jx - 45, jx - 75
for dy, nm, d, rot in ((15, '+5V', 'IN', 0),
(5, 'USB%d_DP' % n, 'BI', 180),
(-5, 'USB%d_DM' % n, 'BI', 180)):
s.w(nm, (px, jy + dy), (ptx, jy + dy))
s.port(d, nm, ptx, jy + dy, rot) # 标签一律朝左(背离座子)
# 地分两段:脚 5(固定脚,在最上)往上挂,脚 4/6 往下挂 ——
# 中间三行是信号,一根竖着的地母线穿过去就是三个交叉点
s.w('GND', (px, jy + 35), (gx, jy + 35))
s.w('GND', (gx, jy + 35), (gx, jy + 55))
s.gnd(gx, jy + 55, 180)
s.w('GND', (px, jy - 15), (gx, jy - 15))
s.w('GND', (px, jy - 35), (gx, jy - 35))
s.w('GND', (gx, jy - 15), (gx, jy - 55))
s.gnd(gx, jy - 55, 0)
# ── 组装 ──────────────────────────────────────────────────────────────
def placement(slots, wvs):
return ([[SLOT_REF[n], EDGE, cx, cy, 0] for n, (cx, cy) in sorted(slots.items())]
+ [[WV_REF[n], WV4, jx, jy, 0] for n, (jx, jy) in sorted(wvs.items())])
def expect(slots, wvs):
out = {}
for n, (cx, cy) in slots.items():
out[SLOT_REF[n]] = by_rel(cx, cy, SLOT_REL) # 脚号 1~64 + 65 屏蔽
for n, (jx, jy) in wvs.items():
out[WV_REF[n]] = by_rel(jx, jy, WV4_REL)
return out
def nc_table(slots):
return dict((SLOT_REF[n], [slot_pin(p) for p in NCPINS]) for n in slots)
def allow_split(slots, wvs):
u"""只放行「靠同名旗/端口成网」的:电源地、跨页 wire-OR 的 INT#、以及本页同时有槽和退路座的 D±。"""
out = set(['GND', '+12V', '+3V3', '+5V', 'INT#'])
for n in wvs:
if n in slots: # 槽和它的退路座在同一页 = 天然两块
out.add('USB%d_DP' % n)
out.add('USB%d_DM' % n)
return out
def build(slots, wvs, texts, page, name):
s = Sheet(page, name)
for n, (cx, cy) in sorted(slots.items()):
draw_slot(s, n, cx, cy)
for n, (jx, jy) in sorted(wvs.items()):
draw_wv(s, n, jx, jy)
for t in texts:
s.txt(t[0], t[1], t[2], t[3])
return s
# ── 逐脚对账(ALLOW_SPLIT 放宽之后的硬判据)──────────────────────────
def check_pin_nets(s, slots, wvs):
u"""5 槽 × 64 针 + 退路座 6 针:断言每根针踩着的导线声明的网络名 == 期望名。"""
errs, seen = [], 0
for n, (cx, cy) in sorted(slots.items()):
g = geom(cx, cy)
for p in sorted(PIN_NET):
x = g['AX'] if p[0] == 'A' else g['BX']
xy = (x, rowy(cy, int(p[1:])))
want = base_net(p, n)
got = sorted(set(w[2] for w in s.W if _on(xy, w[0], w[1])))
seen += 1
if want == 'NC':
if got:
errs.append(u'%s(槽%d).%s 应当悬空,却踩在 %s 上'
% (SLOT_REF[n], n, p, got))
elif got != [want]:
errs.append(u'%s(槽%d).%s 应当是 %s,实际踩着 %s'
% (SLOT_REF[n], n, p, want, got or u'(空)'))
for n, (jx, jy) in sorted(wvs.items()):
for p, dy in (('1', 15), ('2', 5), ('3', -5), ('4', -15), ('5', 35), ('6', -35)):
xy = (jx - 25, jy + dy)
want = WV_NET[p]
want = want % n if '%d' in want else want
got = sorted(set(w[2] for w in s.W if _on(xy, w[0], w[1])))
seen += 1
if got != [want]:
errs.append(u'%s(槽%d).%s 应当是 %s,实际踩着 %s'
% (WV_REF[n], n, p, want, got or u'(空)'))
return errs, seen
def text_clash(s, exp):
u"""文字压在导线 / 引脚 / 旗端口上(抄 a1_input.py,本页文字全靠这条守住空白带)。"""
errs, boxes, pts = [], [], []
for x, y, txt, size in s.TX:
cjk = sum(1 for ch in txt if ord(ch) > 0x2E7F)
w = size * (cjk + 0.5 * (len(txt) - cjk))
boxes.append((x, y - size * 0.8, x + w, y + size * 0.8, txt))
for d, ps in exp.items():
for num, xy in ps.items():
pts.append((xy[0], xy[1], u'引脚 %s.%s' % (d, num)))
for it in s.F + s.PT:
pts.append((it[2], it[3], u'旗/端口 %s' % it[1]))
for x0, y0, x1, y1, txt in boxes:
for px, py, what in pts:
if x0 <= px <= x1 and y0 <= py <= y1:
errs.append(u'文字压住 %s @(%d,%d): %s' % (what, px, py, txt[:26]))
for a, b, nt in s.W:
if a[1] == b[1]:
if y0 <= a[1] <= y1 and min(a[0], b[0]) <= x1 and max(a[0], b[0]) >= x0:
errs.append(u'文字压住导线 %s y=%d: %s' % (nt, a[1], txt[:26]))
elif x0 <= a[0] <= x1 and min(a[1], b[1]) <= y1 and max(a[1], b[1]) >= y0:
errs.append(u'文字压住导线 %s x=%d: %s' % (nt, a[0], txt[:26]))
return sorted(set(errs))
def flag_clash(s, gx=20, gy=10):
u"""两面旗/端口的符号叠在一起 —— crossings() 查不到(旗不是导线)。"""
errs, items = [], [(it[1], it[2], it[3]) for it in s.F + s.PT]
for i in range(len(items)):
for j in range(i + 1, len(items)):
(n1, x1, y1), (n2, x2, y2) = items[i], items[j]
if abs(x1 - x2) < gx and abs(y1 - y2) < gy:
errs.append(u'旗/端口 %s @(%d,%d) 与 %s @(%d,%d) 挨太近'
% (n1, x1, y1, n2, x2, y2))
return sorted(set(errs))
def port_extent(s, slots, wvs, pad=20):
u"""端口的符号+标签往哪边伸多远(契约 §7 第 8 条:约 70~100 个单位,flag_clash 够不着)。
IN + rot0 / BI/OUT + rot180 → 标签在**左**;IN + rot180 / BI/OUT + rot0 → 在**右**(skill §8b)。
这里只查一件事:某个端口的标签有没有伸进**别的槽**的引脚列。
"""
errs = []
cols = []
for n, (cx, cy) in slots.items():
cols.append((SLOT_REF[n], cx - 35, cx + 35, rowy(cy, 32), rowy(cy, 1)))
for n, (jx, jy) in wvs.items():
cols.append((WV_REF[n], jx - 25, jx + 25, jy - 35, jy + 35))
for d, nm, x, y, rot in s.PT:
w = 40 + 6.7 * len(nm) # skill §8b 的保守估算
left = (d == 'IN') == (rot == 0) # IN+rot0 / BI·OUT+rot180 → 标签在左
x0, x1 = (x - w, x) if left else (x, x + w)
# 归属:先按 y 落在谁的引脚带里,再按 x 取最近的一件
cand = [c for c in cols if c[3] <= y <= c[4]] or cols
own = min(cand, key=lambda c: abs(x - (c[1] + c[2]) / 2.0))[0]
for ref, cx0, cx1, cy0, cy1 in cols:
if ref == own:
continue # 自己那一件:标签朝内压在自己的引出线上是有意的
if cy0 - 40 <= y <= cy1 + 40 and x0 <= cx1 + pad and x1 >= cx0 - pad:
errs.append(u'端口 %s @(%d,%d) 的标签(%d~%d)伸进了 %s 的地盘'
% (nm, x, y, x0, x1, ref))
return sorted(set(errs))
# ── 页面文字 ──────────────────────────────────────────────────────────
TEXTS = [
(45, 800, u'【A5-SLOTS】5 个 PCI-E x4 插槽之 槽 1~3(槽 4/5 与 5 个 D± 退路座在 A5B-SLOTS:一页排不下 5 槽,算账见脚本表头)', 13),
(45, 778, u'⚠ 插槽旁必须丝印「NOT PCIe — PhoneRail blade only」(方案 §5.2)—— 本页只是写在这里,PCB 阶段落地', 11),
(45, 758, u'针位逐脚镜像刀片 B4-EDGE 的 PIN_NET(T4 已对 CEM 核过):+12V=A2/A3/B1/B2/B3,+3V3=A9/A10/B8,+5V=A19/A32(每版 CEM 都是 RSVD)', 10),
(45, 740, u'SCL=B5 / SDA=B6 / INT#=B11(五槽 wire-OR,唯一一颗上拉在 A3)/ EFUSE_EN=A11 / VDAC=A16 / D±=B14·B15(两侧 B13·B16 天然夹地)', 10),
(45, 722, u'⚠ B10(+3.3Vaux)必须悬空:真显卡误插进本槽时,底板若在这针上给电会直接打穿它的 aux 域。A5~A8、B9 同样悬空,每槽共 6 针', 10),
(45, 704, u'⚠ B3 是 +12V 不是 RSVD、B12 是 CLKREQ# 不是 RSVD(方案文档旧描述有误,以 b4_edge.py 的 PIN_NET 为准);B12/B17/B30 与全部 lane 针接地', 10),
(45, 686, u'✅ T8 已结(2026-09-08 换料):J34~J38 = SOFNG PCIE-64P11L(C19188869),PC 标准 x4 64PIN。⛔ 贴片立式,保持力只靠焊盘 —— 刀片要另加固定件', 10),
(45, 250, u'⚠ PRSNT 是全页唯一**不镜像**刀片的地方:底板 A1 = GND、B31 = PRSNTn#(上拉与读取都在 A3)。', 11),
(45, 232, u' 刀片上 A1↔B31 是一段悬空短接线,插上才把 B31 拉到地 —— 底板若也把两针并成一个网,回路里没有对地通路,ESP32 永远读「未插入」', 10),
(45, 214, u'每槽 64 针 = 20 地 + 6 悬空 + 18 有名信号。地针走「梳齿 + 母线 + 每段一面地旗」;A 排母线(cx−160)从 A4 连到 A31 不用断,', 10),
(45, 196, u' B 排母线(cx+160)只在 B8 那一行断开,让 +3V3 横穿到母线以东挂旗(抄 B4-EDGE §17.2);所有信号止步于 cx±140 ⇒ 交叉点 0', 10),
]
def main(slots=None, wvs=None, texts=None, page=PAGE, name=NAME):
slots = SLOT_XY if slots is None else slots
wvs = {} if wvs is None else wvs
texts = TEXTS if texts is None else texts
push = '--push' in sys.argv
place = placement(slots, wvs)
exp = expect(slots, wvs)
nc = nc_table(slots)
s = build(slots, wvs, texts, page, name)
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(slots, wvs))
pin_errs, npin = check_pin_nets(s, slots, wvs)
errs += pin_errs + text_clash(s, exp) + flag_clash(s) + port_extent(s, slots, wvs)
nnc = sum(len(v) for v in nc.values())
print(u'%s:%d 个插槽 × 64 针 + %d 个 D± 退路座 × 6 针 = 逐脚对账 %d 根'
% (name, len(slots), len(wvs), npin))
print(u' 每槽 地 %d / 悬空 %d / 有名信号 %d;本页悬空针合计 %d'
% (len(AGND) + len(BGND), len(NCPINS),
64 - len(AGND) - len(BGND) - len(NCPINS), nnc))
s.report(len(place), errs)
if page is None:
print(u'⚠ 本页还没有 uuid(等主代理建页后填 PAGE),--push 不可用')
return
with io.open(os.path.join(HERE, '%s-nc.json' % name.lower()), '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/%s-nc.json' % name.lower())
if __name__ == '__main__':
main()