文件
phonerail/.claude/p5_btb.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

269 行
15 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()