文件
phonerail/.claude/p5_btb.py
T
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

262 行
14 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/针,仍很宽裕
J24 = [
('VO1_P', 'GND'), ('VO1_P', 'GND'), ('VO1_P', 'GND'), # 1~3 -> site1(北)
('+5V', 'GND'), ('+5V', 'GND'), # 4~5 -> C701/U24(北)
('POGO1_PWR', 'POGO4_PWR'), # 6~10 北=CH1 信号
('POGO1_VUP', 'POGO4_VUP'), # 南=CH4 信号
('POGO1_VDN', 'POGO4_VDN'),
('EDL_EN1', 'EDL_EN4'),
('VBUS_EN1', 'VBUS_EN4'),
('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
('POGO2_PWR', 'VBUS_EN5'), # 3~4 南=CH5 的两个使能
('POGO2_VUP', 'EDL_EN5'), # (U28/U45 就在 x133~136)
('POGO2_VDN', 'GND'),
('EDL_EN2', 'GND'),
('VBUS_EN2', 'GND'),
('HUB_RST', 'GND'), # 8
('GND', 'VO5_P'), ('GND', 'VO5_P'), ('GND', 'VO5_P'), # 9~11 -> site5(南)
('POGO3_PWR', 'POGO5_PWR'), # 12~14 北=CH3 南=CH5 顶针
('POGO3_VUP', 'POGO5_VUP'),
('POGO3_VDN', 'POGO5_VDN'),
('VO3_P', 'GND'), ('VO3_P', 'GND'), ('VO3_P', 'GND'), # 15~17 -> site3(北)
('EDL_EN3', 'GND'), ('VBUS_EN3', 'GND'), # 18~19
('+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'合计 54 个信号 + 26 个地(20A 回流 = 0.77A 每针)。', 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()