初始导入: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
这个提交包含在:
@@ -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)
|
||||
在新工单中引用
屏蔽一个用户