文件
phonerail/.claude/obsolete/pcb2_place_topside_2026-09-06.py
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

714 行
38 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板 PCB2-CARRIER 布局(2026-09-04 从零开始)。
板框 **220 × 135 mm**,四角 R5。5 块小米8 主板 **3+2 双排面对面**,第二排居中:
┌──────────────────────────────────────────────┐ 135
│ ┌───CH1───┐ ┌───CH2───┐ ┌───CH3───┐ │
│ │ 主板正面朝天,反面贴 3.0 立柱 │ │ y 85.5~130.16
│ └────▼────┘ └────▼────┘ └────▼────┘ │
│ ▓服务带 A(y 67.5~85.5)▓▓▓▓▓▓▓▓▓▓▓▓ │
│ ▓服务带 B(y 49.66~67.5)▓▓▓▓▓▓ │
│ ┌───CH4───┐ ┌───CH5───┐ │
│ 口袋L │ │ │ │ 口袋R │ y 5~49.66
│ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────┘ 220
**为什么这么排**(2026-09-04 用户拍板):小米8 主板的**所有**接口都挤在同一条边上
(侧键 x 2~5.5 / J600 x≈23.8 / EDL x 42.6,45.4 / J1401 x 47.5~59.7,y 全 < 8),
所以每台手机只需要**一条 18 mm 的服务带**贴着那条边。两排面对面 → 共用一条
36 mm 的中央通道,J31/J32 排母正好落在下板 160×125 的投影里。
## 三条布局上的硬决定
1. **J31/J32 在底层(layer 2)。** 下板装公头朝上,载板必须装母座朝下。
⚠️ 底层元件在顶视图里是镜像的 —— 下板 PCB 放 J24/J25 时必须实打实核一次
「第 r 行对第 r 行」(§22.7 那条编号陷阱)。本文件的 `header_pads()` 会把
载板侧 80 个焊盘的绝对坐标打出来,下板照着对。
2. **USB Hub(U32 + Y1 + strap + 去耦)也在底层,压在通道正中 (110, 67)。**
顶层通道被两条服务带占满了,而 Hub 放到左右口袋里的话,到最远那台的 D±
要走 200+ mm —— USB2517I 手册要求 D± < 6 inch(152 mm),超了。
放中间后最长约 92 mm。底层本来就要贴 J31/J32,不额外增加工序。
⚠️ 底层元件在两板之间那 13 mm 缝里,**布下板时别让 C2/C3(12 mm 电解)撞上**。
3. **35 个定位孔做 NPTH(不金属化)。** 主板 7 个孔是金属化的、连着手机地;
做成金属化 + 金属立柱 = 五台手机的地在载板上并成一根,4 A 回流会从
专用的 P− 针里跑掉一部分。§6.4 明确要求每路独立回流 → **立柱必须用尼龙**。
用法:
/c/Python314/python .claude/pcb2_place.py # 只算,本地查重叠/越框
/c/Python314/python .claude/pcb2_place.py --outline # 画板框 + 载板自己的 M3 孔
/c/Python314/python .claude/pcb2_place.py --mech # 35 定位孔 + 5 个手机位丝印
/c/Python314/python .claude/pcb2_place.py --push # 摆元件
/c/Python314/python .claude/pcb2_place.py --layers # 切 4 层(只需跑一次)
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
MIL = 39.3700787401575
BW, BH = 220.0, 135.0 # 板框 mm
CORNER_R = 5.0
# 载板自己的 M3 安装孔。
# ⚠️ 上面两个原来在 (8,127) / (212,127) —— **压在 CH1 / CH3 的手机本体投影里**,
# 其中 (8,127) 的 Ø6 焊盘和 CH1 的定位孔 P1H2 直接重叠 0.93 mm(pcb2_clear.py 抓到)。
# 四角布孔是在五个手机位拍板**之前**放的,之后没复核。
# 板顶那条 y 130.16~135 的 4.84 mm 缝里塞不下 Ø6 焊盘,三个手机横排也已经把
# x 方向占满(5+67.09+4.41+67.09+4.5+67.09+4.91 = 220.09),
# 所以上面两个孔只能退进中央通道 / 右口袋。
# 2026-09-05 用户拍板补了**两个中跨孔**,解决上半部分 220 mm 跨距无支撑的问题
# (§25.6 ⑤ 那条待办到此关闭)。孔位不是拍脑袋定的,是拿真实焊盘 + 五个 Ø12.5
# 电解罐体做网格搜索找出来的(`_tmp_mh.py`),三个候选窗口的实测净间距:
#
# CH1|CH2 缝下 (76.6, 80.1) 净间距 **5.28 mm** ← 选它
# CH2|CH3 缝下 (148.1, 80.1) 净间距 **5.28 mm** ← 选它
# 走廊正中 (127.9, 67.5) 净间距 2.90 mm ← 窄一半,且离要支撑的地方远 18 mm
#
# 选 y≈80 而不是走廊正中 y≈67.5 的两条理由:
# ① 要支撑的是 y 85.5~130 那一排手机,80.1 离它只有 5 mm,67.5 差了 18 mm;
# ② 走廊正中被 C811 / C831 两个 Ø12.5 电解罐夹着(罐体分别占 y68.25~80.75 和
# y54.41~66.91),只剩一条 6.9 mm 高的缝;手机之间那 4.41 mm 的 x 向空档
# 一路通到 y 84,宽敞得多。
# ⚠️ y 再往上就被**顶针** J48/J52(y 87.96,CH2/CH3 的侧键触点)逼住:
# y=83 时净间距掉到 2.41,y=84 只剩 1.41。80.1 是这个窗口的最宽处。
MH = [(8.0, 8.0), (212.0, 8.0), (7.0, 60.0), (213.0, 45.0),
(76.6, 80.1), (148.1, 80.1)]
MH_HOLE, MH_PAD = 3.2, 6.0
# ---- 小米8 主板(顶视,板左下角原点,mm)----
PW, PH = 67.09, 44.66
HOLES = {'H1': (54.2, 42.3), 'H2': (2.6, 38.2), 'H3': (61.9, 33.7), 'H4': (62.1, 29.2),
'H5': (2.6, 27.0), 'H6': (4.0, 10.8), 'H7': (42.6, 3.7)}
HOLE_D = 2.5 # 载板上的定位孔,NPTH
# 侧键触点(§22.3b,已折算成顶视投影)。顶针顺序 = c4_site.py 的 pogo[0..3]
# pogo[0] POGOn_PWR -> J603 电源键
# pogo[1] POGOn_VUP -> J601 音量+
# pogo[2] POGOn_VDN -> J602 音量−
# pogo[3] (公共地) -> J604
POGO_XY = [(3.11, 6.91), (2.70, 4.60), (5.41, 6.52), (2.12, 2.46)]
POGO_TAG = ['J603 PWR', 'J601 VUP', 'J602 VDN', 'J604 GND']
# ---- 五个手机位:本体左下角 + 旋转(0 = 接口边朝下,180 = 接口边朝上)----
SITES = {
1: (5.0, 85.5, 0),
2: (76.5, 85.5, 0),
3: (148.0, 85.5, 0),
4: (40.75, 5.0, 180),
5: (112.25, 5.0, 180),
}
# 上游 USB-C 的中心 X:壳体前端面与**右**板边齐平(中心离前端面 5.134 mm,§25.5 #1)
J23_X = BW - 5.134
# ---- 服务带:站在手机本体坐标里,v<0 = 接口边外侧 ----
# 行 1(v=-3.0)贴着手机边:要接线/插排线的都在这一行
# 行 2(v=-7.5)小有源器件
# 行 3(v=-12.5)大电解 + VBUS 负载开关
BAND = {
# 行 1(v=-3.2)贴着手机边:要接排线 / 焊飞线的都在这一行
'tp_p': (21.0, -3.2, 0), # TP8n1 P+ Ø2.7 通孔焊盘(电池排线焊这里)
'c6': (23.5, -3.2, 0), # C8n6 100nF 0402,紧夹在两个焊盘中间
'tp_n': (26.0, -3.2, 0), # TP8n2 P−
'c2': (31.0, -3.2, 0), # C8n2 100µF 1206
'c3': (36.5, -3.2, 0), # C8n3
'j6': (44.0, -3.2, 0), # J6n EDL 1.25mm 2P(两根线焊到主板 TP1204/1205)
# ⚠️ 2026-09-05:这不是「正对 J1401」—— `mi8_contacts.json`(卡尺校核)记的
# J1401 中心是 u = 53.58(长轴沿 u,占 47.46~59.71),这里是 57.5,横向差 3.9 mm。
# 不致命(原厂排线本身就是歪的、余量极大),但**不能直接改成 53.58**:
# 那会和同一行的 'j6'(占 u 39.8~48.2)撞 0.7 mm,要连 j6/c3 一起重排。
'j7': (57.5, -3.0, 0), # J7n BM24-50DS(插原厂尾插排线)
# 行 2(v=-8.0)小有源器件
'c4': (31.0, -8.0, 0), # C8n4 22µF 1206
'c5': (36.5, -8.0, 0), # C8n5
'r74': (41.0, -8.0, 0), # R74n 100k EDL_EN 下拉
'u4': (45.0, -8.0, 0), # U4n TS5A3157 模拟开关
'c76': (49.5, -8.0, 0), # C76n 100nF 去耦
# ⚠️ 2026-09-06 rot 0 → **180**(布 D± 时才发现的摆位错误)。
# USBLC6-2SC6 是**直通**器件:pin1/6 是 I/O1、pin3/4 是 I/O2,一排朝连接器、一排朝主机。
# rot 0 时南排(y=76.35)是 pin1/2/3 = 连接器侧(`*_DPJ/DMJ`)、北排(y=78.65)是 pin6/5/4 = 主机侧,
# 而 **J7n 在北边(y=81.44)、Hub 在南边** —— 正好接反,两条网络都得绕过封装走
# (实测 DM ~8 mm、DP ~12 mm,几乎没有耦合,偏斜 3.6 mm),
# 长差分对还被逼到 U5n 与 BM24 焊盘之间那条 2.0 mm 的窄缝里换层。
# SOT-23-6 转 180° 之后 **6 个焊盘落在完全相同的 6 个位置**(封装对称),
# 只是引脚编号对调 ⇒ 没有任何新的间距冲突,`VBUS1`(pin5) 顺带也离 U2n 更近。
'u5': (57.5, -8.0, 180), # U5n USBLC6 ESD,紧贴 BM24
# ⚠️ 2026-09-06 rot 0 → **180**(布线时才发现的方向错误)。
# pin1=CC1 要朝**东**(CC1 从 BM24 中央通道的车道出口 u=62.3 过来),
# pin2=VBUS 要朝**西**(VBUS 从 U5n 的 u=57.5 过来)。rot 0 时两根线
# 各自绕过整颗电阻、当场交叉,五个站的 VBUSn 全部布不通。
'r76': (61.5, -8.0, 180), # R76n 56k CC1→VBUS
'r77': (64.0, -8.0, 0), # R77n 100k 口温→GND
# 行 3:大电解摆最左(那一段没有别的东西),VBUS 负载开关摆 BM24 下面
# ⚠ 2026-09-05:'c1' 的 v 从 −11.0 收到 **−8.0**(和行 2 齐平)。
# 换成直插排母之后 J31/J32 变成**通孔件、顶层也占位**,而 Ø12.5 的罐体
# keep-out 正好探进中央走廊:上排三站的罐底 y=68.25、下排两站的罐顶 y=66.95,
# 中间只剩 1.30 mm,而 J31/J32 的焊盘区高 4.24 mm —— 怎么摆都塞不进去。
# 罐体本身在 u 5.75~18.25 这一段是**独占**的(行 1/2/3 都从 u=21 起),
# 所以 v 可以自由收到「不进手机投影」为止(v ≤ −6.25)。取 −8.0:
# · 罐体边到手机板边 3.00 mm(keep-out 边 1.75)
# · keep-out 边到 J31/J32 焊盘 1.63(上排)/ 1.43(下排)mm
# 实测 v ∈ [−9.0, −6.75] 本地四项检查全过,−8.0 取在中间偏保守处。
'c1': (12.0, -8.0, 0), # C8n1 2200µF Ø10 直插 —— 本带最大件
'c70': (39.5, -13.0, 0), # C70n 2.2µF 输入
'u2': (45.0, -13.0, 0), # U24~28 TPS22918 VBUS 负载开关
'c71': (49.5, -13.0, 0), # C71n 100nF 输出
'c72': (52.5, -13.0, 0), # C72n 100pF CT
'r70': (55.5, -13.0, 0), # R70n 100k 下拉
}
C801_BODY_R = 5.0 + 1.25 # Ø10 罐 + 单边 1.25 余量(留到 Ø12.5,见 §24 的 C801 决定)
def desig(n):
u"""第 n 个手机位用到的全部位号。"""
return {
'pogo': ['J%d' % (41 + (n - 1) * 4 + i) for i in range(4)],
'tp_p': 'TP8%d1' % (n - 1), 'tp_n': 'TP8%d2' % (n - 1),
'c1': 'C8%d1' % (n - 1), 'c2': 'C8%d2' % (n - 1), 'c3': 'C8%d3' % (n - 1),
'c4': 'C8%d4' % (n - 1), 'c5': 'C8%d5' % (n - 1), 'c6': 'C8%d6' % (n - 1),
'j6': 'J6%d' % n, 'u4': 'U4%d' % n, 'r74': 'R74%d' % n, 'c76': 'C76%d' % n,
'j7': 'J7%d' % n, 'u5': 'U5%d' % n, 'r76': 'R76%d' % n, 'r77': 'R77%d' % n,
'u2': 'U%d' % (23 + n), 'c70': 'C70%d' % n, 'c71': 'C71%d' % n,
'c72': 'C72%d' % n, 'r70': 'R70%d' % n,
}
def to_board(n, u, v, rot=0):
u"""手机本体坐标 (u,v) -> 板坐标。rot=180 时整位绕本体中心转半圈。"""
x0, y0, rho = SITES[n]
if rho == 0:
return (x0 + u, y0 + v, rot)
return (x0 + PW - u, y0 + PH - v, (rot + 180) % 360)
# ═══════════════ 全局块 ═══════════════
# 左口袋 x 5~40.75、右口袋 x 179.34~215(第二排居中之后左右各空出 35.75 × 44.66)
#
# 2026-09-04 深夜第二轮,用户拍板改了三处(原因见 §25.6):
# ① J23 从**左**口袋挪到**右**口袋。上游 D± 原来要从 x=5 跑到 Hub 的 x=110,
# 曼哈顿 140 mm,而 USB2517I 的上限是 152 mm —— §25.2 当初只核了下游。
# 挪到右边之后上游 ~104 mm、最远下游 ~107 mm,两头都宽裕。
# ② R717/R718(CC1/CC2 的 5.1k 下拉)原来被 hub_layout() 的网格捞走了,
# 摆在**底层**、离 J23 105 mm。CC 脚挂这么长的残桩没有意义,
# 必须和 J23 同层、贴着 A5/B5 焊盘。
# ③ U31(3V3 LDO)原来在左口袋 (25,42),离 Hub 90 mm ——
# §22.7 白纸黑字写着「§9.5 要求的就近 Hub 供电是 PCB 摆位约束」,
# 这一版没落实。挪到 (105,71),离 Hub 的 +3V3 脚 6 mm。
GLOBAL = {
# 上游 USB-C。方向判据两条(2026-09-04 用户两次指正后定的,见 §25.5 #1):
# ① 12 个信号焊盘在**连接器背面**、4 个屏蔽固定孔靠近口,所以口必须朝板外;
# ② 壳体前端面比最外侧铜还往外 2 mm,焊盘框算不出来,是从渲染图量的:
# 用板框紫线 + 信号焊盘列左沿两点标定,得出「中心离前端面 5.134 mm」。
# 挪到右板边之后就是把这条几何镜像过去:rot 270 -> **90**(口朝 +X),
# 中心 x = 220 − 5.134 = 214.866,壳体前端面仍与板边齐平。
# y 取 62.0,让信号焊盘列(x≈212.4)正对 Hub 的上游脚行(y≈62.55)。
'J23': (J23_X, 62.0, 90),
# CC1/CC2 各 5.1k 下拉,贴着 J23 的 A5/B5 焊盘(焊盘在 x≈212.4,往板内 2 mm)
'R717': (J23_X - 4.5, 60.75, 180), # pin1 = CC1 -> J23.A5
'R718': (J23_X - 4.5, 63.75, 180), # pin1 = CC2 -> J23.B5
# AMS1117-3.3:+5V 从 J31 进来,3V3 就近喂 Hub(§9.5)。
# 顶层 y 67.5~72.5 这条横带是空的(J31/J32 在底层,服务带 A 从 y 72.5 起)。
'U31': (105.0, 71.0, 0),
'C736': (98.0, 71.0, 0), # +5V 输入
'C737': (112.5, 68.8, 0), # +3V3 输出,朝 Hub 那一侧
# ~~'U34': (36.0, 12.0, 0)~~ / ~~'C760': (36.0, 6.5, 0)~~ —— 2026-09-06 用户拍板
# 挪到 J31 旁边(底层,见 BOT_GLOBAL)。原来在左口袋贴着 CH4 量进风侧空气,
# SCL/SDA 要从 J31.21/23 跑 70 mm 过去、和别的线抢左下角唯一那条通道(§25.14 末尾)。
}
# 底层的零散件(不属于 Hub 簇,但必须在底层)
# R721/R722 = 上游 VBUS 分压 -> UP_VBUS_DET -> J31 pin36 (81.049, 65.100)。
# 分压**跟着高阻端走**:UP_VBUS_DET 是高阻节点,留在 J31 脚边只有 3 mm;
# 低阻的 VBUS_UP 从 J23 长跑过来无所谓。原来它俩在左口袋陪着老 J23,是反的。
BOT_GLOBAL = {
'R721': (83.0, 62.2, 180), # pin1 = VBUS_UP(朝东,迎着从 J23 过来的那根)
'R722': (81.0, 62.2, 180), # pin1 = UP_VBUS_DET, pin2 = GND
# LM75BD 测温 + 100nF:2026-09-06 从左口袋挪到 J31 北排正上方(底层)。
# SCL/SDA 在 J31.21/23 (63.3/65.8, 68.77),这里离它们 ~4 mm。
# 顶层这一带塞不下(J31 焊盘顶 69.62 到 U24 底 71.25 只有 1.6 mm),底层是空的。
# ⚠️ 只能**横放(rot 90)**:USB1_DP/DM 那对长差分在底层 y≈75.1~76.0 横穿
# (`pcb2_dp_long.py` 的手写几何,不能动),竖放时北排焊盘正压在上面(实测 −0.14)。
# 这条带子只有 69.62(J31 焊盘顶)~ 74.56(USB1_DM 线下沿)= 4.94 mm,
# 横放的 U34 焊盘占 4.38 ⇒ y 只能在 72.01~72.17 之间,取 72.1(上下各留 ~0.28)。
# x 取 68.0:再往左 U34.4 会撞到 USB1_DM 在 (63.45, 74.70) 的换层过孔(实测 0.12)。
# ⚠️ 挪过来之后量的是**排母走廊**的温度,不再是进风侧空气 —— 风扇闭环的阈值要按新位置定。
'U34': (68.0, 72.1, 90),
'C760': (72.6, 72.1, 90),
}
# 底层:两条排母 + 整个 Hub
HDR = {'J31': (62.0, 67.5, 0), 'J32': (158.0, 67.5, 0)}
HUB_CENTER = (110.0, 67.0)
HUB_PARTS = (['U32', 'Y1'] + ['C7%d' % i for i in range(40, 52)]
+ ['R7%02d' % i for i in (8, 9, 10, 11, 12, 14, 15, 16, 19, 20)])
# ⚠ 17/18 已从这里拿掉 —— R717/R718 是 J23 的 CC 下拉,跟着连接器走,见 GLOBAL。
def th_parts():
u"""直插件:引脚穿板,**顶层和底层都占位**,两层的重叠检查都要算它。
载板上有四类:C8n1(Ø10 电解)、TP8n1/TP8n2(Ø2.7 焊盘)、
**J31/J32(2026-09-05 换成直插排母 C5307344 之后也是通孔件)**。
⚠ 顶层要按**罐体 Ø12.5** 算,底层只有两根引脚(按焊盘包围盒算)——
用同一个框会把 J31 误判成撞上 C811。
"""
out = set(HDR)
for n in SITES:
d = desig(n)
out |= {d['c1'], d['tp_p'], d['tp_n']}
return out
def hub_layout():
u"""Hub 的 24 个卫星件,**逐脚对位**手排(2026-09-04 深夜,替换掉原来的 3 mm 网格粗排)。
U32 是 QFN-64,焊盘中心到 ±4.45、包围盒到 ±4.877,脚距 0.5 mm。
0402 件本体 0.54 mm 宽 —— **一个脚一个件地排是排不下的**(0.5 < 0.54),
所以每条边分两圈:A 圈 5.9~6.3 mm、B 圈 8.0 mm,件按 1.2 mm 间距错开放。
## 让出来的三条通道(这是这版手排的主要工作,网格排法做不到)
| 通道 | 位置(相对 Hub 中心)| 给谁 |
|---|---|---|
| 西侧扇出 | x < −4.45, y −3.8~+1.8 | 五对下游 D±(10 根),整条左边不许放件 |
| 南侧竖井 | x −2.4~−0.2, y < −4.45 | XTAL1/2 下到 Y1 + UP_DP/DM 南下再西行 |
| 晶振窝 | (−4.2, −9.2) 一带 | Y1 与两颗负载电容 |
A 圈 y=−6.3 上 `C743`(x−2.65) 与 `C745`(x+0.05) 之间那 2.16 mm 就是南侧竖井,
四根线(XTAL 用 0.2、UP 用 0.28)刚好过得去。
## 逐件的对位依据
- `R708` **RBIAS**:12k 精密偏置,离 pin63 只有 0.76 mm —— 全簇最近的一件。
- `C742/C743` VDD18 / VDD18PLL:内部 LDO 的储能,各自贴 pin25 / pin62。
- `C744~C750`:七颗 +3V3 脚(5/10/24/46/52/57/64)一脚一颗。
⚠ pin5 / pin10 夹在下游 D± 中间,件只能放到扇出区之外(3.3~3.8 mm)——
靠 Inner2 的 +3V3 灌铜就近打孔补偿,不是靠这段走线。
- `C751` 10 µF 1206 是总储能,放北边不抢位置。
- `R709/R710` VBUS 分压留在**东侧 pin44 边上**:高阻分压节点要短,
低阻的 VBUS_UP 从 J23 长跑过来无所谓。
- `R711/R716/R720` 是纯配置下拉/上拉,放 B 圈(离脚 3.1 mm)完全够。
"""
hx, hy = HUB_CENTER
# ⚠️ RB 2026-09-06 用户拍板 8.0 → **9.5**:A/B 两圈之间原来只有 0.268 mm
# (A 圈外脚 117.016 到 B 圈内脚 117.284),`+3V3_HUB` 进 `R716.2` / `VBUS_UP` 进
# `R709.1` 三段怎么都布不通(§25.15:换出线方向、改顺序、单独布都试过)。
# ⚠️ 9.5 **不行**:B 圈外脚正压在 `USB5_DP` 那条竖走的长差分上(x=120.0,左沿 119.86,
# `pcb2_dp_long.py` 的手写几何),而本地检查只看焊盘不看走线、根本拦不住。
# 外脚边到 DP 左沿留 0.2 ⇒ RB ≤ 8.944,取 **8.9**(留 0.244)。
# A/B 缝 = (118.9−0.433−0.283) − (116.3+0.433+0.283) = 1.17 mm,
# 一根 0.35 的线带两边 0.2 净距要 0.75,够。
RA, RB = 6.3, 8.9
out = {'U32': (hx, hy, 0)}
# 南边 A 圈(rot 270 = pin1 朝上、朝芯片)
for dsg, dx in (('C744', -4.45), # pin64 +3V3
('R708', -3.45), # pin63 RBIAS ★ 0.76 mm
('C743', -2.65), # pin62 VDD18PLL
# ── 竖井 −2.4 ~ −0.2:XTAL1/2 + UP_DP/DM 从这里下去 ──
('C745', 0.05), # pin57 +3V3
('R715', 1.25), # pin56 PRTDIS7
('R714', 2.45), # pin54 PRTDIS6
('C746', 3.65)): # pin52 +3V3
out[dsg] = (hx + dx, hy - RA, 270)
# 晶振窝:Y1 摆西南,两颗负载电容各贴一个端子
out['Y1'] = (hx - 4.2, hy - 9.2, 0) # pin1=XTAL1 在左上, pin3=XTAL2 在右下
out['C740'] = (hx - 7.2, hy - 8.35, 180) # pin1 朝右 -> Y1 pin1
out['C741'] = (hx - 3.1, hy - 12.3, 270) # pin1 朝上 -> Y1 pin3
# 北边 A 圈(rot 90 = pin1 朝下、朝芯片)
out['C749'] = (hx - 0.75, hy + RA, 90) # pin24 +3V3
out['C742'] = (hx + 0.75, hy + RA, 90) # pin25 VDD18
out['C751'] = (hx + 3.0, hy + 7.0, 90) # 10 µF 总储能(1206,本簇最大件)
# 西边:整条留给下游 D± 扇出,只在扇出区上下各塞一颗 +3V3 电容
out['C747'] = (hx - RA, hy - 5.0, 180) # 服务 pin5 (+3V3, 夹在 D± 中间)
out['C748'] = (hx - RA, hy + 3.5, 180) # 服务 pin10 (+3V3, 同上)
# 东边 A 圈(rot 0 = pin1 朝左、朝芯片;R709/R710 反过来放)
out['R719'] = (hx + RA, hy + 0.25, 0) # pin40 NONREM1 -> GND
out['R712'] = (hx + RA, hy - 0.95, 0) # pin42 CFGSEL1 -> GND
out['R709'] = (hx + RA, hy - 2.15, 180) # pin1=VBUS_UP 朝外, pin2=VBUS_DET 朝 pin44
out['R710'] = (hx + RA, hy - 3.35, 180) # pin1=VBUS_DET, pin2=GND
out['C750'] = (hx + RA, hy - 4.55, 0) # pin46 +3V3
# 东边 B 圈:三颗配置电阻,离脚 4.0 mm(原 3.1),纯配置下拉/上拉,够
out['R711'] = (hx + RB, hy - 0.35, 0) # pin41 CFGSEL0 -> GND
out['R716'] = (hx + RB, hy - 1.55, 0) # pin43 HUB_RST -> +3V3
out['R720'] = (hx + RB, hy - 2.75, 0) # pin45 NONREM0 -> GND
missing = set(HUB_PARTS) - set(out)
extra = set(out) - set(HUB_PARTS)
if missing or extra:
raise SystemExit(u'Hub 手排对不上 HUB_PARTS:漏 %s / 多 %s' % (sorted(missing), sorted(extra)))
return out
BOTTOM = set(list(HDR) + list(BOT_GLOBAL) + HUB_PARTS) # 这些件在 layer 2
def plan():
u"""汇总所有元件 -> {位号: (x_mm, y_mm, rot, layer)}"""
pos = {}
for n in SITES:
d = desig(n)
for i, (u, v) in enumerate(POGO_XY):
x, y, r = to_board(n, u, v, 0)
pos[d['pogo'][i]] = (x, y, r, 1)
for key, (u, v, r0) in BAND.items():
x, y, r = to_board(n, u, v, r0)
pos[d[key]] = (x, y, r, 1)
for dsg, (x, y, r) in GLOBAL.items():
pos[dsg] = (x, y, r, 1)
for dsg, (x, y, r) in list(HDR.items()) + list(BOT_GLOBAL.items()):
pos[dsg] = (x, y, r, 2)
for dsg, (x, y, r) in hub_layout().items():
pos[dsg] = (x, y, r, 2)
return pos
# ═══════════════ 本地检查 ═══════════════
def load_bb():
u"""从 pcb2-parts.json 读每个元件的焊盘包围盒(相对元件原点,mm)。"""
raw = json.load(open(os.path.join(HERE, 'pcb2-parts.json'), encoding='utf-8'))
bb = {}
for c in raw:
if not c['bb']:
bb[c['d']] = (-0.5, -0.5, 0.5, 0.5)
continue
x, y = c['x'], c['y']
b = c['bb']
bb[c['d']] = ((b[0] - x) / MIL, (b[1] - y) / MIL, (b[2] - x) / MIL, (b[3] - y) / MIL)
return bb
CAN = None # 延迟算:五个 Ø10 电解的位号
def box(d, rot, bb, layer=1):
global CAN
if CAN is None:
CAN = {desig(n)['c1'] for n in SITES}
if d in CAN and layer == 1: # 顶层按罐体 Ø12.5 留;底层只有两根引脚
return (-C801_BODY_R, -C801_BODY_R, C801_BODY_R, C801_BODY_R)
x0, y0, x1, y1 = bb[d]
if rot % 180 == 90:
x0, y0, x1, y1 = y0, x0, y1, x1
x0, x1 = min(x0, x1), max(x0, x1)
y0, y1 = min(y0, y1), max(y0, y1)
return (x0, y0, x1, y1)
def check(pos):
bb = load_bb()
errs = []
known = set(bb)
for d in pos:
if d not in known:
errs.append(u'PCB 上没有这个位号: %s' % d)
missing = known - set(pos)
if missing:
errs.append(u'没安排位置的元件 %d 个: %s' % (len(missing), sorted(missing)[:12]))
TH = th_parts()
def layers(d, lay):
return (1, 2) if d in TH else (lay,)
# 越框
for d, (x, y, r, lay) in pos.items():
if d not in known:
continue
b = box(d, r, bb, lay)
if x + b[0] < 1.0 or x + b[2] > BW - 1.0 or y + b[1] < 1.0 or y + b[3] > BH - 1.0:
errs.append(u'越框: %s @ (%.1f, %.1f)' % (d, x, y))
# 同层重叠
items = [d for d in pos if d in known]
for i in range(len(items)):
for j in range(i + 1, len(items)):
a, b_ = items[i], items[j]
ax, ay, ar, al = pos[a]
bx, by, br, bl = pos[b_]
shared = set(layers(a, al)) & set(layers(b_, bl))
for L in sorted(shared):
pa, pb = box(a, ar, bb, L), box(b_, br, bb, L)
if (ax + pa[0] < bx + pb[2] - 0.02 and bx + pb[0] < ax + pa[2] - 0.02 and
ay + pa[1] < by + pb[3] - 0.02 and by + pb[1] < ay + pa[3] - 0.02):
errs.append(u'重叠(L%d): %s(%.1f,%.1f) x %s(%.1f,%.1f)' % (L, a, ax, ay, b_, bx, by))
# 载板自己的 M3 孔不许落进手机本体投影(螺钉头会顶到手机板),
# 也不许和 35 个手机定位孔挨太近(立柱和螺钉会打架)。
# 这一条是 2026-09-04 补的 —— 原来的四角布孔就是这么错的。
for i, (mx, my) in enumerate(MH, 1):
for n, (x0, y0, rho) in SITES.items():
if x0 - MH_PAD / 2 < mx < x0 + PW + MH_PAD / 2 and y0 - MH_PAD / 2 < my < y0 + PH + MH_PAD / 2:
errs.append(u'载板 M3 孔 MH%d (%.1f,%.1f) 落在 CH%d 主板投影里' % (i, mx, my, n))
for n in SITES:
for hn, (u_, v_) in HOLES.items():
hx, hy, _ = to_board(n, u_, v_)
d = ((mx - hx) ** 2 + (my - hy) ** 2) ** 0.5
need = MH_PAD / 2 + HOLE_D / 2 + 0.2
if d < need:
errs.append(u'载板 M3 孔 MH%d 离定位孔 P%d%s 只有 %.2f mm(要 >= %.2f)'
% (i, n, hn, d, need))
# 顶层元件不能压到手机本体投影里(顶针和定位孔除外 —— 它们本来就在下面)
allow = set()
for n in SITES:
allow |= set(desig(n)['pogo'])
for d, (x, y, r, lay) in pos.items():
if lay != 1 or d in allow or d not in known:
continue
b = box(d, r, bb, 1)
for n, (x0, y0, rho) in SITES.items():
if (x + b[0] < x0 + PW and x0 < x + b[2] and
y + b[1] < y0 + PH and y0 < y + b[3]):
errs.append(u'%s 压在 CH%d 主板投影里 @ (%.1f, %.1f)' % (d, n, x, y))
return errs
# ═══════════════ 下发 ═══════════════
def call(body, timeout=300):
r = run("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1500));" % PCB + body, timeout=timeout)
if not r.get('success'):
print(u'下发失败:', json.dumps(r, ensure_ascii=False)[:600])
sys.exit(1)
return r['result']
def push_layers():
u"""把载板从 2 层切到 **4 层**(2026-09-04 用户拍板,§25.6)。
转板过来默认是两层。下板 §10.1 已经定了四层,而**载板非上四层不可**:
· 11 对 USB HS 差分要 90 Ω —— W0.28/S0.18 是按四层 1.6 mm 叠层
(L1-L2 介质 h=0.2104)算的;两层板的介质是整个 1.5 mm,
同样阻抗线宽要 2 mm 以上,根本排不下;
· 五路 4 A(合计 20 A)没有内层电源面就只能靠外层大面积铜,
而两面都摆满了元件;
· 顶/底两层都是元件面,地回流会被割得很碎。
叠层沿用下板:L1 顶层信号 / L2(id 15) 完整地平面 / L3(id 16) 电源 / L4(id 2) 底层信号。
"""
r = call("""
const before = await eda.pcb_Layer.getTheNumberOfCopperLayers();
await eda.pcb_Layer.setTheNumberOfCopperLayers(4);
const after = await eda.pcb_Layer.getTheNumberOfCopperLayers();
const all = await eda.pcb_Layer.getAllLayers();
await eda.pcb_Document.save();
return {before, after, copper: all.filter(l => l && l.type === 'SIGNAL').map(l => [l.id, l.name])};
""")
print(u'铜层数 %s -> %s' % (r['before'], r['after']))
for lid, name in r['copper']:
print(u' id %-3s %s' % (lid, name))
if r['after'] != 4:
sys.exit(u'!! 切四层失败')
def outline_poly():
u"""板框:一条闭合多段线(4 直边 + 4 个 R5 圆角)。
⚠️ 必须是**一条 polyline**,拆成独立线弧的话自动布线器认不出板框
(pcb_place.py 的 push_outline 里有完整成因)。
"""
R = CORNER_R
pts = []
L = lambda x, y: pts.append(('L', x, y))
A = lambda x, y: pts.append(('ARC', 90.0, x, y))
L(BW - R, 0.0); A(BW, R)
L(BW, BH - R); A(BW - R, BH)
L(R, BH); A(0.0, BH - R)
L(0.0, R); A(R, 0.0)
src, mode = [round(R * MIL, 4), 0.0], None
for p in pts:
if p[0] == 'L':
if mode != 'L':
src.append('L'); mode = 'L'
src += [round(p[1] * MIL, 4), round(p[2] * MIL, 4)]
else:
src.append('ARC'); mode = 'ARC'
src += [p[1], round(p[2] * MIL, 4), round(p[3] * MIL, 4)]
return src
def push_outline():
n = call("""
let del = 0;
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Layer() === 11) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); del++; }
for (const p of await eda.pcb_PrimitivePolyline.getAll())
if (p.getState_Layer() === 11) { await eda.pcb_PrimitivePolyline.delete(p.getState_PrimitiveId()); del++; }
const poly = await eda.pcb_MathPolygon.createPolygon(%s);
const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, 5);
const left = (await eda.pcb_PrimitivePolyline.getAll()).filter(p => p.layer === 11).length;
return {del, made: r ? r.primitiveId : null, left};
""" % json.dumps(outline_poly()))
if n['left'] != 1:
print(u'!! layer 11 上有 %d 条多段线,应为 1' % n['left'])
sys.exit(1)
print(u'板框: %.0f × %.0f mm,一条闭合多段线(R%.0f 圆角)' % (BW, BH, CORNER_R))
k = call("""
const MH = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
let parent = null;
try { parent = p.getState_ParentComponentPrimitiveId(); } catch(e) {}
if (parent) continue;
const num = p.getState_PadNumber();
if (typeof num === 'string' && num.indexOf('MH') === 0) await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (let i = 0; i < MH.length; i++) {
await eda.pcb_PrimitivePad.create(12, 'MH' + (i + 1), MH[i][0], MH[i][1], 0,
['ELLIPSE', %.2f, %.2f], 'GND', ['ROUND', %.4f], 0, 0, 0, true);
k++;
}
return k;
""" % (json.dumps([[round(x * MIL, 2), round(y * MIL, 2)] for x, y in MH]),
MH_PAD * MIL, MH_PAD * MIL, MH_HOLE * MIL))
print(u'载板自己的 M3 定位孔: %d 个(Φ%.1f / 焊盘 Φ%.1f / 接 GND)' % (k, MH_HOLE, MH_PAD))
call("await eda.pcb_Document.save(); return 1;")
def mech_items():
u"""35 个手机定位孔(NPTH)+ 5 个手机位的丝印外框与标签。"""
holes, lines, texts = [], [], []
for n in SITES:
x0, y0, rho = SITES[n]
for hn, (u, v) in HOLES.items():
x, y, _ = to_board(n, u, v)
holes.append(['P%d%s' % (n, hn), round(x * MIL, 2), round(y * MIL, 2)])
# 主板轮廓(丝印,装配对位用)
c = [(x0, y0), (x0 + PW, y0), (x0 + PW, y0 + PH), (x0, y0 + PH)]
for i in range(4):
a, b = c[i], c[(i + 1) % 4]
lines.append([round(a[0] * MIL, 2), round(a[1] * MIL, 2),
round(b[0] * MIL, 2), round(b[1] * MIL, 2)])
# 接口边加粗一道,提醒装配朝向
ey = y0 if rho == 0 else y0 + PH
lines.append([round((x0 + 2) * MIL, 2), round((ey + (1.5 if rho == 0 else -1.5)) * MIL, 2),
round((x0 + PW - 2) * MIL, 2), round((ey + (1.5 if rho == 0 else -1.5)) * MIL, 2)])
texts.append([round((x0 + PW / 2 - 6) * MIL, 2), round((y0 + PH / 2) * MIL, 2),
u'CH%d 主板' % n])
return holes, lines, texts
def push_mech():
holes, lines, texts = mech_items()
n = call("""
const H = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
let parent = null;
try { parent = p.getState_ParentComponentPrimitiveId(); } catch(e) {}
if (parent) continue;
const num = p.getState_PadNumber();
if (typeof num === 'string' && /^P[1-5]H[1-7]$/.test(num)) await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (const [name, x, y] of H) {
await eda.pcb_PrimitivePad.create(12, name, x, y, 0,
['ELLIPSE', %.4f, %.4f], '', ['ROUND', %.4f], 0, 0, 0, false);
k++;
}
return k;
""" % (json.dumps(holes), HOLE_D * MIL, HOLE_D * MIL, HOLE_D * MIL))
print(u'手机定位孔: %d 个 NPTH Φ%.1f(⚠ 必须配尼龙立柱,见文件头第 3 条)' % (n, HOLE_D))
m = call("""
const L = %s, T = %s;
let del = 0;
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Layer() === 3) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); del++; }
for (const s of await eda.pcb_PrimitiveString.getAll())
if (s.getState_Layer() === 3) { await eda.pcb_PrimitiveString.delete(s.getState_PrimitiveId()); del++; }
let k = 0;
for (const [x1,y1,x2,y2] of L) { await eda.pcb_PrimitiveLine.create('', 3, x1, y1, x2, y2, 6); k++; }
// create(layer, x, y, text, fontFamily, fontSize, lineWidth, alignMode, rotation, reverse, expansion, mirror, lock)
for (const [x,y,t] of T) { await eda.pcb_PrimitiveString.create(3, x, y, t, 'NotoSansSC-Regular', 60, 6, 0, 0, false, 0, false, false); }
return {del, k, t: T.length};
""" % (json.dumps(lines), json.dumps(texts, ensure_ascii=False)))
print(u'手机位丝印: %d 条线 + %d 个标签(顺带删掉 %d 个旧图元)' % (m['k'], m['t'], m['del']))
call("await eda.pcb_Document.save(); return 1;")
def push_parts(pos):
plan_list = [[d, round(x * MIL, 2), round(y * MIL, 2), r, lay]
for d, (x, y, r, lay) in sorted(pos.items())]
total = 0
for k in range(0, len(plan_list), 50):
total += call("""
const P = %s;
const map = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) map[c.getState_Designator()] = c;
let n = 0;
for (const [d, x, y, rot, lay] of P) {
const c = map[d]; if (!c) continue;
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {x, y, rotation: rot, layer: lay});
n++;
}
return n;
""" % json.dumps(plan_list[k:k + 50]))
print(u' 已摆 %d / %d' % (total, len(plan_list)))
call("await eda.pcb_Document.save(); return 1;")
print(u'布局已下发并保存:%d 个元件' % total)
def header_pads():
u"""把载板侧 J31/J32 的 80 个焊盘绝对坐标读出来存盘,给下板 PCB 对位用。
⚠️ 这是**唯一靠得住**的对位方式。J31/J32 在载板底层、J24/J25 在下板顶层,
「第 r 行会不会真对上第 r 行」取决于焊盘编号 + 贴装镜像,
光靠脑补两边的引脚表必错(§22.7 已经把这条列为 PCB 阶段的风险)。
做法:本函数把载板侧每个焊盘的 (位号, 焊盘号, x, y) 落成 json,
下板放 J24/J25 时**按同一个装配坐标系把 pin k 摆到同一个 (x, y)**,
数值对上就一定插得上,不用推理。
装配坐标系 = 载板坐标系;下板 160×125 居中放在载板 220×135 下面,
即下板原点 = 载板 (30, 5)。下板侧的目标坐标 = 这里的坐标 − (30, 5)。
"""
r = call("""
const out = [];
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.getState_Designator();
if (d !== 'J31' && d !== 'J32') continue;
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
for (const p of pins) out.push([d, p.padNumber, p.x, p.y, c.getState_Layer()]);
}
return out;
""")
MILf = MIL
data = {'frame': u'载板坐标系,mm;下板原点 = 载板 (30, 5),下板侧目标 = 本坐标 − (30,5)',
'pads': [{'ref': a, 'pad': b, 'x': round(x / MILf, 3), 'y': round(y / MILf, 3),
'layer': lay} for a, b, x, y, lay in r]}
out = os.path.join(HERE, 'pcb2-header-pads.json')
json.dump(data, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(u'排母焊盘坐标已导出: %s(%d 个)' % (out, len(r)))
if __name__ == '__main__':
pos = plan()
errs = check(pos)
print(u'安排了 %d 个元件(顶层 %d / 底层 %d)'
% (len(pos), sum(1 for v in pos.values() if v[3] == 1),
sum(1 for v in pos.values() if v[3] == 2)))
if errs:
print(u'\n!! %d 个问题:' % len(errs))
for e in errs[:30]:
print(' ', e)
if len(errs) > 30:
print(u' ...还有 %d 条' % (len(errs) - 30))
sys.exit(1)
print(u'本地检查通过:无重叠、无越框、顶层没有元件压进手机投影')
if '--layers' in sys.argv:
push_layers()
if '--outline' in sys.argv:
push_outline()
if '--mech' in sys.argv:
push_mech()
if '--push' in sys.argv:
push_parts(pos)
header_pads()