文件
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

632 行
34 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""PCB1 布局。
板框 **160 × 125 mm**,四角 R5 圆角,四个 M3 金属化定位孔(接 GND)。
原点在**左下角**,**Y 向上**,API 单位是 **mil**。
y=125 ── 上边:每路只有 J2~J6 电源 Micro-Fit(右角式,本体顶到板边)
【A】五路功率级 y 67~125,每路 32 mm 宽,同方向横排(§10.3)
y= 67 ──
【B】J1(左) | 12V→5V | P1 输入 | P3/P3B 控制 | U20+J22(右) y 31~67
y= 31 ──
【C】J23 上游 USB-C + 板载 Hub + 五路负载开关 + J17~J21 出线 y 0~31
y= 0 ── 下边(唯一的外部 USB 口 J23 在这条边上)
**四角定位孔和 band A 的连接器是打架的**,解开靠两条(都写在 CH1 表里):
① J7~J11(按键出线)是**立式**座,本来就不需要板边 —— 挪进通道内部 y=31,
顶边只留 J2~J6 一个 12 mm 宽的右角式座;
② J2 摆在通道中央(local x=15,本体 x 9~21),左右各让出 9 mm,
四角的 Φ6.5 禁布区就落在空处。
不这么做的话,5 × 32 = 160 正好铺满板宽,CH1 / CH5 顶角必然被连接器本体压住。
五路功率级是**显式手排**(§10.2 的热回路 / Kelvin / FB 要求靠手排保证),
CH1 排好后按 32 mm 节距复制五份。
P1 / P3 / P3B 的无源件按**原理图坐标仿射映射**进各自分区,再跑一遍分离迭代消重叠。
用法: /c/Python314/python .claude/pcb_place.py [--push]
/c/Python314/python .claude/pcb_place.py --outline 只重画板框与定位孔
"""
import io, json, math, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
PCB = '7e9415a16a909c51'
MIL = 39.3701 # 1 mm = 39.3701 mil
BW, BH = 160.0, 125.0 # 元件区 mm(原点左下角,坐标系不变)
# 2026-09-06 用户指令:两板尺寸取大对齐 —— 下板板框放大到载板的 220×135,原点不动,
# 板框左下角在 (−30,−5)(下板坐标 = 载板坐标 − (30,5),见 pcb1_env.py)。
OX, OY, OW, OH = -30.0, -5.0, 220.0, 135.0
CORNER_R = 5.0 # 四角圆角半径 mm
# 定位孔:M3 金属化,孔 Φ3.2 / 焊盘 Φ6.0 / 禁布 Φ6.5,孔心与圆角同心
# 定位孔与载板 pcb2_place.MH 对齐(减 (30,5)):四角 + 两侧中点;事实来源是 pcb1_place.MH,这里同表
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
# ── 封装占位尺寸(mm,含焊盘;未旋转时 x 向宽 w、y 向高 h)──────────────
FP = {
'R0402': (1.6, 0.9), 'C0402': (1.6, 0.9), 'C0402_NEW': (1.6, 0.9),
'R0603': (2.2, 1.2), 'C0805': (2.8, 1.6), 'C1206': (3.8, 1.9),
'R2512': (7.4, 3.6),
'SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR': (3.1, 2.8),
'SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BR': (3.1, 3.2),
'MSOP-10_L3.0-W3.0-P0.50-LS5.0-BL': (3.2, 5.2),
'VQFN-14_L3.5-W3.5-P0.50-TL-EP_TPS54622RHLT': (3.7, 3.7),
'WQFN-20_L4.0-W3.0-P0.50-BL-EP': (4.2, 3.2),
'WQFN-16_L3.0-W3.0-P0.50-TL-EP1.7': (3.2, 3.2),
'SOP-8_L5.0-W4.0-P1.27-LS6.0-BL': (5.1, 6.2),
'SOIC-16_L9.9-W3.9-P1.27-LS6.0-BL': (10.0, 6.2),
'TSSOP-24_L7.8-W4.4-P0.65-LS6.4-BL': (7.9, 6.6),
'IND-SMD_L7.1-W6.5': (7.2, 6.6), 'IND-SMD_L4.0-W4.0': (4.1, 4.1),
'SMB_L4.6-W3.6-LS5.3-RD': (4.7, 5.4),
'SOD-123_L2.7-W1.7-LS3.8-RD': (2.8, 3.9),
'FUSE-SMD_L3.2-W1.6-1': (3.3, 1.7),
'CAP-TH_BD8.0-P3.50-D0.6-FD': (8.2, 8.2),
'RES-TH_L13.5-W9.5-P5.80-D0.7': (13.7, 9.7),
'CONN-TH_2P-P3.00_MOLEX_436500200': (8.6, 10.0),
'CONN-SMD_4P-P1.25_A1251WV-S-4P': (6.2, 5.6),
'USB-A-TH_KINGHELM_KH-AF90ZJ-13.5JB-1': (14.2, 14.0),
'CONN-TH_P5.08_KF128-5.08-2P': (10.4, 10.4),
'USB-C_SMD-TYPE-C-31-M-12_1': (9.2, 7.6),
'WIRELM-SMD_ESP32-S3-WROOM-1': (18.2, 25.7),
# ── 板载 USB Hub 改造新增(2026-09-03)──
'SOT-223-3_L6.5-W3.4-P2.30-LS7.0-BR': (6.6, 3.5), # U31 AMS1117-3.3
'QFN-64_L9.0-W9.0-P0.50-TL-EP4.8': (9.1, 9.1), # U32 USB2517I
'CRYSTAL-SMD_4P-L3.2-W2.5-BL': (3.3, 2.6), # Y1 24MHz
}
PARTS = {c['d']: c for c in json.load(io.open(os.path.join(HERE, 'pcb-parts.json'), encoding='utf-8'))}
SCH = json.load(io.open(os.path.join(HERE, 'sch-xy.json'), encoding='utf-8'))
POS = {} # des -> (x_mm, y_mm, rot)
# ── 真实焊盘外框(相对元件原点,mm,未旋转)──────────────────────────────
# 从 pcb-pads.json 里挑 rot==0 的实例取,**不对称的封装靠它**(USB-A 就是)。
# ⚠️ 手量的**不对称本体框**(mm,未旋转,相对元件原点)。
# API 取不到封装本体外形,这两个是对着渲染图量出来的,**要对连接器图纸复核**。
# 右角式连接器的本体本来就该伸到板边,所以这两个框决定它们能贴多近。
FP_BODY = {
'USB-A-TH_KINGHELM_KH-AF90ZJ-13.5JB-1': (-6.1, -10.7, 6.1, 1.0), # J12~J16 已删,留作参考
# 2026-09-03 二次实测修正:原来手量成 12 x 13.5,实际本体只有 **9.8 x 10.1**
# (对得上 Micro-Fit 3.0 43650 两位右角式的 8.6 x 10.1)。偏大 4 mm 的后果是
# J2~J6 离板边差 4 mm —— check() 以为贴着边,渲染图上量出来才发现。
'CONN-TH_2P-P3.00_MOLEX_436500200': (-4.95, -9.0, 4.95, 1.4),
# USB-C:16 个焊盘在 +Y 一侧,壳体与 4 个固定脚往 -Y 伸约 5.2 mm ——
# 也就是 rot=0 时**开口朝 -Y**。板边口要转到开口朝板外那一侧。
'USB-C_SMD-TYPE-C-31-M-12_1': (-4.93, -5.2, 4.93, 3.12),
# ESP32-S3-WROOM-1:模组 18.0 x 25.5,焊盘只在三面,**天线那一端(+Y)没有焊盘**,
# 而封装原点在焊盘形心上,所以本体往 +Y 比焊盘框多伸约 7.4 mm。
# 这是 U20 整块压出板框却查不出来的原因。
'WIRELM-SMD_ESP32-S3-WROOM-1': (-9.5, -9.35, 9.5, 16.75),
}
FP_PAD = {}
for _c in json.load(io.open(os.path.join(HERE, 'pcb-pads.json'), encoding='utf-8')):
if (_c['off'] and _c['rot'] == 0 and _c['fp'] not in FP_PAD
and all(v is not None for v in _c['off'])):
FP_PAD[_c['fp']] = tuple(v / MIL for v in _c['off'])
def _rotbox(b, rot):
x0, y0, x1, y1 = b
if rot % 360 == 90:
return (-y1, x0, -y0, x1)
if rot % 360 == 180:
return (-x1, -y1, -x0, -y0)
if rot % 360 == 270:
return (y0, -x1, y1, -x0)
return b
def box(des, rot=0):
u"""元件相对原点的占位框(mm):标称本体框 ∪ 真实焊盘框,再按 rot 旋转。
⚠️ 这里**没有封装的本体外形**——API 取不到(`pcb_PrimitiveLine.getAll()`
只返回板框那 4 条,元件内部的丝印/外形图元不外露)。所以「本体伸出板框」
这一类只能靠截图人眼看,USB-A 那次就是这么发现的。
"""
fp = PARTS[des]['fp']['name']
if fp in FP_BODY:
b = FP_BODY[fp]
else:
w, h = FP[fp]
b = (-w / 2.0, -h / 2.0, w / 2.0, h / 2.0)
pb = FP_PAD.get(fp)
if pb:
b = (min(b[0], pb[0]), min(b[1], pb[1]), max(b[2], pb[2]), max(b[3], pb[3]))
return _rotbox(b, rot)
def size(des, rot=0):
b = box(des, rot)
return (b[2] - b[0], b[3] - b[1])
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)
# ══════════════ 【A】五路功率级:CH1 手排,其余四路平移复制 ══════════════
# 局部坐标原点 = 本路分区左下角,分区 32 (x) × 58 (y),全局 y0 = 67。
# 自下而上的信号流:12V/反馈 -> Buck -> 电感/输出 -> eFuse -> 分流+检流 -> 连接器。
# 左列 x 1~21 走功率,右列 x 22~31 放按键驱动(慢速信号,远离 SW)。
#
# 200×150 缩到 160×125 时,功率链那一段(y 3~42)**一个件都没动** ——
# §10.2 的四条关键回路是手调出来的,缩板不该动它们。动的只有三处:
# 1. J2 从 local x=7 挪到 15(本体 x 9~21),给四角定位孔让出左右各 9 mm;
# 2. J7 从顶边 (30,56) 挪到板内 (25.5,31),它是立式座,不需要板边;
# 3. 按键驱动那 9 个件从「J2 右边的一横排」改成右列 x 24/28 的纵排,
# 而且全部压到 y<=26 —— CH5 的这一列正对右上角那个定位孔。
CH_W, CH_Y0, CH_H = 32.0, 67.0, 58.0
# (后缀, 局部 x, 局部 y, rot);后缀里的 % 会被通道号替换
CH1 = [
# ── 反馈 / 调压 / 补偿(最下面一排,远离 SW,紧挨下一带的 DAC)──
('R%01', 3.0, 3.0, 0), ('R%02', 3.0, 5.2, 0), # 分压 30.1k / 5.49k
('R%07', 6.5, 3.0, 0), ('R%08', 6.5, 5.2, 0), # Rinj 150k / DAC 串阻 1k
('C%12', 10.0, 5.2, 0), # DAC 低通 10nF
('R%06', 10.0, 3.0, 0), ('C%10', 13.5, 3.0, 0), ('C%11', 13.5, 5.2, 0), # COMP
('R%03', 17.0, 3.0, 0), # RT 100k
('R%04', 17.0, 5.2, 0), ('R%05', 20.0, 5.2, 0), # EN UVLO
('C%09', 20.0, 3.0, 0), # SS/TR
# ── Buck 功率级:C%01/C%02 贴着 U% 的 PVIN/GND,热回路最小(§10.2-1)──
('U%__1', 8.5, 12.0, 0), # TPS54622 (占位名,下面换)
# ⚠️ C%02 的 14.95 不是笔误:14.9 时两片焊盘只剩 0.1905 mm 缝,
# 差 0.2 mm 安全间距 10 µm,五路一起报 DRC。挪 0.05 mm 得 0.2405 mm,
# 对约 4 mm 周长的 Cin 热回路可以忽略(2026-09-03 DRC 实测改的)。
('C%01', 4.0, 11.3, 90), ('C%02', 4.0, 14.95, 90), # Cin 10uF 紧贴 VIN
('C%03', 4.0, 7.7, 90), ('C%04', 13.0, 9.0, 90),
('C%05', 11.5, 16.2, 90), # BOOT
# ── 电感与输出电容 ──
('L%01', 6.5, 20.5, 0),
('C%06', 14.0, 23.4, 90), ('C%07', 14.0, 18.4, 90), ('C%08', 17.5, 20.9, 90),
# ── eFuse ──
('U%__14', 6.0, 29.5, 0), # TPS25940 (占位名)
('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:U%8 就在 Rs 正下方,取样线并行)──
('Rs%01', 6.0, 40.5, 0),
('U%__8', 6.0, 35.5, 0), # INA226 (占位名)
('C%13', 10.5, 35.5, 0),
# ── 放电联锁:Q%01 反相 -> Q%02 放电 -> R%19 10R ──
# ⚠️ R%19 是 **2512(7.4 x 3.6)**,不是 0402 —— 横着放会横跨半个通道。
# 原来它在 (15.5,45),正好落在 J2 新位置的本体底下,所以整块挪到 x 12~19.6,
# 并把 R%19 转 90 度竖起来(3.6 宽 x 7.4 高)才塞得进 Q%02 与 J2 之间。
('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),
# ── 连接器 ──
# J2 右角式,转 180 让开口朝上板边,本体 y 45.1..58 顶到 y=125;
# 摆在通道中央(本体 x 9~21)而不是 x=7,四角定位孔才放得下。
('J%__J2', 15.0, 49.0, 180), # 本体 y 47.6..58,顶到 y=125 板边
# J7 立式 1.25mm 4P,挪进板内 —— 它不需要板边,让出顶角
('J%__J7', 25.5, 31.0, 0),
# ── 按键驱动(右列 x 24/28,远离 SW 节点,§10.3;全部压在 y<=26)──
('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):
u"""把模板里的位号记号换成第 n 路的真实位号。"""
if tok.startswith('U%__'): # U1~U5 / U14~U18 / U8~U12
base = tok[4:]
return {'1': 'U%d' % n, '14': 'U%d' % (13 + n), '8': 'U%d' % (7 + n)}[base]
if tok.startswith('J%__'): # J2~J6 / J7~J11
return {'J2': 'J%d' % (1 + n), 'J7': 'J%d' % (6 + n)}[tok[4:]]
return tok.replace('%', str(n))
for n in range(1, 6):
ox = (n - 1) * CH_W
for tok, lx, ly, rot in CH1:
put(ch_designator(tok, n), ox + lx, CH_Y0 + ly, rot)
# ══════════════════════ 【C】USB 子系统(y 0~31)═══════════════════════
# 从左到右:J23 上游 USB-C + U32 Hub(x 5~62)| 五路 TPS22918 + 出线(x 15~148)。
# 五个出线座 J17~J21 与上面五路的电源座 J2~J6 **同一 x**(15/47/79/111/143),
# 将来换板对板连接器时两排能对齐。
# D± 全程留在这一带,不纵穿五路 Buck 的电感与 SW 铜箔(§15.3)。
# 12V→5V 那一段挪去【B】带了:它是开关电源,离 D± 越远越好,而 12V 在【B】带的左边。
# ── 上游口与 Hub(摆在板**中央**)───────────────────────────────────
# U32 放中间是为了 D±:靠左边的话到 CH5 要走 112 mm,放中央之后
# 五路变成 64 / 32 / 0 / 32 / 64 mm,最长砍掉一半而且左右对称。
# J23 塞在 J19 与 J20 之间的空档,上游 D± 只有 16 mm。
# ⚠️ U32 正上方就是 CH3 的负载开关块(y 10.8~15.95),所以 U32 的去耦
# **不能像原来那样在正下方排一横排** —— 改成上面一排 + 左边一列。
put('J23', 95.0, 5.4, 0) # USB-C UFP,开口朝 -Y = 下板边
put('U32', 79.0, 22.0, 0) # USB2517I QFN-64 9x9
put('Y1', 88.5, 22.0, 0) # 24 MHz 晶振,紧贴 XTAL 脚
put('C740', 88.5, 18.5, 0)
put('C741', 88.5, 25.5, 0)
put('R708', 93.0, 22.0, 0) # RBIAS 12k,必须靠近 U32
put('U31', 64.0, 27.0, 0) # AMS1117-3.3 只给 U32;SOT-223 要铺铜散热(§10)
put('C736', 58.5, 22.0, 0) # U31 输入 5V
put('C737', 63.0, 22.0, 0) # U31 输出 3V3_HUB
# U32 的去耦:上面一排 4 颗 + 左边一列 3 颗 + 0805 体电容。手排——
# 仿射映射会把它们甩到页边,去耦离脚远等于白放。
for _i, _d in enumerate(['C744', 'C745', 'C746', 'C747']):
put(_d, 74.0 + _i * 2.5, 29.0, 0)
for _i, _d in enumerate(['C748', 'C749', 'C750']):
put(_d, 70.5, 18.0 + _i * 3.0, 0)
put('C751', 70.8, 27.0, 0) # 0805 体电容
# ── strap / 下拉,全部手排 ────────────────────────────────────────────
# 仿射映射会把它们挤进 U32 与晶振之间那条缝里,两个落在同一点的器件在推挤迭代里
# 是对称的、永远分不开(R719/R720 踩过)。
put('R717', 88.5, 11.5, 0) # J23 CC1 5.1k 下拉,贴着连接器
put('R718', 101.5, 11.5, 0) # J23 CC2 5.1k 下拉
put('R709', 95.0, 12.0, 0) # VBUS_DET 100k/100k 分压
put('R710', 95.0, 15.0, 0)
for _i, _d in enumerate(['R711', 'R712', 'R714', 'R715', 'R716', 'R719', 'R720',
'C742', 'C743']):
put(_d, 100.0 + (_i % 3) * 4.0, 18.0 + (_i // 3) * 4.0, 0)
# ── 五路负载开关 + 出线 ───────────────────────────────────────────────
# TPS22918 与 U30 同封装,rot=0 时 **1/2/3 在右列、4/5/6 在左列**:
# 右列自下而上 VIN(-0.94) / GND(0) / ON(+0.96);左列 VOUT(-0.94) / QOD(0) / CT(+0.96)。
# 所以 CIN 与 ON 下拉在右,COUT 与 CT 在左。
for n in range(1, 6):
xc = 15.0 + (n - 1) * 32.0
put('J%d' % (16 + n), xc, 5.0, 0) # 1.25mm 4P 出线到手机
put('U%d' % (23 + n), xc, 13.0, 0) # TPS22918
put('C70%d' % n, xc + 3.6, 12.5, 270) # CIN 2.2uF:pin2(+5V) 朝下对 VIN
put('R70%d' % n, xc + 3.6, 15.5, 0) # ON 下拉 100k
put('C71%d' % n, xc - 3.6, 12.06, 180) # COUT 100nF:pin1(VOUT) 朝里
put('C72%d' % n, xc - 3.6, 14.5, 0) # CT 摆率 100pF
# ══════════════ 【B】y 31~67:12V 进来、5V 辅助轨、P1、P3、ESP32 ══════════════
# 板边三个器件的朝向都是从渲染图上核出来的(见 PROGRESS「等用户确认的事」)。
put('J1', 5.8, 49.0, 90) # 12V 接线端子,进线朝 -X 左板边
put('U20', 143.0, 46.0, 270) # ESP32:天线端(+Y 无焊盘) 转到 +X 朝右板边
put('J22', 154.6, 61.0, 90) # USB-C 调试口,开口朝右板边
# ── 12V→5V 辅助轨(U30 LMR51430,SOT-23-6)────────────────────────────
# rot=0 时 1/2/3 在右列:GND(-0.94) / SW(0) / VIN(+0.96);4/5/6 在左列:CB / EN / FB。
# Cin 竖着摆在**右**侧跨 VIN-GND,热回路最小(§10.2-1);
# SW 夹在 VIN 与 GND 中间,只能从 x 21.9~23.4 那条缝往上走去 L3。
put('U30', 20.0, 40.0, 0)
put('C731', 24.2, 40.0, 270) # Cin 10uF:pin1(+12V) 朝上对 VIN,pin2(GND) 朝下对 GND
put('C732', 26.7, 40.0, 270) # Cin 第二颗
put('C733', 20.0, 37.6, 180) # 自举 CB:rot180 让 pin2(CB) 朝左对 pin6
put('R706', 16.0, 41.5, 0) # FB 上分压 100k(pin2=FB 朝右)
put('R707', 16.0, 39.5, 180) # FB 下分压 13.7k(rot180 让 pin1=FB 也朝右)
put('L3', 26.5, 45.5, 0) # 4.7uH
put('C734', 33.5, 45.5, 90) # Cout 22uF
put('C735', 36.0, 45.5, 90) # Cout 22uF
# ══════ 8 A 输入链手排(2026-09-03 布线时重排)══════════════════════════
# 原来这一串是仿射映射甩出来的:J1(x5.8) → F1(x46.6) → Q1(x53) → 体电容(x66/75)
# → Rs6(x84.6~90.7),**8 A 要从 x5.8 一路走到 x90.7,85 mm**。
# 两个后果,都是致命的:
#
# 1. `$1N223`(J1→F1)单独一段就 42 mm。8 A 在 0.5 oz 内层要 ΔT=20 °C
# 需要 **18 mm 宽**的铜面(IPC-2221 内层要按外层的 50% 取,即面积翻倍)。
# band B 只有 36 mm 高,还要塞 U19/U30/Q3/R9,挤不出四条 18 mm。
# 2. 这一串在 y 上是**来回折的**(F1.2 在 y60.08,Q1 源极在 y62.85,
# 而它们之间还夹着 $1N236 在 y60.56 的一个焊盘),
# L3 上想按 y 切开分成四块铜面,切线要绕着走,最窄处只剩 0.2 mm。
#
# 所以整条链挪到 **J1 右手边的走廊 x 11.5~46 / y 49.5~68.5**,
# 外加 J1 上方的口袋 x 0.5~11.5 / y 55~68.5 放栅极网络。
# 挪完之后:
# - `$1N223` 从 42 mm 缩到 **6.4 mm**(J1.2 → F1.1),随便多宽都够;
# - 分叉点从 x90.7 挪到 x≈45,8 A 只在 x 12~45 这一段流;
# - +12V 出了 Rs6 就直接铺开,往左喂 CH1、往右喂 CH2~CH5,
# 最大截面电流降到 ~6.4 A(§10.1 的 8 A 只在分叉点之前)。
#
# ⚠️ 这些件必须进 PINNED,否则 relax() 会推它们;而 relax **看不见** PINNED 的件,
# 所以仿射区也要跟着从 x40 缩到 x48(U20 被 P3 小件挤穿板框就是这么来的)。
# 串联主路,下排 y=52(左 → 右:熔断 → 防反接 → TVS → MLCC → 分流)
# ⚠️ 排这一排别拿 FP 表里的标称尺寸算净空:真占位来自 FP_PAD(实测焊盘外框),
# 普遍比标称大一圈 —— F1 标称 3.3 实际 4.24,Q1 rot270 标称 6.2 实际 7.57。
# 第一版按标称排,check() 立刻报 F1 x Q1 重叠。
put('F1', 14.2, 52.0, 0) # 3.2x1.6 熔断器,紧挨 J1($1N223 只剩 6.4 mm)
put('Q1', 21.0, 52.0, 270) # SOP-8 P-MOS:rot270 让 1~3(进) 在左、5~8(出) 在右
put('D1', 28.5, 53.0, 270) # SMBJ13A TVS,阴极(pin1)朝左对 Q1 输出
put('Rs6', 37.0, 52.0, 0) # 5mR 2512,**体电容与分叉点之间**(§19.1)
put('U13', 37.0, 57.5, 0) # INA226 贴在 Rs6 正上方,Kelvin 取样(§19.2)
put('C11', 41.0, 57.5, 0) # U13 的 3V3 去耦
# 体电容与母线 MLCC,上排(Φ8 立式电解,pin1=+ 接 $1N236)
put('C2', 17.0, 63.0, 0) # 470uF/25V
put('C3', 26.0, 63.0, 0) # 470uF/25V
put('C4', 33.0, 60.0, 0) # 10uF MLCC —— 下排塞不下(六个件要 35.4 mm,走廊只有 35.5)
put('C5', 33.0, 63.0, 0)
put('C6', 33.0, 65.5, 0) # 100nF
# R9 是 13.7 x 9.7 的立式 5 W 泄放电阻。仿射区缩到 x48 之后它被推到 x95,
# **本体框伸到 x102,压进 P3 区**(relax 只夹中心坐标,不看外框)。
# 钉到 Rs6/U13 挪走之后腾出来的那块空地。
put('R9', 90.0, 58.5, 0) # 10R/5W 母线泄放(y 再抬 1.5,避开被 relax 顶到 y50.7 的 L2)
# 栅极网络放 J1 上方的口袋 —— 走廊里塞不下(DZ1 与 C2 只剩 0.15 mm)
put('DZ1', 7.0, 58.0, 0) # Vgs 钳位稳压管
put('C1', 7.0, 62.0, 0)
put('R1', 10.0, 62.0, 0) # GATE 下拉
# ══════════ 仿射映射:P1 与 P3/P3B 的其余器件按原理图分组落位 ══════════
ZONES = {
'P1-INPUT': (['P1-INPUT'], 48.0, 35.0, 49.0, 15.0),
'P3': (['P3-CONTROL', 'P3B-EXPAND'], 102.0, 35.0, 29.0, 28.0),
}
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]
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)
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)
for _k, (_pgs, _x, _y, _w, _h) in ZONES.items():
affine(_pgs, _x, _y, _w, _h)
# ═════════════ 分离迭代:把仿射映射造成的重叠推开(只动这两块)═════════════
def relax(names, box, clearance=0.6, iters=400):
x0, y0, x1, y1 = box
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]
aw, ah = size(a, ar); bw, bh = size(b, br)
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: # x 向重叠少,往 x 推
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 会推动 names 里的每一个,
# 它不知道哪些是钉死的 —— U20 就这么被 P3 的小件从 x=143 挤到了 131(压穿右板框)。
PINNED = {'J1', 'J22', 'U20',
# 8 A 输入链,手排,见上面那一大段注释
'F1', 'Q1', 'D1', 'C4', 'C5', 'C6', 'Rs6', 'U13', 'C11',
'C2', 'C3', 'DZ1', 'C1', 'R1', 'R9'}
P1_SET = [p[0] for p in SCH['P1-INPUT'] if p[0] in PARTS and p[0] not in PINNED]
P3_SET = [p[0] for p in SCH['P3-CONTROL'] + SCH['P3B-EXPAND']
if p[0] in PARTS and p[0] not in PINNED]
relax(P1_SET, (47.0, 34.0, 98.0, 51.0))
relax(P3_SET, (101.0, 34.0, 132.0, 63.0))
# ════════════════════════════ 本地检查 ════════════════════════════
def check():
errs = []
missing = sorted(set(PARTS) - set(POS))
if missing:
errs.append(u'没安排位置: %d 个 %s' % (len(missing), ' '.join(missing[:12])))
for d, (x, y, r) in POS.items():
b = box(d, r)
# 右角式板边连接器(FP_BODY 里的)本体本来就要贴到板边,留 0 余量;其余留 0.4 mm
m = 0.0 if PARTS[d]['fp']['name'] in FP_BODY else 0.4
if (x + b[0] < m or x + b[2] > BW - m or
y + b[1] < m or y + b[3] > BH - m):
errs.append(u'焊盘越出板框: %s @ (%.1f, %.1f) 框 %.1f..%.1f / %.1f..%.1f'
% (d, x, y, x + b[0], x + b[2], y + b[1], y + b[3]))
# 定位孔禁布区:Φ6.5 的圆,元件占位框不许碰到。
# 这一条是**几何检查里最容易漏的**——孔不是元件,前面两项检查都看不见它。
r = MH_KEEPOUT / 2.0
for d, (x, y, rot) in POS.items():
b = box(d, rot)
ax0, ay0, ax1, ay1 = x + b[0], y + b[1], x + b[2], y + b[3]
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'压定位孔禁布区: %s @ (%.1f, %.1f) 撞孔 (%.0f, %.0f)'
% (d, x, y, hx, hy))
items = sorted(POS)
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)
if (ax + pa[0] < bx + pb[2] - 0.01 and bx + pb[0] < ax + pa[2] - 0.01 and
ay + pa[1] < by + pb[3] - 0.01 and by + pb[1] < ay + pa[3] - 0.01):
errs.append(u'重叠: %s(%.1f,%.1f) x %s(%.1f,%.1f)' % (a, ax, ay, b, bx, by))
return errs
def outline_poly():
u"""板框多边形 -> TPCB_PolygonSourceArray(mil)。
逆时针一圈:底边 → 右下角 → 右边 → 右上角 → 顶边 → 左上角 → 左边 → 左下角。
最后一段圆弧正好回到起点,多边形天然闭合(`pcb_MathPolygon.createPolygon`
要求闭合,不闭合就建不出来)。ARC 的角度**正值 = 逆时针**(本工程 Y 轴朝上)。
"""
R = CORNER_R
X0, Y0, X1, Y1 = OX, OY, OX + OW, OY + OH
pts = []
L = lambda x, y: pts.append(('L', x, y))
A = lambda x, y: pts.append(('ARC', 90.0, x, y))
start = (X0 + R, Y0)
L(X1 - R, Y0); A(X1, Y0 + R)
L(X1, Y1 - R); A(X1 - R, Y1)
L(X0 + R, Y1); A(X0, Y1 - R)
L(X0, Y0 + R); A(X0 + R, Y0) # 回到起点
src, mode = [start[0] * MIL, start[1] * MIL], None
for p in pts:
if p[0] == 'L':
if mode != 'L':
src.append('L'); mode = 'L'
src += [p[1] * MIL, p[2] * MIL]
else:
src.append('ARC'); mode = 'ARC'
src += [p[1], p[2] * MIL, p[3] * MIL]
return [round(v, 4) if isinstance(v, float) else v for v in src]
def push_outline():
u"""重画板框(四角 R5 圆角)+ 四个 M3 金属化定位孔。
⚠️⚠️ **板框必须是一条闭合的多段线(polyline),不能是 4 直线 + 4 圆弧。**
2026-09-04 实测:拆成独立的 `pcb_PrimitiveLine` / `pcb_PrimitiveArc` 图元,
渲染、铺铜、Gerber 全都正常,**只有自动布线器不认**——它取板框走的是
`fu()`:只扫 polygon 管理器里 `type=="Board Outline"` 或
`type=="Line" && layerId==11` 的**整条多段线**,独立线/弧根本不在那个表里。
结果是 `getFilterBoard()` 返回空,弹「请先绘制板框!」然后静默退出——
对话框正常关闭、一根线不画、控制台无异常,**极难往板框上想**。
同一个 `fu()` 还卡着 `getDsnFile()` / `getAutoRouteJsonFile()`(都返回 undefined)。
正确姿势(两步,缺一不可):
const poly = await eda.pcb_MathPolygon.createPolygon(SRC);
await eda.pcb_PrimitivePolyline.create('', 11, poly, 5);
**不能把 `TPCB_PolygonSourceArray` 直接喂给 `pcb_PrimitivePolyline.create()`**:
它会抛「错误:无法创建多边形图元」而且是**真的没建**(`getAll()` 仍是 0)——
注意这跟铺铜那条「create() 抛了也算成功」的经验**方向相反**,别混用。
板框在 **layer 11**,线宽 5 mil;定位孔是 **layer 12(MULTI)的自由焊盘**,
`pad=['ELLIPSE', w, h]` 和 `hole=['ROUND', d]` **写进去时都是 mil**(渲染实测)。
⚠️ 但 `getState_Hole()` **读回来的不是 mil**:J1 (KF128) 读回 pad 94.5(=2.4mm 对)
/ hole 6.2992(真孔 1.6mm)。照读回值反推着写,孔会小 25 倍 —— 踩过一次,
第一版 MH 打出来只有 0.32 mm。**孔径只能靠渲染图量,不能信 getState_Hole。**
"""
from eda import run
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1500));" % PCB)
def call(body, timeout=240):
r = run(OPEN + body, timeout=timeout)
if not r.get('success'):
print(u'下发失败:', json.dumps(r, ensure_ascii=False)[:500]); sys.exit(1)
return r['result']
m = lambda v: round(v * MIL, 4)
js_src = json.dumps(outline_poly())
js_mh = json.dumps([[m(x), m(y)] for x, y in MH])
# 先把旧的独立直线/多段线清掉。⚠️ `pcb_PrimitiveArc.getAll()` 在本工程恒返回 0,
# 独立圆弧删不掉(见文件头)——所以**别再用独立线弧画板框**,一旦画了就只能
# 在客户端里手删。这里能删的都删,剩下的用一条 polyline 覆盖。
n = call("""
let del = 0;
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Layer() === 11) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); del++; }
for (const p of await eda.pcb_PrimitivePolyline.getAll())
if (p.getState_Layer() === 11) { await eda.pcb_PrimitivePolyline.delete(p.getState_PrimitiveId()); del++; }
// ⚠️ 必须先过 createPolygon(),直接传源数组会抛「无法创建多边形图元」且真的没建
const poly = await eda.pcb_MathPolygon.createPolygon(%s);
const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, 5);
const left = (await eda.pcb_PrimitivePolyline.getAll()).filter(p => p.layer === 11).length;
return {del, made: r ? r.primitiveId : null, left};
""" % js_src)
if n['left'] != 1:
print(u' !! layer 11 上有 %d 条多段线,应为 1 —— 板框没建成或有残留' % n['left'])
sys.exit(1)
print(u'板框: 1 条闭合多段线(4 直边 + 4 圆角 R%.0f),%.0f x %.0f mm,左下角 (%.0f,%.0f);'
u'顺带删掉 %d 个旧图元' % (CORNER_R, OW, OH, OX, OY, n['del']))
n = call("""
const MH = %s;
// 先删掉上一次打的。⚠️ pcb_PrimitivePad.getAll() 把**元件焊盘也算进来**(1300+ 个),
// 只按孔径删会误伤通孔元件,所以必须同时确认它没有父元件。
for (const p of await eda.pcb_PrimitivePad.getAll()) {
let parent = null;
try { parent = p.getState_ParentComponentPrimitiveId(); } catch(e) {}
if (parent) continue;
const h = p.getState_Hole();
if (h && Math.abs(h[1] - %.4f) < 0.01) await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (let i = 0; i < MH.length; i++) {
await eda.pcb_PrimitivePad.create(12, 'MH' + (i + 1), MH[i][0], MH[i][1], 0,
['ELLIPSE', %.2f, %.2f], 'GND', ['ROUND', %.4f], 0, 0, 0, true);
k++;
}
return k;
""" % (js_mh, MH_HOLE * MIL, MH_PAD * MIL, MH_PAD * MIL, MH_HOLE * MIL))
print(u'定位孔: %d 个 M3 金属化(孔 Φ%.1f / 焊盘 Φ%.1f / 接 GND)' % (n, MH_HOLE, MH_PAD))
call("await eda.pcb_Document.save(); return 1;")
if __name__ == '__main__':
if '--outline' in sys.argv:
push_outline(); sys.exit(0)
errs = check()
print(u'已安排 %d / %d 个元件' % (len(POS), len(PARTS)))
if errs:
print(u'\n!! %d 个问题:' % len(errs))
for e in errs[:40]:
print(' ', e)
if len(errs) > 40:
print(u' ...还有 %d 条' % (len(errs) - 40))
sys.exit(1)
print(u'本地检查通过:无重叠、无越框')
if '--push' not in sys.argv:
sys.exit(0)
from eda import run
OPEN = "await eda.dmt_EditorControl.openDocument('%s');" % PCB
def call(body, timeout=240):
r = run(OPEN + body, timeout=timeout)
if not r.get('success'):
print(u'推送失败:', json.dumps(r, ensure_ascii=False)[:500]); sys.exit(1)
return r['result']
# mil,Y 向上;EasyEDA 的元件坐标就是 mil
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):
part = plan[k:k + 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(), {x, y, rotation: rot});
n++;
}
return n;
""" % json.dumps(part))
print(u' 已摆 %d / %d' % (total, len(plan)))
call("await eda.pcb_Document.save(); return 1;")
print(u'布局已下发并保存:%d 个元件' % total)
# ⚠️ 板框**不要**跟着布局一起重画。现在它是一条 polyline,先删后建是干净的,
# 但板框没变就没必要动它(改了要重新铺铜)。要重画显式加 --outline。
if '--outline' in sys.argv:
push_outline()