# -*- 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()