R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 cd01640c40
当前提交 cf7cc9cf90
共修改 759 个文件,包含 76962 行新增和 5299 行删除
+272 -57
查看文件
@@ -5,6 +5,13 @@ u"""R2 刀片 PCB 摆位(M4c-2;2026-09-08 改成**双面**:器件在手机
/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` 四个裸铜盘**(主板弹片直接压,
@@ -112,7 +119,16 @@ PACK_GAP_BY_REGION = {
}
MILL_KEEP = 0.5 # 器件离铣穿内槽的净距
# 排线在 B 面占的车道:从槽顶到连接器顶沿,宽 = 排线宽 + 2×0.5。**一件不放**(排线要平贴)。
LANE_TOP = {'BAT': 34.5, 'TAIL': 35.0}
# ⛔ 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/侧键都在上面)朝下**。
@@ -136,7 +152,6 @@ TOP_ONLY = ['J1', 'K1', 'K2', 'K3', 'K4']
# 隔壁槽的手机主板,而**几何自检和 DRC 一条都不报**。
HEIGHT = {
'C60': 10.5, # Ø10.3 铝电解(T2 实测)
'J2': 5.0, # 1.25 mm 4P 线座(估)
'J3': 5.0, # 1.25 mm 2P 线座(估)
'L1': 3.0, # SPM6530T
}
@@ -145,15 +160,15 @@ H_B_SVC = 20.32 - 1.6 # B 面服务带的净空(对面 A 面
def phone(u, v):
return (round(PHONE_ORG[0] + u, 3), round(PHONE_ORG[1] + v, 3))
return B.phone_xy(u, v)
def lanes():
u"""排线在 B 面的车道 {名字: (x0, y0, x1, y1)}:槽顶 → 连接器顶沿,一件不放。"""
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), B.SLOT_Y[1], round(cx + hw, 3), LANE_TOP[k])
out[k] = (round(cx - hw, 3), LANE_BOT[k], round(cx + hw, 3), B.SLOT_Y[0])
return out
@@ -170,26 +185,39 @@ def anchors():
# ── 电池排线焊盘 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**(见模块头)。
a['J5'] = (fx['BAT'], 32.0, 0)
# ── BM24 尾插座 J4:B 面、尾插槽正北。2.48 + 5 + 2.7 + 1.6 + 6.7 ≈ 18.5 ≤ 50(实长 55)。
# 本体 2.6 高 → 31.7~34.3;H4 (44.9,30.9) 在西边 5.4 mm 外。**底层 rot 0**。
a['J4'] = (fx['TAIL'], 33.0, 0)
# ── C60:Ø10.3 / 高 10.5,只能在服务带(B 面净空 18.7)。夹在两条槽之间:
# 电池槽东沿 32.77 / 尾插槽西沿 46.58,本体 11 → 中心 40.0,西余 1.7 / 东余 1.1。
# 顶 24.0 离槽底 24.8 有 0.8 ≥ MILL_KEEP。
a['C60'] = (40.0, 18.5, 0)
# ── J2:D± 退路座,靠金手指 D± 针(B14/B15,x≈62/63);下沿 12.6 让开 LAUNCH_Y 11.75。
# 竖放沿用 2026-09-09 的决定,出线朝西(服务带里 DIS 与 J2 之间是空的)。
# ⚠ 底层是「先 y 镜像再转」:读回实测 rot 90 把信号盘放到了**东**边(63.25),
# rot 270 才回到西边(60.35)—— 本项目约定出线 = 信号盘那一侧(§25.x J2 的记法)。
# 本体 5.2×10.8 → x 59.2~64.4 / y 12.6~23.4,顶离尾插槽 1.4。
a['J2'] = (61.8, 18.0, 270)
# ── J3:EDL 座。两根飞线要去主板朝上面的 TP1204/1205 (48.4/45.6, 34.6),
# 只能**跟着尾插排线穿尾插槽**(R1 同一做法),所以座子摆在尾插车道东侧的走廊里、
# 出线朝南对着槽:底层 rot 0 读回信号盘在南(y 镜像后),本体 8.35×5.2 →
# x 66.2~74.6 / y 30.4~35.6;离车道东沿 66.08 有 0.1,离 EFUSE_AUX 区 0.9。
# 高 5 < 手机投影下净空 10.1。⚠ 出线侧的约定见 J2 那条,装配时确认。
a['J3'] = (70.4, 33.0, 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
@@ -213,11 +241,28 @@ def anchors():
STACK_GAP = 0.65 # 手排竖直堆叠的件间净距
# (位号, rot, dx):dx = 相对功率列中心线 PWR_X 的横向偏移;y 由 stack() 自上而下算
PWR_X = 89.0
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_Y = 50.9 # ⚠ M7 从 49.5 抬到 50.9:U2 上下的缝各加了 0.8/0.6,
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。
@@ -252,12 +297,14 @@ STACK_DN = [ # U1 往下:电感 → 输出电容 → eFuse
HAND = {
# BOOT 电容:BOOT 脚(p13)在 U1 下边、SW 也在下边,但正下方被 L1 占满了。
# ⇒ 摆到 L1 左侧,`SW` 那一脚接 **L1 的 1 号焊盘**(同一个节点,不必绕回 U1)。
'C5': (PWR_X - 5.3, U1_Y - 6.3, 0),
# ⚠ 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_X + 3.45, U1_Y - 1.72, 0), # 和 p9 同一条水平线,2.0 mm
'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_X - 4.2, U1_Y + 3.1, 90),
'D1': (PWR_X0 - 4.2, U1_Y0 + 3.1, 90),
# ── 尾插那两颗(U7 ESD / U5 VBUS 开关):J4 现在也在 B 面,排线从南边来,
# 它们只能在 J4 **北边**(插头背后)—— 交给 SVC_R 区的排布器,U7 排第一个
# 落在区的西南角(离 J4 的 D± 脚最近)。
@@ -276,7 +323,12 @@ HAND = {
# 同时 `p13~p24`(`SDA/SCL/+3V3` + 四路状态)朝北,而金手指在**南**边。
# 两排正好各自背对自己的去向,等于每根线都要绕整颗芯片一圈 —— M6 在功率列
# 上就是这么丢掉 4 处的(原 `HAND` 注释里那段「转 180」说的是同一件事)。
'U4': (12.5, 15.6, 0),
# ⛔⛔ 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)
@@ -284,15 +336,30 @@ HAND = {
# 摆在东边的话这根线要绕整颗芯片 —— 布线器就是这么报「$1N128: U1.1 没布通」的,
# `place_audit.py` 也把它列在「摆错边」里。
# 口袋是 L1 顶(47.2) / U1 西沿(86.875) / D1 南沿(50.6,已北移让路) 夹出来的那一小块。
'R3': (85.95, 48.8, 90),
'R3': (round(85.95 + PX, 3), 48.8, 90),
# INA226 的 +3V3 旁路,摆在 U3 右侧(U3 上下两排脚都被 Kelvin / I2C 占着)
'C13': (86.8, 18.0, 0),
'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 正下方 ──
'Q3': (5.5, 29.9, 90),
'Q4': (2.0, 29.9, 90),
'Q5': (5.5, 26.1, 90),
# ⚠ 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):
@@ -301,24 +368,25 @@ def stack(pos, sz):
⚠ 只用 bbox 高度 + `STACK_GAP` 算 —— 不手抄 y。改一颗件的封装,
整条链自动重排,不会留下一个「当初算好、现在对不上」的常数。
"""
out = {'U1': (PWR_X, U1_Y, 270)}
y = U1_Y + sz['U1'][1] / 2.0
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_X + dx, 3), round(y, 3), rot)
out[ref] = (round(PWR_X0 + dx, 3), round(y, 3), rot)
y += h / 2.0
y = U1_Y - sz['U1'][1] / 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_X + dx, 3), round(y, 3), rot)
out[ref] = (round(PWR_X0 + dx, 3), round(y, 3), rot)
y -= h / 2.0
return out
# 2026-09-11 晚:在旧框架里堆完,整条链刚体转 180°(见 flip_pwr)
return {r: flip_pwr(v, r) for r, v in out.items()}
# ── 分组:**顺序 = 电流/信号流向**,排布器按这个顺序摆 ──────────────────
@@ -335,6 +403,9 @@ GROUPS = [
# 四件要绕整颗芯片才够得着,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 脚都在左长边),
@@ -346,7 +417,6 @@ GROUPS = [
('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']),
('SVC_E', u'EDL 模拟开关', ['U6', 'R63', 'C69']),
]
# ── 区(x0, y0, x1, y1),排布器在里面自下而上、自左而右铺 ────────────────
@@ -358,12 +428,23 @@ 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 起)。
'CTRL': (17.2, 12.0, 20.8, 20.5),
# ⚠ 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 高的带子里排不下第二行。
'DIS': (21.0, 12.0, 34.2, 24.0),
# ⛔ 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 憋死的。
@@ -376,27 +457,43 @@ REGIONS = {
# 自动排成 p17→p20 的顺序(目标点精确到焊盘)。
# ⚠ 北沿放到 36.0:目标驱动的排布器会为了靠近自己那只脚把件排在非对齐的 y 上,
# 5.4×10 的框里 4 件就可能塞不下(实测 `R13` 放不下)。东边这一条本来就是空的。
'EFUSE_E': (92.0, 23.0, 97.4, 36.0),
'EFUSE_AUX': (72.3, 20.0, 84.0, 36.0),
# ⛔ 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:再往东就是板边。
'BUCK_FB': (91.3, U1_Y - 8.0, 97.4, U1_Y + 15.0),
# ⛔ 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 这一带本来就是空的)。
'KEY': (0.6, 22.0, 13.0, 38.0),
# 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() 兜着)。
'SVC_M': (33.0, 29.0, 44.5, 45.5),
# 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 件。
'SVC_R': (47.5, 35.5, 65.5, 46.0),
# J3 正北、尾插车道(东沿 65.58)与功率列(72)之间的走廊;手机投影下(高度都 < 2)
'SVC_E': (66.6, 36.4, 72.5, 50.0),
# ⚠ 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 面,
@@ -444,6 +541,10 @@ def blockers():
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
@@ -532,6 +633,7 @@ def plan():
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 的**退回去**。
@@ -595,7 +697,13 @@ def ctrl_hand(pos, sz=None):
ROW_PITCH = 2.2
sz = sz or sizes()
half_u = (BODY['U4'][1] if rows else BODY['U4'][0]) / 2.0
items = [('C40', '24'), ('R41', '23'), ('R40', '22')] # 顺序 = 脚的顺序
# ⛔ 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 {}
@@ -616,7 +724,9 @@ def ctrl_hand(pos, sz=None):
cand = []
for k, (ref, _q) in enumerate(items):
t = ctr + (k - (len(items) - 1) / 2.0) * ROW_PITCH
cand.append((ref, (round(ux + t, 3), round(uy + side * off, 3), 90) if rows
# ⚠ 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,插槽体要夹住)。
@@ -713,7 +823,14 @@ def target_point(ref, peers, pos, ref_pt=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
@@ -729,6 +846,92 @@ def target_point(ref, peers, pos, ref_pt=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"""在**同一区、同一尺寸**的一组货架位置之间重排,让每件离自己的对端更近。
@@ -745,13 +948,22 @@ def reassign(pos, slots, peers):
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))
@@ -949,7 +1161,8 @@ def selfcheck(pos, rects, unplaced, sz):
continue
x, y, rr = pos[ref]
w, h = wh(ref, rr, sz)
in_phone = y + h / 2 > py0 and x + w / 2 > px0 and x - w / 2 < px1
# ⚠ 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)'
@@ -967,7 +1180,9 @@ def selfcheck(pos, rects, unplaced, sz):
% (ref, hname, math.hypot(dx, dy), B.HOLE_KEEP))
# C60 不许在手机底下(已由上一条覆盖),另外校核它确实在服务带/功率列
cx, cy, _ = pos['C60']
if not (12.0 <= cy - BODY['C60'][1] / 2 and cy + BODY['C60'][1] / 2 <= 28.0):
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