文件
phonerail/.claude/pcb1_place.py
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

1082 行
60 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""下板 PCB1 布局 v6(2026-09-06 两板拆分之后从头重排)。
板框 **160 × 125 mm**,四角 R5,四个 M3 金属化定位孔。原点左下角、Y 向上、API 单位 **mil**。
全部元件在**顶层**(朝载板那一面)。
## 和 v5(单板时代)比,重排的三个理由
1. **整个 USB 子系统搬去载板了**(U32/Y1/U24~U28/J23/J17~J21 + 53 个 7xx 件),
原来的【C】带整条空出来;同时新增 J24/J25 两条 2×20 排针、U33、Q6/D2、J26~J30 风扇座。
2. **J24 / J25 的位置是钉死的** —— 由载板 J31/J32 的焊盘反推(`pcb1-env.json` 的 anchors,
下板坐标 = 载板坐标 − (30, 5))。它们横在 y 60.4~64.6,把板子劈成南北两块。
3. **载板底面的元件决定了下板每个地方能放多高的件**(`pcb1-env.json` 的 zones):
五颗 Ø10 电解罐(h=10.5)在下板投影上留了五块 **hmax 1.5 mm** 的禁区,
电感(3.0)、直插电解(12)、接线端子(10)都进不去。check() 里有一条逐件核算。
## 五路怎么分到南北两块
每路的 VOn_P 只有 3 根针,位置由载板几何定死(§25.10),x 从左到右是
VO1(10.4) VO4(46.0) VO2(53.6) VO5(126.7) VO3(142.0)。母版(CH1)的输出端在
**局部 (6, 40.5)**,也就是左上角,所以:
* 放**南**边(rot 0,原样):输出在上,正对排针 ⇒ bx = VOc − 6
* 放**北**边(rot 180,整路转半圈):输出转到右下,正对排针 ⇒ bx = VOc + 6
VO1 太靠左板边、VO3 太靠右板边 ⇒ **CH1 只能在南、CH3 只能在北**,
其余按不重叠排下来只有一种解:**南 = CH1 / CH4 / CH5,北 = CH2 / CH3**。
⚠️ 不做 y 方向镜像 —— 那需要把元件翻到底层。转 180° 是纯旋转,母版里
§10.2 那四条手调出来的关键回路(Cin 热回路 / SW 铜箔 / FB / Kelvin)原样保留。
用法:
/c/Python314/python .claude/pcb1_place.py 只跑本地检查
/c/Python314/python .claude/pcb1_place.py --push 下发摆位
/c/Python314/python .claude/pcb1_place.py --outline 重画板框与定位孔
/c/Python314/python .claude/pcb1_place.py --verify 回读焊盘,核 J24/J25 有没有对上载板
"""
import io, json, math, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '7e9415a16a909c51'
MIL = 39.3700787401575
# ⚠️⚠️ 2026-09-07 修正:这两个数从 2026-09-06「两板尺寸取大对齐」那一轮起就过期了 ——
# 板上实测的板框是 **220 x 135、左下角 (-30,-5)**(`pcb1_outline.py` 就是这么画的,
# 回读多段线 x -1181.1~7480.3 mil / y -196.9~5118.1 mil 逐条对上)。
# 旧值 (0,0,160,125) 是真板框的**子集**,所以越框检查一直是「偏严但不错」——
# 代价是**左右各 30 mm、上下各 6 mm 的板面从来没进过摆位视野**。
# 板边元件(风扇座、电源座、调试口)按旧值挑位置会挑到板子中间去。
BX0, BY0, BX1, BY1 = -30.0, -5.0, 190.0, 130.0 # 真板框,mm
BW, BH = BX1 - BX0, BY1 - BY0 # 220 x 135
CORNER_R = 5.0
# 2026-09-06 用户指令「两板尺寸取大对齐、四角定位孔对齐」:板框改成和载板一样的 220×135,
# **坐标原点不动**(下板坐标 = 载板坐标 − (30,5),pcb1_env.py 那条约定不变),板框画到 (−30,−5)~(190,130)。
# 定位孔 = 载板 `pcb2_place.MH` 减 (30,5):四角 + 两侧中点。载板中跨那两个 (46.6,74.5)/(118.1,74.5)
# 落在下板 CH2 功率级 / 布好的线上,不打。板框与孔由 `pcb_place.py --outline` 下发(同一张表)。
MH = [(-22.0, 3.0), (182.0, 3.0), (-26.0, 126.0), (186.0, 126.0), (-23.0, 55.0), (183.0, 40.0)]
MH_HOLE, MH_PAD, MH_KEEPOUT = 3.2, 6.0, 6.5
PARTS = {c['d']: c for c in json.load(io.open(os.path.join(HERE, 'pcb1-parts.json'), encoding='utf-8'))}
SCH = json.load(io.open(os.path.join(HERE, 'sch-xy.json'), encoding='utf-8'))
ENV = json.load(io.open(os.path.join(HERE, 'pcb1-env.json'), encoding='utf-8'))
# ── 本体外形(mm,未旋转,相对元件原点)。API 取不到封装本体,只能手填 ──────
# 只列「本体明显大于焊盘框」的;其余直接用 pcb1-parts.json 的真实焊盘框。
FP_BODY = {
'CAP-TH_BD8.0-P3.50-D0.6-FD': (-4.10, -4.10, 4.10, 4.10), # Ø8 立式电解
'RES-TH_L13.5-W9.5-P5.80-D0.7': (-6.75, -4.75, 6.75, 4.75), # 5 W 泄放电阻
# ⚠️⚠️ **本体框现在有第二个来源了**:器件的 `3D Model Transform`
# (v[0],v[1] = 本体 XY 尺寸 mil,v[6],v[7] = 中心偏移 mil)。
# `.claude/fp_body.py --check` 会拿它和这张表逐条对账;
# 上面这几条当年是拿放大截图量出来的,六条全部对得上(误差 ≤0.31)。
# ⇒ 新加板边连接器时先跑 `fp_body.py --scan <关键词>`,别再猜 ±4.5。
# ── 2026-09-06 换件:J1 → XT30PW-M,J26~J30 → 2510-3A 直针座 ──
'CONN-TH_XT30PW-M': (-6.65, -7.98, 6.65, 5.97), # 12V XT30 公头(已换掉,见下一条)
# ── 2026-09-08 换件:J1 → 圆形 DC 电源座 DC-005-5.5*2.5(用户拍板,不降载)──
# ⚠️ 这个数不是量截图量出来的,是 `pcb_Primitive.getPrimitivesBBox([pid])` 直接读的
# (把件建在板外 (200,0) rot 0 读完再删)。**它含丝印/装配层,比塑壳本体大一圈** ——
# 同法读 XT30 得 14.85 x 13.55,而本体表里是 13.30 x 13.95,x 方向多 0.90。
# 偏大的方向是安全的(净距检查偏严),本体真值待打样时拿实物量(§12 待确认项)。
# 本体几乎全在 −X 侧、三个脚挤在 +X 端 ⇒ **桶口朝 −X**,朝西板边用 rot 0,不用转。
'DC-IN-TH_DC-005-2': (-10.68, -7.16, 3.92, 3.22), # 圆形 DC 座 5.5x2.5
'CONN-TH_2510-3A': (-3.81, -2.88, 3.81, 2.93), # 风扇 3P **直针**座(已换掉,见下一条)
# ── 2026-09-07 FAN-R2:五个 3 针风扇座 → 两个 4 针(一进一出,山洋 9G0612P4S001)──
'CONN-TH_4P-P2.54_CAX_2510-4A': (-5.08, -2.79, 5.08, 3.01), # 风扇 4P 直针座(fp_body --scan 读出)
'SOD-123_L2.7-W1.6-LS3.7-RD-1': (-1.93, -0.85, 1.93, 0.85), # DZ3 BZT52C8V2;本体比焊盘框宽 0.4
# 下面两条是换下来的,留着当对照(**别再用**):
'CONN-TH_P5.08_KF128-5.08-2P': (-5.20, -5.20, 5.20, 5.20), # 旧 12V 螺钉端子
# ⚠️ KF2510-3A 是**弯针**座:焊盘一排,塑壳整个往 +Y 伸 11.7 mm,开口在远端。
# 名字像直针,实测是弯针 —— 2026-09-06 换成真直针 `CONN-TH_2510-3A`
# 之后总进深 12.01 → 5.81,**每个座省 6.2 mm**。
'CONN-TH_KF2510-3A-1': (-3.90, -0.20, 3.90, 11.70), # 旧风扇 3P 弯针座
'IND-SMD_L7.1-W6.5': (-3.80, -3.35, 3.80, 3.35),
'IND-SMD_L4.0-W4.0': (-2.40, -2.00, 2.40, 2.00),
# USB-C:焊盘在 +Y,壳体往 −Y 伸 5.2 ⇒ rot 0 时**开口朝 −Y**
'USB-C_SMD-TYPE-C-31-M-12_1': (-4.93, -5.20, 4.93, 3.12),
# ESP32-S3-WROOM-1:天线那一端(+Y)没有焊盘,本体比焊盘框多伸 7.4
'WIRELM-SMD_ESP32-S3-WROOM-1': (-9.50, -9.35, 9.50, 16.75),
}
# ── 元件高度(mm,离下板顶面)。check() 拿它和 pcb1-env 的 hmax 对账 ──────────
H_FP = {
'C0402': 0.6, 'C0402_NEW': 0.6, 'R0402': 0.5, 'R0603': 0.6,
'C0805': 1.0, 'C1206': 1.4, 'R2512': 0.8,
'SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR': 1.2,
'SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BR': 1.2,
'SOP-8_L5.0-W4.0-P1.27-LS6.0-BL': 1.8,
'SOIC-16_L9.9-W3.9-P1.27-LS6.0-BL': 1.8,
'TSSOP-24_L7.8-W4.4-P0.65-LS6.4-BL': 1.2,
'MSOP-10_L3.0-W3.0-P0.50-LS5.0-BL': 1.1,
'WQFN-16_L3.0-W3.0-P0.50-TL-EP1.7': 1.0,
'WQFN-20_L4.0-W3.0-P0.50-BL-EP': 1.0,
'VQFN-14_L3.5-W3.5-P0.50-TL-EP_TPS54622RHLT': 1.0,
'SMB_L4.6-W3.6-LS5.3-RD': 2.4, 'SMA_L4.3-W2.6-LS5.2-RD': 2.3,
'SOD-123_L2.7-W1.7-LS3.8-RD': 1.2, 'FUSE-SMD_L3.2-W1.6-1': 1.2,
'F1206': 1.2, # F1 12 A 保险丝(2026-09-06 从 10 A 那颗换来,同是 1206)
'IND-SMD_L7.1-W6.5': 3.0, # SPM6530T
'IND-SMD_L4.0-W4.0': 2.0,
'USB-C_SMD-TYPE-C-31-M-12_1': 3.5,
'WIRELM-SMD_ESP32-S3-WROOM-1': 3.3,
'CAP-TH_BD8.0-P3.50-D0.6-FD': 12.0, # ⚠ 470µF/25V Ø8×12,见文件末尾的说明
# ⚠️⚠️ 板上已经**没有**这个封装了(R9 在 2026-09-06 换成 5 × 2Ω 2512)。
# 留在这里是为了记住那个数曾经是错的:写的 9.5 是照封装名里的 `W9.5` 抄的,
# 而 `P5.80` 的脚距说明它是**立式**件 —— 22 mm 的本体站起来,
# 同族 C9900012721 的 3D 模型直接标 `H52.0`。那一片 hmax 只有 12.0,
# 所以「高度预算全过」这句话在它身上整整假了一轮。
# ⇒ 往 H_FP 里加直插件时,**脚距和本体长度对不上就是立式**,
# 高度不是封装名里的 W,是那个对不上的方向。
'RES-TH_L13.5-W9.5-P5.80-D0.7': 22.0,
'CONN-TH_P5.08_KF128-5.08-2P': 10.0, # 旧件
'CONN-TH_KF2510-3A-1': 9.0, # 旧件(弯针,躺着所以矮)
# ⚠️⚠️ 换成立式**省的是板面、付的是高度**:直针座塑壳竖起来。
# `3D Model Transform` 只给 XY,**Z 读不出来** —— 下面两个数是按同系列
# 常规值取的保守估计,**打样前必须拿实物量**(§12 待确认项)。
'CONN-TH_XT30PW-M': 11.0, # XT30 90° 板装公头,本体躺着(已换掉)
'DC-IN-TH_DC-005-2': 11.0, # 圆形 DC 座:同系列常规值,**打样前拿实物量**
# 放在西条(x −29~−1)上,那一条 hmax = 99(载板底面没东西),所以这个数目前不吃紧
'CONN-TH_2510-3A': 9.0, # 2510 直针座,塑壳竖立(3P,已换掉)
'CONN-TH_4P-P2.54_CAX_2510-4A': 11.5, # 2510 直针 4P:厂家属性 Z-Height of the Board = 11.5
'SOT-23_L2.9-W1.3-P1.90-LS2.4-BR': 1.2, # Q7 AO3401A(和另一个 SOT-23-3 同高,封装名不同)
'SOD-123_L2.7-W1.6-LS3.7-RD-1': 1.2, # DZ3 BZT52C8V2
# 排针本身就是对插件,不参与高度核算
'HDR-TH_40P-P2.54_AZ-PZ2.54-V11-2X20P': None,
}
POS = {}
WARN = [] # 摆位本身没错、但要人拍板的事(目前只有排针针号错位那一条)
def _rotbox(b, rot):
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 fp(des):
return PARTS[des]['fp']
def box(des, rot=0):
u"""元件相对原点的占位框(mm):真实焊盘框 ∪ 手填本体框,再按 rot 旋转。"""
p = PARTS[des]
b = tuple(v / MIL for v in p['bb'])
body = FP_BODY.get(p['fp'])
if body:
b = (min(b[0], body[0]), min(b[1], body[1]), max(b[2], body[2]), max(b[3], body[3]))
return _rotbox(b, rot)
def height(des):
f = fp(des)
if f not in H_FP:
raise KeyError(u'没有高度数据的封装: %s (%s)' % (f, des))
return H_FP[f]
def put(des, x, y, rot=0):
assert des in PARTS, u'位号不存在: %s' % des
assert des not in POS, u'重复放置: %s' % des
POS[des] = (round(x, 3), round(y, 3), rot % 360)
# ══════════════════ 五路功率级:母版 + 五次摆放 ══════════════════
# 局部坐标沿用 v5 的母版(§10.2 手调出来的,别动相对关系)。J2~J11 已从原理图删掉。
CH1 = [
# 反馈 / 调压 / 补偿
('R%01', 3.0, 3.0, 0), ('R%02', 3.0, 5.2, 0),
('R%07', 6.5, 3.0, 0), ('R%08', 6.5, 5.2, 0),
('C%12', 10.0, 5.2, 0),
('R%06', 10.0, 3.0, 0), ('C%10', 13.5, 3.0, 0), ('C%11', 13.5, 5.2, 0),
('R%03', 17.0, 3.0, 0),
('R%04', 17.0, 5.2, 0), ('R%05', 20.0, 5.2, 0),
('C%09', 20.0, 3.0, 0),
# Buck 功率级(C%01/C%02 贴 PVIN/GND,热回路最小;14.95 不是笔误,见 v5 注释)
('U%__1', 8.5, 12.0, 0),
('C%01', 4.0, 11.3, 90), ('C%02', 4.0, 14.95, 90),
('C%03', 4.0, 7.7, 90), ('C%04', 13.0, 9.0, 90),
# 自举电容:从 (11.5,16.2,90) 挪到这里,rot 0 ⇒ BOOT 脚在西 (12.0)、SW 脚在东 (12.82)。
# ⚠️ 原来那个位置正卡在 SW 立管唯一能走的那条缝上:U1.13(BOOT) 的东缘 10.624 到
# C105 西缘 11.229 只有 0.605 mm,连 0.28 的线都塞不进去(要 0.68)。
# 挪到 y 13.0 之后 BOOT 从 U1.13 平着往东 1.8 mm 就到,SW 立管从它北边过。
# ⚠️ 13.0 -> 13.6(2026-09-06 第二十一轮):**为了让 SW 不再往南潜。**
# SW 要从引脚列(x 11.2)往东走到立管(x 13.5),而 C%05.1(BOOT) 的焊盘底在 12.732,
# 从它下面平着过只有 0.581 mm 的缝 —— 装不下 SW 那条线,所以老版本先往南潜到 y 11.6。
# 那一潜正好压在 p9(COMP) / p10(EN) 唯一的出线走廊上:两个脚只能出来一个。
# C%05 北移 0.6 之后 BOOT 焊盘底 13.332,SW 平着走 y 12.5(1.2 宽,顶 13.10)就过得去,
# y < 11.9 整片让给 p9 / p10 —— 两条各走自己那一行,互不打架。
# 副作用还是好的:SW 铜面积 10.9 -> 9.5 mm²,节点长度 9.6 -> 8.9 mm(§10.2-2 要它小)。
('C%05', 12.4, 13.6, 0),
# 电感与输出电容(2026-09-06 第十九轮重排,用户拍板;理由见设计文档 §26.13)
# ⚠️ L101 是 **180°**:footprint 的 pin1 = SW 在西边,rot 0 时正好背对 U1 的 SW 脚
# (U1 的 SW 在东,x 10.224),SW 要绕过整个电感走 13.1 mm。转过来只剩 6.3 mm。
# ⚠️ L101 **不能往南挪**:C102 的 0805 罐就压在它北边 1 mm 内(本地检查会报重叠)。
# 空袋是靠 U14 北移 0.8 mm 腾出来的 —— 23.75(L101 本体顶) ~ 28.35(U14 南排焊盘),4.6 mm。
# ⚠️ 顺带**东移到 10.7**:U1 的 SW 脚(p11/p12, x 10.224)被 p10(EN) 和 p13(BOOT)
# 夹在 0.5 mm 间距的东排里,只能先往东出再往北拐,立管落在 x≈13.5。
# 电感留在 x 6.5 的话,立管到顶还要再往西折回 4 mm —— 那 4 mm 也是 SW 铜。
# 摆到 (10.7,18.0) 之后 SW 脚 (13.501,18.001) 正对立管,一路直上不回头。
('L%01', 10.7, 18.0, 180),
# 输出电容从 x 14~17.5 挪进那块空袋,全部改成横放(rot 0):
# ⚠️ 1206 的焊盘 rot 0 时是 **1.49 宽 x 1.73 高**(不是反过来),横放整体 4.676 x 1.73;
# 两颗摞起来 1.73 + 0.37 + 1.73 = 3.83,袋子 4.6 —— 上下各留 0.385。竖放摆不下。
# pin1(VO1) 在西 ⇒ 三颗的 VO1 脚排成一列 x≈3.8,正对 L101 转过来之后的 VO1 脚 (3.700)。
# C%08(0402 高频)贴在 L%01 的 VO 脚正北 1 mm;两颗 1206 排在 L%01 与 U%14 之间,
# p1(VO) 对齐成一列 x = 5.200,正对 U%14 转过来之后的输入脚 (x 4.750~5.751)。
('C%08', 8.4, 22.5, 0), ('C%07', 6.793, 25.0, 0), ('C%06', 6.793, 27.1, 0),
# eFuse
# ⚠️ U14 也是 **180°**:rot 0 时输入脚在北 (y 31)、输出脚在南 (y 28),
# 而源在南(L101)、去处在北(Rs101)—— 两条 4 A 各横穿一次封装($2N699 要绕 17 mm)。
# 转过来输入朝南、输出朝北,$2N699 只剩 8 mm;
# 而且编程脚 p17~p20 从西侧转到东侧,正对 R109~R117 那一排电阻。
('U%__14', 6.0, 30.3, 180),
('R%09', 11.0, 32.0, 0), ('R%10', 11.0, 30.0, 0), ('R%11', 11.0, 28.0, 0),
('C%14', 14.5, 32.0, 0), ('R%12', 14.5, 30.0, 0), ('R%13', 14.5, 28.0, 0),
('R%14', 18.0, 32.0, 0), ('R%15', 18.0, 30.0, 0), ('R%16', 18.0, 28.0, 0),
('R%17', 18.0, 26.0, 0),
# 分流 + INA226(Kelvin)—— **这一组就是输出端**,要正对排针的 VOn_P 针
# ⚠️ Rs101 也是 **180°**:rot 0 时「西 = VO1_P / 东 = $2N699」,而 U8 的
# IN+ 是 p10(西, x 4.999)、IN− 是 p9(x 5.500)—— 两条取样线**必然交叉**。
# 转过来变成「西 = $2N699 / 东 = VO1_P」,IN+ 往西、IN− 往东,各走各的。
# VO1_P 挪到 x 9.066 仍然落在底层 VO1P-ISL 岛里(岛盖到局部 x 19.2),铺铜不用改。
# ⚠️⚠️ **U%8 / Rs%01 / C%13 三个整体北移 0.8 mm(2026-09-06 第二十一轮,用户拍板)。**
# 原来 eFuse(U%14)北排焊盘顶 47.75 到 INA226(U%8)南排焊盘底 48.08
# **只隔 0.33 mm** —— 一根 0.2 的线要 0.2 + 2 x 0.2 = 0.6,过不去。
# 于是 U14~U18 北排那 14 个脚(`EFUSE_PG*` / `$*N698` / `EFUSE_EN*` …)
# **一个都出不来**(`pcb1_escape_audit.py`:两层加起来只能走 0.26 mm²)。
# 这一列从下往上是 C%06(顶 43.46) → U%14(43.85~47.75) → U%8(48.08~53.92)
# → Rs%01(54.27~57.73),缝一律 0.33~0.39,**一点余量都没有**;
# 唯一的空地是 Rs%01 顶到 J24 焊盘底(60.33)之间的 2.60 mm。
# 北移 0.8 之后:eFuse 那条缝 0.33 → **1.13 mm**,Rs%01 到 J24 还剩 1.80 mm。
# ⚠️ 动了这三个就要同步改:`pcb_fanout._CH_FORCE_LOCAL[('U%__8','1')]`
# 和 `pcb1_route_pwr` 的 `F-trunk` / `K-IN+` / `K-IN−`(Kelvin 线在母版里)。
('Rs%01', 6.0, 41.3, 180),
('U%__8', 6.0, 36.3, 0),
('C%13', 10.5, 36.3, 0),
# 放电联锁
('Q%01', 13.8, 35.5, 0), ('R%18', 16.4, 35.5, 0),
('Q%02', 13.8, 39.5, 0), ('R%19', 17.8, 40.0, 90),
# 按键驱动 Q%03~05 / R%20~25:2026-09-06 随 74HC595 整窝挪到载板(§26.23),这一片空出来了
]
def ch_designator(tok, n):
if tok.startswith('U%__'):
base = tok[4:]
return {'1': 'U%d' % n, '14': 'U%d' % (13 + n), '8': 'U%d' % (7 + n)}[base]
return tok.replace('%', str(n))
def ch_extent():
u"""母版的局部包围盒。"""
x0 = y0 = 1e9
x1 = y1 = -1e9
for tok, lx, ly, rot in CH1:
d = ch_designator(tok, 1)
b = box(d, rot)
x0 = min(x0, lx + b[0]); y0 = min(y0, ly + b[1])
x1 = max(x1, lx + b[2]); y1 = max(y1, ly + b[3])
return (x0, y0, x1, y1)
VO_X = {1: 10.409, 2: 53.589, 3: 141.969, 4: 45.969, 5: 126.729} # VOn_P 三针中点 x
CH_PLACE = {
# rot bx by
1: (0, 8.300, 15.5), # 南;bx 右移让出左板边给风扇座(输出仍在 VO1 三针右侧 1.3 mm)
4: (0, VO_X[4] - 6.0, 15.5), # 南
5: (0, 119.500, 15.5), # 南;比 VO_X[5]−6 再左 1.2,给右板边的风扇座让位
2: (180, VO_X[2] + 6.0, 110.0), # 北(整路转 180°)
3: (180, VO_X[3] + 6.0, 110.0), # 北
}
for _n, (_rot, _bx, _by) in CH_PLACE.items():
_s = -1 if _rot == 180 else 1
for _tok, _lx, _ly, _r in CH1:
put(ch_designator(_tok, _n), _bx + _s * _lx, _by + _s * _ly, (_r + _rot) % 360)
# ══════════════════ 板对板排针(位置由载板反推,不许改)══════════════════
put('J24', 32.0, 62.5, 0)
put('J25', 128.0, 62.5, 0)
# ══════════════════ 【N-L】P1-INPUT:12V 进线 + 8 A 输入链 ══════════════════
# 北左块 x 1~30 / y 66~124.5。⚠️ 载板 C801 罐在 (1.34~14.24, 106~116.8) 只留 1.5 mm,
# 所以电解 / 电感 / 端子全部压在 y < 100。
# ⚠️⚠️ **XT30 比原来的 KF128 宽一圈,输入链跟着重排了一次**(2026-09-06)。
# `CONN-TH_XT30PW-M` 本体 13.30 x 13.95,插口在 −Y 端 ⇒ 要它朝左板边就得 **rot 270**,
# 转过来之后往 −X 伸 7.98 / 往 +X 伸 5.97。放在 x=8.5 时框是 (0.52,64.85)-(14.47,78.15),
# **原来 F1 那个位置 (14.5,71.5) 正好被吃掉**。
# 而东边的墙是硬的:北排 CH2 从 **x 29.6** 起,D1(SMB) 再往东就撞上去。
# ⇒ J1 东边只剩 15 mm 给 F1/Q1/D1/DZ1/C1/R1 六件,一行排不下(要 15+ mm),改**两行**。
# 2026-09-08 ⛔ **硬缺陷修复**:J1 原来在 x=8.5(本体 x 0.5~14.7)——
# 那是按**过期的假板框 160×125**挑的“西板边”。真板框西边在 x=−30,
# 于是这个 **90° 板装** XT30 的插口缩在板内 30.5 mm,上方 13 mm 处还压着载板
# —— **插头根本插不进去**。不是优化问题,是板子能不能用的问题。
# → 整个输入首级(J1/F1/Q1/D1 + Q1 的栅极卫星 DZ1/C1/R1)西移 30 mm。
# 西条 x −29~−1 整条 hmax = 99(载板底面没东西),高件随便放。
# 代价:加权 HPWL +1~3%(源头离负载远了)—— 这是板子可装配的必要代价。
# 2026-09-08 换成圆形 DC 电源座(用户拍板:DC-005 5.5x2.5,**不降载**,接受插座超额定)。
# ⚠️ 整机 12V 最坏 9.99 A(§22.6),而 DC-005 家族额定 2~5 A —— 这是全板唯一一处
# 明知超额定还要装的地方,发热在**插头内芯与插座弹片的接触面**,板上加铜救不了。
# 板端能做的只有一件:pin2 / pin3 两个负极触点**都接地并联**,接触电阻减半(见 p1_dcjack.py)。
# 桶口朝 −X ⇒ **rot 0**(XT30 那颗要 rot 270,别照抄)。本体 x −10.68~+3.92 相对中心,
# 放 x=−18.8 时本体 x −29.48~−14.88,离真西板边 (−30) 0.52 mm,和原来 XT30 的 0.5 一致。
put('J1', -18.8, 72.0, 0) # 12V 圆形 DC 座,桶口朝**真**西板边(本体 x −29.5~−14.9)
# ⚠⚠ **v11 的两行排法把 8 A 链卡死了**(2026-09-06 第二十三轮重排)。
# XT30 rot 270 之后 **GND 在北针 (13.5,74.5)、VIN 在南针 (13.5,69.5)**,
# 而 F1 卵在 (17.5,74.5) 卧着放 ⇒ VIN_RAW 要从南针绕到 GND 盘东侧再北上,
# 中间又被 D1(17.5,69.5) 堵住 —— 实测通道只剩 **1.674 mm**(1 oz 外层约 4.5 A)。
# 三处改动把它拉回来:
# ① F1 竖过来(rot 90)⇒ F1.1 落到 (17.5,73.055),正对着 J1.2 那一片空地;
# ② D1 挪到东边空列 ⇒ J1.2 → F1.1 一片 6.5 × 7.9 mm 的铜,不再有颈;
# ③ C4/C5/C6 一起东移 ⇒ 腾出 y 78.85~83.5 那条 4.65 mm 的 VIN_SW 西行干道
# (干道北提是因为 J1 的机械脚 J1.4 在 (3.5,77.5),占到 y 78.6)。
put('F1', -12.5, 74.5, 90) # 立着放:F1.1(VIN_RAW) 在南、F1.2(VIN_FUSED) 在北
put('Q1', -6.0, 74.5, 270) # SOP-8 P-MOS 防反接
put('D1', 1.0, 74.5, 270) # SMBJ13A TVS(挪出 VIN_RAW 通道,贴在漏极铜东缘)
put('DZ1', -8.0, 68.0, 0) # 栅极钳位
put('C1', -4.5, 68.0, 0)
put('R1', -2.0, 71.5, 0)
put('C2', 6.5, 84.5, 0) # 470µF/25V
put('C3', 16.0, 84.5, 0)
# C4/C5/C6:rot 180 让 $1N236 那一脚朝西迎着漏极铜,GND 脚朝东好打过孔
put('C4', 30.5, 80.0, 180)
put('C5', 30.5, 83.0, 180)
put('C6', 30.5, 86.0, 180)
put('Rs6', 6.5, 93.5, 0) # 5 mΩ 输入检流(体电容与分叉点之间,§19.1)
put('U13', 6.5, 99.0, 0) # INA226 贴在 Rs6 正上方做 Kelvin
put('C11', 11.0, 99.0, 0)
put('U29', 12.5, 94.0, 0) # 母线泄放开关
# 母线泄放电阻:**5 × 2 Ω 2512 串联 = 10 Ω/10 W**(2026-09-06 用户拍板)。
# 原来是一颗 `5W水泥电阻 5W10RJ`,封装 `RES-TH_L13.5-W9.5-P5.80-D0.7` ——
# **P5.80 的脚距意味着它是立式的**(同族 C9900012721 的 3D 模型标 H=52.0 mm),
# 而这里的 H_FP 把它当成 9.5,那一片 hmax 只有 12.0 ⇒ 高度检查一直在拿错数放行。
# 库里的卧式 5W 脚距 26 mm、占位 28×11 mm,北左角塞不下;2512 只有 0.8 mm 高。
#
# 可用空地是 x 14.5~30.0 / y 86.5~101.8(西 U29(→14.0) / 东 R220(30.87→) /
# 北 L2,U19(→102.25) / 南 C6),15.5 × 15 mm。一颗 2512 占 7.42 × 3.46,
# 一行放不下五颗(要 39.4 mm),排成 **1 + 2 + 2 的蛇形**,串联顺序跟着几何走:
# Q3 在正北 (25.5,119) ⇒ R9A 放最上面离它最近,R9E 收在最下面接地。
put('R9A', 18.0, 100.5, 0)
put('R9B', 18.0, 96.5, 0)
put('R9C', 26.0, 96.5, 0)
put('R9D', 26.0, 92.0, 0)
put('R9E', 18.0, 92.0, 0)
# 12V→3V3 辅助轨(U19 组):只能待在 x > 14.3 那一侧(C801 罐的禁区)
put('U19', 19.0, 104.0, 0)
put('L2', 25.5, 104.0, 0)
put('C7', 19.0, 108.5, 0)
put('C8', 23.5, 108.5, 0)
put('C9', 19.0, 112.5, 0)
put('C10', 24.5, 112.5, 0)
put('R2', 18.0, 116.0, 0)
put('R3', 21.0, 116.0, 0)
put('R4', 18.0, 118.5, 0)
put('R5', 21.0, 118.5, 0)
put('R6', 18.0, 121.0, 0)
put('Q3', 25.5, 119.0, 0)
# ══════════════════ 【N-M】P1B 12V→5V + P6 风扇/EDL ══════════════════
# 北中块 x 58.5~118.5 / y 66~124.5(CH2 到 x 57.4、CH3 从 x 119.2 起)
put('U30', 64.0, 72.0, 0)
put('C731', 68.2, 72.0, 270)
put('C732', 70.7, 72.0, 270)
put('C733', 64.0, 69.6, 180)
put('R706', 60.0, 73.5, 0)
put('R707', 60.0, 71.5, 180)
put('L3', 70.5, 78.0, 0)
put('C734', 77.5, 78.0, 90)
put('C735', 80.0, 78.0, 90)
# ── 风扇 FAN-R2(2026-09-07):两只 4 线 PWM 风扇,一进一出 ──────────────
# 旧的「低边切回流 + 5 个 3 针座 + TACH 分压」整块作废:R730~R732、J28~J30 已删。
# 现在是「高边默认关电 + 独立 PWM 开漏 + 两路独立测速接收」,见设计文档 §22.6。
# ⚠️ 摆位约束不只是「元件之间不重叠」:这一块北中区 y 64~100 底下压着已布好的
# +12V / +3V3 / GND 铜和过孔,而 `pcb1_route_sig --ripup` **只拆自己画过的信号线**,
# 拆不掉电源铜。第一版把新件摆进 y 66~88 直接刷出 65 条间距错误。
# ⇒ 新件整体上移到 **y 104~121 那条空带**(J28 删掉之后腾出来的),
# 留用件 Q6 / R733 / R734 原地不动(它们原来的位置本来就是干净的)。
# 【A】高边电源开关:+12V --Q7(P-MOS)--> FAN_12V_SW,MCU 高阻时默认断电
put('R746', 91.0, 120.0, 0) # 100k 栅极拉回源极
put('Q7', 95.0, 120.0, 0) # AO3401A
put('DZ3', 99.5, 120.0, 0) # BZT52C8V2 钳位负 Vgs(Q7 的 Vgs 绝对最大 ±12V)
put('C738', 105.0, 120.0, 0) # 22uF/25V,开关后母线本地储能
put('D2', 111.5, 120.0, 0) # SS34:K 接 FAN_12V_SW、A 接 GND(负向钳位,不再是续流)
put('R747', 91.0, 116.0, 0) # 1k 栅极串阻 -> Q8 漏极
put('Q8', 95.0, 116.0, 0) # AO3400A 拉低 Q7 栅极
put('R748', 99.5, 116.0, 0) # 1k FAN_EN(IO5) -> Q8 栅极
put('R749', 103.0, 116.0, 0) # 100k Q8 栅极下拉 = 复位默认断电
# 【B】两路独立测速接收(10k 上拉到 FAN_12V_SW -> 15k/10k 分压 -> N-MOS -> 3V3 上拉)
# 2026-09-08:**跟着各自的风扇座走到真板边**。
# 链路是 J2n.TACH → R75x(上拉到 FAN_12V_SW) → 分压 → Q(N-MOS) → FAN_TACH_xx(3V3 逻辑)。
# `FAN_IN_TACH_RAW` / `FAN_OUT_TACH_RAW` 是**座子到分压器**的本地网络:
# 接收电路留在中间的话,这两个网络要横跨大半块板(121 / 104 mm);
# 挪到座子旁边只剩几毫米。代价是 `FAN_TACH_IN/OUT`(已经是 3.3 V 逻辑、
# 经 N-MOS 整形过)各长约 16 mm —— 净赚一百多毫米,而且换掉的是**未整形的
# 12 V 摆幅裸信号**,抗扰上也是对的。
put('R750', -26.0, 108.0, 0) # 进风:贴着西板边的 J26
put('R751', -21.0, 108.0, 0)
put('Q9', -26.0, 104.0, 0)
put('R752', -21.0, 104.0, 0)
put('R753', -26.0, 100.0, 0)
put('R754', 181.0, 108.0, 0) # 排风:贴着东板边的 J27
put('R755', 186.0, 108.0, 0)
put('Q10', 181.0, 104.0, 0)
put('R756', 186.0, 104.0, 0)
put('R757', 181.0, 100.0, 0)
# 【C】PWM 低边开漏:Q6 / R733 / R734 保持 2026-09-06 的原位(现在只拉风扇 PWM 脚)
put('Q6', 102.0, 69.5, 0)
put('R733', 98.0, 80.5, 0)
put('R734', 101.5, 80.5, 0)
# 风扇座:一进一出,分列**真**左右板边(顶排风孔取消,见 THERMAL_TWO_FAN)
# ⚠️ 4P 座比 3P 宽 2.54 ⇒ 半宽 3.81 → 5.08。本体 (-5.08,-2.79)-(5.08,3.01),rot 0。
#
# 2026-09-08 挪位:原来在 (6.3,121) / (151,121) —— 那是按**过期的假板框 160×125** 挑的
# 「两个上角」,在真板框 220×135 / 左下角 (−30,−5) 里其实都在**板子中间**,
# 风扇线缆没法横着出去(直针座是垂直插拔,板边的意义就在于让线缆走得掉)。
#
# 当年退到「上角」的理由是:4P 座塑壳 11.5 mm(厂家 Z-Height),而下板 hmax 由
# **载板底面元件**的投影压出来(pcb1-env.json zones),假板框上 hmax >= 11.5 的
# 板边只有 y 68~92 一段,还被输入链占满。**真板框下这个约束消失**:
# 左条 x −30~0 整条 hmax 无限制,右条 x 160~190 有 84% 无限制。
# 实扫(`_fanpos2.py`):西边 y 2~49 与 61~120、东边 y 2~34 与 67~120 都可用。
#
# ⚠️ 压降复核(原注释担心的那一条):`FAN_12V_SW` 总共 2×0.67 A,但 Q7 居中、
# 两支**各只走 0.67 A**。0.8 mm 线宽下 R = ρL/(Wt):
# 西支 125 mm → 0.077 Ω → 51 mV;东支 95 mm → 0.058 Ω → 39 mV
# 都远在 FAN_CIRCUIT 那 0.2 V 预算之内。当年算的 0.26 V 是「单条 196 mm 走全电流」,
# 分成两支之后不成立。⇒ **这条线要按 0.8 mm 下发,别当普通信号走 0.20。**
put('J26', -23.9, 115.0, 0) # 真西板边 = 进风(本体 x −29.0~−18.8,离板框 1.0)
put('J27', 183.9, 115.0, 0) # 真东板边 = 排风(本体 x 178.8~189.0)
# ══════════════════ 【S-M】P3 / P3B 控制块 ══════════════════
# 南中块 x 69.5~119.5 / y 0.5~59
put('U20', 88.0, 18.2, 180) # ESP32-S3:天线端转到 −Y 朝下板边
# 2026-09-08 ⛔ 同一类硬缺陷:J22 原来在 x=154.4(本体到 159.6)= 假板框的东边,
# 真板框东边在 x=190,于是 USB-C 开口缩在板内 30.4 mm,线插不进去。
put('J22', 184.4, 52.0, 90) # 调试 USB-C,rot 90 = 开口朝**真**东板边(本体到 189.6)
# (下板边放不下:C841 罐的 1.5 mm 禁区压到 y 8.4)
put('RT601', 70.8, 36.0, 90) # 电感区测温:贴着 CH4 的电感那一排
put('RT602', 70.8, 26.0, 90) # MOS 区测温
# ══════ 去耦电容跟着本尊走(2026-09-06 加)══════════════════════════
# ⚠️ 下面那句 `affine(['P3-CONTROL','P3B-EXPAND'], ...)` 是**按原理图坐标**做仿射映射,
# 它保的是「原理图上挨着的,板上也挨着」,**保不住「去耦贴着电源脚」**。
# 而 U20 / J22 / RT60x 是上面手工单独摆的 —— 芯片被拎出了仿射集合,
# **它的去耦电容却留在原地**。2026-09-06 实测的后果:
#
# C601 22 µF「ESP32 储能」 -> U20.2 **48.2 mm**
# C602 100 nF(同为 P3 页)-> U20.2 **45 mm**(现在贴在 U22 旁边,纯属巧合)
# C607 / C610 -> U6.15 / U7.15 10.5 / 11.0 mm
# C604 / C605 / C606 -> U21.10 / U22.10 / U23.24 8.2 / 8.8 / 9.2 mm
#
# U20 的电源脚周围 12 mm 内一个元件都没有。ESP32-S3 发射时是几百 mA 的突发电流,
# 48 mm 外的储能电容等于没有。这一条**几何检查一条都报不出来** ——
# 不重叠、不越框、高度也够,纯粹是「电学上错了」。
#
# ⇒ 这几颗不参与仿射摆位,等芯片摆完(affine + relax 之后)各自贴到本尊的电源脚旁边,
# 并且在 check() 里加一条**硬校核**:超过 DECAP_MAX 就报错。表在这里,改摆位不用改代码。
# ⚠️ 表的顺序 = 抢位置的顺序,**小的高频那颗必须排在大的储能之前**。
# 反过来写实测过:C601(1206,本体 4.7 x 1.7)先占掉贴身的那一圈,
# C602 只能落到 3.52 mm 外 —— 而两颗里真正要贴身的恰恰是 100 nF 那颗。
# 第四列是**这一颗**的上限(mm),不是全表一个数:
# 100 nF 靠的是自身低 ESL,只有贴身才有意义;22 µF 储能管的是毫秒级的电流坑,
# 几毫米的走线电感对它无所谓 —— 给它 8 mm,它自然会让开贴身那一圈给 100 nF。
DECAP = [
('C602', 'U20', '2', 3.0), # 100 nF 0402,ESP32 高频去耦 —— 先占位
('C601', 'U20', '2', 8.0), # 22 µF 1206,ESP32 储能
# C604 / C605(74HC595 去耦)2026-09-06 随 595 挪到载板
('C606', 'U23', '24', 3.0), # PCA9555 VDD
('C607', 'U6', '15', 3.0), # DAC63204 #1 VDD
('C610', 'U7', '15', 3.0), # DAC63204 #2 VDD
]
_LATE = set(c for c, _u, _p, _m in DECAP)
def affine(pagelist, x0, y0, w, h):
pts = []
for pg in pagelist:
pts += [p for p in SCH[pg]
if p[0] in PARTS and p[0] not in POS and p[0] not in _LATE]
if not pts:
return []
sx0 = min(p[1] for p in pts); sx1 = max(p[1] for p in pts)
sy0 = min(p[2] for p in pts); sy1 = max(p[2] for p in pts)
out = []
for des, sx, sy, _r in pts:
fx = (sx - sx0) / float(sx1 - sx0 or 1)
fy = (sy - sy0) / float(sy1 - sy0 or 1)
put(des, x0 + fx * w, y0 + fy * h, 0)
out.append(des)
return out
# ══════ 控制块:按**连接几何**摆,不再按原理图坐标仿射 ══════════════════
#
# ⚠️⚠️ **`affine()` 摆控制块是这块板布线拥挤的根源。** 它把 P3 / P3B 两页的原理图坐标
# 线性映射到 42 x 22 的一小块上 —— 保的是「原理图上挨着的板上也挨着」,
# 和「谁连谁」没有半点关系。2026-09-06 实测的三条后果:
#
# 1. 焊盘框总面积只有 253 mm²,却全挤在 924 mm² 里(填充率 27%),而 S-M 块可用 50 x 58 ——
# `y 0.5~32` 大片空着。§26.14 那 91 条布不通正好集中在 U21~U23 周边。
# 2. **U22 和 U23 放反了**:U22 只服务 CH3/CH4/CH5(理想 x≈105)却待在 79.6,
# U23 服务五路全部(理想 x≈78)却待在 103.7。
# 3. U21(79.61,56.00) 和 U22(79.61,47.96) 中心只隔 8.04 mm,两个 SOIC-16 的焊盘框
# (7.44 高)之间只剩 0.6 mm —— **一根线都插不进去**。
#
# ⇒ 五颗芯片的位置改由 `pcb1_ctrl_place.py` 退火求解(目标 = HPWL,预留框故意开大
# 给走线留通道),结果钉在下面这张表里。附属的 22 个 R/C 不再跟着仿射走,
# 改成 `SAT` 表逐个贴到**它真正的对端引脚**旁边。
CTRL = {
# U21 (92.25,52.37) / U22 (92.84,38.01) 2026-09-06 挪到载板(§26.23)—— x 88~97 / y 35~55 空出来
'U23': (76.50, 49.63),
'U6': (79.58, 38.01),
'U7': (109.57, 34.71),
}
for _d, (_x, _y) in CTRL.items():
put(_d, _x, _y, 0)
# ── 附属小件:贴到**对端引脚**旁边(2026-09-06 建,取代 affine 对这 22 件的摆布)──
#
# ⚠️⚠️ 这张表里最要紧的是头两行。`R602` / `R603` 是 **USB-C 的 CC1 / CC2 下拉 5.1k**,
# 按 affine 摆在 (77.2,33.2) / (76.4,36.8),而 J22 在 (154.4,52) —— **离了 77 mm**
# (`$3N203` / `$3N204` 那两条 77 mm 的网络就是它们)。CC 是 Type-C 的角色检测线,
# 下拉电阻必须贴着座子。这和 §26.16 那颗「ESP32 储能电容在 48 mm 外」是同一类错:
# **几何检查一条都报不出来** —— 不重叠、不越框、高度也够,纯粹是电学上错了。
#
# 第四列是**这一颗**的上限(mm),超了 check() 直接报错。
SAT = [
('R602', 'J22', 'A5', 5.0, u'USB-C CC1 下拉 5.1k —— 必须贴座子'),
('R603', 'J22', 'B5', 5.0, u'USB-C CC2 下拉 5.1k —— 必须贴座子'),
('C613', 'U20', '39', 4.0, u'NTC1 滤波,贴 ADC 脚'),
('C614', 'U20', '38', 4.0, u'NTC2 滤波,贴 ADC 脚'),
('R618', 'U20', '39', 7.0, u'NTC1 分压上拉'),
('R619', 'U20', '38', 7.0, u'NTC2 分压上拉'),
('C603', 'U20', '3', 5.0, u'EN/strap 的 RC'),
('R601', 'U20', '3', 7.0, u'EN/strap 的 RC'),
('R604', 'U20', '22', 9.0, u'I2C SDA 上拉 —— 惯例放主机端'),
('R605', 'U20', '8', 9.0, u'I2C SCL 上拉 —— 惯例放主机端'),
('R608', 'U23', '1', 6.0, u'IOEXP_INT# 上拉'),
('R606', 'U23', '9', 6.0, u'BUCK_PG 上拉'),
('R607', 'U23', '10', 6.0, u'INA_ALERT 上拉'),
('C608', 'U6', '13', 4.0, u'DAC #1 内部基准/输出滤波'),
('C609', 'U6', '16', 4.0, u'DAC #1'),
('R614', 'U6', '16', 5.0, u'DAC #1'),
('R615', 'U6', '5', 5.0, u'DAC #1'),
('C611', 'U7', '13', 4.0, u'DAC #2 内部基准/输出滤波'),
('C612', 'U7', '16', 4.0, u'DAC #2'),
('R616', 'U7', '16', 5.0, u'DAC #2'),
('R617', 'U7', '5', 5.0, u'DAC #2'),
]
_P3_SET = list(CTRL)
def relax(names, rect, clearance=0.5, iters=800):
x0, y0, x1, y1 = rect
items = [n for n in names if n in POS]
for _ in range(iters):
moved = False
for i in range(len(items)):
for j in range(i + 1, len(items)):
a, b = items[i], items[j]
ax, ay, ar = POS[a]; bx, by, br = POS[b]
pa = box(a, ar); pb = box(b, br)
aw, ah = pa[2] - pa[0], pa[3] - pa[1]
bw, bh = pb[2] - pb[0], pb[3] - pb[1]
dx = abs(ax - bx) - (aw + bw) / 2.0 - clearance
dy = abs(ay - by) - (ah + bh) / 2.0 - clearance
if dx >= 0 or dy >= 0:
continue
moved = True
if dx > dy:
push = (-dx) / 2.0 + 0.05
s = 1 if ax >= bx else -1
ax += s * push; bx -= s * push
else:
push = (-dy) / 2.0 + 0.05
s = 1 if ay >= by else -1
ay += s * push; by -= s * push
POS[a] = (min(max(ax, x0), x1), min(max(ay, y0), y1), ar)
POS[b] = (min(max(bx, x0), x1), min(max(by, y0), y1), br)
if not moved:
break
# ⚠️ 不再 relax:五颗芯片的位置是退火**在预留框约束下**求出来的,两两之间已经留够了
# 走线通道。relax 只会把它们重新推到一起(它只管不重叠,不管通道)。
# ════════════════════════════ 本地检查 ════════════════════════════
def hmax_over(rect):
u"""这块矩形上,下板允许的最大元件高度(取所有相交受限区的最小值)。"""
x0, y0, x1, y1 = rect
h = ENV['gap'] - 1.0
for z in ENV['zones'] + ENV['lanes']:
a, b, c, d = z['rect']
if x0 < c and a < x1 and y0 < d and b < y1:
h = min(h, z['hmax'])
for z in ENV['slots']: # 排线从槽里下穿,别在正下方放高件
a, b, c, d = z['rect']
if x0 < c and a < x1 and y0 < d and b < y1:
h = min(h, 3.0)
return h
def abs_box(d):
x, y, r = POS[d]
b = box(d, r)
return (x + b[0], y + b[1], x + b[2], y + b[3])
def pad_xy(des, pin):
u"""这个焊盘在板上的绝对坐标(mm)。元件全在顶层,不用管镜像。"""
x, y, r = POS[des]
for num, lx, ly, _w, _h, _net in PARTS[des]['pads']:
if str(num) == str(pin):
lx, ly = lx / MIL, ly / MIL
a = math.radians(r)
return (x + lx * math.cos(a) - ly * math.sin(a),
y + lx * math.sin(a) + ly * math.cos(a))
raise KeyError(u'%s 没有引脚 %s' % (des, pin))
def _pad_net_xy(des, cx, cy, rot, net):
u"""假设这个元件摆在 (cx, cy, rot),它挂在 net 上的那个焊盘会落在哪。"""
a = math.radians(rot)
for num, lx, ly, _w, _h, n in PARTS[des]['pads']:
if n == net:
lx, ly = lx / MIL, ly / MIL
return (cx + lx * math.cos(a) - ly * math.sin(a),
cy + lx * math.sin(a) + ly * math.cos(a))
return (cx, cy)
def _fits(des, cx, cy, rot, clear=0.3):
u"""摆在这里会不会撞人 / 越框 / 压定位孔 / 超高。"""
b = box(des, rot)
ab = (cx + b[0], cy + b[1], cx + b[2], cy + b[3])
if ab[0] < BX0 + 0.4 or ab[2] > BX1 - 0.4 or ab[1] < BY0 + 0.4 or ab[3] > BY1 - 0.4:
return False
r = MH_KEEPOUT / 2.0
for hx, hy in MH:
dx = max(ab[0] - hx, 0.0, hx - ab[2])
dy = max(ab[1] - hy, 0.0, hy - ab[3])
if dx * dx + dy * dy < r * r:
return False
if height(des) > hmax_over(ab) + 1e-6:
return False
for d in POS:
B = abs_box(d)
if (ab[0] - clear < B[2] and B[0] < ab[2] + clear
and ab[1] - clear < B[3] and B[1] < ab[3] + clear):
return False
return True
def place_decaps():
u"""把 DECAP 表里的电容一颗颗贴到本尊的电源脚旁边。
⚠️ 必须在 affine + relax **之后**跑:U6/U7/U21/U22/U23 的位置是仿射摆出来的,
在那之前根本不知道它们的电源脚在哪。U20 虽然是手工摆的,也一起走这条路,
省得同一件事有两套写法。
找位置的办法就是绕着电源脚一圈圈往外试,取**第一个放得下的**,
同分时选「电容的 +3V3 脚离芯片电源脚更近」的那个朝向 ——
这样两颗焊盘之间那段回路才是最短的。
"""
out = []
for cap, ic, pin, _m in DECAP:
if cap in POS:
continue
tx, ty = pad_xy(ic, pin)
best = None
for k in range(1, 46): # 0.7 -> 5.2 mm,0.1 一档
rr = 0.6 + k * 0.1
for j in range(36): # 10° 一档
a = 2 * math.pi * j / 36
cx, cy = tx + rr * math.cos(a), ty + rr * math.sin(a)
for rot in (0, 90, 180, 270):
if not _fits(cap, cx, cy, rot):
continue
px, py = _pad_net_xy(cap, cx, cy, rot, '+3V3')
d = math.hypot(px - tx, py - ty)
if best is None or d < best[0]:
best = (d, cx, cy, rot)
if best is not None:
break
if best is not None:
break
assert best is not None, u'%s 在 %s.%s 周围 5 mm 内找不到位置' % (cap, ic, pin)
put(cap, best[1], best[2], best[3])
out.append((cap, ic, pin, best[0]))
return out
def place_sats():
u"""把 `SAT` 表里的小件贴到**对端引脚**旁边。找位置的办法同 `place_decaps()`:
绕着目标脚一圈圈往外试,取第一个放得下的。"""
out = []
for des, ic, pin, _m, _why in SAT:
if des in POS:
continue
tx, ty = pad_xy(ic, pin)
best = None
for k in range(1, 96): # 0.7 -> 10.2 mm,0.1 一档
rr = 0.6 + k * 0.1
for j in range(36):
a = 2 * math.pi * j / 36
cx, cy = tx + rr * math.cos(a), ty + rr * math.sin(a)
for rot in (0, 90):
if _fits(des, cx, cy, rot):
d = math.hypot(cx - tx, cy - ty)
if best is None or d < best[0]:
best = (d, cx, cy, rot)
if best is not None:
break
if best is not None:
break
assert best is not None, u'%s 在 %s.%s 周围 10 mm 内找不到位置' % (des, ic, pin)
put(des, best[1], best[2], best[3])
out.append((des, ic, pin, best[0]))
return out
DECAP_PLACED = place_decaps()
SAT_PLACED = place_sats()
MIN_CLEARANCE = 0.2 # pcb1_rules.MIN_CLEARANCE,DRC 里是 7.9 mil
def all_pad_boxes():
u"""全板每个焊盘的绝对外框 [(x0,y0,x1,y1, 位号, 引脚, 网络), ...]。"""
out = []
for d, (x, y, r) in POS.items():
a = math.radians(r)
ct, st = math.cos(a), math.sin(a)
for num, lx, ly, w, h, net in PARTS[d]['pads']:
lx, ly, w, h = lx / MIL, ly / MIL, w / MIL, h / MIL
if r in (90, 270):
w, h = h, w
px, py = x + lx * ct - ly * st, y + lx * st + ly * ct
out.append((px - w / 2.0, py - h / 2.0, px + w / 2.0, py + h / 2.0,
d, str(num), net or ''))
return out
def pad_clearance_errs(limit=MIN_CLEARANCE):
u"""跨元件的**焊盘到焊盘**净距。
⚠️ 2026-09-06 加。原来 check() 只查「元件包围盒重不重叠」——
**不重叠不等于够净距**:C607 贴到 U6 北排脚旁边时,包围盒还差 0.358 mm 不碰,
DRC 实测焊盘间只有 0.096 mm(3.8 mil,规则要 7.9)。
「本地六项全过、DRC 四条间距错误」就是这么来的。
这一条直接照着 DRC 那条规则算,量的是焊盘不是包围盒。
同一个元件内部不查(封装自带的间距是厂商定的,改不了);
同网络也不查(同网铜可以贴在一起)。
"""
boxes = all_pad_boxes()
cell = 2.0
grid = {}
for i, b in enumerate(boxes):
for gx in range(int((b[0] - limit) // cell), int((b[2] + limit) // cell) + 1):
for gy in range(int((b[1] - limit) // cell), int((b[3] + limit) // cell) + 1):
grid.setdefault((gx, gy), []).append(i)
errs, seen = [], set()
for ids in grid.values():
for ii in range(len(ids)):
for jj in range(ii + 1, len(ids)):
a, b = boxes[ids[ii]], boxes[ids[jj]]
if a[4] == b[4] or (a[6] and a[6] == b[6]):
continue
key = (min(ids[ii], ids[jj]), max(ids[ii], ids[jj]))
if key in seen:
continue
seen.add(key)
dx = max(a[0] - b[2], b[0] - a[2], 0.0)
dy = max(a[1] - b[3], b[1] - a[3], 0.0)
d = math.hypot(dx, dy)
if d < limit - 1e-6:
errs.append((d, u'焊盘净距不足: %s.%s(%s) 到 %s.%s(%s) 只有 %.3f mm(要 %.2f)'
% (a[4], a[5], a[6] or '-', b[4], b[5], b[6] or '-', d, limit)))
errs.sort()
return [m for _d, m in errs]
def check():
errs = []
# ⭐ 焊盘到焊盘净距(DRC 那条 7.9 mil 的规则,本地先算一遍)
errs += pad_clearance_errs()
# ⭐ 去耦电容必须贴着本尊的电源脚(见 DECAP 表上面那段)
for cap, ic, pin, dmax in DECAP:
px, py = _pad_net_xy(cap, POS[cap][0], POS[cap][1], POS[cap][2], '+3V3')
tx, ty = pad_xy(ic, pin)
d = math.hypot(px - tx, py - ty)
if d > dmax + 1e-6:
errs.append(u'去耦太远: %-6s 的 +3V3 脚离 %s.%s 有 %.2f mm(上限 %.1f)'
% (cap, ic, pin, d, dmax))
missing = sorted(set(PARTS) - set(POS))
if missing:
errs.append(u'没安排位置: %d 个 %s' % (len(missing), ' '.join(missing[:12])))
for d in sorted(POS):
x0, y0, x1, y1 = abs_box(d)
m = 0.4
if x0 < BX0 + m or x1 > BX1 - m or y0 < BY0 + m or y1 > BY1 - m:
errs.append(u'越出板框: %-6s 框 %.2f..%.2f / %.2f..%.2f' % (d, x0, x1, y0, y1))
r = MH_KEEPOUT / 2.0
for d in sorted(POS):
ax0, ay0, ax1, ay1 = abs_box(d)
for hx, hy in MH:
dx = max(ax0 - hx, 0.0, hx - ax1)
dy = max(ay0 - hy, 0.0, hy - ay1)
if dx * dx + dy * dy < r * r:
errs.append(u'压定位孔禁布区: %-6s 撞孔 (%.0f, %.0f)' % (d, hx, hy))
items = sorted(POS)
boxes = {d: abs_box(d) for d in items}
for i in range(len(items)):
a = items[i]; A = boxes[a]
for j in range(i + 1, len(items)):
b = items[j]; B = boxes[b]
if A[0] < B[2] - 0.01 and B[0] < A[2] - 0.01 and A[1] < B[3] - 0.01 and B[1] < A[3] - 0.01:
errs.append(u'重叠: %s(%.1f,%.1f) x %s(%.1f,%.1f)'
% (a, POS[a][0], POS[a][1], b, POS[b][0], POS[b][1]))
# ⭐ 高度预算:载板底面的东西决定下板同一 (x,y) 能放多高
for d in items:
h = height(d)
if h is None:
continue
lim = hmax_over(boxes[d])
if h > lim + 1e-6:
errs.append(u'高度超限: %-6s h=%.1f > 允许 %.1f 框 %.1f..%.1f / %.1f..%.1f'
% (d, h, lim, boxes[d][0], boxes[d][2], boxes[d][1], boxes[d][3]))
# ⭐ J24 / J25:焊盘阵列要和载板锚点重合,而且要核清楚**哪根针压在哪根针上**
for hdr in ('J24', 'J25'):
m = mate_map(hdr)
off = [pin for pin, v in m.items() if v[0] is None]
if off:
errs.append(u'%s 有 %d 个焊盘不在载板任何一根针的正下方(阵列没对上): %s'
% (hdr, len(off), off[:6]))
continue
# ⭐ 判据是**网络**不是针号(§26.5:下板 pin(2r−1) 正对载板 pin(2r))
wrong = [(pin, v[2], v[0], v[3]) for pin, v in m.items() if v[2] != v[3]]
if wrong:
wrong.sort(key=lambda t: int(t[0]))
errs.append(u'%s 有 %d 根针对错网络!前 4 条: %s' % (hdr, len(wrong),
' / '.join(u'pin%s(%s)->载板pin%s(%s)' % t for t in wrong[:4])))
return errs
def mate_map(hdr):
u"""下板这颗排针的每个焊盘,正下方压着载板的哪一根针。
返回 {下板 pin: (载板 pin 或 None, 距离 mm)}。
⚠️ 这是整块板唯一「对不上就要重打样」的几何量,别用引脚号推理,只认坐标。
"""
x, y, rot = POS[hdr]
pads = {p[0]: (p[1] / MIL, p[2] / MIL, p[5]) for p in PARTS[hdr]['pads']}
anc = ENV['anchors'][hdr]
out = {}
for pin, (px, py, net) in pads.items():
bx, by = _rotbox((px, py, px, py), rot)[0:2]
gx, gy = x + bx, y + by
best, bd = None, 1e9
for apin, (ax, ay, _anet) in anc.items():
d = math.hypot(gx - ax, gy - ay)
if d < bd:
best, bd = apin, d
out[pin] = (best if bd < 0.05 else None, bd, net,
anc[best][2] if best is not None else None)
return out
def mate_report():
for hdr in ('J24', 'J25'):
m = mate_map(hdr)
okn = sum(1 for p in m if m[p][2] == m[p][3])
print(u'%s: %d/%d 个焊盘落在载板针正下方,其中**网络一致 %d 个**'
% (hdr, sum(1 for p in m if m[p][0]), len(m), okn))
for p in ('1', '2', '3', '4', '39', '40'):
v = m[p]
print(u' 下板 pin%-3s %-12s -> 载板 pin%-3s %-12s %s'
% (p, v[2], v[0], v[3], u'✔' if v[2] == v[3] else u'✘'))
# ══════════ 出线道加宽(2026-09-06 第二十六轮,用户拍板)══════════
#: **「够净距」不等于「出得去线」。** `pad_clearance_errs` 判的是焊盘净距 ≥ 0.20,
#: 而一根 0.2 mm 的线要从两个焊盘中间出去,需要 0.20 + 0.20 + 0.20 = **0.60**。
#: U6 / U7(WQFN-16 3×3,四边出脚)周围的卫星件正好卡在 **0.31 ~ 0.38** ——
#: 本地六项全过、DRC 也没有间距错误,可 `pcb1_escape_audit` 报「被围死」、
#: 布线器报「几何上就出不去」,`SCL`(U6.6) / `$6N532`(U6.16) / `$6N547`(U7.16)
#: 三个网络因此永远布不通。这里把堵住出线道的六颗按**背离芯片**的方向各挪 0.30 mm。
#:
#: ⚠️ 挪完 `C607` / `R617` 会分别压到 `SDA` / `SCL` 的换层孔(净距 0.029 / 0.113)——
#: 那两个孔是 `pcb1_i2c_l3.py` 每轮重算的,**挪件之后必须重跑它**,孔会自己让开。
#:
#: ⚠️ §26.22b:门槛不是 0.60 而是 **0.741**(CLR + 半线宽 + 栅格量化余量 QERR,两侧各 0.3707)。
#: 0.30 挪出来的 0.612/0.615/0.676 理论上塞得下、0.1 栅格上塞不下 ⇒ 2026-09-06 第二十八轮
#: 改成 **0.50(东列 0.45)**,三个方向都 ≥ 0.80。板上现在(`_pcb1_place_now.json`)是按 0.30 摆的,
#: 再挪 0.20 / 0.15 会压到的铜见 `.claude/_pcb1_unrouted_report.md` §A(VDAC4 孔、SDA 线+孔,
#: 以及 10 段要按新位置重做的扇出短线)。
#: ⚠️⚠️ **这一手救不了 `U6.16` / `U7.16`**(`pcb1_ch_reroute.py` 本地实测:挪完照样「几何上就出不去」)——
#: 它们被 C609 / C612 的 GND 脚 + GND 扇出孔从西边、C607 / C610 从北边、U6.15 的 +3V3 出线段从东边
#: 三面围死,北挪 C607 只是把北墙推远,西墙东墙不动。真正的解法是下面的 `SAT_FLIP`。
ESCAPE_NUDGE = {
'R615': (0.0, -0.50), # U6 南排(U6.5 / U6.6=SCL) 0.312 -> 0.812
'C607': (0.0, +0.50), # U6 北排(U6.13/15/16) 0.315 -> 0.815
'C608': (+0.45, 0.0), # U6 东列(VDAC2 / VDAC4) 0.376 -> 0.826
'R617': (0.0, -0.50), # U7 南排(U7.5 / U7.6) 0.315 -> 0.815
'C610': (0.0, +0.50), # U7 北排(U7.13/15/16) 0.312 -> 0.812
'C611': (+0.45, 0.0), # U7 东列(VDAC3 / VDAC5) 0.376 -> 0.826
}
for _d, (_dx, _dy) in sorted(ESCAPE_NUDGE.items()):
if _d in POS:
_x, _y, _r = POS[_d]
POS[_d] = (round(_x + _dx, 3), round(_y + _dy, 3), _r)
# ══════════ VREF 电容转 180°(2026-09-06 第二十八轮,A 类唯一真正的几何解)══════════
#: `C609` / `C612` 是 DAC63204 的 VREF 电容(pin 16),`place_sats()` 把它们竖着(rot 90)贴在
#: U6.16 / U7.16 的西北角:**GND 脚在南(离引脚近)、VREF 脚在北(离引脚远)**,
#: GND 扇出孔又打在 GND 脚正南 —— 于是 U6.16 往西北去找 VREF 脚的那条道被自家 GND 孔 + GND 短线堵死。
#: 转 180°(rot 90 -> 270)之后 VREF 脚朝南正对 U6.16(0.8 mm),GND 脚朝北。
#: `pcb1_ch_reroute.py --win 73,32,87,47 --nets '$6N532' --move C609:0:0:180 --drop-via 78.03,39.61`
#: 本地实测:`$6N532` 3.6 mm 一次布通、净距 0;U7 同理。下发时要删 2 段扇出短线 + 补 1 个 GND 孔
#: (见 `_pcb1_unrouted_report.md` §A)。
SAT_FLIP = {'C609': 180, 'C612': 180}
for _d, _dr in sorted(SAT_FLIP.items()):
if _d in POS:
_x, _y, _r = POS[_d]
POS[_d] = (_x, _y, (_r + _dr) % 360)
def summary():
ext = ch_extent()
print(u'母版局部包围盒 x %.2f~%.2f y %.2f~%.2f(%.1f x %.1f)'
% (ext[0], ext[2], ext[1], ext[3], ext[2] - ext[0], ext[3] - ext[1]))
for n in sorted(CH_PLACE):
ds = [ch_designator(t, n) for t, _, _, _ in CH1]
bs = [abs_box(d) for d in ds]
print(u' CH%d rot%-4d x %6.2f~%6.2f y %6.2f~%6.2f'
% (n, CH_PLACE[n][0], min(b[0] for b in bs), max(b[2] for b in bs),
min(b[1] for b in bs), max(b[3] for b in bs)))
print()
print(u'去耦贴身(+3V3 脚 -> 芯片电源脚,上限见 DECAP 表):')
for cap, ic, pin, dmax in DECAP:
px, py = _pad_net_xy(cap, POS[cap][0], POS[cap][1], POS[cap][2], '+3V3')
tx, ty = pad_xy(ic, pin)
d = ((px - tx) ** 2 + (py - ty) ** 2) ** 0.5
print(u' %-6s -> %-4s.%-3s %.2f mm(上限 %.1f) 摆在 (%.2f, %.2f) rot %d'
% (cap, ic, pin, d, dmax, POS[cap][0], POS[cap][1], POS[cap][2]))
# ════════════════════════════ 下发 ════════════════════════════
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_parts():
plan = [[d, round(x * MIL, 2), round(y * MIL, 2), r] for d, (x, y, r) in sorted(POS.items())]
total = 0
for k in range(0, len(plan), 60):
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] of P) {
const c = map[d]; if (!c) continue;
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {layer: 1});
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {x, y, rotation: rot});
n++;
}
return n;
""" % json.dumps(plan[k:k + 60]))
print(u' 已摆 %d / %d' % (total, len(plan)))
call("await eda.pcb_Document.save(); return 1;")
print(u'布局已下发并保存:%d 个元件' % total)
def verify():
u"""回读板上的真实焊盘,重跑 J24/J25 的对位核算(本地模型可能和板上不一致)。"""
r = call("""
const M = %s;
const out = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.getState_Designator();
if (d !== 'J24' && d !== 'J25') 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.net]]))};
}
return out;
""" % MIL)
ok = True
for hdr in ('J24', 'J25'):
d = r[hdr]
anc = ENV['anchors'][hdr]
print(u'%s @ (%.2f, %.2f) rot %d layer %d' % (hdr, d['x'], d['y'], d['rot'], d['layer']))
mism = []
for pin, (px, py, net) in sorted(d['pins'].items(), key=lambda t: int(t[0])):
best, bd = None, 1e9
for apin, (ax, ay, _n) in anc.items():
dd = math.hypot(px - ax, py - ay)
if dd < bd:
best, bd = apin, dd
anet = anc[best][2] if best is not None else None
if bd > 0.05 or net != anet:
mism.append((pin, net, best, anet, round(bd, 3)))
if mism:
ok = False
print(u' ✘ %d / %d 个焊盘对错网络(或没对上位置);前 6 条:' % (len(mism), len(d['pins'])))
for t in mism[:6]:
print(u' 下板 pin%-3s %-12s 压住载板 pin%-3s %-12s 距 %.3f' % t)
else:
print(u' ✔ 40 个焊盘逐个**网络对上**载板(针号按 §26.5 差一位,是预期的)')
return ok
if __name__ == '__main__':
summary()
e = check()
print(u'\n安排了 %d / %d 个元件' % (len(POS), len(PARTS)))
if e:
print(u'!! %d 个问题:' % len(e))
for t in e[:60]:
print(' ', t)
if len(e) > 60:
print(u' ... 还有 %d 条' % (len(e) - 60))
sys.exit(1)
print(u'本地检查通过:焊盘净距≥0.2 / 无重叠 / 无越框 / 不压定位孔 / 高度预算全过 / 排针阵列对上锚点 / 去耦贴身')
if WARN:
print(u'')
print(u'!! %d 条需要人拍板的:' % len(WARN))
for w in WARN:
print(u' ⚠ %s' % w)
mate_report()
if '--push' in sys.argv:
push_parts()
if '--verify' in sys.argv:
if not verify():
sys.exit(1)