1296 行
77 KiB
Python
1296 行
77 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""R2 刀片 PCB 摆位(M4c-2;2026-09-08 改成**双面**:器件在手机的另一面)。
|
||
|
||
/c/Python314/python .claude/r2/pcb_place.py # 只算 + 自检(不下发)
|
||
/c/Python314/python .claude/r2/pcb_place.py --push # 下发摆位
|
||
/c/Python314/python .claude/r2/pcb_place.py --verify # 读回校验
|
||
|
||
## ⛔⛔ 2026-09-11 晚:主板转 180°、两条槽到顶部、U4 到右侧(方案 §35,用户拍板)
|
||
|
||
下面各节里的「接口边 y=29 / 槽在接口边外 24.8~27.8 / 排线往 +y / 功率链 F1 在顶」都是**旧几何**的说法。
|
||
现在:接口边朝**上**(y 55.66),两条槽和 C60 / J3 在顶部服务带,排线穿槽后往 **−y** 折回到 J5 / J4
|
||
(底层 rot 180);功率链在旧框架里堆完再 `flip_pwr()` 刚体转 180°(F1 在底、U3 在顶);
|
||
U4 在右侧、侧键正南。离线试布线用 `synth_pads.py` 合成焊盘表,下发后必须重抓对账。
|
||
|
||
## ⛔ 分面:**手机在 A 面,器件全部在 B 面**(2026-09-09 用户拍板)
|
||
|
||
小米8 主板压在 **A 面**(顶层)。A 面**只剩 `K1~K4` 四个裸铜盘**(主板弹片直接压,
|
||
物理上只能在这一面)和两面都是指的 `J1`;**其余 78 件全部在 B 面**。
|
||
|
||
`J600` 的电池排线和 `J1401` 的尾插排线从接口边翻下来,落到 A 面服务带后**穿板**
|
||
(`pcb_blade.flex_slots()`,接口边外侧 y 24.8~27.8 的两条铣穿槽),U 形折回 B 面,
|
||
贴着 B 面往 +y 走到**手机投影下方**的 `J5` / `J4` —— 和 R1 的 J8n / J7n 一模一样。
|
||
排线在 B 面占的车道 `LANES` 里**一件不放**。
|
||
|
||
**高度预算分两段**(槽距 20.32,板厚 1.6,螺柱间隙 1.0):
|
||
|
||
| B 面区域 | 对面是什么 | 净空 | 落地 |
|
||
|---|---|---|---|
|
||
| 服务带 y 12~28 | 隔壁刀片的 A 面服务带,**现在是空的** | 20.32 − 1.6 = **18.7** | `C60`(10.5) / `J2` / `J3`(5) 只能放这里 |
|
||
| 手机投影 y ≥ 29 | 隔壁刀片的手机主板 | 20.32 − 1.6 − 1.0 − 7.6 = **10.1**(h_phone 取物理上限)| `J4` BM24 + 排线、`L1` 3.0 都够 |
|
||
|
||
⚠ 以前「B 面只有 3.0」是把 `L1` 的高度当预算写的,不是净空;净空一直有 10 mm。
|
||
`HEIGHT` 里登记的超高件禁止进手机投影,自检会拦。
|
||
|
||
**`J5` / `J4` 的朝向**:排线穿槽折回是绕 x 轴翻 180°(x 不变、y 镜像),立创底层
|
||
正是「先 y 镜像再转 rot」(方案 §25.3)⇒ **底层 rot 0**,pin1 仍在西边(`J5.1 = P+`
|
||
在 −x,与 §13.5 用户实测「P+ 在左」一致)。⚠ 这是推导 + R1 §25.16 同一结论,
|
||
**装配首件仍要两点通断复核**,几何校验抓不到接反。
|
||
|
||
## 三类件,三种定位方式
|
||
|
||
1. **机械锚定件**(位置由小米8 主板决定,不许挪):`K1~K4` 侧键触点盘、
|
||
`J5` 电池排线焊盘、`J4` BM24 尾插座。坐标从 **`.claude/mi8_mech.json`** 换算,
|
||
不手抄。手机主板在刀片上的落点是 `PHONE_ORG`(方案 §3.1:x 3~70.09 / y 29~73.66,
|
||
长边水平、**接口边朝下**)。
|
||
2. **手工钉死件**:`J1` 金手指(`pcb_blade.J1_X`)、`C60` Ø10.3 的 2200 µF、
|
||
`J2` D± 退路座、`J3` EDL 座 —— 各有各的理由,见 `ANCHORS` 的注释。
|
||
3. **其余 73 件**:按功能分组、按「电流流向」排序,塞进六个区(`REGIONS`),
|
||
用一个简单的货架式排布器摆。**顺序是设计意图,坐标不是** ——
|
||
要改布局改 `GROUPS` 里的顺序和区,不要去改具体坐标。
|
||
|
||
## 分区(方案 §3.1,坐标见 `pcb_blade.py`;除 K1~K4 外全部是 B 面)
|
||
|
||
| 区 | 范围 | 放什么 |
|
||
|---|---|---|
|
||
| 金手指区 | y 0~11 | **只有 J1,其余一件不放**(插槽体 ~9 mm 要让开),两面都不放 |
|
||
| 排线槽 + 车道 | BAT x 20.8~32.8 / TAIL x 46.6~66.6,y 24.8 → J5/J4 顶 | **铣穿 + 排线平贴,一件不放** |
|
||
| `KEY` | x 0.6~13 / y 22~38 | 侧键 MOS + 栅极电阻,`K1~K4` 的正背面 |
|
||
| `DIS` | x 21~34.2 / y 12~24 | `VO_P` 泄放,电池槽下方(VO-L3 岛内) |
|
||
| `CTRL` | x 54~63 / y 47~56 | I²C 上拉,跟着 `U4` 走(M7 起在板中央) |
|
||
| `SVC_M` | x 33~42.5 / y 29~50 | `VO_P` 阻尼组,电池车道东侧、`J5` 旁边(H4 螺钉头以西) |
|
||
| `SVC_R` | x 47.5~65.5 / y 35.5~46 | 尾插 USB + VBUS 开关,`J4` **北边(插头背后)** |
|
||
| `SVC_E` | x 66.6~72.5 / y 36.4~50 | EDL 模拟开关,尾插车道与功率列之间的走廊,`J3` 正北 |
|
||
| 功率列 | x 72.3~97.5 / y 12~74 | Buck / eFuse / 检流,**按「离手机越远越吵」内外分两条** |
|
||
|
||
功率列内部:外条(x 86~97.5,靠板边)放 SW 节点、电感、Cin —— 最吵的;
|
||
内条(x 72~84,贴手机)放 FB 分压、DAC 低通、INA226、eFuse 外围 —— 最怕吵的。
|
||
这是方案 §3.1 的「按离手机越远越吵排」和风险 R-12。
|
||
|
||
## M7(2026-09-09):为「先自动布线」重排 B 面
|
||
|
||
用户拍板「B 面全部重排 → 先跑 EDA 自动布线 → 再优化」。这一版**动的是拥塞,不是拓扑**
|
||
—— §7.2 那四条关键回路(输入环路 / SW 面积 / FB 远离 SW / Kelvin)靠的是功率链手排,
|
||
一个数都没改;机械锚定件(`K1~K4` / `J5` / `J4` / `J1` / 七个孔)同样一个数没改。改的是:
|
||
|
||
| 改动 | 为什么 |
|
||
|---|---|
|
||
| 排布器节距 1.1 → **1.8** | M6 欠的 5 只脚全是「出口被自家邻脚封死」。1.1 只够过一根线;板面占用率才 6%,加节距是白捡的 |
|
||
| `U4` PCA9555 从 (76.3, 18.5) 挪到 **(48, 52)** | 它 8×8 的本体卡在「功率列内条 × 金手指北岸」那个口袋里,五只不通的脚全在它周边一圈 |
|
||
| `EFUSE_AUX` 从 9.5×11.5 扩到 **11.7×16**,东沿退到 84.0 | 给 `U2` 西边留 2.55 的扇出通道(M6 只有 1.55) |
|
||
| `U2` 上下的缝 1.8 → **2.6 / 2.4**,`U1_Y` 49.5 → **50.9** | WQFN-20 上下两排脚要横着爬出去;抬 U1 是为了不把链尾 `U3` 顶到金手指禁布区上 |
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
sys.stdout.reconfigure(encoding='utf-8')
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
# ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place),
|
||
# 先插 .claude 再插 HERE 的话,`import pcb_pour` 会拿到 R1 那一份,
|
||
# 而且**不报错**,只在调到 R2 专有的函数时才炸。
|
||
sys.path.insert(0, os.path.dirname(HERE))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run # noqa: E402
|
||
import pcb_blade as B # noqa: E402
|
||
|
||
MIL = B.MIL
|
||
PCB = B.PCB
|
||
BBOX = os.path.join(HERE, '_blade_bbox.json')
|
||
MECH = os.path.join(os.path.dirname(HERE), 'mi8_mech.json')
|
||
|
||
GAP = 0.6 # 件间净距(mm):**装配**下限,自检按它判重叠
|
||
# ⛔ 排布器的**摆放节距**要比装配下限大得多 —— 0.6 的缝里塞不进一根线:
|
||
# 0.2 的线要 0.2 + 2×0.18(净距) = **0.56**,再加上 0.25 的网格吸附,
|
||
# 实际上等于没有通道。实测 0.6 排下来,17 处「出不来」里大半在 **0402 电阻**上
|
||
# (不是细间距 IC),布线器怎么调顺序都是 28~30 条接不上。
|
||
# 1.1 能过一根线还剩余量;区里都有空地(`EFUSE_AUX` 12 件只占了 79 mm² 中的 45)。
|
||
PACK_GAP = 1.8 # 排布器的摆放节距(M7 从 1.1 加到 1.8,见下)
|
||
# ⛔ **M7(2026-09-09)把节距从 1.1 加到 1.8**:1.1 只够过**一根** 0.2 的线
|
||
# (0.2 + 2×0.18 = 0.56,加 0.25 网格吸附实际剩不到一根的余量),而 M6 欠的
|
||
# 5 只脚全是「出口被自家邻脚封死」——不是没地方走线,是**脚出不来**。
|
||
# 1.8 能并排走两根线还剩余量。板子一点都不挤:83 件本体面积加起来 ~430 mm²,
|
||
# 板面 7448 mm²,占用率 6%,加节距是白捡的。
|
||
# 个别区可以覆盖(值 = 该区的节距)。
|
||
PACK_GAP_BY_REGION = {
|
||
'BUCK_FB': 1.4, # U1 右边只有 6 mm 宽的缝,1.8 会退化成单列
|
||
}
|
||
MILL_KEEP = 0.5 # 器件离铣穿内槽的净距
|
||
# 排线在 B 面占的车道:从槽顶到连接器顶沿,宽 = 排线宽 + 2×0.5。**一件不放**(排线要平贴)。
|
||
# ⛔ 2026-09-11 刀片 98×76 → 90×68:本文件里的手排坐标都是 M7(手机原点 (3, 29)、板宽 98)
|
||
# 那一版算出来的。**跟手机绑定的**加 (DX, DY),**功率区**加 PX(整块西移,内部回路关系不动)。
|
||
# ⛔⛔ 2026-09-11 晚(方案 §35):**主板转 180°、两条槽到顶部、U4 到右侧、功率链整块转 180°**。
|
||
# · 跟手机绑定的件一律 `B.phone_xy(u, v)`(u, v = 本体坐标),底层件 rot 在原来基础上 +180;
|
||
# · 功率链的手排仍按「F1 在顶」那一版的锚 (PWR_X0, U1_Y0) 算相对关系,再经 `flip_pwr()`
|
||
# **刚体转 180°** 落到新锚 (PWR_X, U1_Y):§7.2 四条回路一个相对数都不动,
|
||
# 12V 从底部进(离金手指近)、VO_P 从顶部出(离 J5 / C60 近),U2 的信号脚全部朝西(朝手机、朝 U4)。
|
||
PX = -8.0 # 旧框架:98 → 90 时功率区西移 8(只在 flip 之前的坐标里用)
|
||
# 排线在 B 面的车道:从槽底往 **−y** 到连接器远沿(J5 / J4 本体南沿再让一点),一件不放。
|
||
LANE_BOT = {'BAT': round(B.IF_Y - 5.5, 3), 'TAIL': round(B.IF_Y - 6.0, 3)}
|
||
|
||
# 手机主板在刀片上的落点:本体 (u,v) -> 板 (x,y) = (PHONE_ORG + (u,v))
|
||
# 方案 §3.1:主板 67.09 × 44.66,长边水平、**接口边(v=0 那条,J600/J1401/侧键都在上面)朝下**。
|
||
# ⚠ 事实来源在 pcb_blade(板级),这里只做别名,不要抄第二份
|
||
PHONE_ORG = B.PHONE_ORG
|
||
PHONE_WH = B.PHONE_WH
|
||
|
||
# 焊盘外框量不到本体的几个件:给真实本体尺寸(mm)
|
||
BODY = {
|
||
'C60': (11.0, 11.0), # Ø10.3 铝电解,2200 µF(T2 = 高 10.5)
|
||
'L1': (7.6, 7.0), # SPM6530T-4R7M
|
||
'J4': (13.2, 3.6), # BM24-50DS
|
||
'U4': (8.0, 8.0),
|
||
}
|
||
|
||
# ── 分面 ───────────────────────────────────────────────────────────────
|
||
# **A 面(顶层)只留这 10 件**,其余全部 B 面。理由逐条写在模块头的表里。
|
||
TOP_ONLY = ['J1', 'K1', 'K2', 'K3', 'K4']
|
||
|
||
# 装配高度(mm)。只登记**可能超手机投影预算**的件 —— 放进 B 面手机投影会顶到
|
||
# 隔壁槽的手机主板,而**几何自检和 DRC 一条都不报**。
|
||
HEIGHT = {
|
||
'C60': 10.5, # Ø10.3 铝电解(T2 实测)
|
||
'J3': 5.0, # 1.25 mm 2P 线座(估)
|
||
'L1': 3.0, # SPM6530T
|
||
}
|
||
H_B_PHONE = 20.32 - 1.6 - 1.0 - 7.6 # B 面手机投影下的净空(h_phone 取物理上限)= 10.12
|
||
H_B_SVC = 20.32 - 1.6 # B 面服务带的净空(对面 A 面服务带是空的)= 18.72
|
||
|
||
|
||
def phone(u, v):
|
||
return B.phone_xy(u, v)
|
||
|
||
|
||
def lanes():
|
||
u"""排线在 B 面的车道 {名字: (x0, y0, x1, y1)}:连接器远沿 → 槽底,一件不放。"""
|
||
out = {}
|
||
for k, cx in B.flex_x().items():
|
||
hw = B.FLEX_W[k] / 2.0 + 0.5
|
||
out[k] = (round(cx - hw, 3), LANE_BOT[k], round(cx + hw, 3), B.SLOT_Y[0])
|
||
return out
|
||
|
||
|
||
def anchors():
|
||
u"""机械锚定 + 手工钉死的件 -> (x, y, rot)。K1~K4 在 A 面,其余 B 面。"""
|
||
m = json.load(io.open(MECH, encoding='utf-8'))
|
||
sk = m['side_key_contacts']
|
||
fx = B.flex_x()
|
||
a = {}
|
||
# ── 侧键触点盘:主板自己的弹片直接压在这四个裸铜盘上(R1 §22.3e 中框式)──
|
||
# 位置**完全由主板决定**,Ø1.5 的盘只容许 0.50 mm 位置误差(pcb2_place 的算账)。
|
||
for ref, key in (('K1', 'PWR'), ('K2', 'VUP'), ('K3', 'VDN'), ('K4', 'GND')):
|
||
a[ref] = phone(*sk[key]) + (0,)
|
||
# ── 电池排线焊盘 J5:B 面、电池槽正北、手机投影下方(排线穿槽后折回 +y)──
|
||
# y 由排线长度定:J600→接口边 4.6 + 绕边 5 + 到槽心 2.7 + 穿板 1.6 + 背面 (y−26.3)
|
||
# ≈ 19.6 ≤ 35(实长 38)。焊盘 4 高,底沿 30 离槽顶 27.8 有 2.2。**底层 rot 0**(见模块头)。
|
||
# ⛔ 2026-09-11 晚主板转 180°:接口边朝上、槽在接口边以北,排线折回后往 **−y** 走 ⇒
|
||
# J5 / J4 在接口边**以南** 3.0 / 4.0(和原来「以北 3.0 / 4.0」镜像),**底层 rot 180**:
|
||
# 整条排线(连同两头的插头)是原来那一版绕板法线转了半圈,折回的镜像关系不变,
|
||
# 底层「先 y 镜像再转 rot」⇒ 原来 rot 0 的图案再转 180。J5.1(P+) 从西端换到东端(跟 J600 一起转的,
|
||
# 排线本身没翻面)。⚠ 装配首件仍要两点通断复核。
|
||
a['J5'] = (fx['BAT'], round(B.IF_Y - 3.0, 3), 180)
|
||
# ── BM24 尾插座 J4:B 面、尾插槽正南。2.48 + 5 + 2.7 + 1.6 + 6.7 ≈ 18.5 ≤ 50(实长 55)。**底层 rot 180**。
|
||
a['J4'] = (fx['TAIL'], round(B.IF_Y - 4.0, 3), 180)
|
||
# ── C60:Ø10.3 / 高 10.5,只能在服务带(B 面净空 18.7)。夹在两条槽之间,
|
||
# x 取两槽正中(焊盘外沿 12.9 宽,离两侧铣槽各 0.45)。
|
||
# ⛔ y **贴着金手指区上沿**(本体 11 的下沿 = 11.0),不是居中:居中(上下各余 0.17)时
|
||
# H4 螺钉头圈只剩 2.07 < HOLE_KEEP 2.2;贴下沿之后上沿 22.0 离接口边 22.34 余 0.34,
|
||
# 离 H4 2.24。(真实罐体 Ø10.3,比 BODY 的 11 每边还小 0.35。)
|
||
# ⛔ 2026-09-11 晚:服务带到了**顶部**。x 仍取两槽正中;y 由 H4 定:H4 转过来在 C60 西南角下方
|
||
# (26.69, 53.76),螺钉头圈要 2.2 ⇒ C60 本体下沿 ≥ 55.96,取 56.0 ⇒ 上沿 67.0,离板边 1.0
|
||
# (顶边导轨只能咬 ≤ 1 mm,和原来手机顶离板边 1.0 同一个数)。
|
||
s = B.flex_slots()
|
||
gap_x = (min(s.values())[2] + max(s.values())[0]) / 2.0 # 西槽东沿 / 东槽西沿的正中
|
||
h4y = B.phone_holes()['H4'][1]
|
||
a['C60'] = (round(gap_x, 3), round(max(B.IF_Y, h4y + B.HOLE_KEEP + 0.04) + BODY['C60'][1] / 2.0, 3), 0)
|
||
# (J2 D± 退路座 2026-09-11 已删,见 b4_edge.py 表头)
|
||
# ── J3:EDL 座。两根飞线要去主板朝上面的 TP1204/1205,只能**跟着尾插排线穿尾插槽**
|
||
# (R1 同一做法)⇒ 座子贴着尾插槽东头。90×68 之后车道东侧那条走廊被功率区西移吃掉了,
|
||
# 改放**服务带里、尾插槽东头与功率列之间**(x 65.6~70.6 / y 11~22):
|
||
# 竖放 rot 270 ⇒ 本体 5.2×8.35,信号盘朝**西**正对槽口。
|
||
# ⚠ 底层「先 y 镜像再转」:rot 0 读回信号盘在南,按 p = R(rot)·(lx, −ly) 推 rot 270 在西
|
||
# (原 J2 实测同一结论:rot 90 在东、rot 270 在西)。放完用焊盘读回核一次。
|
||
# ⛔ 2026-09-11 晚:J3 放**尾插槽正北**(顶部服务带里、槽和板边之间那条 7 mm 的肋上),
|
||
# rot 0 ⇒ 本体 8.35×5.2 横放、信号盘朝**南**正对槽口(rot 0 读回信号盘在南,见上)。
|
||
# x 贴着槽东头(EDL 测试点 TP1204/1205 转过来在 x 23~26、接口边以南 5.6)。
|
||
tail = s['TAIL']
|
||
a['J3'] = (round(tail[2] - 8.35 / 2 - 0.6, 3),
|
||
round(tail[3] + MILL_KEEP + GAP / 2 + 5.2 / 2 + 0.2, 3), 0)
|
||
return a
|
||
|
||
|
||
# ── 功率级:**手排**,不交给排布器 ────────────────────────────────────
|
||
# ⛔ 上一版把功率级也交给货架排布器,结果 §7.2 的四条关键回路**破了三条**:
|
||
# Cin 离 U1 **12.4 mm**(输入环路)、SW 从 U1 绕到 L1 走了 ~7 mm(SW 面积)、
|
||
# FB 分压压在 SW 那一带。货架算法只会「按面积塞满」,它不认识回路。
|
||
#
|
||
# **`U1` 转 90° 是这一版的关键一步**:TPS54622 的 VIN/GND 在一条长边、SW 在对边,
|
||
# 不转的话 Cin 和电感必然分居左右两侧 —— 而 §3.1 要求 SW / 电感 / Cin **同时**
|
||
# 靠板外边(x 88~98,离手机最远)。转成横放之后 VIN/GND 朝上、SW 朝下,
|
||
# 于是竖着堆成一条:**D1/F1 → Cin ×4 → U1 → L1 → Cout → eFuse → 分流 → INA**,
|
||
# 电流自上而下,出口 `VO_P` 正好落在 `VO-L3` 岛(y ≤ 28.8)里。
|
||
#
|
||
# 三条回路各自的落地判据(改坐标前先想清楚会破哪一条):
|
||
# · **输入环路**:`C4/C1/C2/C3` 正压在 U1 的 VIN/GND 引脚上方,最近一颗环路 ~1.0 mm;
|
||
# · **SW 面积**:`L1` 转 90° 让 1 号脚朝上,和 U1 的 SW 脚**焊盘对焊盘 0.6 mm**,
|
||
# SW 铜就是那一小块 3.4 mm 宽的方块,绕不出去;
|
||
# · **Kelvin**:`U3` 转 180° 让 9/10 脚朝上正对 `Rs1` —— **不转就是交叉**
|
||
# (9 脚在 10 脚右边,却要接左边那个焊盘),一交叉就得打过孔,§7.2 明令不许。
|
||
STACK_GAP = 0.65 # 手排竖直堆叠的件间净距
|
||
|
||
# (位号, rot, dx):dx = 相对功率列中心线 PWR_X 的横向偏移;y 由 stack() 自上而下算
|
||
PWR_X0 = round(89.0 + PX, 3) # 旧框架(F1 在顶)的功率链中心线 81.0 —— 下面的手排相对关系都按它写
|
||
# ⛔ 2026-09-11 晚(方案 §35):整条功率链**刚体转 180°**,落到新锚 (PWR_X, U1_Y)。
|
||
# U1_Y 的取法:转过来 F1 在最底、U3 在最顶;F1 本体下沿 ≥ 12(金手指区 11 + 1)且 U3 顶 ≤ 67.5
|
||
# ⇒ U1_Y ∈ [25.5, 30.5],取 27.5(F1 下沿 13.4 / U3 顶 64.5)。
|
||
# PWR_X 80(原 81):转过来 Rs1 / U3 / C5 / D1 这些原来在西边的件跑到东边,最东到 88.0,还离板边 2.0;
|
||
# L1 离手机右沿 ~8 mm(原来 8.6),风险 R-12 的「SW 离手机远」基本不变。
|
||
# ⛔ U1_Y 不能随便取:转过来 U2 的中心 = U1_Y + 21.9,它的四边脚(0.5 节距)要**全落在 0.25 布线网格上**,
|
||
# 否则西列 p17~p20 的出线挤到 0.25 以下、布线器报「出不来」(第一版取 27.5 ⇒ U2 在 y 49.4、脚在 x.15 / x.65,
|
||
# 服务器三轮 U2.2 / U2.15 / U2.20 换着断)。27.6 ⇒ U2 在 49.5,脚在 48.75 / 49.25 / … 全对格。
|
||
PWR_X, U1_Y = 80.0, 27.6
|
||
FLIP_KEEP_ROT = {'F1': 90} # 保险丝:+12V 脚要朝**南**(12V 从底下来),刚体转会让它朝北,保留原 rot
|
||
|
||
|
||
def flip_pwr(p, ref=None):
|
||
u"""功率链「F1 在顶」框架里的 (x, y, rot) → 刚体转 180° 之后的板坐标。"""
|
||
x, y, r = p
|
||
rot = FLIP_KEEP_ROT.get(ref, (r + 180) % 360)
|
||
return (round(PWR_X - (x - PWR_X0), 3), round(U1_Y - (y - U1_Y0), 3), rot)
|
||
# 功率链的锚:U1 的中心 y。⚠ 抬高 2.5 mm 是**布线逼出来的**:链尾的 `U3`(INA226)
|
||
# 底边五个 0.5 mm 间距的脚要朝下扇出到金手指那一侧,原来只剩 1.0 mm 净空,
|
||
# 布线器报「INA_ALERT 一个焊盘都出不来」。抬完剩 3.5 mm,五根线排得开。
|
||
U1_Y0 = 50.9 # (旧框架)⚠ M7 从 49.5 抬到 50.9:U2 上下的缝各加了 0.8/0.6,
|
||
# 整条链会往下顺移同样多,而链尾 `U3`(INA226) 底边那五个 0.5 mm 间距的脚
|
||
# 离金手指禁布区只剩 1.67 mm —— 正是 M4c 当初抬高 2.5 mm 要解决的那件事。
|
||
# 抬 1.4 抵掉,U3 底沿回到 14.07(禁布区 11),扇出仍有 3.0 mm。
|
||
STACK_UP = [ # U1 往上:Cin 由近到远,再是保险/TVS
|
||
('C4', 180, 0.0), # 0402 HF,压在 VIN/GND 脚正上方
|
||
('C1', 180, 0.0),
|
||
('C2', 180, 0.0),
|
||
('C3', 180, 0.0),
|
||
('F1', 90, 0.0), # rot=90 ⇒ p1(+12V) 朝上接 V12-L3 岛、p2(+12V_F) 朝下
|
||
]
|
||
# 第 4 个字段 = 这一件和上一件之间的净距(缺省 STACK_GAP)。
|
||
# ⚠ `U2` 上下要留 1.8:它是 WQFN-20,四条边都有脚,**上下两排要从 0.65 mm 的
|
||
# 缝里横着爬出去** —— 实测这一带堵掉了 eFuse 的 6 个信号($1N147/$1N151/
|
||
# $1N164/EFUSE_EN/EFUSE_PG/EFUSE_FLT)。功率链在 y 上有 15 mm 富余,加得起。
|
||
STACK_DN = [ # U1 往下:电感 → 输出电容 → eFuse → 分流 → INA
|
||
# ⚠ 这个 1.5 不是随便给的:U1 底边除了 SW(11/12) 还有 p9/p10/p13(BOOT),
|
||
# 贴到 0.65 等于把整条边封死,那三只脚**只能横着爬**而两侧都是焊盘 ——
|
||
# 实测 `$1N136`/`$1N138` 死活出不来。1.5 给它们让出一条通道,
|
||
# SW 焊盘净距从 0.65 变 1.5,仍远小于 §7.2 的 2.0 判据。
|
||
('L1', 270, -0.25, 1.5), # p1(SW) 朝上正对 U1 的 SW 脚
|
||
('C6', 0, -0.25),
|
||
('C7', 0, -0.25),
|
||
('C8', 0, -0.25), # 输出 HF,接着 C6/C7 排在同一条线上
|
||
# ⚠ M7 从 1.8 加到 2.6 / 2.4:U2 是 WQFN-20,上下两排脚要横着爬出去,
|
||
# M6 欠的 5 只脚里有 3 只(U2.15 / U2.17 / U2.2)就死在这两条缝里。
|
||
# 功率链在 y 上还有 ~14 mm 富余(U3 底 14.06 vs 禁布区 11),加得起。
|
||
('U2', 180, 0.0, 2.6), # rot=180 ⇒ VO 从上边进、$1N165 从下边出
|
||
('Rs1', 0, -4.2, 2.4), # p2(右) 接 U2 的输出、p1(左) = VO_P 出岛
|
||
('U3', 180, -4.95, 1.4), # rot=180 ⇒ 9/10 脚朝上,正对 Rs1 两端(不交叉)
|
||
]
|
||
# 不进竖直堆叠、但位置由引脚决定的几件
|
||
HAND = {
|
||
# BOOT 电容:BOOT 脚(p13)在 U1 下边、SW 也在下边,但正下方被 L1 占满了。
|
||
# ⇒ 摆到 L1 左侧,`SW` 那一脚接 **L1 的 1 号焊盘**(同一个节点,不必绕回 U1)。
|
||
# ⚠ 2026-09-11 晚:C5 / C9 / D1 / R3 / C13 这五件的坐标仍写在「F1 在顶」的旧框架里,
|
||
# dict 末尾统一 flip_pwr() 刚体转过去(注释里的上下左右都是旧框架的方位)。
|
||
'C5': (PWR_X0 - 5.3, U1_Y0 - 6.3, 0),
|
||
# U1 的 p9($1N135) 旁路:p9 在下边偏右,摆到 U1 右下角外
|
||
'C9': (PWR_X0 + 3.45, U1_Y0 - 1.72, 0), # 和 p9 同一条水平线,2.0 mm
|
||
# TVS:竖在 Cin 堆左侧,p1(+12V_F) 朝上,和保险的出线端同一节点
|
||
# ⚠ M7:从 U1_Y+2.5 挪到 +3.1(北移 0.6),给 `R3` 让出 U1 西边那个口袋(见下)。
|
||
'D1': (PWR_X0 - 4.2, U1_Y0 + 3.1, 90),
|
||
# ── 尾插那两颗(U7 ESD / U5 VBUS 开关):J4 现在也在 B 面,排线从南边来,
|
||
# 它们只能在 J4 **北边**(插头背后)—— 交给 SVC_R 区的排布器,U7 排第一个
|
||
# 落在区的西南角(离 J4 的 D± 脚最近)。
|
||
# ── PCA9555:TSSOP-24,两排各 12 只脚 0.65 mm 间距 ────────────────────
|
||
# **M7 用户拍板:挪到左下角**(原来 M7 第一版放板中央 (48,52),更早在功率列
|
||
# 那个拥塞口袋 (76.3,18.5))。左下角这块 x 4~20 / y 11.6~21 本来是空的,
|
||
# 四面净空都 ≥ 2 mm,而且离侧键 MOS(`Q3~Q5` 在 x 2~5.5 / y 26~30)最近 ——
|
||
# `KEY_PWR/VUP/VDN` 三根从 M6 到现在一直是布不通的钉子户,这一挪从 70 mm 变 12 mm。
|
||
# ⛔ **y 不能照用户拖的 10.67**:本体 8×8 ⇒ 下沿会落到 **6.67**,
|
||
# 而 y < 11 是**金手指区**,PCIe 插槽体(~9 mm 高)要从两面夹住这一段板 ——
|
||
# 放件进去装不上,几何自检会拦(`侵入金手指区`)。抬到 15.6:下沿 11.6,
|
||
# 离禁布区 0.6,离 KEY 区下沿(22)2.4。
|
||
# ⚠ 用户拖过来时是 rot 180,**改成 rot 0**:两者都是「南北两排」,
|
||
# 差别在哪一排朝哪边 —— rot180 时 `p1~p12`(`KEY_PWR/VUP/VDN`、`INT#`、
|
||
# `EDL_EN`、`VBUS_EN`)朝**南**,而侧键 MOS `Q3~Q5` 在**北**边(y 26~30);
|
||
# 同时 `p13~p24`(`SDA/SCL/+3V3` + 四路状态)朝北,而金手指在**南**边。
|
||
# 两排正好各自背对自己的去向,等于每根线都要绕整颗芯片一圈 —— M6 在功率列
|
||
# 上就是这么丢掉 4 处的(原 `HAND` 注释里那段「转 180」说的是同一件事)。
|
||
# ⛔⛔ 2026-09-11 晚(用户:「U4 移到右侧」):主板转 180° 之后侧键触点 K1~K4 落在手机**右上角**
|
||
# (64~67, 49~52),功率链就在东边 ⇒ U4 放右侧、侧键正南:KEY_* 三根就近,EFUSE_FLT/PG(U2)、
|
||
# BUCK_PG(U1)、INA_ALERT(U3)四根往东一步到功率列(原来在左下角要横穿整块板,
|
||
# 90×68 那一版 5 个没布通的网里 3 个就是这几根)。rot 0:南排东头 p13~p16 朝功率列,
|
||
# 南排西头 p22~p24(SCL/SDA/+3V3)朝西南去金手指,北排 p4~p8 朝北去侧键 / 上去服务带。
|
||
'U4': (60.0, 38.0, 0),
|
||
# PCA9555 去耦 + I²C 上拉:**跟着 U4 走**,坐标由 `pin_xy()` 从板上读回的
|
||
# U4 引脚位置现算(见该函数的说明),不手抄。
|
||
# ⛔ `R3`(EN 分压的下臂,`$1N128`)**必须在 U1 西边**:`U1.1`(EN) 和 `p14`(BUCK_PG)
|
||
# 都在西短边,而 `BUCK_FB` 区在 U1 **东**边(那是给 FB/COMP 的,见 REGIONS 注释)。
|
||
# 摆在东边的话这根线要绕整颗芯片 —— 布线器就是这么报「$1N128: U1.1 没布通」的,
|
||
# `place_audit.py` 也把它列在「摆错边」里。
|
||
# 口袋是 L1 顶(47.2) / U1 西沿(86.875) / D1 南沿(50.6,已北移让路) 夹出来的那一小块。
|
||
'R3': (round(85.95 + PX, 3), 48.8, 90),
|
||
# INA226 的 +3V3 旁路,摆在 U3 右侧(U3 上下两排脚都被 Kelvin / I2C 占着)
|
||
'C13': (round(86.8 + PX, 3), 18.0, 0),
|
||
# ── U7(D± ESD):**手排在 J4 那两只 D± 脚正北**(2026-09-11,90×68 + U7 两侧对调之后)──
|
||
# 下排 6/4 脚(DMJ 西 / DPJ 东)正对 J4 的 4/6 脚(DMJ 51.23 西 / DPJ 51.58 东),
|
||
# 直直往南 ~2 mm 接过去,零交叉;上排 1/3 脚(USB_DM / USB_DP)朝北,
|
||
# 金手指来的一对在顶层掉头从北边进来、脚旁各一个孔下 B 面(手性推导见 b3b_usb.py)。
|
||
# J4 两只 D± 脚相对本体 (−3.85/−3.50, +1.06),U7 焊盘排距 ±1.15(都是 dump 出来的封装常量);
|
||
# 本体下沿离 J4 本体顶 0.73,在尾插车道上沿(LANE_TOP)以北。
|
||
# ⛔ 2026-09-11 晚:J4 跟主板一起转了 180°(rot 180),U7 也绕 J4 转 180° ⇒ 在 J4 **正南**、rot 180:
|
||
# 上排 4/6 脚(DPJ 西 / DMJ 东)正对 J4 的 6/4 脚;下排 3/1 脚(USB_DP 西 / USB_DM 东)朝南,
|
||
# 金手指来的一对从南边直接进来(不用再绕到另一面),每根一个孔下 B 面。手性照样零交叉(router.dp_blade 断言)。
|
||
'U7': (round(B.flex_x()['TAIL'] + 3.675, 3), round(B.IF_Y - 4.0 - (1.059 + 2.0 + 1.148), 3), 180),
|
||
# EDL 模拟开关:跟着 J3 走,交给 SVC_E 区排布器(U6 排第一个)
|
||
# ── 侧键 MOS:转 90° 让 3 脚(接触点盘那一路)朝上,排在 K1~K4 正下方 ──
|
||
# ⚠ 90×68:y 跟手机走(+DY),**x 不动** —— Q4 本来就贴着板边(x 0.73~3.28),
|
||
# 跟着手机左移 1.5 就出板;三颗一起不动,彼此间距不变,离 K 盘的上下余量也不变。
|
||
# ⛔ 2026-09-11 晚:跟主板转 180°(本体坐标 u/v 是 90×68 第一版的值反推的),rot 90 → 270。
|
||
'Q3': B.phone_xy(4.0, 0.9) + (270,),
|
||
'Q4': B.phone_xy(0.5, 0.9) + (270,),
|
||
'Q5': B.phone_xy(4.0, -2.9) + (270,),
|
||
}
|
||
for _r in ('C5', 'C9', 'D1', 'R3', 'C13'):
|
||
HAND[_r] = flip_pwr(HAND[_r], _r)
|
||
|
||
|
||
def stack(pos, sz):
|
||
u"""把 STACK_UP / STACK_DN 按 bbox 高度自 U1 向两头堆开,返回补进去的坐标。
|
||
|
||
⚠ 只用 bbox 高度 + `STACK_GAP` 算 —— 不手抄 y。改一颗件的封装,
|
||
整条链自动重排,不会留下一个「当初算好、现在对不上」的常数。
|
||
"""
|
||
out = {'U1': (PWR_X0, U1_Y0, 270)}
|
||
y = U1_Y0 + sz['U1'][1] / 2.0
|
||
for item in STACK_UP:
|
||
ref, rot, dx = item[:3]
|
||
gap = item[3] if len(item) > 3 else STACK_GAP
|
||
h = sz[ref][0] if rot % 180 else sz[ref][1]
|
||
y += gap + h / 2.0
|
||
out[ref] = (round(PWR_X0 + dx, 3), round(y, 3), rot)
|
||
y += h / 2.0
|
||
y = U1_Y0 - sz['U1'][1] / 2.0
|
||
for item in STACK_DN:
|
||
ref, rot, dx = item[:3]
|
||
gap = item[3] if len(item) > 3 else STACK_GAP
|
||
h = sz[ref][0] if rot % 180 else sz[ref][1]
|
||
y -= gap + h / 2.0
|
||
out[ref] = (round(PWR_X0 + dx, 3), round(y, 3), rot)
|
||
y -= h / 2.0
|
||
# 2026-09-11 晚:在旧框架里堆完,整条链刚体转 180°(见 flip_pwr)
|
||
return {r: flip_pwr(v, r) for r, v in out.items()}
|
||
|
||
|
||
# ── 分组:**顺序 = 电流/信号流向**,排布器按这个顺序摆 ──────────────────
|
||
GROUPS = [
|
||
# (区, 组名, [位号...])
|
||
# 功率链本体已经手排(STACK_*/HAND),这里只剩「跟着某颗 IC 走」的外围。
|
||
('CTRL', u'I2C 上拉 + 去耦(正常情况下由 ctrl_hand 钉在 U4 脚上)', ['C40', 'R40', 'R41']),
|
||
# ⚠ `VO_P` 泄放支路(R19 10Ω/1W + Q2)挪到**服务带左段**:它接的是 `VO_P`,
|
||
# 而 `VO_P` 在 L3 上是整块岛(服务带全覆盖),放哪一头都一样近;
|
||
# 放这里把功率列内条腾出来给 U4 的扇出带。
|
||
('DIS', u'VO_P 泄放支路', ['R19', 'Q2']),
|
||
# ⛔ M7 三版拆成东西两组。`U2` 是 WQFN-20,**四条边都有脚**,而 M7 二版把 12 件
|
||
# 全塞在它西边 —— 接东边 `p17~p20`(`$1N156`/`$1N158`/`$1N151`/`EFUSE_FLT`)的
|
||
# 四件要绕整颗芯片才够得着,U2 周围本来就只有那么几个出口,一绕就把别人也堵死
|
||
# (M6/M7 一直布不通的 `$1N156: U2.17` 就是这么来的)。
|
||
('EFUSE_E', u'eFuse 东侧(p17~p20)', ['R11', 'C14', 'R12', 'R13']),
|
||
# ⚠ 90×68:EDL 开关排在 EFUSE_AUX **之前** —— 两区在 J3 正北重叠,谁先摆谁先挑;
|
||
# 第一版反过来,U6 被挤到 y 37.5(离 J3 20 mm),`$3N45: J3.2 ↔ U6.4` 没布通。
|
||
('SVC_E', u'EDL 模拟开关', ['U6', 'R63', 'C69']),
|
||
('EFUSE_AUX', u'eFuse 西侧 + 北侧', ['R9', 'R10', 'R16', 'R17',
|
||
'R15', 'R14', 'Q1', 'R18']),
|
||
# ⚠ FB / 补偿**必须在这个区**:它贴着 U1 的左边(FB/COMP 脚都在左长边),
|
||
# 同时离 SW(x 87~94、y 34~42 那一块)≥ 2 mm —— §7.2 第三条「FB 远离 SW」。
|
||
('BUCK_FB', u'FB 分压 + 补偿 + DAC 低通', ['R1', 'R2', 'R7', 'R8', 'C12',
|
||
'R4', 'R5', 'R6', 'C10', 'C11']), # R3 见 HAND
|
||
# ⚠ 侧键组:三颗 MOS 已手排在 K1~K4 正下方,这里只剩栅极电阻,跟着 MOS 走。
|
||
('KEY', u'侧键栅极电阻', ['R42', 'R43', 'R44', 'R45', 'R46', 'R47']),
|
||
('SVC_M', u'VO_P 阻尼组 + 电池外围', ['C61', 'C62', 'C63', 'C64', 'C65',
|
||
'R60', 'R61']),
|
||
('SVC_R', u'尾插 USB + VBUS 外围', ['U7', 'U5', 'C66', 'C67', 'C68', 'R62', 'R64', 'R65']),
|
||
]
|
||
|
||
# ── 区(x0, y0, x1, y1),排布器在里面自下而上、自左而右铺 ────────────────
|
||
# ⚠ 三个服务带小区各自贴着自己那件连接器:
|
||
# SVC_M ↔ J5(26.8,32),SVC_R ↔ J4(56.6,33) 北边,SVC_E ↔ J3(69.9,33) 北边。
|
||
# 区改小是故意的:范围给大了货架排布器会把件铺到离连接器 15 mm 外去。
|
||
# ⚠ KEY 区上沿 36.6 = H1 螺钉头(4.4,38.9) 让 2.2。
|
||
REGIONS = {
|
||
# ⚠ M7:跟着 U4 搬到板中央,落在 U4 东列(x 51.0)外侧 —— SDA/SCL 上拉
|
||
# 就地接到 p22/p23 上,不用绕。北边到 H5(60.4,70.2) 还有 14 mm。
|
||
# ⚠ M7 二版:跟着 U4 挪到左下角,占 U4 东边那条 4 mm 宽的缝(DIS 区从 x 21 起)。
|
||
# ⚠ 90×68:上沿收到电池槽下沿(槽现在在 y 18.14~21.14)
|
||
# ⛔ 2026-09-11 晚(方案 §35):下面九个区全部按「主板转 180° + 功率链转 180° + U4 在右侧」重划。
|
||
# CTRL 只是退路(C40/R40/R41 正常由 ctrl_hand 钉在 U4 南排西头)。
|
||
'CTRL': (50.0, 28.0, 66.0, 46.0),
|
||
# ⚠ 必须落在 VO-L3 岛(x ≥ 20)里:R19 的 VO_P 脚要就地扎过孔进岛,
|
||
# 放到岛外面布线器会报「找不到落过孔的地方」。东沿 34.2 贴 C60 本体西沿 34.5。
|
||
# ⚠ M7 上沿从 18.8 抬到 24.0(= 电池铣槽下沿 24.8 让 0.8):节距加到 1.8 之后
|
||
# R19(7.4×3.5) + Q2 在 6.8 高的带子里排不下第二行。
|
||
# ⛔ 90×68 第三版:挪到**电池车道正北、手机投影下**(B 面,R19 2512 / Q2 都 < 1 mm 高)。
|
||
# 第一版放在电池槽正下方(y 11.2~17.6),而服务带压到 11.34 之后,**槽下那条 6.4 mm 高的带
|
||
# 是 U4(左下角)往东去的唯一走廊** —— ~8 个网(SDA/SCL/BUCK_PG/EDL_EN/VBUS_EN/EFUSE_FLT/
|
||
# INA_ALERT/EFUSE_PG)要从这儿过,R19+Q2 把 B 面那一层从 x 18 到 32 整段占满,服务器上
|
||
# 调布线顺序只能此消彼长(2026-09-11 第三轮:三个长网通了、SDA 整网断成 4 块)。
|
||
# 新位置紧挨电池座 J5(R19 本来就接 VO_P = 电池正)、正下方是 VO-L3 岛的北叶,就地扎孔照旧。
|
||
# ⛔ 2026-09-11 晚:J5 转到接口边以南之后,DIS 放 J5 / 电池车道**正南**(车道底 50.16),
|
||
# 仍在手机投影下(R19 2512 / Q2 都 < 1 mm 高),VO_P 脚就地扎孔进 L3 的 VO 岛。
|
||
'DIS': (37.5, 42.5, 52.8, round(LANE_BOT['BAT'] - 0.3, 3)),
|
||
# ⚠ M7 大改:U4 搬走腾出的整块口袋都给它(原来 9.5×11.5 塞 12 件,节距 1.1)。
|
||
# 南沿 13.0 = 金手指禁布区 11 让 2.0;东沿 84.0 给 U2 西边留 2.55 的扇出通道
|
||
# (U2 本体西沿 86.55),M6 就是这条通道只有 1.55 才把 R11/R16 憋死的。
|
||
# ⚠ 南沿定在 20.0 而不是 13.0:货架排布器是**自下而上**填的,y0 给到 13
|
||
# 会把 12 件全堆到离 U2(y 27.2~31.1) 最远的那一头,ILIM/dV-dt 这些
|
||
# 本该贴着芯片的件跑到 12 mm 外。y 13~20 那块留空,正好是 U3 底排
|
||
# 五只 0.5 mm 间距脚往金手指方向扇出的地方。
|
||
# `U2` 东边(本体东沿 91.45)到板边之间那条缝。上沿 33.5 让开 `C8`(y 32.3~32.9) 所在的
|
||
# 功率链中心线;下沿 23.5 让开 `Rs1`(y 19.9~23.4)。4 件一列,顺序由「按连线就近落位」
|
||
# 自动排成 p17→p20 的顺序(目标点精确到焊盘)。
|
||
# ⚠ 北沿放到 36.0:目标驱动的排布器会为了靠近自己那只脚把件排在非对齐的 y 上,
|
||
# 5.4×10 的框里 4 件就可能塞不下(实测 `R13` 放不下)。东边这一条本来就是空的。
|
||
# ⛔ 2026-09-11 晚:U2 刚体转 180°(rot 0)之后 p17~p20 在**西**边一列 ⇒ EFUSE_E 贴 U2 西沿。
|
||
'EFUSE_E': (72.3, 43.5, 77.4, 55.5),
|
||
# ⚠ 90×68:整体西移 8 会压上尾插槽 / 车道(东沿 65.58)和新的 J3 ⇒ 区整体放到 J3 **以北**、
|
||
# 车道东沿以东,宽度保持 M7 的 11.2(东沿仍离 U2 西沿 2.55)。第一版把西沿收到 68.6(宽 7.4),
|
||
# R16/R17 被挤到离 U2 7~11 mm,`$1N165: R16.1` 没布通。
|
||
# ⛔ 2026-09-11 晚:U2 的 p2/p3(北排西半)、p14/p15(南排西半)转过来也都朝西 ——
|
||
# 原来 EFUSE_AUX 摆在 U2 西边、这几只脚却在东半边,`U2.2 / U2.15` 从 M6 起就出不来。
|
||
# 现在外围和脚在同一侧:区贴 U2 西边、比 EFUSE_E 再往西一格(EFUSE_E 先摆、先挑)。
|
||
'EFUSE_AUX': (68.8, 38.0, 77.4, 61.0),
|
||
# ⚠ FB / 补偿在 U1 的**右短边**(转 270 之后 p7=FB、p8=COMP 都在 x=+1.72 那一侧),
|
||
# 所以这个区在 U1 右边、板边那一条 6 mm 的缝里 —— 离 SW(x 87~90)≥ 2 mm。
|
||
# ⚠ M7 只把 y 拉长(1.4 的节距下 6.1 宽刚好两列),不动 x:再往东就是板边。
|
||
# ⛔ 2026-09-11 晚:U1 转成 rot 90,FB/COMP 在**西**短边 ⇒ BUCK_FB 在 U1 西边(贴手机那一侧,
|
||
# 正好是 §3.1「内条放最怕吵的」;原来在东边板沿其实是反的)。y 是旧区 (U1_Y−8, +15) 转过来的。
|
||
'BUCK_FB': (69.5, round(U1_Y - 15.0, 3), 77.5, round(U1_Y + 8.5, 3)),
|
||
# ⚠ M7 东沿从 8.5 拉到 13.0:K1~K4 四个盘 + 三颗 MOS 已经占掉了西边这一条,
|
||
# 1.8 节距下六只栅极电阻排不进去(x 9~13 这一带本来就是空的)。
|
||
# 2026-09-11 晚:侧键跟主板转到右上角;KEY 区是旧区绕手机投影中心转过来的,东沿收到 68.8 让给 EFUSE_AUX。
|
||
'KEY': (55.5, 45.5, 68.8, 63.0),
|
||
# 电池车道东沿 31.77+0.5 → 33;H4 (44.9,30.9) 螺钉头西沿 42.7。
|
||
# ⚠ M7 三版:北沿 **45.5**(不是 50)—— `VO-L3` 岛的北叶只到 **y=46**
|
||
# (`pcb_pour.VO_LOBE`),排到 48.8 的那颗阻尼电容 `VO_P` 脚就地扎不了过孔,
|
||
# 布线器报「找不到落过孔的地方」。而且阻尼组本来就该贴着 `J5`(26.8,32),
|
||
# 越往北越没意义。少的高度往横里补:东沿 42.5 → 44.5(H4 螺钉头的避让圈
|
||
# 由 blockers() 兜着)。
|
||
# 2026-09-11 晚:旧区绕手机投影中心转过来 ⇒ 电池车道**西**侧、J5 旁边(H4 螺钉头由 blockers 兜着)。
|
||
'SVC_M': (27.0, 38.5, 39.8, 55.6),
|
||
# J4 北边:本体顶 34.3 + 0.6。⚠ M7 北沿收到 42.0(给 U4 的南面让出 6 mm 扇出净空),
|
||
# 东沿从 66 收到 64.5(尾插车道东沿 66.08 之内),少的高度靠 1.8 节距下仍够 8 件。
|
||
# ⚠ 90×68:只留 U7 换层孔空地**以东**那一块(U7 已手排)—— 第一版给了整条 18 mm,
|
||
# 排布器的「同尺寸互换」把 C67 / C68 换到了离 U5 6~8 mm 的地方,`$5N71: U5.4` 没布通。
|
||
# 2026-09-11 晚:J4 转到接口边以南之后,尾插 USB / VBUS 外围在 J4 **正南**(插头背后),尾插车道底 49.66 以下。
|
||
'SVC_R': (4.5, 37.5, 17.0, round(LANE_BOT['TAIL'] - 0.4, 3)),
|
||
# ⚠ 90×68:J3 挪到服务带(尾插槽东头)之后,EDL 开关排在它**正北**那一小块
|
||
# (车道东沿 64.58 以东、J3 本体顶 20.45 以北);GROUPS 里排在 EFUSE_AUX 前面。
|
||
# 2026-09-11 晚:EDL 开关跟着 J3 到顶部服务带(尾插槽以北那条肋,净空 18.7)。
|
||
'SVC_E': (0.8, round(B.SLOT_Y[1] + MILL_KEEP, 3), 26.0, 67.4),
|
||
}
|
||
|
||
# ⛔ 排布器的障碍**要分面**。A 面的 `J4` / `J5` / `C60` / `J2` / `J3` 挡的是 A 面,
|
||
# 把它们当成 B 面的障碍,「USB 组落在 J4 正背面」这条设计意图就永远实现不了
|
||
# (排布器会绕开 J4 的本体框,把件推到 5 mm 以外)。
|
||
# ⇒ 只有下面这几件对**两面**都算障碍:
|
||
# · `K1~K4`:Ø1.5 裸铜盘,B 面要在旁边扎过孔把漏极引下来,正背面得空着;
|
||
# 而且手机弹片压在上面,底下不摆件也让受力干净。
|
||
# · `J1`:金手指,两面都是指。
|
||
BLOCK_BOTH = ('J1', 'K1', 'K2', 'K3', 'K4')
|
||
|
||
|
||
def bottom_refs(pos=None):
|
||
u"""B 面件 = 全部 - TOP_ONLY。"""
|
||
d = json.load(io.open(BBOX, encoding='utf-8'))
|
||
return [r for r in (pos or d) if r not in TOP_ONLY]
|
||
|
||
|
||
def sizes():
|
||
d = json.load(io.open(BBOX, encoding='utf-8'))
|
||
out = {}
|
||
for ref, v in d.items():
|
||
w, h = BODY.get(ref, (v['w'], v['h']))
|
||
out[ref] = (max(w, 0.6), max(h, 0.6))
|
||
return out
|
||
|
||
|
||
def overlaps(r, rects):
|
||
for q in rects:
|
||
if r[0] < q[2] and q[0] < r[2] and r[1] < q[3] and q[1] < r[3]:
|
||
return True
|
||
return False
|
||
|
||
|
||
def blockers():
|
||
u"""排布器的**固定障碍**:铣穿的排线槽 + 排线车道 + 七个定位孔的螺钉头避让圈。
|
||
|
||
⛔ 不把它们喂进排布器,货架算法照样会往里塞件,然后自检报一堆错 ——
|
||
窗里那几件是**摆在空气里**(内槽铣穿),孔那几件是被螺钉头压着。
|
||
"""
|
||
out = []
|
||
for (a, b, c, d) in B.mill_cuts():
|
||
out.append((a - MILL_KEEP, b - MILL_KEEP, c + MILL_KEEP, d + MILL_KEEP))
|
||
out += list(lanes().values())
|
||
for (hx, hy) in B.phone_holes().values():
|
||
out.append((hx - B.HOLE_KEEP, hy - B.HOLE_KEEP,
|
||
hx + B.HOLE_KEEP, hy + B.HOLE_KEEP))
|
||
# D± 在顶层从北边来、在 U7 上排(1/3 脚)北边换层下 B 面 ⇒ 那一小块 B 面不放件
|
||
# ⛔ 2026-09-11 晚:U7 转了 180°,D± 从**南边**进、孔在下排以南
|
||
ux, uy, _r = HAND['U7']
|
||
out.append((ux - 1.8, uy - 4.2, ux + 1.8, uy - 1.7))
|
||
return out
|
||
|
||
|
||
def wh(ref, rot, sz):
|
||
u"""转 90/270 之后 bbox 的宽高要对调。**手排件全靠这一步**——
|
||
忘了对调,`L1`(7.6×7.0 转 90) 和 `D1`(6.8×2.2 转 90) 的占位就是错的,
|
||
而错的方向是「看起来更小」,于是重叠检查放行、截图上却压在一起。"""
|
||
w, h = sz[ref]
|
||
return (h, w) if rot % 180 else (w, h)
|
||
|
||
|
||
def plan():
|
||
u"""返回 {位号: (x, y, rot)} 和占位矩形表。"""
|
||
sz = sizes()
|
||
pos = dict(anchors())
|
||
pos['J1'] = (B.J1_X, B.J1_Y, B.J1_ROT)
|
||
pos.update(stack(pos, sz)) # 功率链(手排,竖直堆叠)
|
||
pos.update(HAND) # 其余按引脚钉死的手排件
|
||
pos.update(ctrl_hand(pos, sz)) # C40/R40/R41 跟着 U4 的引脚现算
|
||
# 排布器只摆 B 面的件 ⇒ 障碍表里只放「对 B 面也成立」的东西(见 BLOCK_BOTH)
|
||
# ⛔ M7 修掉一个 M5 遗留:这里原来把 `anchors()`(J2/J3/J4/J5/C60)从障碍表里
|
||
# 排除掉,理由是「它们在 A 面,挡的是 A 面」—— 但 M6 已经把它们全翻到 B 面了,
|
||
# 排除条件没跟着改。M6 没暴露是因为当时没有哪个区压到连接器上;M7 的
|
||
# EFUSE_AUX 一往西扩就把 Q1 摆进了 J3 的本体里(自检那一关拦下来的)。
|
||
# ⇒ 只要不在 TOP_ONLY 里,就是 B 面的固定障碍。
|
||
fixed = list(BLOCK_BOTH) + sorted(set(pos) - set(TOP_ONLY))
|
||
rects = list(blockers())
|
||
for ref in fixed:
|
||
x, y, r = pos[ref]
|
||
w, h = wh(ref, r, sz)
|
||
rects.append((x - w / 2 - GAP / 2, y - h / 2 - GAP / 2,
|
||
x + w / 2 + GAP / 2, y + h / 2 + GAP / 2))
|
||
|
||
# 每个区一个 y 游标:**一个组占一条完整的横带**,下一组从带子上沿重新开始。
|
||
# 不这么做的话小件会回填到前面组的空隙里,功能级在 y 上互相咬 ——
|
||
# 后面 L3 要按「哪一段是 12V、哪一段是 VO」切铜面就切不出来了。
|
||
cursor = {k: v[1] for k, v in REGIONS.items()}
|
||
unplaced = []
|
||
slots = {} # {(区, 尺寸类): [(位号, (x,y))]}
|
||
peers0 = net_peers()
|
||
STEP = 0.5 # 候选格点的步长(和老版扫描一致)
|
||
for region, _name, refs in GROUPS:
|
||
x0, _y0, x1, y1 = REGIONS[region]
|
||
gap = PACK_GAP_BY_REGION.get(region, PACK_GAP)
|
||
y0 = cursor[region]
|
||
todo = [r for r in refs if r not in pos]
|
||
if not todo:
|
||
continue
|
||
# ⛔ **挑「离目标最近的空位」,不是「自下而上第一个空位」**。
|
||
# 老版按面积从下往上塞,位置和这件连到哪儿毫无关系 —— 用户一眼看出来
|
||
# 「没摆顺」(`U2` 东边四只脚的外围件全被塞到西边去了)。
|
||
# 目标点由 `target_point()` 精确到**对端焊盘**给出。
|
||
# 大件先挑(它们的目标最硬、可选位置最少),同尺寸按位号稳定排序。
|
||
order = sorted(todo, key=lambda r: (-sz[r][0] * sz[r][1], r))
|
||
band_top = y0
|
||
for ref in order:
|
||
w, h = sz[ref][0] + gap, sz[ref][1] + gap
|
||
t = target_point(ref, peers0, pos, ((x0 + x1) / 2.0, (y0 + y1) / 2.0))
|
||
best = None
|
||
yy = y0
|
||
while yy + h <= y1 + 1e-9:
|
||
xx = x0
|
||
while xx + w <= x1 + 1e-9:
|
||
r = (xx, yy, xx + w, yy + h)
|
||
if not overlaps(r, rects):
|
||
cx, cy = xx + w / 2, yy + h / 2
|
||
key = ((math.hypot(cx - t[0], cy - t[1]) if t else 0.0), yy, xx)
|
||
if best is None or key < best[0]:
|
||
best = (key, r, cx, cy)
|
||
xx += STEP
|
||
yy += STEP
|
||
if best is None:
|
||
unplaced.append((ref, region))
|
||
continue
|
||
_key, r, cx, cy = best
|
||
rects.append(r)
|
||
pos[ref] = (round(cx, 3), round(cy, 3), 0)
|
||
slots.setdefault((region, size_class(sz[ref])), []).append((ref, (round(cx, 3), round(cy, 3))))
|
||
band_top = max(band_top, r[3])
|
||
cursor[region] = band_top + 0.4
|
||
peers = net_peers()
|
||
if peers:
|
||
# ⚠ 迭代 3 轮:同一组里的件互为目标点(`C68` 要贴 `U5`,而 `U5` 自己也是
|
||
# 排布器摆的),一轮收敛不了 —— 第一轮 `C68` 按 `U5` 的**初始货架位**去靠,
|
||
# 而 `U5` 那一轮也在动。实测第 2 轮就基本不动了,第 3 轮兜底。
|
||
for _ in range(3):
|
||
if not reassign(pos, slots, peers):
|
||
break
|
||
flip_2pin(pos, slots, peers)
|
||
# ⚠ 网格微调**所有件**(含手排的功率链):0.5 mm 间距的 QFN 不落在 0.25 格点上时,
|
||
# 相邻两只脚的出线只能挤到 0.25,比净距(0.15+0.16)还窄,布线器就报「出不来」。
|
||
# `U2.19` / `U1.1` 都是这么卡住的。挪完再逐对校核净距,破了 GAP 的**退回去**。
|
||
before = dict(pos)
|
||
grid_snap(pos)
|
||
# ⚠ 退回要**迭代到不动为止**:退回 A 会让它和「已经挪过、没退回」的 B 之间重新变窄
|
||
# (实测 C1 退回、C2 没退,缝从 0.65 变 0.55,自检报重叠)。
|
||
done = set()
|
||
for _ in range(6):
|
||
bad = snap_conflicts(pos, sz) - done
|
||
if not bad:
|
||
break
|
||
for ref in bad:
|
||
pos[ref] = before[ref]
|
||
done |= bad
|
||
return pos, rects, unplaced, sz
|
||
|
||
|
||
def u4_local_pins():
|
||
u"""从 `_blade_pads.json` 读回 `U4` 24 只脚**相对质心**的局部偏移 {脚号: (dx, dy)}。
|
||
|
||
TSSOP 两排脚对称 ⇒ 质心就是本体中心,不需要另外知道当初摆在哪。
|
||
⚠ 这份文件是**上一次下发之后 dump 的**:改了 `U4` 的旋转角要先
|
||
`pcb_route.py --dump` 再重跑一次 `--push`(两趟收敛),否则下面
|
||
`ctrl_hand()` 会认出「排布方向对不上」并退回给货架排布器。
|
||
"""
|
||
if not os.path.exists(PADS_JSON):
|
||
return {}
|
||
d = [r for r in json.load(io.open(PADS_JSON, encoding='utf-8')) if r[0] == 'U4']
|
||
if len(d) != 24:
|
||
return {}
|
||
cx = sum(r[2] for r in d) / len(d)
|
||
cy = sum(r[3] for r in d) / len(d)
|
||
return {r[1]: (round(r[2] - cx, 3), round(r[3] - cy, 3)) for r in d}
|
||
|
||
|
||
def ctrl_hand(pos, sz=None):
|
||
u"""`C40` / `R40` / `R41` 跟着 `U4` 的引脚现算位置 —— 不手抄坐标。
|
||
|
||
· `C40` 100nF 去耦要贴 `p24(+3V3)` / `p21(GND)`:交给货架排布器它会落到区角上、
|
||
离电源脚 5 mm 以上,去耦离得远等于没有。
|
||
· `R40` / `R41` I²C 上拉贴 `p22(SCL)` / `p23(SDA)`。
|
||
|
||
⛔ 三件不能各自贴各自的脚:`p22/p23/p24` 相邻只差 **0.65 mm**,而 0402 加装配净距
|
||
要 2 mm 才排得下 —— 第一版就是这么写的,自检直接报三处重叠。
|
||
⇒ 排成**一排**,间距 `ROW_PITCH`,整排贴在 `U4` 本体那一侧的外面
|
||
(`本体半宽 + GAP + 件的半高`,从 `BODY['U4']` 现算,不写死)。
|
||
|
||
排布方向(南北两排 / 东西两列)从局部偏移反推,和计划的 rot 对不上就返回空,
|
||
让这三件回到 `CTRL` 区由排布器摆(下一趟 `--dump` + `--push` 自动归位)。
|
||
"""
|
||
lp = u4_local_pins()
|
||
if not lp or 'U4' not in pos:
|
||
return {}
|
||
ux, uy, urot = pos['U4']
|
||
rows = len(set(round(v[1], 2) for v in lp.values())) == 2 # 南北两排
|
||
if rows != (urot % 180 == 0):
|
||
print(u' [!] _blade_pads.json 里 U4 还是上一版的朝向,C40/R40/R41 先交给排布器;'
|
||
u'跑一次 pcb_route.py --dump 再 --push 就归位')
|
||
return {}
|
||
ROW_PITCH = 2.2
|
||
sz = sz or sizes()
|
||
half_u = (BODY['U4'][1] if rows else BODY['U4'][0]) / 2.0
|
||
# ⛔ 2026-09-11 晚:上拉电阻按**焊盘网名**反查,不写死位号 —— 原来写的 R41→p23(SDA)、R40→p22(SCL)
|
||
# 正好反了(R40 才是 SDA 上拉),两根在 U4 脚下交叉,服务器报「SDA: U4.23 出不来」。
|
||
netset = {}
|
||
for row in json.load(io.open(PADS_JSON, encoding='utf-8')):
|
||
netset.setdefault(row[0], set()).add(row[7])
|
||
pull = {n: r for r in ('R40', 'R41') for n in netset.get(r, ()) if n in ('SDA', 'SCL')}
|
||
items = [('C40', '24'), (pull.get('SDA', 'R40'), '23'), (pull.get('SCL', 'R41'), '22')] # 顺序 = 脚的顺序
|
||
pins = [lp[q] for _r, q in items if q in lp]
|
||
if len(pins) != len(items):
|
||
return {}
|
||
out = {}
|
||
# 整排放在这几只脚所在的那一侧外面;件转 90°(长边垂直于本体边),半高 0.67
|
||
side = 1.0 if (pins[0][1] if rows else pins[0][0]) > 0 else -1.0
|
||
# ⚠ 半高要按**转 90° 之后**的 bbox 取(0402 转 90 是 0.54 宽 × 1.43 高,半高 0.715)——
|
||
# 写死 0.67 的那一版差 0.045 mm,自检报「R41 和 U4 重叠」。
|
||
half_sat = max(wh(r, 90 if rows else 0, sz)[1 if rows else 0]
|
||
for r, _q in (('C40', '24'), ('R41', '23'), ('R40', '22'))) / 2.0
|
||
# ⚠ 试过「贴引脚排而不是本体框」(想把这三件塞到 U4 南边、脚的正下方):**不行**。
|
||
# TSSOP 的焊盘本身长 ~1.5 mm,脚心 ±3.02 加上焊盘就是 ±3.8,再加净距和件的半高,
|
||
# 南边那一排会落到 y≈11.3 —— 而金手指禁布区上沿是 11.0,等于贴着禁区边缘,
|
||
# 自检直接报「和 U4 重叠」。⇒ 仍按**本体框**让开,靠下面的退路走西边。
|
||
off = half_u + GAP + half_sat
|
||
ctr = sum((q[0] if rows else q[1]) for q in pins) / len(pins)
|
||
# 首选:贴着这三只脚所在的那条边,排成一排。
|
||
cand = []
|
||
for k, (ref, _q) in enumerate(items):
|
||
t = ctr + (k - (len(items) - 1) / 2.0) * ROW_PITCH
|
||
# ⚠ 2026-09-11 晚:pad1(SDA / SCL / +3V3 那只)要**朝着 U4**:排在 U4 南边用 rot 270、北边 rot 90。
|
||
# 原来一律 rot 90,排到南边时 +3V3 那只盘挡在 U4.23 和 R40.1 之间,服务器报「SDA: U4.23 出不来」。
|
||
cand.append((ref, (round(ux + t, 3), round(uy + side * off, 3), 90 if side > 0 else 270) if rows
|
||
else (round(ux + side * off, 3), round(uy + t, 3), 0)))
|
||
# ⛔ 但这一排可能落进禁区:U4 挪到左下角之后,`p13~p24` 那条边朝南,
|
||
# 往南 5.3 mm 就是**金手指区**(y < 11,插槽体要夹住)。
|
||
# ⇒ 退到**垂直的那条边**(离板边近的一侧),y 对齐引脚排,横着堆。
|
||
lim = B.ZONES['finger'][3] + 0.3
|
||
if rows and min(c[1][1] for c in cand) - 0.72 < lim:
|
||
sx = -1.0 if ux < B.BW / 2.0 else 1.0
|
||
py = uy + (pins[0][1] if rows else 0.0) # 引脚排的 y
|
||
cand = [(ref, (round(ux + sx * off, 3),
|
||
round(py + k * ROW_PITCH, 3), 0))
|
||
for k, (ref, _q) in enumerate(items)]
|
||
return dict(cand)
|
||
|
||
|
||
# ── M7:按连线就近落位(货架排完之后,在**同尺寸**的位置之间重排)────────
|
||
PADS_JSON = os.path.join(HERE, '_blade_pads.json')
|
||
SNAP_JSON = os.path.join(HERE, '_blade_pad_offsets.json')
|
||
GRID = 0.25 # 和 router.py 的网格一致
|
||
# 平面网 / 巨网:不参与「往哪儿靠」的计算
|
||
# GND / VO_P / +12V 走铜面(`router.py` 的 SKIP / PLANE_VIA_NETS / ISLAND_NETS),
|
||
# 把它们算进去等于把所有件都往铜面中心拽,没有意义。
|
||
PLANE_NETS = ('GND', 'VO_P', '+12V', '+12V_F')
|
||
|
||
|
||
def size_class(wh_):
|
||
u"""重排时哪些件可以互换位置。
|
||
|
||
⛔ 不能按「尺寸完全相同」分类:`C14`(1.88×0.54) 和 `R12`(1.43×0.54) 差 0.45 mm
|
||
就换不了位置了,于是 `U2` 东侧那一列排成 p17→p19→p18→p20,两根线白白交叉一次。
|
||
⇒ 只要都是**小贴片**(≤ 2.5 × 1.5)就归一类:货架格子是按 1.8 的节距留的
|
||
(1.43 + 1.8 = 3.23),塞个 1.88 的件进去仍有 1.35 的富余,
|
||
而真挤到了自检那一关(同面重叠,净距 0.6)会拦下来。
|
||
"""
|
||
w, h = wh_
|
||
if w <= 2.5 and h <= 1.5:
|
||
return 'SMALL'
|
||
if h <= 2.5 and w <= 1.5:
|
||
return 'SMALL90'
|
||
return wh_
|
||
|
||
|
||
def net_peers():
|
||
u"""{位号: [(网名, [(对端位号, 焊盘局部偏移 dx, dy), ...])]},从 `_blade_pads.json` 反查。
|
||
|
||
⚠ 存的是**对端焊盘相对它自己本体中心的偏移**,不是绝对坐标 —— 焊盘表是上一次
|
||
下发之后 dump 的,绝对坐标会过期;局部偏移只跟封装和旋转角有关。本体中心用
|
||
该件所有焊盘的**质心**近似(两排对称的封装上就是精确值)。
|
||
|
||
⛔ 为什么非要精确到**焊盘**:按「器件中心」算目标点的话,`U2` 周围那 12 个外围件
|
||
的目标点全是同一个点,谁摆哪儿都一样 —— 排出来就是「没摆顺」:接东边
|
||
`p17~p20` 的件被扔到西边去,线要绕整颗芯片一圈。精确到焊盘之后
|
||
`R11`→`p17`、`C14`→`p18`、`R12`→`p19`、`R13`→`p20`,一列排下来顺序自然就对。
|
||
"""
|
||
if not os.path.exists(PADS_JSON):
|
||
return {}
|
||
d = json.load(io.open(PADS_JSON, encoding='utf-8'))
|
||
cen = {}
|
||
for row in d:
|
||
cen.setdefault(row[0], []).append((row[2], row[3]))
|
||
npin = {r: len(ps) for r, ps in cen.items()}
|
||
cen = {r: (sum(p[0] for p in ps) / len(ps), sum(p[1] for p in ps) / len(ps))
|
||
for r, ps in cen.items()}
|
||
bynet = {}
|
||
for row in d:
|
||
ref, net = row[0], row[7]
|
||
if not net or net in PLANE_NETS:
|
||
continue
|
||
cx, cy = cen[ref]
|
||
bynet.setdefault(net, []).append((ref, round(row[2] - cx, 3), round(row[3] - cy, 3),
|
||
npin[ref]))
|
||
out = {}
|
||
for net, items in bynet.items():
|
||
for r in set(it[0] for it in items):
|
||
others = [it for it in items if it[0] != r]
|
||
if others:
|
||
out.setdefault(r, []).append((net, others))
|
||
return out
|
||
|
||
|
||
def target_point(ref, peers, pos, ref_pt=None):
|
||
u"""这个件「该往哪儿去」:每个网取**离参考点最近的那个对端焊盘**,再按网平均。
|
||
|
||
对端焊盘位置 = 对端件在本次方案里的中心 + 它的焊盘局部偏移 ⇒ 不吃过期坐标。
|
||
|
||
⛔ 不用「同网所有对端焊盘的质心」:`C66` 是 `U5` 的 `+5V` 输入电容,而 `+5V` 这个网上
|
||
还挂着金手指 `J1.A19/A32`(在板子最南边)—— 质心被拽到 10 mm 外,
|
||
排出来「去耦离电源脚 9.9 mm」。取**最近的那一个**才是人排板的做法:
|
||
旁路电容跟着芯片的那只脚走,不管这个网还去了哪儿。
|
||
|
||
`ref_pt` 是「从哪儿看最近」,一般给所在区的中心;不给就用件当前位置(若已摆)。
|
||
"""
|
||
if ref not in peers:
|
||
return None
|
||
if ref_pt is None:
|
||
ref_pt = pos.get(ref, (0.0, 0.0))[:2]
|
||
xs, ys = [], []
|
||
# ⛔ 2026-09-11 晚:上拉 / 分压这类件只要还挂着别的信号网,就**不按 +3V3 找去向** —— +3V3 哪儿都有,
|
||
# 按它的最近焊盘算只会把件拽走(R15 = EFUSE_PG 的上拉,被 U3.6 拽到 U2 东北,EFUSE_PG 和 $1N164 在 U2 角上交叉)。
|
||
# 纯去耦(+3V3 + GND)照旧跟着 +3V3 走。
|
||
nets_here = [n for n, _o in peers[ref]]
|
||
skip_3v3 = any(n != '+3V3' for n in nets_here)
|
||
for _net, others in peers[ref]:
|
||
if skip_3v3 and _net == '+3V3':
|
||
continue
|
||
cand = [(pos[q][0] + dx, pos[q][1] + dy, n) for q, dx, dy, n in others if q in pos]
|
||
if not cand:
|
||
continue
|
||
# ⛔ 有芯片对端时**只认芯片**:`C11` 是 `U1` 的补偿电容,同网还挂着 `R6`,
|
||
# 按「最近的对端」会跟着 `R6` 跑到 9 mm 外。旁路 / 补偿件要跟着**芯片的那只脚**。
|
||
ic = [c for c in cand if c[2] >= 4]
|
||
pick = ic or cand
|
||
q = min(pick, key=lambda c: math.hypot(c[0] - ref_pt[0], c[1] - ref_pt[1]))
|
||
xs.append(q[0])
|
||
ys.append(q[1])
|
||
if not xs:
|
||
return None
|
||
return (sum(xs) / len(xs), sum(ys) / len(ys))
|
||
|
||
|
||
CROSS_PEN = 6.0 # 每交叉一次折合多少 mm 的距离代价(reassign)
|
||
PLACE_SYNTH = os.path.join(HERE, '_blade_place_synth.json') # 焊盘表是按哪一版摆位做的(synth_pads 每趟写)
|
||
|
||
|
||
def flip_2pin(pos, slots, peers):
|
||
u"""排布器摆的两脚件在 rot 0 / 180 之间挑:**接芯片的那只盘朝着芯片**。
|
||
|
||
⛔ 2026-09-11 晚:排布器一律 rot 0,从不看哪只盘接芯片。服务器第十轮最后一个断网 `$1N158`:
|
||
C14 摆在 U2 西边,GND 盘朝芯片、$1N158 盘在远端,U2.18 的线要绕整颗电容(上拉 R40 早先同一个毛病,
|
||
那次是 ctrl_hand 手排的)。只挑 0 / 180(90 / 270 会改 bbox,货架位是按 rot 0 留的)。
|
||
焊盘局部偏移取自 `_blade_pads.json`,它是按 `PLACE_SYNTH`(下发后 = 板上)那一版的 rot 做的 ⇒ 以它为基准翻。
|
||
"""
|
||
if not os.path.exists(PADS_JSON) or not os.path.exists(PLACE_SYNTH):
|
||
return 0
|
||
prev = json.load(io.open(PLACE_SYNTH, encoding='utf-8'))
|
||
byref = {}
|
||
for row in json.load(io.open(PADS_JSON, encoding='utf-8')):
|
||
byref.setdefault(row[0], []).append(row)
|
||
n = 0
|
||
for ref in sorted(r for items in slots.values() for r, _xy in items):
|
||
ps = byref.get(ref, [])
|
||
if len(ps) != 2 or ref not in prev:
|
||
continue
|
||
px, py, prot = prev[ref]
|
||
offs = [(q[2] - px, q[3] - py, q[7]) for q in ps]
|
||
x, y = pos[ref][:2]
|
||
pins = {net: (ix, iy) for net, ix, iy in ic_pins(ref, peers, pos)}
|
||
|
||
def cost(sign):
|
||
return sum(math.hypot(x + sign * dx - pins[net][0], y + sign * dy - pins[net][1])
|
||
for dx, dy, net in offs if net in pins)
|
||
rot = (prot + 180) % 360 if cost(-1) < cost(1) - 0.05 else prot
|
||
if rot != pos[ref][2]:
|
||
n += 1
|
||
pos[ref] = (x, y, rot)
|
||
return n
|
||
|
||
|
||
def ic_pins(ref, peers, pos):
|
||
u"""这件和芯片(≥ 4 脚)相连的那几只脚 [(网, x, y)]:每个网取离本件最近的芯片焊盘。平面网 / +3V3 不算。"""
|
||
out = []
|
||
if ref not in peers or ref not in pos:
|
||
return out
|
||
x0, y0 = pos[ref][:2]
|
||
for net, others in peers[ref]:
|
||
if net == '+3V3':
|
||
continue
|
||
ic = [(pos[q][0] + dx, pos[q][1] + dy) for q, dx, dy, n in others if q in pos and n >= 4]
|
||
if ic:
|
||
px, py = min(ic, key=lambda p: math.hypot(p[0] - x0, p[1] - y0))
|
||
out.append((net, px, py))
|
||
return out
|
||
|
||
|
||
def sat_lines(ref, peers, pos):
|
||
u"""已摆好的件的「芯片脚 → 本件」连线(只算两脚 / 三脚小件)。"""
|
||
if ref not in pos or ref not in peers or len(peers[ref]) > 3:
|
||
return []
|
||
return [(net, px, py, pos[ref][0], pos[ref][1]) for (net, px, py) in ic_pins(ref, peers, pos)]
|
||
|
||
|
||
def _cross(a, b):
|
||
u"""两条线段是否**真**相交(共端点 / 同网不算)。"""
|
||
if a[0] == b[0]:
|
||
return False
|
||
(x1, y1, x2, y2), (x3, y3, x4, y4) = a[1:], b[1:]
|
||
|
||
def orient(ax, ay, bx, by, cx, cy):
|
||
v = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
|
||
return 0 if abs(v) < 1e-9 else (1 if v > 0 else -1)
|
||
if (abs(x1 - x3) < 1e-6 and abs(y1 - y3) < 1e-6) or (abs(x2 - x4) < 1e-6 and abs(y2 - y4) < 1e-6):
|
||
return False
|
||
return (orient(x1, y1, x2, y2, x3, y3) * orient(x1, y1, x2, y2, x4, y4) < 0 and
|
||
orient(x3, y3, x4, y4, x1, y1) * orient(x3, y3, x4, y4, x2, y2) < 0)
|
||
|
||
|
||
def crossings(lines, fixed=()):
|
||
n = 0
|
||
for i in range(len(lines)):
|
||
for j in range(i + 1, len(lines)):
|
||
n += _cross(lines[i], lines[j])
|
||
for f in fixed:
|
||
n += _cross(lines[i], f)
|
||
return n
|
||
|
||
|
||
def reassign(pos, slots, peers):
|
||
u"""在**同一区、同一尺寸**的一组货架位置之间重排,让每件离自己的对端更近。
|
||
|
||
先贪心(按「最想去的位置」排序取),再做 2-opt 交换到不能再降。
|
||
位置只在同尺寸之间换 —— 换了尺寸货架占位就错了,而**几何自检抓不到**
|
||
(它只看重叠,不看这个位置当初是给谁留的)。
|
||
"""
|
||
moved = 0
|
||
for (region, cls), items in sorted(slots.items(), key=lambda kv: (kv[0][0], str(kv[0][1]))):
|
||
if len(items) < 2:
|
||
continue
|
||
refs = [r for r, _xy in items]
|
||
pts = [xy for _r, xy in items]
|
||
tgt = {r: target_point(r, peers, pos) for r in refs}
|
||
if not any(tgt.values()):
|
||
continue
|
||
# ⛔ 2026-09-11 晚:代价里加「芯片脚 → 外围件」连线的**交叉数**。只按距离排,U2 西北角
|
||
# EFUSE_PG(p2) 的对端比 $1N164(p3) 的对端更靠东北,两根一出脚就交叉,服务器五轮都是
|
||
# U2.2 / U2.15 出不来(换布线顺序只是换一只断)。别的组已摆好的连线当固定障碍一起算。
|
||
anchors_ = {r: ic_pins(r, peers, pos) for r in refs}
|
||
fixed_lines = [ln for r2 in pos if r2 not in anchors_ for ln in sat_lines(r2, peers, pos)]
|
||
|
||
def cost(assign):
|
||
c = 0.0
|
||
lines = []
|
||
for r, k in assign.items():
|
||
t = tgt.get(r)
|
||
if t:
|
||
c += math.hypot(pts[k][0] - t[0], pts[k][1] - t[1])
|
||
for (net, px, py) in anchors_[r]:
|
||
lines.append((net, px, py, pts[k][0], pts[k][1]))
|
||
c += CROSS_PEN * crossings(lines, fixed_lines)
|
||
return c
|
||
|
||
order = sorted(range(len(refs)), key=lambda i: -(1 if tgt[refs[i]] else 0))
|
||
assign, used = {}, set()
|
||
for i in order:
|
||
r = refs[i]
|
||
t = tgt.get(r)
|
||
cand = sorted((k for k in range(len(pts)) if k not in used),
|
||
key=lambda k: (math.hypot(pts[k][0] - t[0], pts[k][1] - t[1])
|
||
if t else k))
|
||
assign[r] = cand[0]
|
||
used.add(cand[0])
|
||
improved = True
|
||
while improved:
|
||
improved = False
|
||
for a in range(len(refs)):
|
||
for b in range(a + 1, len(refs)):
|
||
ra, rb = refs[a], refs[b]
|
||
c0 = cost(assign)
|
||
assign[ra], assign[rb] = assign[rb], assign[ra]
|
||
if cost(assign) < c0 - 1e-9:
|
||
improved = True
|
||
else:
|
||
assign[ra], assign[rb] = assign[rb], assign[ra]
|
||
for r, k in assign.items():
|
||
if pos[r][:2] != pts[k]:
|
||
moved += 1
|
||
pos[r] = (pts[k][0], pts[k][1], pos[r][2])
|
||
return moved
|
||
|
||
|
||
def grid_snap(pos, only=None):
|
||
u"""把货架排布器摆的件各挪 ≤ ±0.125 mm,让焊盘尽量落在 **0.25 mm 布线网格**上。
|
||
|
||
`router.py` 是 0.25 网格 A*,焊盘不在格点上就要先长一段**引出脚**;
|
||
M6 欠的 5 只脚全是「出口被自家邻脚的引出脚封死」(方案 §26.6)。
|
||
焊盘落到格点上,这一类堵塞就没有了。
|
||
|
||
⚠ 一个件只有一个 (dx, dy):**只有焊盘间距是 0.25 的整数倍时才能全对齐**
|
||
(0402 两脚共线 ⇒ 能对齐一只;QFN 0.5 ⇒ 整排都能;TSSOP 0.65 / BM24 0.4 ⇒ 不能)。
|
||
判据是「对齐数变多」,不是「全对齐」。
|
||
|
||
⛔ 手排件(功率链 `STACK_*` / `HAND`)和机械锚定件不许动:
|
||
前者的净距是按 §7.2 的回路算死的(Cin 堆只有 0.65),两件相向挪 0.125 就破 0.6 的装配下限;
|
||
后者的位置是小米8 主板定的。
|
||
"""
|
||
if not os.path.exists(SNAP_JSON):
|
||
return 0
|
||
offs = json.load(io.open(SNAP_JSON, encoding='utf-8'))
|
||
n = 0
|
||
for ref, po in offs.items():
|
||
if ref not in pos or (only is not None and ref not in only):
|
||
continue
|
||
x, y, r = pos[ref]
|
||
|
||
def best(vals):
|
||
bd, bs = 0.0, None
|
||
for k in range(-125, 126):
|
||
d = k / 1000.0
|
||
sc = sum(abs((v + d) / GRID - round((v + d) / GRID)) for v in vals)
|
||
if bs is None or sc < bs - 1e-9:
|
||
bd, bs = d, sc
|
||
return bd
|
||
|
||
dx = best([x + o[0] for o in po])
|
||
dy = best([y + o[1] for o in po])
|
||
if dx or dy:
|
||
pos[ref] = (round(x + dx, 3), round(y + dy, 3), r)
|
||
n += 1
|
||
return n
|
||
|
||
|
||
def snap_conflicts(pos, sz):
|
||
u"""网格微调之后,哪些件把净距挤到了 `GAP` 以下 —— 这些退回原位。
|
||
|
||
⛔ 不能一刀切「手排件不许动」:`U2` / `U1` 这些 0.5 间距的 QFN 恰恰**最需要**
|
||
落在格点上。也不能一刀切「都动」:Cin 堆的缝只有 0.65,两件相向挪 0.125 就破 0.6。
|
||
⇒ 挪完再算一遍,只把真挤到的退回去。
|
||
"""
|
||
bad = set()
|
||
items = sorted(pos.items())
|
||
rect = {}
|
||
for ref, (x, y, r) in items:
|
||
w, h = wh(ref, r, sz)
|
||
rect[ref] = (x - w / 2, y - h / 2, x + w / 2, y + h / 2)
|
||
for i in range(len(items)):
|
||
for j in range(i + 1, len(items)):
|
||
a, b = items[i][0], items[j][0]
|
||
ra, rb = rect[a], rect[b]
|
||
dx = max(ra[0] - rb[2], rb[0] - ra[2])
|
||
dy = max(ra[1] - rb[3], rb[1] - ra[3])
|
||
if dx < GAP and dy < GAP and (dx < 0 or dy < 0 or True):
|
||
if max(dx, dy) < GAP:
|
||
bad.add(a)
|
||
bad.add(b)
|
||
return bad
|
||
|
||
|
||
def bands():
|
||
u"""每个组占的 y 带 —— L3 切铜面按这个来,别手抄坐标。"""
|
||
pos, _r, _u, sz = plan()
|
||
out = []
|
||
for region, name, refs in GROUPS:
|
||
ys = [(pos[r][1] - wh(r, pos[r][2], sz)[1] / 2,
|
||
pos[r][1] + wh(r, pos[r][2], sz)[1] / 2) for r in refs if r in pos]
|
||
if ys:
|
||
out.append((region, name, round(min(a for a, _ in ys), 2),
|
||
round(max(b for _, b in ys), 2)))
|
||
return out
|
||
|
||
|
||
def selfcheck(pos, rects, unplaced, sz):
|
||
bad = []
|
||
if unplaced:
|
||
bad.append(u'放不下: %s' % ', '.join(u'%s(%s)' % t for t in unplaced))
|
||
d = json.load(io.open(BBOX, encoding='utf-8'))
|
||
missing = sorted(set(d) - set(pos))
|
||
if missing:
|
||
bad.append(u'没安排位置: %s' % ' '.join(missing))
|
||
bot = set(bottom_refs(pos))
|
||
# 重叠。⚠ K1~K4 例外:四个盘的位置是**主板定的**(实测最小间距 1.78 mm、
|
||
# 盘 Ø1.5 ⇒ 净距 0.28),通用的 GAP=0.6 会把它们互判成重叠。
|
||
# 它们之间改按「铜净距 ≥ 0.2」单独校核。
|
||
# ⚠ **只判同一面**:A 面的 J4/J5/C60 和 B 面的 USB/阻尼组是**故意重叠**的
|
||
# (正背面),跨面判重叠会把这一版的核心设计意图全报成错。
|
||
import math
|
||
keys = ['K1', 'K2', 'K3', 'K4']
|
||
rmap = {}
|
||
for ref in pos:
|
||
if ref in keys:
|
||
continue
|
||
x, y, rr = pos[ref]
|
||
w, h = wh(ref, rr, sz)
|
||
rmap[ref] = (x - w / 2 - GAP / 2, y - h / 2 - GAP / 2,
|
||
x + w / 2 + GAP / 2, y + h / 2 + GAP / 2)
|
||
rl = sorted(rmap.items())
|
||
for i in range(len(rl)):
|
||
for j in range(i + 1, len(rl)):
|
||
if (rl[i][0] in bot) != (rl[j][0] in bot):
|
||
continue
|
||
a, b = rl[i][1], rl[j][1]
|
||
if a[0] < b[2] - 1e-6 and b[0] < a[2] - 1e-6 and \
|
||
a[1] < b[3] - 1e-6 and b[1] < a[3] - 1e-6:
|
||
bad.append(u'%s 和 %s 重叠' % (rl[i][0], rl[j][0]))
|
||
PAD_D = 1.5
|
||
for i in range(len(keys)):
|
||
for j in range(i + 1, len(keys)):
|
||
ax, ay, _ = pos[keys[i]]
|
||
bx, by, _ = pos[keys[j]]
|
||
gap = math.hypot(ax - bx, ay - by) - PAD_D
|
||
if gap < 0.2:
|
||
bad.append(u'%s/%s 铜净距 %.2f < 0.2' % (keys[i], keys[j], gap))
|
||
# 全部在板内
|
||
for ref, (x, y, rr) in pos.items():
|
||
w, h = wh(ref, rr, sz)
|
||
if ref == 'J1':
|
||
continue
|
||
if x - w / 2 < 0.5 or x + w / 2 > B.BW - 0.5 or \
|
||
y - h / 2 < 0.5 or y + h / 2 > B.BH - 0.5:
|
||
bad.append(u'%s 出板框: (%.2f,%.2f) %.1fx%.1f' % (ref, x, y, w, h))
|
||
# 金手指区(y<11)除 J1 外一件不许有 —— 两面都是(插槽体夹住的是整块板)
|
||
for ref, (x, y, rr) in pos.items():
|
||
if ref == 'J1':
|
||
continue
|
||
if y - wh(ref, rr, sz)[1] / 2 < 11.0:
|
||
bad.append(u'%s 侵入金手指区(y=%.2f,插槽体要 ~9 mm)' % (ref, y))
|
||
# ⛔ 铣穿的排线槽:**两面都不许有件**(摆进去等于摆在空气里);
|
||
# B 面的排线车道也一件不放(排线要平贴着走到连接器)。
|
||
for ref, (x, y, rr) in pos.items():
|
||
w, h = wh(ref, rr, sz)
|
||
for (a, b, c, d2) in B.mill_cuts():
|
||
if x + w / 2 > a - MILL_KEEP and x - w / 2 < c + MILL_KEEP and \
|
||
y + h / 2 > b - MILL_KEEP and y - h / 2 < d2 + MILL_KEEP:
|
||
bad.append(u'%s 落进铣穿的排线槽(那里没有板子)' % ref)
|
||
break
|
||
if ref in bot and ref not in ('J4', 'J5'):
|
||
for k, (a, b, c, d2) in lanes().items():
|
||
if x + w / 2 > a and x - w / 2 < c and y + h / 2 > b and y - h / 2 < d2:
|
||
bad.append(u'%s 压在 %s 排线车道上' % (ref, k))
|
||
break
|
||
# A 面的手机投影里只许 K1~K4(螺柱间隙只有 1.0 mm)。
|
||
# ⚠ **B 面不受这一条约束** —— 这正是这一版把器件翻过去的原因。
|
||
px0, py0 = PHONE_ORG
|
||
px1, py1 = px0 + PHONE_WH[0], py0 + PHONE_WH[1]
|
||
for ref, (x, y, rr) in pos.items():
|
||
if ref.startswith('K') or ref in bot:
|
||
continue
|
||
w, h = wh(ref, rr, sz)
|
||
if x + w / 2 > px0 and x - w / 2 < px1 and y + h / 2 > py0 and y - h / 2 < py1:
|
||
bad.append(u'%s 在 A 面落进手机投影(螺柱间隙只有 1.0 mm)' % ref)
|
||
# ⛔ B 面高度预算分两段(模块头的表)。超高件进手机投影会顶到隔壁槽的手机主板,
|
||
# 而这件事几何自检和 DRC **一条都不报**。
|
||
for ref, hh in HEIGHT.items():
|
||
if ref not in bot:
|
||
continue
|
||
x, y, rr = pos[ref]
|
||
w, h = wh(ref, rr, sz)
|
||
# ⚠ 2026-09-11 晚:服务带到了顶部,手机投影上下两边都要判
|
||
in_phone = y + h / 2 > py0 and y - h / 2 < py1 and x + w / 2 > px0 and x - w / 2 < px1
|
||
lim = H_B_PHONE if in_phone else H_B_SVC
|
||
if hh > lim:
|
||
bad.append(u'%s 高 %.1f mm 放在 B 面%s(净空只有 %.1f)'
|
||
% (ref, hh, u'手机投影下' if in_phone else u'服务带', lim))
|
||
# ⛔ 螺钉头避让:任何元件离主板定位孔中心都要 >= HOLE_KEEP。
|
||
# M4c 第一版把这七个孔整个忘了 —— 高度预算和 K1~K4 的接触压力全靠它们。
|
||
# ⚠ 两面都要让:螺钉过孔是 NPTH,头在 B 面、尼龙立柱在 A 面。
|
||
for hname, (hx, hy) in B.phone_holes().items():
|
||
for ref, (x, y, rr) in pos.items():
|
||
w, h = wh(ref, rr, sz)
|
||
dx = max(abs(hx - x) - w / 2.0, 0.0)
|
||
dy = max(abs(hy - y) - h / 2.0, 0.0)
|
||
if math.hypot(dx, dy) < B.HOLE_KEEP:
|
||
bad.append(u'%s 离定位孔 %s 只有 %.2f < %.1f(螺钉头压上去)'
|
||
% (ref, hname, math.hypot(dx, dy), B.HOLE_KEEP))
|
||
# C60 不许在手机底下(已由上一条覆盖),另外校核它确实在服务带/功率列
|
||
cx, cy, _ = pos['C60']
|
||
sv = B.ZONES['service']
|
||
if not (sv[1] - 1e-9 <= cy - BODY['C60'][1] / 2
|
||
and cy + BODY['C60'][1] / 2 <= sv[3] - 0.5 + 1e-9):
|
||
bad.append(u'C60 不在服务带里(B 面只有服务带有 18.7 的净空)')
|
||
return bad
|
||
|
||
|
||
OPEN_JS = (u"await eda.dmt_EditorControl.openDocument('%s');"
|
||
u"await new Promise(r=>setTimeout(r,2500));")
|
||
|
||
# ⚠ **必须分批**:桥接服务端的单请求上限是**硬编码的 30 s**
|
||
# (`bridge-server.mjs` 的 `REQUEST_TIMEOUT_MS`,客户端 `timeout=` 调不动它)。
|
||
# 83 件 × 2 次 modify 一口气发过去必超时,而且**超时不等于没执行** ——
|
||
# 客户端还在跑,只是结果回不来,于是「读回校验」会看到做了一半。
|
||
PUSH_JS = u"""
|
||
const MIL = %s, P = %s, BOT = %s, ONLY = %s;
|
||
const bot = new Set(BOT), only = new Set(ONLY);
|
||
let n = 0, miss = [];
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
|
||
const d = c.designator;
|
||
if (!only.has(d)) continue;
|
||
if (!P[d]) { miss.push(d); continue; }
|
||
const id = c.getState_PrimitiveId();
|
||
await eda.pcb_PrimitiveComponent.modify(id, {layer: bot.has(d) ? 2 : 1});
|
||
await eda.pcb_PrimitiveComponent.modify(id,
|
||
{x: P[d][0]*MIL, y: P[d][1]*MIL, rotation: P[d][2]});
|
||
n++;
|
||
}
|
||
await eda.pcb_Document.save();
|
||
return {n, miss};
|
||
"""
|
||
|
||
BATCH = 12
|
||
|
||
|
||
def push(pos):
|
||
r = run(OPEN_JS % PCB + 'return 1;', timeout=120)
|
||
if not r.get('success'):
|
||
sys.exit(json.dumps(r, ensure_ascii=False)[:600])
|
||
bot = json.dumps(bottom_refs(pos))
|
||
refs = sorted(pos)
|
||
done, miss = 0, []
|
||
for k in range(0, len(refs), BATCH):
|
||
chunk = refs[k:k + BATCH]
|
||
r = run(PUSH_JS % (MIL, json.dumps(pos), bot, json.dumps(chunk)), timeout=120)
|
||
if not r.get('success'):
|
||
sys.exit(json.dumps(r, ensure_ascii=False)[:600])
|
||
done += r['result']['n']
|
||
miss += r['result']['miss']
|
||
print(u' %d/%d' % (done, len(refs)))
|
||
print(u'已摆 %d 件;没找到位置的: %s' % (done, miss or u'无'))
|
||
|
||
|
||
def verify(pos, sz):
|
||
r = run("await eda.dmt_EditorControl.openDocument('%s');"
|
||
"await new Promise(r=>setTimeout(r,2000));"
|
||
"return (await eda.pcb_PrimitiveComponent.getAll()).map("
|
||
" c=>[c.designator, c.x, c.y, c.rotation, c.layer]);" % PCB, timeout=300)
|
||
if not r.get('success'):
|
||
sys.exit(json.dumps(r, ensure_ascii=False)[:600])
|
||
got = {a[0]: (a[1] / MIL, a[2] / MIL, a[3], a[4]) for a in r['result']}
|
||
bot = set(bottom_refs(pos))
|
||
bad = []
|
||
for ref, (x, y, rot) in pos.items():
|
||
if ref not in got:
|
||
bad.append(u'板上没有 %s' % ref)
|
||
continue
|
||
gx, gy, grot, glay = got[ref]
|
||
if abs(gx - x) > 0.02 or abs(gy - y) > 0.02:
|
||
bad.append(u'%s 在 (%.2f,%.2f),应为 (%.2f,%.2f)' % (ref, gx, gy, x, y))
|
||
want_lay = 2 if ref in bot else 1
|
||
if glay != want_lay:
|
||
bad.append(u'%s 在 layer %s,应为 %s' % (ref, glay, want_lay))
|
||
print(u'读回 %d 件(A 面 %d / B 面 %d)'
|
||
% (len(got), sum(1 for v in got.values() if v[3] == 1),
|
||
sum(1 for v in got.values() if v[3] == 2)))
|
||
if bad:
|
||
print(u'[X] %d 条不过:' % len(bad))
|
||
for b in bad[:20]:
|
||
print(u' ' + b)
|
||
else:
|
||
print(u'[OK] %d 件位置 / 层 全部对上' % len(got))
|
||
return bad
|
||
|
||
|
||
if __name__ == '__main__':
|
||
pos, rects, unplaced, sz = plan()
|
||
bad = selfcheck(pos, rects, unplaced, sz)
|
||
bot = set(bottom_refs(pos))
|
||
print(u'安排了 %d 件(含 J1):A 面 %d / B 面 %d'
|
||
% (len(pos), len(pos) - len(bot), len(bot)))
|
||
for region, name, refs in GROUPS:
|
||
xs = [pos[r] for r in refs if r in pos]
|
||
if xs:
|
||
print(u' %-11s %-24s x %5.1f~%5.1f y %5.1f~%5.1f %d 件'
|
||
% (region, name, min(p[0] for p in xs), max(p[0] for p in xs),
|
||
min(p[1] for p in xs), max(p[1] for p in xs), len(xs)))
|
||
print(u' A 面留守: ' + ' '.join(u'%s(%.1f,%.1f)' % (k, pos[k][0], pos[k][1])
|
||
for k in TOP_ONLY))
|
||
if bad:
|
||
print(u'[X] 自检不过 (%d):' % len(bad))
|
||
for b in bad[:20]:
|
||
print(u' ' + b)
|
||
if '--push' in sys.argv:
|
||
sys.exit(1)
|
||
else:
|
||
print(u'[OK] 自检通过:同面无重叠 / 全在板内 / 金手指区干净 / 槽与车道内无件 /'
|
||
u' A 面只有 K1~K4 / B 面高度分段达标 / 七个螺钉头都让开了')
|
||
if '--push' in sys.argv:
|
||
push(pos)
|
||
verify(pos, sz)
|
||
elif '--verify' in sys.argv:
|
||
verify(pos, sz)
|