- 规则:铜层 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
458 行
24 KiB
Python
458 行
24 KiB
Python
# -*- 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()
|