初始导入:PhoneRail 五路手机直供电源板

原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
这个提交包含在:
zhou yang
2026-09-06 18:18:14 +08:00
共同撰写人 Claude Opus 5
当前提交 2bfe308b30
共修改 526 个文件,包含 80633 行新增和 0 行删除
+958
查看文件
@@ -0,0 +1,958 @@
# -*- 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
BW, BH = 160.0, 125.0
CORNER_R = 5.0
MH = [(5.0, 5.0), (BW - 5.0, 5.0), (5.0, BH - 5.0), (BW - 5.0, BH - 5.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 公头(90°板装)
'CONN-TH_2510-3A': (-3.81, -2.88, 3.81, 2.93), # 风扇 3P **直针**座
# 下面两条是换下来的,留着当对照(**别再用**):
'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° 板装公头,本体躺着
'CONN-TH_2510-3A': 9.0, # 2510 直针座,塑壳竖立
# 排针本身就是对插件,不参与高度核算
'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),
# 按键驱动(远离 SW)
('Q%03', 24.0, 12.0, 0), ('Q%04', 28.0, 12.0, 0),
('Q%05', 24.0, 16.0, 0), ('R%20', 28.0, 16.0, 0),
('R%21', 24.0, 19.5, 0), ('R%22', 28.0, 19.5, 0),
('R%23', 24.0, 22.5, 0), ('R%24', 28.0, 22.5, 0),
('R%25', 24.0, 25.5, 0),
]
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),改**两行**。
put('J1', 8.5, 72.0, 270) # 12V XT30 公头,插口朝左板边(rot 270)
# ⚠⚠ **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', 17.5, 74.5, 90) # 立着放:F1.1(VIN_RAW) 在南、F1.2(VIN_FUSED) 在北
put('Q1', 24.0, 74.5, 270) # SOP-8 P-MOS 防反接
put('D1', 31.0, 74.5, 270) # SMBJ13A TVS(挪出 VIN_RAW 通道,贴在漏极铜东缘)
put('DZ1', 22.0, 68.0, 0) # 栅极钳位
put('C1', 25.5, 68.0, 0)
put('R1', 28.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)
# 第三片 595(EDL_EN1~5)+ 风扇低边驱动
put('U33', 92.0, 71.0, 0)
put('Q6', 102.0, 69.5, 0)
put('D2', 108.5, 69.5, 0)
put('C738', 102.0, 74.0, 0)
put('C739', 107.0, 74.0, 0)
put('R730', 98.0, 78.0, 0)
put('R731', 101.5, 78.0, 0)
put('R732', 105.0, 78.0, 0)
put('R733', 98.0, 80.5, 0)
put('R734', 101.5, 80.5, 0)
# 风扇座:4 个侧吸 + 1 个顶排,全部放板边(§22.6)
put('J26', 5.0, 16.0, 0) # 左板边(南)
put('J27', 5.0, 30.0, 0)
put('J29', 154.9, 16.0, 0) # 右板边(南)
put('J30', 154.9, 30.0, 0)
put('J28', 100.0, 112.7, 0) # 顶板边一侧;弯针塑壳往 +Y 伸 11.7,壳顶顶到 y 124.4
# ══════════════════ 【S-M】P3 / P3B 控制块 ══════════════════
# 南中块 x 69.5~119.5 / y 0.5~59
put('U20', 88.0, 18.2, 180) # ESP32-S3:天线端转到 −Y 朝下板边
put('J22', 154.4, 52.0, 90) # 调试 USB-C,rot 90 = 开口朝 +X 右板边
# (下板边放不下: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', 'U21', '10', 3.0), # 74HC595 #1 VCC
('C605', 'U22', '10', 3.0), # 74HC595 #2 VCC
('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),
'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 上拉'),
('R609', 'U21', '13', 7.0, u'KEY_OE# 上拉(三片 595 共用)'),
('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] < 0.4 or ab[2] > BW - 0.4 or ab[1] < 0.4 or ab[3] > BH - 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 < m or x1 > BW - m or y0 < m or y1 > BH - 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'✘'))
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)