原理图/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
871 行
47 KiB
Python
871 行
47 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""载板 PCB2-CARRIER 布局 —— **v2:元件全部在底层,排线穿槽(2026-09-06 用户拍板)**。
|
||
|
||
上一版(顶层服务带 + 底层 Hub)存档在 `obsolete/pcb2_place_topside_2026-09-06.py`,
|
||
当时的全部铜/元件状态在 `_pcb2_snap_topside.json`(`pcb2_snapshot.py` 抓的)。
|
||
|
||
板框 **220 × 135 mm**,四角 R5。5 块小米8 主板 **3+2 双排面对面**,第二排居中:
|
||
|
||
┌──────────────────────────────────────────────┐ 135
|
||
│ ┌───CH1───┐ ┌───CH2───┐ ┌───CH3───┐ │
|
||
│ │ 主板正面朝天,反面贴 3.0 立柱 │ │ y 85.5~130.16
|
||
│ └═════════┘ └═════════┘ └═════════┘ │ ═ = 两条穿线槽(贴着接口边外侧)
|
||
│ 中央走廊:底层排 TP/J7/U5 两行 + J31/J32 + Hub │ y 49.66~85.5
|
||
│ ┌═════════┐ ┌═════════┐ │
|
||
│ │ CH4 │ │ CH5 │ │ y 5~49.66
|
||
│ └─────────┘ └─────────┘ │
|
||
└──────────────────────────────────────────────┘ 220
|
||
|
||
## v2 的四条硬决定(2026-09-06)
|
||
|
||
1. **顶层只剩 20 根顶针**(它们必须顶着主板反面的侧键触点,别无选择)。
|
||
其余 141 件全部 layer 2。顶面成为一张干净的「床」——主板压板、风道都好做。
|
||
2. **每站两条 3.0 mm 穿线槽**(`SLOTS`,本体坐标;按 2026-09-06 实测排线尺寸定):
|
||
电池槽 u 18.8~28.8 / v −4.2~−1.2(接口边外侧,正对 J600,排线 8×38);
|
||
**尾插槽 u 45~65 / v 1.4~4.4,在 J1401 正下方(v4,用户拍板)**,排线 18×55:
|
||
从 J1401 出线朝接口边 → 绕过接口边折回主板下方 → 正下方下穿 → **沿载板背面往 +v 走 ~34 mm** 到 J7n(v 37)。
|
||
EDL 两根飞线跟着尾插排线走。
|
||
槽在 EasyEDA 里是 **layer 12 (MULTI) 的填充 = 挖槽区域**(`pcb_PrimitiveFill`)。
|
||
3. **C8n1 换成矮封装 SMD 电解**(2026-09-06 用户拍板):红宝石 6.3TPV2200M10X10.5,C3828985,
|
||
D10×L10.5,罐体 10.3 见方(`CAN_BODY`)。两板净空 13 mm ⇒ **下板在这五块投影下只能放 ≤ 1.5 mm 的件**
|
||
(`--heights` 会把底层高度图导出给下板用)。
|
||
4. **底层元件离 35 个定位孔中心 ≥ 2.8 mm、离 6 个 M3 孔中心 ≥ 3.5 mm**:
|
||
尼龙立柱从底面用螺钉锁,螺钉头要地方。
|
||
|
||
## 底层排法(本体坐标,v<0 = 接口边外侧 = 中央走廊;v>0 = 主板投影下方)
|
||
|
||
(v4,全部在主板投影下方;`BAND` 是事实来源)
|
||
v −4.2~−1.2 ═ 电池槽 u 18.8~28.8 ═ 接口边外侧
|
||
v −4.2~−1.2 ═ 电池槽 u 18.8~28.8 ═ ═ 尾插槽 u 45~65 ═ (v4c:两条槽同一条线,都在接口边外侧)
|
||
v 0~13 │ 电池排线车道 u 18.8~27.8 │ C8n1(34.3,11.3)
|
||
v 15 C8n2 C8n6 J8n(排线直焊, 23.8) │
|
||
v 18.5 C8n3 C8n4 C8n5 R78n R79n │ 尾插排线车道 u 44.6~62.6
|
||
v 8.5 行 B C8n4 C8n5 C8n1(33,11) │
|
||
v 21 行 C R74n U4n C76n │ 尾插排线车道 u 44.6~62.6(背面,不放元件)
|
||
v 26 行 D C70n U2n C71n C72n R70n │
|
||
v 35 行 F J7n(52.5, rot 0) ← 排线从南边来
|
||
v 37~39.5 行 E U5n R76n R77n J6n(62.5,41) (插头背后)
|
||
|
||
走廊里两站面对面各占 v −4.2 ~ −13.7,中间 y 63.4~71.8 那条 8.4 mm 的带子放
|
||
J31/J32(y 65.4~69.6);Hub 簇从 (110,67) 挪到 **(100,68)**,避开 CH5 的行 B 和
|
||
CH2 的行 A(原位置 C749/C742 会压在 CH2 的 C8n4 上)。U31 组、U34/C760 跟着重排,见 `GLOBAL`。
|
||
|
||
## 底层镜像语义(2026-09-06 实测,U51/J71/R761 三件翻到底层读焊盘)
|
||
|
||
**底层 = 先把封装在自己的本地系里沿 x 轴翻(ly → −ly),再按 rot 逆时针转。**
|
||
后果:
|
||
· 两脚件(0402/0603/1206/电解)焊盘在本地 y=0 上,**pin1 方向和顶层完全一样**;
|
||
· rot 0 的 SOT23-6 / BM24:**南北两排对调**(J7n 奇数排从北变到南);
|
||
· rot 90 时东西对调(顶层 rot 90 引脚在东,底层 rot 90 在西)。
|
||
所以 J23 在底层要用 **rot 270** 才能保持口朝 +X,R717/R718 的 y 要对调。
|
||
⚠️ **`--verify` 会回读 J23/R717/R718/J7n 的焊盘坐标核这一条**,别靠推。
|
||
|
||
用法:
|
||
/c/Python314/python .claude/pcb2_place.py # 只算,本地检查
|
||
/c/Python314/python .claude/pcb2_place.py --clean-copper # 删 1/2/15/16 层导线 + 全部过孔 + 铺铜(换版必做)
|
||
/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 --verify # 回读焊盘坐标核镜像语义
|
||
/c/Python314/python .claude/pcb2_place.py --heights # 导出底层高度图(给下板布局避让)
|
||
/c/Python314/python .claude/pcb2_place.py --layers # 切 4 层(只需跑一次,已跑过)
|
||
"""
|
||
import json
|
||
import math
|
||
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 安装孔(来历见 obsolete/pcb2_place_topside_2026-09-06.py 文件头,位置没动)。
|
||
MH = [(8.0, 8.0), (212.0, 8.0), (7.0, 60.0), (213.0, 45.0),
|
||
(76.6, 79.5), (148.1, 79.5)] # v3:中跨孔随上排下移 0.6(焊盘顶 82.5 < 83.0)
|
||
MH_HOLE, MH_PAD = 3.2, 6.0
|
||
MH_KEEP = 3.5 # 底层元件离 M3 孔中心的最小距离(螺钉头 Ø6)
|
||
|
||
# ---- 小米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
|
||
HOLE_KEEP = 2.8 # 底层元件离定位孔中心的最小距离(M2.5 尼龙螺钉头 Ø4.5 + 余量)
|
||
|
||
# 侧键触点(§22.3b,已折算成顶视投影)。顶针顺序 = c4_site.py 的 pogo[0..3]
|
||
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 = 接口边朝上;to_board 里 rot 180 是绕本体中心转,位置矩形不变)----
|
||
# v3(2026-09-06 用户拍板):穿线槽挪到**主板投影外侧、靠板边**那一条,排线(按 6 cm 算)
|
||
# 跨过主板正面到远端边、下穿、再折回底面。上排下移 / 下排上移各 2.5 mm,让板边那条留出
|
||
# 3.5 mm 的筋(主板顶边 127.66 → 槽 128.5~131.5 → 板边 135)。走廊 y 52.16~83.0(30.8 mm)。
|
||
# v5(2026-09-06 用户拍板):**五块主板连槽整体转 180°,接口边一律朝板边**。上排接口边在 y 127.66(槽 128.86~131.86,
|
||
# 离板边 3.14),下排接口边在 y 7.5(槽 3.3~6.3)。两条排线都从板边翻下来、在背面往板中心走;走廊(y 52.16~83)完全空出来,
|
||
# 只剩 J31/J32 / Hub / U31 组 / U34 / J23。顶针(侧键触点,v 2.5~7)随之到板边一侧。
|
||
SITES = {
|
||
1: (5.0, 83.0, 180),
|
||
2: (76.5, 83.0, 180),
|
||
3: (148.0, 83.0, 180),
|
||
4: (40.75, 7.5, 0),
|
||
5: (112.25, 7.5, 0),
|
||
}
|
||
|
||
# ---- 穿线槽(本体坐标):(u0, u1, v0, v1) ----
|
||
# 2026-09-06 实测排线:电池排线 8 宽 × 38 长,尾插排线 18 宽 × 55 长。
|
||
# · 电池排线 38 mm 够不到远端边(J600 v=4.6 到远端边就要 40),改回**接口边外侧**开 10 × 3 的小槽,
|
||
# P+/P− 焊盘放主板投影下方贴着接口边(排线下穿后折回 ~6 mm);
|
||
# · 尾插排线 55 mm 走远端边:J1401(v 2.86) → 远端边 41.8 → 斜下到槽 ~4.2 → 穿板 1.6 → 折回到 J7n ~3.5 ≈ 51,
|
||
# 前提是 **J7n 贴着槽放(v 42.5)**,且槽按 18 mm 宽的排线开 20 mm 长。
|
||
# 2026-09-06 v4(用户拍板):**尾插槽挪到 J1401 正下方**(J1401 本体 u 47.5~59.7 / v 1.7~4.0),
|
||
# 排线从 J1401 出线朝接口边、绕过接口边折回主板下方、在 J1401 正下方下穿,
|
||
# 然后**沿载板背面往 +v 走**到远处的 J7n(用户给的排线可达长度 50 mm)。
|
||
# 长度核算:J1401→接口边 2.9 + 绕边下落(插头 1 + 主板 1 + 立柱 3)5 + 折回到槽心 2.9 + 穿板 1.6 + 背面 34.1 ≈ 46.5 ≤ 50 ✔
|
||
# 槽长 20(排线 18 宽,中心 u 53.6,实际覆盖 44.6~62.6);西端 45.0 离 H7 (42.6,3.7) 2.4 ≥ 2.25。
|
||
SLOTS = {
|
||
'bat': (18.8, 28.8, -4.2, -1.2), # 电池排线:接口边外侧,正对 J600(u≈23.8)
|
||
# 2026-09-06 v4c(用户拍板):尾插槽挪到**和电池槽同一条线**(接口边外侧 v −4.2~−1.2),排线翻过接口边直接下穿,
|
||
# 不再从主板反面 3.0 mm 的缝里钻。背面到 J7n 变长 5.6,J7n 从 v 37 收到 **v 35**:
|
||
# 2.86 + 绕边 5 + 到槽心 2.7 + 穿板 1.6 + 背面 37.7 ≈ 49.9 ≤ 50 ✔(实长 55)。
|
||
'tail': (45.0, 65.0, -4.2, -1.2), # 尾插排线:接口边外侧,与电池槽同线,正对 J1401 (u 47.5~59.7)
|
||
}
|
||
# 排线在背面占的车道(本体坐标)。**车道里不放任何元件**(排线要平贴着走到插头 / 焊盘,0402 也别放):
|
||
# 尾插排线 u 44.6~62.6,v 4.4 ~ J7n 北沿(EDL 组 / VBUS 组因此整体西移到 u ≤ 44);
|
||
# 电池排线 u 18.8~27.8(8 mm 宽,对准 J600,到 J8n 偏西 1 mm),v −1.2(槽)~ 13(J8n 南沿)。
|
||
TAIL_LANE = (44.6, 62.6, -1.2, 36.3)
|
||
BAT_LANE = (18.8, 27.8, -1.2, 13.0) # 槽中心 23.8、J8n 中心 22.8,车道取两者并集
|
||
LANES = {'tail': (TAIL_LANE, 'j7'), 'bat': (BAT_LANE, 'j8')}
|
||
SLOT_KEEP = 0.5 # 铜/元件离槽边
|
||
|
||
# 上游 USB-C 的中心 X:壳体前端面与**右**板边齐平(中心离前端面 5.134 mm,§25.5 #1)
|
||
J23_X = BW - 5.134
|
||
|
||
# ---- 服务带(全部底层):本体坐标,v>0 = 主板投影内 ----
|
||
BAND = {
|
||
# ── v3b:电池侧靠接口边、尾插侧靠远端边,全部在主板投影下方 ──
|
||
# ── 电池侧(2026-09-06 v4b,用户拍板):TP8n1/TP8n2 换成排线直焊座 J8n(个人库 FPC-4P-P2.0_MI8-BAT,
|
||
# 4 焊盘 1.1×4.0 间距 2.0,fp_batfpc.py),并且**放到主板下方远处、35 mm 排线够得到的地方**(v 15)。
|
||
# 路径:J600(v 4.6) → 接口边 → 下落 ~5 → 电池槽(v −2.7) → 穿板 1.6 → 背面往 +v 走 17.7 到 J8n ≈ 31.6 ≤ 35 ✔(实长 38)。
|
||
# 排线车道 `BAT_LANE` u 18.8~27.8 / v −1.2~13 不放元件;中心 22.8(J600 在 23.77,偏 1 mm 让开 C8n1 焊盘框),焊盘 u 19.8/21.8/23.8/25.8。
|
||
# ⚠️ rot 0 = pin1(P+) 在 −u(西);rot 180 = P+ 在 +u。**手性待用户实测**(设计文档 §13.5 末尾),封装上下对称所以只改这一个 rot。
|
||
'j8': (22.8, 15.0, 0), # J8n 排线直焊座(框 u 19.25~26.35 / v 13~17;C8n1 的焊盘框跨 26.55~39.45,比罐体宽,留 0.2)
|
||
'c6': (17.5, 15.0, 0), # C8n6 100nF 0402,贴着 J8n 西侧
|
||
'c2': (13.5, 15.0, 0), # C8n2 100µF 1206(框 11.16~15.84)
|
||
'c3': (13.0, 18.5, 0), # C8n3 上一行西端
|
||
'c4': (18.0, 18.5, 0), # C8n4 22µF 1206
|
||
'c5': (23.0, 18.5, 0), # C8n5 J8n 正北(框 20.66~25.34 / 17.64~19.36)
|
||
'r78': (29.5, 18.5, 0), # R78n 10k TEMP→GND(代替保护板 NTC),C8n1 罐体北边
|
||
'r79': (31.5, 18.5, 0), # R79n 68k ID→GND
|
||
'c1': (34.3, 11.3, 0), # C8n1 2200µF/6.3V SMD 10×10.5(焊盘跨 27.85~40.75,罐体 28.9~40.0 × 5.9~16.7;v4b 东移 1.3 让开电池排线车道 27.8、上移 0.3 让 H7 (42.6,3.7) 的螺钉头 ≥ 2.8)
|
||
# 行 C(v=21):EDL 开关组
|
||
'r74': (34.5, 21.0, 0), # R74n 100k EDL_EN 下拉
|
||
'u4': (38.5, 21.0, 0), # U4n TS5A3157
|
||
'c76': (42.0, 21.0, 0), # C76n 100nF
|
||
# 行 D(v=26):VBUS 负载开关组(v4:整体西移 4.5,让开尾插排线车道 u ≥ 44.6)
|
||
'c70': (30.0, 26.0, 0), # C70n 2.2µF 0805 输入
|
||
'u2': (34.5, 26.0, 0), # U24~28 TPS22918
|
||
'c71': (38.5, 26.0, 0), # C71n 100nF 输出
|
||
'c72': (41.0, 26.0, 0), # C72n 100pF CT
|
||
'r70': (43.5, 26.0, 0), # R70n 100k 下拉(东沿 44.2 < 车道 44.6)
|
||
# 行 F(v=35,v4c):J7n 在排线车道尽头、主板投影下方远处。中心 u 52.5(J1401 53.58,偏西 1 mm 让开 H3 螺钉头)。
|
||
# 框 u 46.2~58.8 / v 33.7~36.3:离 H3 (61.9,33.7) 3.1、离 H1 (54.2,42.3) 6.0(都 ≥ 2.8)。
|
||
# ⚠️ 底层 **rot 0**(v3b 是 rot 180,改掉了)—— 由几何推出来的,理由见设计文档 §25.16 v4:
|
||
# 排线只绕 u 轴弯折(不在面内转),J7n 的触点图案 = J1401 的触点图案平移 + 翻面;
|
||
# J1401 的 pin1/2 端在 +u(用户实测 G1 在右端),所以 J7n 的 pin1/2 端必须在 −u,
|
||
# 而封装 pin51(= pin1/2 端)在本地 −X ⇒ 底层 rot 0(rot 180 会把 VBUS 端放到东边,与 §24.3 矛盾)。
|
||
# rot 0 底层:奇数排南、偶数排北;.6=DPJ 在 u 50.0、.4=DMJ 在 49.65(北排,朝 U5n);pin51=VBUS 西端。
|
||
# 镜像位 2026-09-05 已实测(J1401 靠接口边那一排是奇数排 = fp_bm24.ODD_ROW_TOP=True),rot 0 回读与推导一致 ⇒ 不用再量;装好后两点通断复核(设计文档 §12)。
|
||
'j7': (52.5, 35.0, 0), # J7n BM24-50DS
|
||
# 行 E(v≈37~39.5):ESD + CC 电阻在 J7n **北边**(插头背后,排线车道之外)
|
||
'u5': (48.8, 39.5, 180), # U5n USBLC6,rot 180 南排 1/2/3 朝 J7n(.3=DPJ 47.85 / .1=DMJ 49.75,与 J7n 次序相反,DMJ 走顶层交叉)
|
||
# 2026-09-06(第十三轮):两颗从 v 37.15 挪到 **v 38.0**。理由是 J7n 前排的逃逸带在 v5 里只剩 0.58 mm:
|
||
# J7n 前排焊盘外沿 36.30 ←→ R76n/R77n 焊盘下沿 36.88。0.20 的线要 0.58 才过得去,等于一根都不许错位;
|
||
# 而 `THERM_n`(J7n.14,u 50.40)要一路东行到 R77n.1(u 54.37),`CC1_n`(J7n.22,u 51.80)要往北进 R76n.1(u 51.37)
|
||
# —— 一个往东一个往北,**在 u≈51.6 处必然交叉**,同层无解;想让 THERM 翻顶层又打不下过孔
|
||
# (孔要离 J7n 前排 0.5 ⇒ v ≥ 36.80,又要离 R76n 焊盘 0.5 ⇒ v ≤ 36.38)。
|
||
# 挪到 38.0 之后逃逸带变成 1.43 mm,THERM 在 (50.90, 36.90) / (54.37, 36.90) 打两个孔走顶层跨过去,
|
||
# CC1 全程底层斜进 R76n.1,两者不再打架。北边 U5n 的焊盘在 u ≤ 50.015,离 R76n 西沿 51.084 还有 1.07。
|
||
# 中心 52.233 是算出来的:0603 两焊盘中心距 0.866,要让 **pin1 正好落在 u 51.800**
|
||
# = J7n.22 (CC1) 的正北,CC1 就是一根直上的竖线,不用斜、也不会蹭到 THERM 的换层孔。
|
||
'r76': (52.233, 38.0, 0), # R76n 56k CC1→VBUS,pin1 (51.800) = J7n.22 正北
|
||
'r77': (54.8, 38.0, 0), # R77n 100k,pin1 (54.367) 收 THERM
|
||
'j6': (62.5, 41.0, 0), # J6n EDL 1.25mm 2P,J7n 东北、H1 keep-out 东侧;EDL 飞线随尾插排线穿槽走背面 ~40 mm
|
||
}
|
||
# SMD 罐体 keep-out(相对元件原点):10.3 见方 + 单边 0.25
|
||
CAN_BODY = (-5.4, -5.4, 5.4, 5.4)
|
||
|
||
|
||
def desig(n):
|
||
u"""第 n 个手机位用到的全部位号。"""
|
||
return {
|
||
'pogo': ['J%d' % (41 + (n - 1) * 4 + i) for i in range(4)],
|
||
'j8': 'J8%d' % n, 'r78': 'R78%d' % n, 'r79': 'R79%d' % n,
|
||
'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)
|
||
|
||
|
||
def slot_rects():
|
||
u"""全部 10 条槽的板坐标矩形 [(name, x0, y0, x1, y1)]。"""
|
||
out = []
|
||
for n in SITES:
|
||
for key, (u0, u1, v0, v1) in SLOTS.items():
|
||
ax, ay, _ = to_board(n, u0, v0)
|
||
bx, by, _ = to_board(n, u1, v1)
|
||
out.append(('CH%d-%s' % (n, key), min(ax, bx), min(ay, by), max(ax, bx), max(ay, by)))
|
||
return out
|
||
|
||
|
||
# ═══════════════ 全局块(全部底层)═══════════════
|
||
GLOBAL = {
|
||
# 上游 USB-C,口朝 +X、壳体前端面与右板边齐平。**底层要 rot 270**(镜像语义见文件头)。
|
||
'J23': (J23_X, 62.0, 270),
|
||
# CC1/CC2 各 5.1k 下拉,贴着 J23 的 A5/B5 焊盘。底层 rot 270 之后 A5 在北、B5 在南,
|
||
# 所以和顶层那版相比 y 对调(--verify 会核)。0603 的 pin1 方向不受镜像影响,仍朝东。
|
||
# v3 布线:UP_DP/DM 从西边 y 61.25~62.75 进 J23,两颗 CC 电阻退到 x 209、上下各让开 1.5
|
||
'R717': (209.0, 64.5, 180), # pin1 = CC1 -> J23.A5 (212.39, 63.25)
|
||
'R718': (209.0, 59.5, 180), # pin1 = CC2 -> J23.B5 (212.39, 60.25)
|
||
# AMS1117-3.3 组:J31 东端与 Hub 西侧之间(+5V 从 J31 来,3V3 就近喂 Hub)
|
||
# v3 布线:U31 组挪到 J31 东端**南**侧(走廊北带 y 70~80 留给差分对上顶层北上)
|
||
'U31': (87.5, 60.5, 0), # 底边 57.65 让开 CH4 电池槽(y ≤ 56.36 + 0.5);USB1 车道改走 y 64.0/64.46
|
||
'C736': (80.5, 58.5, 0), # +5V 输入 0805(U31 西侧)
|
||
'C737': (93.0, 54.0, 0), # +3V3 输出 1206,Hub 晶振窝西南、CH4 电池槽东侧
|
||
# LM75BD 测温 + 100nF(SCL/SDA 在 J31.21/23 = x 63.3/65.8,北排;走线从排母针缝里穿到南边)
|
||
# v2 第二版:挪到 J31 **南**边(走廊北带 y 70~76 要留给差分高速路)。CH4 行 B 只剩 U54(48.3)/R764/R774,这里是空的。
|
||
# v3 布线:J31 **南**边(USB1 那对在底层 y 62.9/63.6 从排母南边一路西行,U34 顶边 62.2)
|
||
'U34': (58.0, 60.0, 90),
|
||
'C760': (62.5, 60.0, 90),
|
||
# 上游 VBUS 分压 -> UP_VBUS_DET -> J31 pin36 (81.049, 65.100),跟着高阻端走
|
||
'R721': (80.0, 61.5, 180), # pin1 = VBUS_UP(v3:让开 U31 与 USB1 车道)
|
||
'R722': (78.0, 61.5, 180), # pin1 = UP_VBUS_DET, pin2 = GND
|
||
}
|
||
HDR = {'J31': (62.0, 67.5, 0), 'J32': (158.0, 67.5, 0)}
|
||
HUB_CENTER = (100.0, 68.0) # v1 是 (110, 67):西移 10 让开 CH2 行 A / CH5 行 B,北移 1 让 C741 离开 CH4 电池槽
|
||
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)])
|
||
|
||
|
||
def th_parts():
|
||
u"""直插件:引脚穿板,**顶层和底层都占位**。只剩 J31 / J32(C8n1 换 SMD、TP8n1/2 换 J8n 之后)。"""
|
||
return set(HDR)
|
||
|
||
|
||
def hub_layout():
|
||
u"""Hub 的 24 个卫星件,逐脚对位手排(几何同 v1,只是整簇随 HUB_CENTER 平移)。
|
||
通道:西侧 x < −4.45 留给五对下游 D± 扇出;南侧竖井 x −2.4~−0.2 给 XTAL + UP_DP/DM;
|
||
晶振窝 (−4.2, −9.2)。RB=8.9 是 2026-09-06 拍板的 B 圈半径。"""
|
||
hx, hy = HUB_CENTER
|
||
RA, RB = 6.3, 8.9
|
||
out = {'U32': (hx, hy, 0)}
|
||
for dsg, dx in (('C744', -4.45), ('R708', -3.45), ('C743', -2.65),
|
||
('R715', 1.25), ('R714', 2.45), ('C746', 3.65)):
|
||
out[dsg] = (hx + dx, hy - RA, 270)
|
||
# ⚠️ C745 **不在 A 圈上**(用户 2026-09-06 拍板)。它原来在 (hx+0.05, hy−RA),
|
||
# 那个位置的 GND 脚是全板唯一扇不出去的焊盘 —— 四面都被堵死:
|
||
# 南 `UP_DM` 东行段 y=60.60(孔要 0.64,只有 0.41)
|
||
# 北 U32 南排焊盘 y=63.55 与 A 圈之间的 0.73 mm 死带(孔要 1.0)
|
||
# 东 R715 pin2(缝 0.667) 西 `UP_DP`/`UP_DM` 两条竖段(相距 0.5,塞不下 Ø0.6)
|
||
# 往南跨过 UP 那一对就宽敞了。GND 脚 (100.0, 58.60) / +3V3 脚 (100.0, 59.44),
|
||
# 过孔打在正南 (100.0, 57.83)。代价是离 Hub 远了 2.7 mm ——
|
||
# 它只是 33 个 +3V3_HUB 焊盘里的一颗去耦,A 圈上还有 C746、北边还有 C742/C749。
|
||
out['C745'] = (hx + 0.00, hy - 8.98, 270)
|
||
out['Y1'] = (hx - 4.2, hy - 9.2, 0)
|
||
out['C740'] = (hx - 7.2, hy - 8.35, 180)
|
||
out['C741'] = (hx - 3.1, hy - 12.3, 270)
|
||
out['C749'] = (hx - 0.75, hy + RA, 90)
|
||
out['C742'] = (hx + 0.75, hy + RA, 90)
|
||
# v2:10 µF 总储能挪到西北角横放 —— 原 (hx+3, hy+7) 竖放会压 CH2 的 TP812 / C702
|
||
out['C751'] = (hx - 4.0, hy + 6.3, 0)
|
||
# v3 布线:pin5/pin10 的 +3V3 电容不再贴西侧 —— 西侧 x 93.8~87.7 是十条 D± 车道扇开 + 换层过孔的地方。
|
||
# 两颗放到扇开后各自车道的尽头(pin5 车道 y 65.875 → 拐到 66.6;pin10 车道 y 69.625)。
|
||
# v5 布线(2026-09-06):pin5/pin10 的 +3V3 改走**顶层**(西缘 12 条车道里只有它俩能翻层),
|
||
# 两颗电容挪到 U31 pin2(+3V3) 东侧的 +3V3 主干上、竖放;x 88~90 那一带让给 USB2 北上车道。
|
||
out['C747'] = (88.2, 71.5, 90) # J31 最东列 (86.98) 与 USB2 北上车道 (89.27) 之间,pin5 从顶层绕过来
|
||
out['C748'] = (92.5, 71.2, 90) # USB3 东车道 (91.53) 与 USB5 西车道 (93.47) 之间,pin10 直接进
|
||
out['R719'] = (hx + RA, hy + 0.25, 0)
|
||
out['R712'] = (hx + RA, hy - 0.95, 0)
|
||
out['R709'] = (hx + RA, hy - 2.15, 180)
|
||
out['R710'] = (hx + RA, hy - 3.35, 180)
|
||
out['C750'] = (hx + RA, hy - 4.55, 0)
|
||
out['R711'] = (hx + RB, hy - 0.35, 0)
|
||
out['R716'] = (hx + RB, hy - 1.55, 0)
|
||
out['R720'] = (hx + RB, hy - 2.75, 0)
|
||
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
|
||
|
||
|
||
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, 2)
|
||
for dsg, (x, y, r) in list(GLOBAL.items()) + list(HDR.items()) + list(hub_layout().items()):
|
||
pos[dsg] = (x, y, r, 2)
|
||
return pos
|
||
|
||
|
||
# ═══════════════ 本地检查 ═══════════════
|
||
def load_bb():
|
||
u"""从 pcb2-parts.json 读每个元件的焊盘包围盒(相对元件原点、rot 0 本地系,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 rot_box(b, rot):
|
||
u"""本地包围盒按 rot(0/90/180/270,逆时针)转到板系(仍相对元件原点)。"""
|
||
x0, y0, x1, y1 = b
|
||
r = rot % 360
|
||
if r == 90:
|
||
return (-y1, x0, -y0, x1)
|
||
if r == 180:
|
||
return (-x1, -y1, -x0, -y0)
|
||
if r == 270:
|
||
return (y0, -x1, y1, -x0)
|
||
return b
|
||
|
||
|
||
def box(d, rot, bb, layer=1):
|
||
u"""元件在 `layer` 上占的框(相对元件原点,板系)。底层先把本地 y 翻一次(镜像语义)。"""
|
||
global CAN
|
||
if CAN is None:
|
||
CAN = {desig(n)['c1'] for n in SITES}
|
||
b = bb[d]
|
||
if layer == 2: # 焊盘包围盒:底层本地 y 翻一次
|
||
b = (b[0], -b[3], b[2], -b[1])
|
||
if d in CAN and layer == 2: # SMD 罐体(底层)比焊盘框大得多,按 CAN_BODY 留
|
||
x0, y0, x1, y1 = b
|
||
b = (min(x0, CAN_BODY[0]), min(y0, CAN_BODY[1]), max(x1, CAN_BODY[2]), max(y1, CAN_BODY[3]))
|
||
return rot_box(b, rot)
|
||
|
||
|
||
def _overlap(a, b, eps=0.02):
|
||
return (a[0] < b[2] - eps and b[0] < a[2] - eps and a[1] < b[3] - eps and b[1] < a[3] - eps)
|
||
|
||
|
||
def _dist_box_pt(b, px, py):
|
||
dx = max(b[0] - px, 0.0, px - b[2])
|
||
dy = max(b[1] - py, 0.0, py - b[3])
|
||
return math.hypot(dx, dy)
|
||
|
||
|
||
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,)
|
||
|
||
def abs_box(d, L):
|
||
x, y, r, lay = pos[d]
|
||
b = box(d, r, bb, L)
|
||
return (x + b[0], y + b[1], x + b[2], y + b[3])
|
||
|
||
items = [d for d in pos if d in known]
|
||
|
||
# 越框
|
||
for d in items:
|
||
for L in layers(d, pos[d][3]):
|
||
b = abs_box(d, L)
|
||
if b[0] < 1.0 or b[2] > BW - 1.0 or b[1] < 1.0 or b[3] > BH - 1.0:
|
||
errs.append(u'越框: %s @ (%.1f, %.1f)' % (d, pos[d][0], pos[d][1]))
|
||
|
||
# 同层重叠
|
||
for i in range(len(items)):
|
||
for j in range(i + 1, len(items)):
|
||
a, b_ = items[i], items[j]
|
||
shared = set(layers(a, pos[a][3])) & set(layers(b_, pos[b_][3]))
|
||
for L in sorted(shared):
|
||
if _overlap(abs_box(a, L), abs_box(b_, L)):
|
||
errs.append(u'重叠(L%d): %s(%.1f,%.1f) x %s(%.1f,%.1f)'
|
||
% (L, a, pos[a][0], pos[a][1], b_, pos[b_][0], pos[b_][1]))
|
||
|
||
# 槽:任何层的任何元件都不许压进槽(含 0.5 余量)
|
||
slots = slot_rects()
|
||
for d in items:
|
||
for L in layers(d, pos[d][3]):
|
||
b = abs_box(d, L)
|
||
for name, x0, y0, x1, y1 in slots:
|
||
if _overlap(b, (x0 - SLOT_KEEP, y0 - SLOT_KEEP, x1 + SLOT_KEEP, y1 + SLOT_KEEP), 0.0):
|
||
errs.append(u'%s (L%d) 压在槽 %s 上' % (d, L, name))
|
||
# 槽不许碰定位孔 / M3 孔
|
||
for name, x0, y0, x1, y1 in slots:
|
||
for n in SITES:
|
||
for hn, (u_, v_) in HOLES.items():
|
||
hx, hy, _ = to_board(n, u_, v_)
|
||
if _dist_box_pt((x0, y0, x1, y1), hx, hy) < HOLE_D / 2 + 1.0:
|
||
errs.append(u'槽 %s 离定位孔 P%d%s 太近' % (name, n, hn))
|
||
for i, (mx, my) in enumerate(MH, 1):
|
||
if _dist_box_pt((x0, y0, x1, y1), mx, my) < MH_PAD / 2 + 1.0:
|
||
errs.append(u'槽 %s 离 M3 孔 MH%d 太近' % (name, i))
|
||
|
||
# 排线在背面的车道里不许有任何底层元件(排线要平贴到插头 / 焊盘),车道尽头那个座子自己除外
|
||
for d in items:
|
||
if 2 not in layers(d, pos[d][3]):
|
||
continue
|
||
b = abs_box(d, 2)
|
||
for n in SITES:
|
||
for lname, ((u0, u1, v0, v1), owner) in LANES.items():
|
||
if d == desig(n)[owner]:
|
||
continue
|
||
ax, ay, _ = to_board(n, u0, v0)
|
||
bx, by, _ = to_board(n, u1, v1)
|
||
lane = (min(ax, bx), min(ay, by), max(ax, bx), max(ay, by))
|
||
if _overlap(b, lane, 0.0):
|
||
errs.append(u'%s 压在 CH%d 的%s排线车道上' % (d, n, u'尾插' if lname == 'tail' else u'电池'))
|
||
|
||
# 顶层只许有顶针
|
||
allow = set()
|
||
for n in SITES:
|
||
allow |= set(desig(n)['pogo'])
|
||
for d in items:
|
||
if pos[d][3] == 1 and d not in allow:
|
||
errs.append(u'%s 还在顶层' % d)
|
||
|
||
# 底层元件离定位孔 / M3 孔(螺钉头)
|
||
for d in items:
|
||
if 2 not in layers(d, pos[d][3]):
|
||
continue
|
||
b = abs_box(d, 2)
|
||
for n in SITES:
|
||
for hn, (u_, v_) in HOLES.items():
|
||
hx, hy, _ = to_board(n, u_, v_)
|
||
dd = _dist_box_pt(b, hx, hy)
|
||
if dd < HOLE_KEEP:
|
||
errs.append(u'%s 离定位孔 P%d%s 只有 %.2f(要 >= %.1f)' % (d, n, hn, dd, HOLE_KEEP))
|
||
for i, (mx, my) in enumerate(MH, 1):
|
||
dd = _dist_box_pt(b, mx, my)
|
||
if dd < MH_KEEP:
|
||
errs.append(u'%s 离 M3 孔 MH%d 只有 %.2f(要 >= %.1f)' % (d, i, dd, MH_KEEP))
|
||
|
||
# 载板自己的 M3 孔不许落进手机本体投影,也不许和定位孔挨太近
|
||
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_)
|
||
dd = ((mx - hx) ** 2 + (my - hy) ** 2) ** 0.5
|
||
need = MH_PAD / 2 + HOLE_D / 2 + 0.2
|
||
if dd < need:
|
||
errs.append(u'载板 M3 孔 MH%d 离定位孔 P%d%s 只有 %.2f mm(要 >= %.2f)'
|
||
% (i, n, hn, dd, need))
|
||
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():
|
||
r = call("""
|
||
const before = await eda.pcb_Layer.getTheNumberOfCopperLayers();
|
||
await eda.pcb_Layer.setTheNumberOfCopperLayers(4);
|
||
const after = await eda.pcb_Layer.getTheNumberOfCopperLayers();
|
||
await eda.pcb_Document.save();
|
||
return {before, after};
|
||
""")
|
||
print(u'铜层数 %s -> %s' % (r['before'], r['after']))
|
||
|
||
|
||
def clean_copper():
|
||
u"""换版:删信号层导线(白名单 1/2/15/16)、全部过孔、全部铺铜。丝印/装配/文档层不动(§9n)。"""
|
||
r = call("""
|
||
let dl = 0, dv = 0, dp = 0;
|
||
// ⚠️ 一遍删不干净(实测 1416 条删完回读还剩 16 条),循环到剩 0(skill §9x)
|
||
for (let pass = 0; pass < 4; pass++) {
|
||
const todo = (await eda.pcb_PrimitiveLine.getAll()).filter(l => [1, 2, 15, 16].includes(l.getState_Layer()));
|
||
if (!todo.length) break;
|
||
for (const l of todo) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++; }
|
||
}
|
||
for (const v of await eda.pcb_PrimitiveVia.getAll()) { await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++; }
|
||
for (const p of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(p.getState_PrimitiveId()); dp++; }
|
||
const left = {};
|
||
for (const l of await eda.pcb_PrimitiveLine.getAll()) { const L = l.getState_Layer(); left[L] = (left[L] || 0) + 1; }
|
||
const vias = (await eda.pcb_PrimitiveVia.getAll()).length;
|
||
const pours = (await eda.pcb_PrimitivePour.getAll()).length;
|
||
await eda.pcb_Document.save();
|
||
return {dl, dv, dp, left, vias, pours};
|
||
""", timeout=600)
|
||
print(u'清铜: 删导线 %d / 过孔 %d / 铺铜 %d;剩余导线按层 %s,过孔 %d,铺铜 %d'
|
||
% (r['dl'], r['dv'], r['dp'], r['left'], r['vias'], r['pours']))
|
||
for L, want in ((3, 50), (9, 100), (13, 20)):
|
||
if r['left'].get(str(L), 0) < want:
|
||
print(u'!! 层 %d 只剩 %d 条,丝印/装配/文档被误删?' % (L, r['left'].get(str(L), 0)))
|
||
|
||
|
||
def _polysrc(start, pts):
|
||
src, mode = list(start), 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 outline_poly():
|
||
u"""板框:一条闭合多段线(4 直边 + 4 个 R5 圆角),逆时针、正角弧。"""
|
||
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)
|
||
return _polysrc([round(R * MIL, 4), 0.0], pts)
|
||
|
||
|
||
def slot_poly(x0, y0, x1, y1):
|
||
u"""跑道形槽:两端半圆(半径 = 槽宽/2)。逆时针,每端两段 90° 弧。"""
|
||
r = (y1 - y0) / 2.0
|
||
cy = (y0 + y1) / 2.0
|
||
pts = [('L', x1 - r, y0), ('ARC', 90.0, x1, cy), ('ARC', 90.0, x1 - r, y1),
|
||
('L', x0 + r, y1), ('ARC', 90.0, x0, cy), ('ARC', 90.0, x0 + r, y0)]
|
||
return _polysrc([round((x0 + r) * MIL, 4), round(y0 * MIL, 4)], pts)
|
||
|
||
|
||
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))
|
||
|
||
# 槽:layer 12 (MULTI) 的填充 = 挖槽区域。先删旧的再建。
|
||
slots = slot_rects()
|
||
s = call("""
|
||
const S = %s;
|
||
let del = 0;
|
||
for (const f of await eda.pcb_PrimitiveFill.getAll())
|
||
if (f.getState_Layer() === 12) { await eda.pcb_PrimitiveFill.delete(f.getState_PrimitiveId()); del++; }
|
||
let made = 0, fail = [];
|
||
for (const [name, src] of S) {
|
||
const poly = await eda.pcb_MathPolygon.createPolygon(src);
|
||
const r = await eda.pcb_PrimitiveFill.create(12, poly);
|
||
if (r) made++; else fail.push(name);
|
||
}
|
||
const fills = (await eda.pcb_PrimitiveFill.getAll()).map(f => f.getState_Layer());
|
||
return {del, made, fail, fills};
|
||
""" % json.dumps([[name, slot_poly(x0, y0, x1, y1)] for name, x0, y0, x1, y1 in slots]))
|
||
print(u'穿线槽: 建 %d / 失败 %s(删旧 %d);板上 layer12 填充 %d 个'
|
||
% (s['made'], s['fail'], s['del'], sum(1 for L in s['fills'] if L == 12)))
|
||
if s['made'] != len(slots):
|
||
sys.exit(u'!! 槽没建全')
|
||
for name, x0, y0, x1, y1 in slots:
|
||
print(u' %-9s x %.2f~%.2f y %.2f~%.2f (%.1f × %.1f)' % (name, x0, x1, y0, y1, x1 - x0, y1 - y0))
|
||
|
||
k = call("""
|
||
const MH = %s;
|
||
for (const p of await eda.pcb_PrimitivePad.getAll()) {
|
||
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()) {
|
||
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(⚠ 必须配尼龙立柱)' % (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++; }
|
||
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(), {layer: lay});
|
||
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {x, y, rotation: rot});
|
||
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 verify(pos):
|
||
u"""回读关键件的焊盘坐标,核镜像语义(J23 口朝向、R717/R718 对 A5/B5、J7n 排向、层)。"""
|
||
r = call("""
|
||
const M = %s;
|
||
const want = %s;
|
||
const out = {};
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
|
||
const d = c.getState_Designator();
|
||
if (!want.includes(d)) continue;
|
||
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
|
||
out[d] = {x: c.getState_X()/M, y: c.getState_Y()/M, rot: c.getState_Rotation(), layer: c.getState_Layer(),
|
||
pins: Object.fromEntries(pins.map(p => [p.padNumber, [+(p.x/M).toFixed(3), +(p.y/M).toFixed(3), p.layer]]))};
|
||
}
|
||
const layers = {};
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) { const L = c.getState_Layer(); layers[L] = (layers[L]||0)+1; }
|
||
out._layers = layers;
|
||
return out;
|
||
""" % (MIL, json.dumps(['J23', 'R717', 'R718', 'J71', 'U51', 'C801', 'J31', 'R761', 'U24'])))
|
||
print(u'元件按层: %s' % r.pop('_layers'))
|
||
ok = True
|
||
j23 = r['J23']['pins']
|
||
sig_x = [v[0] for k, v in j23.items() if k in ('A5', 'B5', 'A6', 'B6', 'A7', 'B7', 'A8', 'B8')]
|
||
if max(sig_x) < r['J23']['x']:
|
||
print(u'✔ J23 信号焊盘在中心西侧 (x≈%.2f),口朝 +X' % sig_x[0])
|
||
else:
|
||
print(u'✘ J23 信号焊盘在东侧,口朝板内!'); ok = False
|
||
for rr, pad in (('R717', 'A5'), ('R718', 'B5')):
|
||
p1 = r[rr]['pins']['1']; tgt = j23[pad]
|
||
dd = math.hypot(p1[0] - tgt[0], p1[1] - tgt[1])
|
||
print(u'%s %s.pin1 (%.2f,%.2f) -> J23.%s (%.2f,%.2f) 距 %.2f' % (u'✔' if dd < 4 else u'✘', rr, p1[0], p1[1], pad, tgt[0], tgt[1], dd))
|
||
ok &= dd < 4
|
||
j71 = r['J71']['pins']
|
||
print(u'J71 (L%d) pin1 %s / pin2 %s / pin49 %s / pin50 %s' % (r['J71']['layer'], j71['1'], j71['2'], j71.get('49'), j71.get('50')))
|
||
print(u'J71 pin4(DMJ) %s / pin6(DPJ) %s / pin51(VBUS) %s / pin52(GND) %s' % (j71.get('4'), j71.get('6'), j71.get('51'), j71.get('52')))
|
||
# 按**本体坐标**判(站可能整体转了 180°):pin51(VBUS) 与 pin6(DPJ) 都要在 −u 侧
|
||
def _u(x):
|
||
x0, _, rho = SITES[1]
|
||
return (x - x0) if rho == 0 else (x0 + PW - x)
|
||
cu = _u(r['J71']['x'])
|
||
if j71.get('51') and _u(j71['51'][0]) < cu and _u(j71['6'][0]) < cu:
|
||
print(u'✔ J71 pin51(VBUS)/pin6(DPJ) 在本体 −u 侧 —— 与 §24.3「VBUS 落到 −X 端」一致(底层 rot 0 + 站向)')
|
||
else:
|
||
print(u'✘ J71 pin51 不在本体 −u 端!rot/镜像语义不对'); ok = False
|
||
print(u'U51 (L%d) pin1 %s pin6 %s' % (r['U51']['layer'], r['U51']['pins']['1'], r['U51']['pins']['6']))
|
||
print(u'C801 (L%d) pin1 %s pin2 %s' % (r['C801']['layer'], r['C801']['pins']['1'], r['C801']['pins']['2']))
|
||
print(u'R761 (L%d) pin1 %s pin2 %s' % (r['R761']['layer'], r['R761']['pins']['1'], r['R761']['pins']['2']))
|
||
print(u'U24 (L%d) pin1 %s pin4 %s' % (r['U24']['layer'], r['U24']['pins']['1'], r['U24']['pins']['4']))
|
||
print(u'J31 (L%d) pin1 %s pin2 %s' % (r['J31']['layer'], r['J31']['pins']['1'], r['J31']['pins']['2']))
|
||
return ok
|
||
|
||
|
||
def header_pads():
|
||
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;
|
||
""")
|
||
data = {'frame': u'载板坐标系,mm;下板原点 = 载板 (30, 5),下板侧目标 = 本坐标 − (30,5)',
|
||
'pads': [{'ref': a, 'pad': b, 'x': round(x / MIL, 3), 'y': round(y / MIL, 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)))
|
||
|
||
|
||
# 底层元件高度(mm,离载板底面),给下板布局避让用。没查到规格书的按同类估。
|
||
HEIGHT = {
|
||
'J31': 8.5, 'J32': 8.5, # 直插排母本体(C5307344)
|
||
'J23': 3.2, 'U31': 1.8, 'U32': 0.9, 'Y1': 1.2, 'U34': 1.6,
|
||
}
|
||
|
||
|
||
def part_height(d):
|
||
if d in HEIGHT:
|
||
return HEIGHT[d]
|
||
if d in CAN:
|
||
return 10.5 # SMD 10×10.5 罐
|
||
if d.startswith('TP'):
|
||
return 0.0
|
||
if d.startswith('J7'):
|
||
return 1.0 # BM24-50DS 母座 + 插头(对插高 0.6~1.0)
|
||
if d.startswith('J8'):
|
||
return 0.5 # 电池排线直焊:排线 0.2 + 焊锡
|
||
if d.startswith('J6'):
|
||
return 3.6 # 1.25 mm 2P 卧式座
|
||
if d.startswith('U'):
|
||
return 1.2 # SOT23-6
|
||
if d.startswith('R'):
|
||
return 0.5
|
||
if d.startswith('C'):
|
||
return 1.7 # 1206 电容取最大
|
||
return 2.0
|
||
|
||
|
||
def heights(pos):
|
||
global CAN
|
||
bb = load_bb()
|
||
CAN = {desig(n)['c1'] for n in SITES}
|
||
out = []
|
||
for d, (x, y, r, lay) in sorted(pos.items()):
|
||
if lay != 2 and d not in th_parts():
|
||
continue
|
||
b = box(d, r, bb, 2)
|
||
out.append({'ref': d, 'x0': round(x + b[0], 2), 'y0': round(y + b[1], 2),
|
||
'x1': round(x + b[2], 2), 'y1': round(y + b[3], 2), 'h': part_height(d)})
|
||
slots = [{'name': n, 'x0': x0, 'y0': y0, 'x1': x1, 'y1': y1} for n, x0, y0, x1, y1 in slot_rects()]
|
||
data = {'frame': u'载板坐标系 mm,h = 元件离载板底面的高度;两板净空 13 mm ⇒ 下板同一 (x,y) 上元件高 ≤ 13 − h − 1',
|
||
'parts': out, 'slots': slots}
|
||
p = os.path.join(HERE, 'pcb2-bottom-heights.json')
|
||
json.dump(data, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
||
tall = [o for o in out if o['h'] >= 3.0]
|
||
print(u'底层高度图已导出: %s(%d 件,其中 ≥3 mm 的 %d 件)' % (p, len(out), len(tall)))
|
||
for o in tall:
|
||
print(u' %-6s h=%.1f x %.1f~%.1f y %.1f~%.1f' % (o['ref'], o['h'], o['x0'], o['x1'], o['y0'], o['y1']))
|
||
|
||
|
||
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[:40]:
|
||
print(' ', e)
|
||
if len(errs) > 40:
|
||
print(u' ...还有 %d 条' % (len(errs) - 40))
|
||
sys.exit(1)
|
||
print(u'本地检查通过:无重叠、无越框、没人压槽、顶层只有顶针、底层件避开了螺钉头')
|
||
if '--layers' in sys.argv:
|
||
push_layers()
|
||
if '--clean-copper' in sys.argv:
|
||
clean_copper()
|
||
if '--outline' in sys.argv:
|
||
push_outline()
|
||
if '--mech' in sys.argv:
|
||
push_mech()
|
||
if '--push' in sys.argv:
|
||
push_parts(pos)
|
||
header_pads()
|
||
if '--verify' in sys.argv:
|
||
if not verify(pos):
|
||
sys.exit(1)
|
||
if '--heights' in sys.argv:
|
||
heights(pos)
|