- 规则:铜层 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
269 行
15 KiB
Python
269 行
15 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""P5-HARNESS -> P5-BTB:板对板排针接口页(两条 2x20)。
|
||
|
||
下板侧装公头 **C55020601**(AZ-PZ2.54-V11-2*20P,塑高 2.5 / 配合针长 6,
|
||
2026-09-05 定稿,取代占位 C9900001574)。见设计文档 §22.3f。
|
||
|
||
该符号是 **IDC 奇偶式**编号:第 r 行 = 引脚 2r-1(左列)/ 2r(右列),
|
||
第 1 行在中心上方 95 个单位。载板母座 C5307344 **完全一样**,
|
||
所以换件之后两块板同一套编号,§22.2 那个「两套编号」的陷阱消失了。
|
||
|
||
用法: /c/Python314/python .claude/p5_btb.py [--push]
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
|
||
PAGE = '58bacf3ddd60ed4e'
|
||
LIB = '0819f05c4eef4c71ace90d822a990e87'
|
||
HDR = 'c0ab04240dc64fca8d6203d15f8a8a68' # C55020601 AZ-PZ2.54-V11-2*20P
|
||
|
||
# ═══════════ 逐针表(2026-09-05 按载板几何重排,取代 09-04 那版)═══════════
|
||
#
|
||
# 每条排针 20 个位置,(北排网络, 南排网络)。
|
||
# **北排 = 奇数脚 2r−1(PCB 上 y = 68.77);南排 = 偶数脚 2r(y = 66.23)。**
|
||
#
|
||
# ## 为什么要重排(§25.10)
|
||
#
|
||
# 09-04 那版是按「通道号」切的(J24 = CH1+CH2+全局 / J25 = CH3+CH4+CH5),
|
||
# 当时载板 PCB 还不存在。载板画出来之后发现两件事:
|
||
#
|
||
# ① 排母南排前 16 位全是 GND 焊盘,Ø1.70 / 间距 2.54 ⇒ 相邻边距 0.84,
|
||
# 扣掉两边 0.2 净距,Inner1 上每个缝只剩 **0.44 mm 铜网**。
|
||
# 4 A 按 IPC-2221 内层 0.5 oz 要 6.97 mm ——
|
||
# **4 A 的电源面不可能穿过排母的地针行。**
|
||
# ② 而 09-04 那版把 VO4/VO5 放在 J32 的**北**排,它们的站却在排母**南**边。
|
||
#
|
||
# ⇒ 规矩:**北排喂北边的站,南排喂南边的站**,一条都不许跨行。
|
||
#
|
||
# ## 载板几何(决定了谁放哪)
|
||
#
|
||
# J31 焊盘 x 37.87~86.13 正对 site1(x 5~72,北) 和 site4(x 41~108,南)
|
||
# J32 焊盘 x 133.87~182.13 正对 site3(x 148~215,北) 和 site5(x 112~179,南)
|
||
# site2 (x 76.5~143.5,北) 夹在两颗中间 —— 它的 VO2_P 取 J31 北排最右三针(x 81~86),
|
||
# 正好顶着 site2 最左边的负载 C811(x 86.0)
|
||
#
|
||
# 重排后五路 VOn_P **全部同侧**,最远曼哈顿从 ~120 mm(VO4 原来要绕排母端头)降到 ≤42 mm。
|
||
#
|
||
# ## 三条不变的约定
|
||
#
|
||
# · 每路 VOn_P 仍是 3 针,**正对面 3 针 GND** 做本路回流(§15.4)
|
||
# · +5V 从 3 针加到 **10 针**、分散在两颗排母的南北四处 ——
|
||
# 不是为了电流(2.8 A / 10 = 0.28 A/针),是为了**让它就近落地、
|
||
# 不必在 Inner1 上拉一条横贯全板的脊**(§25.10)
|
||
# · GND 从 33 针减到 **26 针**,20 A 回流 = 0.77 A/针,仍很宽裕
|
||
# ## 2026-09-06 第三版:74HC595 + 15 颗按键 MOS 整窝挪到载板(§26.23)
|
||
#
|
||
# 跨排针的不再是 15 条 `POGO*` + 5 条 `EDL_EN*` + `HUB_RST`(21 针),
|
||
# 而是移位链的 4 根线 `KEY_SER` / `KEY_SRCLK` / `KEY_RCLK` / `KEY_OE#`。
|
||
# 净腾 17 针,全部补成 GND(回流从 26 针加到 43 针)。
|
||
# 4 根 KEY_* 放在 **J25 北排 3~6 行**(载板 x 139~147):595 摆在载板走廊中段
|
||
# (J31 与 J32 之间,x 110~133),从 J32 西端出线最近;下板这头 ESP32 在 (88,18),
|
||
# U21/U22 腾出来之后 x 88~97 / y 35~55 整片是空的,四根线北上到 J25 西端没有障碍。
|
||
# `POGO*` / `EDL_EN*` / `HUB_RST` 从此是**载板内部网络**。
|
||
J24 = [
|
||
('VO1_P', 'GND'), ('VO1_P', 'GND'), ('VO1_P', 'GND'), # 1~3 -> site1(北)
|
||
('+5V', 'GND'), ('+5V', 'GND'), # 4~5 -> C701/U24(北)
|
||
('GND', 'GND'), # 6~9 原 POGO1/POGO4/EDL_EN1/EDL_EN4,
|
||
('GND', 'GND'), # 2026-09-06 随 595 上载板腾空 -> GND
|
||
('GND', 'GND'),
|
||
('GND', 'GND'),
|
||
('VBUS_EN1', 'VBUS_EN4'), # 10
|
||
('SDA', '+5V'), ('SCL', '+5V'), # 11~12 南=U27/C704(南)
|
||
('GND', 'UP_VBUS_DET'), # 13 R722 在底层 y62.2
|
||
('GND', 'GND'), # 14
|
||
('GND', 'VO4_P'), ('GND', 'VO4_P'), ('GND', 'VO4_P'), # 15~17 -> site4(南)
|
||
('VO2_P', 'GND'), ('VO2_P', 'GND'), ('VO2_P', 'GND'), # 18~20 -> site2(北)
|
||
]
|
||
J25 = [
|
||
('+5V', '+5V'), ('+5V', '+5V'), # 1~2 北->C702/U25 南->U28/C705
|
||
('KEY_SER', 'VBUS_EN5'), # 3~6 北=74HC595 移位链 4 根(原 POGO2_* / EDL_EN2)
|
||
('KEY_SRCLK', 'GND'), # 南=CH5 的 VBUS 使能(原 EDL_EN5 -> GND)
|
||
('KEY_RCLK', 'GND'),
|
||
('KEY_OE#', 'GND'),
|
||
('VBUS_EN2', 'GND'), # 7
|
||
('GND', 'GND'), # 8 原 HUB_RST -> 载板内部
|
||
('GND', 'VO5_P'), ('GND', 'VO5_P'), ('GND', 'VO5_P'), # 9~11 -> site5(南)
|
||
('GND', 'GND'), ('GND', 'GND'), ('GND', 'GND'), # 12~14 原 POGO3_* / POGO5_* -> GND
|
||
('VO3_P', 'GND'), ('VO3_P', 'GND'), ('VO3_P', 'GND'), # 15~17 -> site3(北)
|
||
('GND', 'GND'), ('VBUS_EN3', 'GND'), # 18~19 原 EDL_EN3 -> GND
|
||
('+5V', '+5V'), # 20 -> C703/U26(北)
|
||
]
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# ⭐ MATE_SWAP:下板这一侧的网络要**奇偶对调**(2026-09-06 用户拍板,方案 A,§26.5)
|
||
#
|
||
# 上面两张表是按**载板**几何写的:(北排, 南排) = (载板 y 68.77, 载板 y 66.23),
|
||
# 而载板的北排 = 奇数脚 2r−1。`c1_conn.py` 直接用这两张表生成载板 C1-CONN,**不受本开关影响**。
|
||
#
|
||
# 但下板不一样:母座 J31/J32 装在载板**底层**,顶视图里镜像一次(skill §9w),
|
||
# 公头 J24/J25 装在下板**顶层**不镜像。两颗封装的焊盘编号又是**同手性**的
|
||
# (本地 pin1 都在西南角)。⇒ 对插时下板 pin(2r−1) 压在载板 pin(2r) 上,整体错开一行位。
|
||
# 实测(`pcb1_place.py --verify`):80 个焊盘全部落在载板针正下方 0.001 mm,针号全错开。
|
||
#
|
||
# 两颗都是**无防呆的 2×20**,物理完全对称,"pin1 在哪一排"纯粹是封装画法约定,
|
||
# 不存在谁画错。所以修法就是把下板这一侧的网络按针号对调一次:
|
||
# 下板 pin(2r−1) := 载板 pin(2r) 的网络,下板 pin(2r) := 载板 pin(2r−1) 的网络。
|
||
#
|
||
# ⚠️ 关掉这个开关 = 下板 VO1_P 顶在载板 GND 上(4 A 直接对地)。
|
||
MATE_SWAP = True
|
||
|
||
|
||
def board_rows(rows):
|
||
u"""把「按载板写的 (北, 南)」翻成「下板符号的 (奇数脚, 偶数脚)」。"""
|
||
return [(s, n) for n, s in rows] if MATE_SWAP else list(rows)
|
||
|
||
|
||
CONNS = [('J24', 300, 500, board_rows(J24)), ('J25', 800, 500, board_rows(J25))]
|
||
|
||
STUB = 40 # 引脚向外引出的长度
|
||
ROW0 = 95 # 第 1 行相对元件中心的 y 偏移(实测,公母两颗一样)
|
||
DY = 10
|
||
|
||
|
||
def call(js, timeout=180):
|
||
r = run(js, timeout=timeout)
|
||
if not r.get('success'):
|
||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:600])
|
||
return r['result']
|
||
|
||
|
||
def plan():
|
||
u"""生成导线 / 端口 / 文字。
|
||
|
||
⚠️ 2026-09-05 起 **GND 也用 netport,不再画地母线**。
|
||
原因:重排之后 GND 不再是「右列从第 1 行起的连续块」,而是散在两列的若干段
|
||
(J24 北 13~17 / 南 1~5,14,18~20;J25 北 9~11,20 / 南 5~8,15~19)。
|
||
给每一段配母线 + 地旗的话,旗会掉进相邻行的端口里(行距只有 10,
|
||
而旗高 15~20,见 skill §7)。端口在 10 的行距上是验证过能排下的
|
||
(这一页左列本来就是 20 个端口叠着),所以统一用端口最省事也最不容易错。
|
||
"""
|
||
segs, ports, texts = [], [], []
|
||
for desig, cx, cy, rows in CONNS:
|
||
assert len(rows) == 20, u'%s 必须正好 20 行' % desig
|
||
lx, rx = cx - 30, cx + 30
|
||
lp, rp = lx - STUB, rx + STUB
|
||
for i, (nnet, snet) in enumerate(rows):
|
||
y = cy + ROW0 - DY * i
|
||
segs.append((lx, y, lp, y))
|
||
ports.append(('BI', nnet, lp, y, 180))
|
||
segs.append((rx, y, rp, y))
|
||
ports.append(('BI', snet, rp, y, 0))
|
||
texts.append((cx - 60, cy + ROW0 + 45,
|
||
u'%s 奇数脚(左) = 载板南排 / 偶数脚(右) = 载板北排 —— 已按 §26.5 奇偶对调' % desig, 11))
|
||
|
||
# 每路 VOn_P 正对面必须是 3 针 GND(§15.4 各路独立回地)
|
||
for desig, cx, cy, rows in CONNS:
|
||
for i, (n, sth) in enumerate(rows):
|
||
if n.startswith('VO') and n.endswith('_P'):
|
||
assert sth == 'GND', u'%s 第%d行 北=%s 但南不是 GND' % (desig, i + 1, n)
|
||
if sth.startswith('VO') and sth.endswith('_P'):
|
||
assert n == 'GND', u'%s 第%d行 南=%s 但北不是 GND' % (desig, i + 1, sth)
|
||
|
||
texts += [
|
||
(60, 780, u'【板对板接口】下板 → 载板,两条 2x20 加高排针(2.54mm)', 14),
|
||
(60, 758, u'公头在下板(C55020601 AZ-PZ2.54-V11-2*20P,塑高 2.5 / 针长 6,直插镀金),母座在载板(C5307344 PC104-2*20,塑高 10.5)。', 10),
|
||
(60, 738, u'编号:第 r 行 = 引脚 2r-1(左)/ 2r(右)。⚠ 本页的网络已相对载板 C1-CONN **奇偶对调**(§26.5):', 10),
|
||
(60, 722, u' 母座装在载板底层、顶视图镜像一次,对插时下板 pin(2r-1) 正对载板 pin(2r)。两颗都是无防呆 2x20,编号只是画法约定。', 10),
|
||
(60, 718, u'分配按载板几何排(§25.10):J31 正对 site1(北)+site4(南),J32 正对 site3(北)+site5(南),site2 取 J31 北排最右三针。', 10),
|
||
(60, 698, u'⚠ 北排喂北边的站、南排喂南边的站,一路都不许跨行 —— 4A 的电源面穿不过排母的地针行(缝里只剩 0.44mm 铜)。', 10),
|
||
(60, 215, u'每路 VOn_P 用 3 针并联(4A / 3 = 1.33A 每针,母座额定 2.5A 每针),正对面 3 针地做本路回流(§15.4)。', 10),
|
||
(60, 195, u'+5V 用 10 针、分散在两颗排母的南北四处:不是为了电流(0.28A 每针),是为了就近落地,免得在 Inner1 上拉一条横贯全板的脊。', 10),
|
||
(60, 175, u'合计 37 个信号 + 43 个地(2026-09-06 起:74HC595 + 按键 MOS 整窝在载板,跨板只剩 KEY_SER/SRCLK/RCLK/OE# 四根,§26.23)。', 10),
|
||
(60, 155, u'对插高度 = 2.5 + 10.5 = 13.0mm(塑料对塑料定位),满足 §22.3d 的净空要求。', 10),
|
||
(60, 135, u'⚠ 下板 C2/C3 罐高必须 <= 11.5mm,且 C2~C6 压在 J24 位置上、PCB 上要挪开(§22.3f)。', 10),
|
||
]
|
||
# ---- 下发前的几何断言(skill §6d 横平竖直 / §8a-2b 坐标吸附到 5 的倍数)----
|
||
for a, b, c, e in segs:
|
||
assert (a == c) != (b == e), u'有一段导线不是横平竖直或零长度: %s' % ((a, b, c, e),)
|
||
for v in (a, b, c, e):
|
||
assert v % 5 == 0, u'导线坐标 %d 不是 5 的倍数: %s' % (v, (a, b, c, e))
|
||
for it in ports:
|
||
assert it[2] % 5 == 0 and it[3] % 5 == 0, u'port %s 的坐标 (%d,%d) 不是 5 的倍数' % (it[1], it[2], it[3])
|
||
return segs, ports, texts
|
||
|
||
|
||
def main():
|
||
push = '--push' in sys.argv
|
||
segs, ports, texts = plan()
|
||
|
||
print(u'导线 %d 端口 %d 文字 %d' % (len(segs), len(ports), len(texts)))
|
||
names = {}
|
||
for _, n, _, _, _ in ports:
|
||
names[n] = names.get(n, 0) + 1
|
||
for n in sorted(names):
|
||
print(u' %-14s %d' % (n, names[n]))
|
||
if not push:
|
||
print(u'(未推送,加 --push)')
|
||
return
|
||
|
||
call("""
|
||
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,800));
|
||
if ((await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name.indexOf('P5')!==0) throw new Error('page');
|
||
let n=0;
|
||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||
if (c.getState_ComponentType()==='sheet') continue;
|
||
await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); n++;
|
||
}
|
||
for (const w of await eda.sch_PrimitiveWire.getAll()) { await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId()); n++; }
|
||
for (const r of await eda.sch_PrimitiveRectangle.getAll()) { await eda.sch_PrimitiveRectangle.delete(r.getState_PrimitiveId()); n++; }
|
||
for (const p of await eda.sch_PrimitivePolygon.getAll()) { await eda.sch_PrimitivePolygon.delete(p.getState_PrimitiveId()); n++; }
|
||
for (const t of await eda.sch_PrimitiveText.getAll()) { await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId()); n++; }
|
||
await eda.dmt_Schematic.modifySchematicPageName('%s','P5-BTB');
|
||
return n;
|
||
""" % (PAGE, PAGE))
|
||
print(u'已清空并改名 P5-BTB')
|
||
|
||
for desig, cx, cy, _ in CONNS:
|
||
call("""
|
||
await eda.sch_PrimitiveComponent.create({libraryUuid:'%s', uuid:'%s'}, %d, %d, undefined, 0, false, true, true);
|
||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||
if (c.getState_ComponentType()!=='part') continue;
|
||
if (Math.round(c.getState_X())!==%d) continue;
|
||
await c.toAsync().setState_Designator('%s').done();
|
||
}
|
||
return 1;
|
||
""" % (LIB, HDR, cx, cy, cx, desig))
|
||
print(u'已放置 J24 / J25')
|
||
|
||
real = call("""
|
||
const out={};
|
||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||
if (c.getState_ComponentType()!=='part') continue;
|
||
const d=c.getState_Designator();
|
||
const ps=await c.getAllPins();
|
||
out[d]=ps.map(p=>[p.getState_PinNumber(),Math.round(p.getState_X()),Math.round(p.getState_Y())]);
|
||
}
|
||
return out;
|
||
""")
|
||
for desig, cx, cy, rows in CONNS:
|
||
pm = {p: (x, y) for p, x, y in real[desig]}
|
||
for i in range(20):
|
||
y = cy + ROW0 - DY * i
|
||
lp_, rp_ = str(2 * i + 1), str(2 * i + 2)
|
||
assert pm[lp_] == (cx - 30, y), u'%s 第%d行左 = pin%s,期望 %s 实测 %s' % (
|
||
desig, i + 1, lp_, (cx - 30, y), pm[lp_])
|
||
assert pm[rp_] == (cx + 30, y), u'%s 第%d行右 = pin%s,期望 %s 实测 %s' % (
|
||
desig, i + 1, rp_, (cx + 30, y), pm[rp_])
|
||
print(u'引脚坐标与预期一致')
|
||
|
||
def batch(items, size, js):
|
||
for i in range(0, len(items), size):
|
||
call(js % json.dumps(items[i:i + size], ensure_ascii=False))
|
||
|
||
batch([list(s) for s in segs], 40,
|
||
"for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;")
|
||
batch([list(p) for p in ports], 25,
|
||
"for (const p of %s) await eda.sch_PrimitiveComponent.createNetPort(p[0],p[1],p[2],p[3],p[4],false); return 1;")
|
||
batch([list(t) for t in texts], 15,
|
||
"for (const t of %s) await eda.sch_PrimitiveText.create(t[0],t[1],t[2],0,null,null,t[3],false,false,false,0); return 1;")
|
||
print(u'推送完成')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|