# -*- 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()