# -*- coding: utf-8 -*- u"""R2 底板 PCB 的缝合过孔 + 铺铜 + 断面校核(M8e)。 /c/Python314/python .claude/r2/pour_base.py # 只算 + 自检 + 断面校核 /c/Python314/python .claude/r2/pour_base.py --stitch # 打 GND 缝合过孔 /c/Python314/python .claude/r2/pour_base.py --push # 清旧铜面 + 重建 + 重填 /c/Python314/python .claude/r2/pour_base.py --rebuild # 只重填 ⛔ **顺序**(刀片 M7 §27.10 的结论,两层板同样成立): 摆位 → 布线 → 下发 → **缝合过孔** → 斜切 → **铺铜**。 重跑布线器要把缝合过孔和斜切一起重跑。 ## 叠层(方案 §6.1) | 设计 | EasyEDA | id | 内容 | |---|---|---|---| | L1 | Top Layer | 1 | 全部元件 + 全部信号 + 12V 宽铜 + GND 底填 | | L2 | Bottom Layer | 2 | **完整地平面**(D± 走廊下方一刀不许割,§6.2 第 1 条)| ## 两个只有这块板才有的约束 1. **`U20` 天线净空**:ESP32-S3-WROOM-1 的天线那一头(焊盘北沿外 7.5 mm) **两层都不许有铜**。铜面不能带洞(`createComplexPolygon` 传多个环会把客户端挂死, 刀片 §24 的坑),⇒ 用**若干块单环矩形**绕开它,同网接壤会自然连成一片。 2. **入口链 9 A 靠铺铜**:`VIN_RAW`/`VIN_FUSED`/`VIN_SW`/`+12V` 在 input 带里各自 铺一块顶层铜(优先级比 GND 高,先占地盘),断面要 ≥ 方案 §6.4 的要求。 ⚠ 这四个是**串联的四段**,不是一个网 —— 每段各铺各的,中间隔着 F1 / Q1 / Rs6。 ## 断面校核(`--cuts`,办法沿用 R1 `pcb1_pour.py`) 在铺铜上画一条断面线,数「这条线上属于本网、且没有被别人的焊盘/过孔/导线占掉」的 长度。⚠ 它算的是**计划的铜面**,不是板上填出来的铜 —— 填的时候还会被反焊盘再吃掉一点, 所以判据留了 15% 余量。真正的判据仍是「铺完截图 + DRC」。 """ import io import json import math import os import sys import time sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 import pcb_base as B # noqa: E402 import place_base as PL # noqa: E402 PCB = B.PCB MIL = B.MIL L_TOP, L_BOT = 1, 2 INSET = 0.5 # 铜面离板边 CLR = 0.25 # 断面校核里别人的铜要让开多少 VIA_D, VIA_HOLE = 0.6, 0.3 STITCH_PITCH = 4.0 STITCH_CLR = 0.5 # 缝合过孔到任何焊盘 / 导线的净距 PADS = os.path.join(HERE, '_base_pads.json') ROUTES = os.path.join(HERE, '_base_routes.json') ROUTE_SCHEMA = 7 # 入口链四段的铺铜:从各自的焊盘外框算,不手抄坐标(摆位一改就跟着走) POWER_NETS = [('VIN_RAW', 1), ('VIN_FUSED', 2), ('VIN_SW', 3)] # ⚠ `+12V` **不按焊盘外框算** —— 它不是入口链里的一段,是遍布全板的母线。 # 三段几何写在 `pcb_base.BUS_IN/BUS_UP/BUS_SLOT`(那里是唯一事实来源, # `place_base` 也拿它当摆位禁区)。 # ⛔ 入口链的铜只能从 y=6.3 往上:**输入带最南那 5 mm 归 12V 横脊**(`pcb_base.SPINE`)。 # 两者同层不同网,搭上就把 `Rs6` 短路了。 # M9:南带整条都归入口链(横脊没了 —— 12V 不再需要穿过 D±,它全在东缘) POWER_BAND = (0.0, 0.8, B.BW, B.ZONES['input'][3]) POWER_GROW = {'VIN_RAW': 1.2, 'VIN_FUSED': 1.2, 'VIN_SW': 1.2} # Q1 栅极网络(DZ1 钳位 / C1 栅源电容 / R1 下拉)。它们的焊盘**不撑大**入口链铜面 # (和 U13.10 同理:不是 9 A 主电流焊盘),而且整团从铜面里挖出一个缺口给 GATE 走线。 # ⛔ 不挖的话 Q1.4 北、西、南三面是 VIN_FUSED、东面是 VIN_SW,中间只剩 0.25 mm 的缝 —— # 布线器报「GATE: Q1.4 出不来」,看着像拥塞,其实是铜面把栅极脚封死了。 GATE_PARTS = ('DZ1', 'C1', 'R1') NOTCH_CLR = 0.3 # 缺口在 GATE 焊盘外框外再让多少 NOTCH_HEAD = 1.5 # 缺口在栅极件北侧留多高:GATE 要从 C1 北边绕过去 # 落在硬功率铜面里 / GATE 缺口里的 **GND 贴片焊盘**:(位号, 脚, 过孔在哪一侧, 是否挖地岛)。 # ⛔ GND 不走线、只靠铺铜 + 过孔。这几只脚四周全是 VIN_SW 铜(或是缺口里的一小块孤立地铜), # 没有专用过孔的话它们根本接不到地 —— D1 是 TVS,浪涌电流全靠这一只脚回地。 # 通孔地脚(C2/C3 的电解、J1)自己穿到底层地平面,不在此列。 # 地岛从铜面里挖一条缝通到铜面外(S = 向南到板边,N = 向北出 VIN_SW 顶边),过孔排在开口那一侧。 GND_PAD_VIAS = [('D1', '2', 'S', True), ('C5', '1', 'N', True), ('R1', '1', 'N', False)] ISL_M = 0.6 # 地岛在焊盘外框外让多少 ISL_VIA_GAP = 0.25 # 过孔铜边到焊盘边(别让焊锡流进孔里) ISL_VIA_PITCH = 0.9 # 同排过孔中心距(≥ 0.6 的同网孔距) # 断面校核(方案 §6.4)。入口链电流方向是 +x ⇒ 断面是**竖直**的,量 y 方向的净铜高, # 而且要取沿途的**最小值**(第一版取了最大值,等于没查)。 # (名字, 网, 是否强制) # (铜面名, 标签, 网, 沿哪个轴流 'x'/'y', 要求净铜, 是否强制) CUTS = [ ('PWR-VIN_RAW', u'J1→F1 入口', 'VIN_RAW', 'x', 8.0, False), ('PWR-VIN_FUSED', u'F1→Q1', 'VIN_FUSED', 'x', 8.0, False), # ⚠ `VIN_SW` 的最窄点在 `Rs6`/`U13` 那一头(Kelvin 要求它们贴在一起)。 # 6.5 mm 在 9 A 下 ΔT≈25 K 而不是 20 K,只有分流电阻前后 2~3 mm 一小段,可接受。 ('PWR-VIN_SW', u'Q1→储能→Rs6', 'VIN_SW', 'x', 6.5, True), # 西缘高速路的电流是**沿 y** 流的 ⇒ 断面是水平的,量 x 方向净铜宽。 # 写成 'x' 的话量到的是它 105 mm 的长度,永远「通过」,等于没查。 ('PWR-12V-0', u'12V 西缘高速路', '+12V', 'y', 8.0, True), ] CUT_MIN_W = 8.0 # IPC-2221 1 oz 外层 9 A ΔT20K ⇒ ≥8 mm 净铜宽 # ── 低压轨的「软铺铜」(2026-09-09 用户拍板:先按分布把电源网铺铜)──────────── # ⛔ **为什么单独一类、名字不叫 `PWR-`**: # `PWR-` 是**硬铺铜** —— 几何算出来、要过断面校核,而且会当障碍喂给布线器 # (`route_base.pour_blocks()`),别的网一律不许穿过去。12V 母线被一根信号线 # 切断 = 9 A 没了,而铺铜管理器 / DRC / 连通性三个都只说「有铜」。 # `RAIL-` 是**软铺铜**:只是「这一带的余铜归这个网」,铺铜时自然绕开已布的信号线, # **不喂给布线器** —— 喂了的话 ctrl 带 57 个件的信号一根都出不去。 # 判据也换一个:硬铺铜看断面,软铺铜看 **`board_check` 从板上读回来的连通性**, # 因为它被信号切碎成孤岛是完全可能的,而那只有读铜才看得见。 # # 依据是 `_base_pads.json` 实测的分布,不是画出来的(见 §28.15): # `+3V3` 47 只脚里 **26 只**在 ctrl 带 x 28~86 / y 86~112(其余 15 只在槽上、4 只在风扇带) # `+3V3_HUB` 21 只脚分成 U32/去耦组与 U31/C737 两簇 # `+5V` ctrl 带东头 2 只(`R706` / `L3`,5V Buck 那一窝);槽上那 10 只要走走廊,不在这 # ⚠ 三块互不重叠(不同网同层重叠 = 短路),`selfcheck()` 会查。 # ⚠ M9 暂时清空:`+3V3` 与 `+3V3_HUB` 的脚在西区互相穿插(排布器按「离目标最近」摆的), # 两个网划不出互不重叠的矩形。新拓扑没有 M8 那道墙,先让布线器直接布, # 看失败分布再决定要不要为它们改摆位 + 加软铺铜。 RAILS = [ # U31 与 U32 相距较远,分成两块局部软铜,中间由 A* 明确连接。 # 这比一块 24x60 mm 大面更不容易被控制信号切成孤岛。 # M12 采用当前手工布局后旧坐标已失效:U31 移到 y=73.6,U32 移到 y=52.9。 ('RAIL-3V3HUB-LDO', '+3V3_HUB', (77.8, 71.5, 90.2, 75.7)), ('RAIL-3V3HUB-HUB', '+3V3_HUB', (84.0, 47.8, 102.0, 60.7)), # ⛔ **`+3V3` 试过铺两块,反而更差**(45 → 58 条):软铺铜会逼每只**区外**的脚 # 各自去够铜面,而原来布线器可以把它们串成一条链。 # ⇒ 判据:软铺铜只在「大部分脚都在区内」时划算(`+3V3_HUB` 21 只脚全在西区 ⇒ 值), # `+3V3` 47 只脚里 15 只在槽上、6 只在风扇带 ⇒ 不值,留给布线器。 ] # ⚠ `VIN_RAW` / `VIN_FUSED` 两段**不强制**:它们的瓶颈是 DC-005 的三只脚和 1206 保险 # 本身(座子额定 2~5 A、这块板要过 9 A,是用户 2026-09-08 明确接受的已知风险, # 设计文档 §22.6)。板上再宽的铜也救不了插头内芯与弹片的接触面。 # 真正必须够的是它们之间那两段长铜:`VIN_SW`(储能到检流)和 `+12V`(母线)。 def mil(v): return round(v * MIL, 4) def call(js, timeout=180): r = run(js, timeout=timeout) if not r.get('success'): print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:600]) sys.exit(1) return r['result'] OPEN = ("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2500));" % PCB) _PADS = [None] def pads(): u"""底板焊盘,**按 `place_base.ROUTE_FIX` 的计划位置**(与 `route_base` 的路由模型一致)。 ⛔ 以前这里读的是实测焊盘,而布线器读的是改位后的:DZ1 从 x=38.6 挪到 29.3 之后, 实测算出的 VIN_FUSED/VIN_SW 分界是 32.9,改位后算出的是 30.5(VIN_SW 盖住 Q1.3)—— 布线器看到的铜面和摆位下发后真正铺出来的对不上,而且哪一边都不报错。 """ if _PADS[0] is None: _PADS[0], _moved = PL.route_fixed_pads(json.load(io.open(PADS, encoding='utf-8'))) return _PADS[0] def routes(): if not os.path.exists(ROUTES): return [], [] c = json.load(io.open(ROUTES, encoding='utf-8')) if c.get('schema') != ROUTE_SCHEMA or c.get('slot_rot') != B.SLOT_ROT: return [], [] return c['segs'], c['vias'] # ── 铜面几何 ───────────────────────────────────────────────────────────── def keepouts(): u"""两层都不许铺铜的地方:`U20` 天线净空 + **天线缺口** + 四个 M3 安装孔。 ⛔ 缺口那一块**板材是挖掉的**,而这份脚本的铜面几何是按 `BW × BH` 的矩形算的 (`tiles()` 只认矩形,不认板框折线)。不把缺口喂进来,GND 瓦片会照样铺到 板外的空气里 —— 板框对不上铜面,DRC 报「铜到板边」,而铺铜管理器不报。 """ sz = PL.sizes() n = B.notch_rect() out = [PL.antenna_keepout(sz), (n[0] - INSET, n[1] - INSET, n[2] + INSET, n[3] + INSET)] r = B.HOLE_D / 2.0 + 0.6 for (hx, hy) in B.MOUNT_HOLES.values(): out.append((hx - r, hy - r, hx + r, hy + r)) return out def tiles(x0, y0, x1, y1, cuts): u"""把矩形按 `cuts` 切成若干块**单环**矩形。 ⛔ 不能用带洞的复杂多边形:`createComplexPolygon()` 传多个环会把客户端挂死 (刀片 §24,请求 30 s 超时、进度恒 0/0,和「按钮失灵」分不清)。 同网的矩形接壤会自然连成一片铜,效果一样。 """ xs = sorted(set([x0, x1] + [v for c in cuts for v in (c[0], c[2]) if x0 < v < x1])) out = [] for xa, xb in zip(xs, xs[1:]): if xb - xa < 0.6: continue # ⛔ **禁区的 y 区间必须先合并再配对**。原来是「按 c[1] 排序后把 # [起, 止] 一对一对塞进 ys,再 `zip(ys[0::2], ys[1::2])`」—— # 那套只在禁区互不重叠时成立。天线缺口 (84.5~107.5) 和天线净空 (86~106) # 是**嵌套**的,塞进去就变成 [.., 84.5, 107.5, 86.0, 106.0, ..], # 奇偶配对当场错位 ⇒ 瓦片照样盖住缺口,而**只有 `selfcheck` 那一条 # 「压进净空区」看得出来**(铺铜管理器和 DRC 都不会响)。 iv = [] for c in cuts: if c[0] < xb - 1e-9 and xa < c[2] - 1e-9: iv.append([max(y0, c[1]), min(y1, c[3])]) iv.sort() merged = [] for a, b in iv: if merged and a <= merged[-1][1] + 1e-9: merged[-1][1] = max(merged[-1][1], b) else: merged.append([a, b]) cur = y0 for a, b in merged + [[y1, y1]]: if a - cur > 0.6: out.append((round(xa, 3), round(cur, 3), round(xb, 3), round(a, 3))) cur = max(cur, b) return out def power_rect(net): u"""入口链某一段的铜面:这个网在 input 带里全部焊盘的外框 + `POWER_GROW`。""" z = POWER_BAND xs, ys = [], [] for p in pads(): if p[7] != net or not (z[1] <= p[3] <= z[3]): continue # U13.10 是 Rs6 上游的 Kelvin/监测脚,不是 9 A 主电流焊盘。 # 把它纳入包围盒会让 VIN_SW 铜面跨过 Rs6 的 +12V 端。 if net == 'VIN_SW' and p[0] == 'U13': continue if p[0] in GATE_PARTS: continue xs += [p[2] - p[4] / 2, p[2] + p[4] / 2] ys += [p[3] - p[5] / 2, p[3] + p[5] / 2] if not xs: return None g = POWER_GROW[net] return (max(INSET, min(xs) - g), max(z[1] + INSET, min(ys) - g), min(B.BW - INSET, max(xs) + g), min(z[3], max(ys) + g)) def clip_chain(chain): u"""入口链四段是**串联**的(中间隔着 F1 / Q1 / Rs6),铜面搭上就把器件短路了。 `chain` = [(net, rect)],已按 x 排好。相邻两段重叠时竖切一刀,两边各退 CLR/2。 ⛔ 刀口**按焊盘空隙定,不按重叠区中点**:在两块铜 y 方向重叠的那一带里,取 「西段网最东的焊盘」与「东段网最西的焊盘」之间的中点。 取重叠区中点的旧写法在 Q1 这里是错的 —— Q1 是南北两排脚(南排源极、北排漏极), VIN_SW 的外框又被北边的 C4/C6 拉到 x=25.8,中点直接落进 Q1 里面(x=30.6, VIN_SW 盖住 Q1.3)。以前没出事,只是因为 DZ1 旧位置把 VIN_FUSED 撑到了 x=40。 ⚠ 不能靠铺铜优先级解决:优先级只保证「低的那块被让开」,让出来的形状是不可预期的 细条,断面校核就没意义了。 """ out = [r for _n, r in chain] ps = pads() def band(net, y0, y1): return [p for p in ps if p[7] == net and p[0] not in GATE_PARTS and not (net == 'VIN_SW' and p[0] == 'U13') and p[3] - p[5] / 2 < y1 and p[3] + p[5] / 2 > y0] for i in range(len(out) - 1): a, b = out[i], out[i + 1] if a[2] <= b[0]: continue y0, y1 = max(a[1], b[1]), min(a[3], b[3]) pa, pb = band(chain[i][0], y0, y1), band(chain[i + 1][0], y0, y1) ea = max([p[2] + p[4] / 2 for p in pa] or [None]) wb = min([p[2] - p[4] / 2 for p in pb] or [None]) if ea is not None and wb is not None and ea < wb: m = (ea + wb) / 2.0 else: m = (a[2] + b[0]) / 2.0 # 没有干净的焊盘空隙时退回旧规则 out[i] = (a[0], a[1], round(min(a[2], m - CLR / 2), 3), a[3]) out[i + 1] = (round(max(b[0], m + CLR / 2), 3), b[1], b[2], b[3]) return out def gate_notch(): u"""GATE 缺口(矩形列表,喂给 `tiles()`)。 主块:GATE 焊盘 + 三颗栅极件的外框,向外让 NOTCH_CLR、北侧留 NOTCH_HEAD,南到板边。 竖条:比主块更高的 GATE 焊盘(Q1.4 是 2 mm 长的 SOIC 脚)整只让出来 —— 只挖主块的话 Q1.4 北半截仍在 VIN_FUSED 铜面里,模型里等于异网铜压在焊盘上。 """ ps = pads() g = [p for p in ps if p[7] == 'GATE' and p[3] <= POWER_BAND[3]] sm = [p for p in ps if p[0] in GATE_PARTS] if not g or not sm: return [] y1 = round(max(p[3] + p[5] / 2 for p in sm) + NOTCH_HEAD, 3) out = [(round(min(p[2] - p[4] / 2 for p in g) - NOTCH_CLR, 3), 0.0, round(max(p[2] + p[4] / 2 for p in g + sm) + NOTCH_CLR, 3), y1)] for p in g: if p[3] + p[5] / 2 + NOTCH_CLR > y1: out.append((round(p[2] - p[4] / 2 - NOTCH_CLR, 3), 0.0, round(p[2] + p[4] / 2 + NOTCH_CLR, 3), round(p[3] + p[5] / 2 + NOTCH_CLR, 3))) return out def gnd_pad_vias(): u"""`GND_PAD_VIAS` 的几何:(要从入口链铜面里挖掉的地岛矩形, 专用 GND 过孔坐标)。""" ps = pads() islands, vias = [], [] for ref, pin, side, cut in GND_PAD_VIAS: hit = [p for p in ps if p[0] == ref and str(p[1]) == pin and p[6] == L_TOP] if len(hit) != 1 or hit[0][7] != 'GND': raise RuntimeError(u'%s.%s 应是唯一的顶层 GND 贴片焊盘' % (ref, pin)) p = hit[0] x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2 x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2 n = int((x1 - x0) / ISL_VIA_PITCH) + 1 vy = y0 - ISL_VIA_GAP - VIA_D / 2 if side == 'S' else y1 + ISL_VIA_GAP + VIA_D / 2 for k in range(n): vias.append((round(p[2] + (k - (n - 1) / 2.0) * ISL_VIA_PITCH, 3), round(vy, 3))) if cut: # 缝一直通到板边 / 板顶:`tiles()` 只拿它切入口链那几块铜,切不到别的东西 islands.append((round(x0 - ISL_M, 3), 0.0 if side == 'S' else round(y0 - ISL_M, 3), round(x1 + ISL_M, 3), round(y1 + ISL_M, 3) if side == 'S' else B.BH)) return islands, vias def drain_piece(outs): u"""Q1 漏极排(北排四脚,VIN_SW)的铜面。 ⛔ 入口链是按 x 串起来切的,而 Q1 是**南北两排**:南排源极归 VIN_FUSED,北排漏极 在 VIN_SW 那块的西边界之外 —— 9 A 只能靠 A* 的 2 mm 走线从四只漏极脚爬进铜面 (2 mm 1 oz 外层只带 ~4 A)。这里按漏极排外框补一块,东头与 VIN_SW 已有的块搭接, 南沿让开 VIN_FUSED。西沿只让 CLR:再往西是死头,断面校核会把它量成瓶颈。 """ ps = [p for p in pads() if p[0] == 'Q1' and p[7] == 'VIN_SW'] if not ps: return None g = POWER_GROW['VIN_SW'] x0 = min(p[2] - p[4] / 2 for p in ps) - CLR x1 = max(p[2] + p[4] / 2 for p in ps) + g y0 = min(p[3] - p[5] / 2 for p in ps) - g y1 = max(p[3] + p[5] / 2 for p in ps) + g vf = [r for (_n, net, _l, r, _p) in outs if net == 'VIN_FUSED' and r[0] < x1 and x0 < r[2]] y0 = max([y0] + [r[3] + CLR for r in vf]) sw = [r for (_n, net, _l, r, _p) in outs if net == 'VIN_SW' and r[1] < y1 and y0 < r[3] and r[0] >= x0] if sw: x1 = max(x1, min(r[0] for r in sw) + 0.6) # 保证与已有 VIN_SW 块搭接 return (round(x0, 3), round(y0, 3), round(x1, 3), round(y1, 3)) def plan(): u"""[(name, net, layer, rect, priority)],priority 小的先铺、先占地盘。""" outs = [] chain = [(net, prio, power_rect(net)) for net, prio in POWER_NETS] chain = [c for c in chain if c[2]] chain.sort(key=lambda c: (c[2][0] + c[2][2]) / 2.0) clipped = clip_chain([(c[0], c[2]) for c in chain]) cuts = gate_notch() + gnd_pad_vias()[0] for (net, prio, _r), r in zip(chain, clipped): parts = tiles(r[0], r[1], r[2], r[3], cuts) if cuts else [r] # 最大一块沿用原名(CUTS 按名找),其余加序号;check_cuts 会把同名前缀的块一起算 parts.sort(key=lambda t: -(t[2] - t[0]) * (t[3] - t[1])) for k, t in enumerate(parts): outs.append(('PWR-%s%s' % (net.strip('+'), '-%d' % k if k else ''), net, L_TOP, t, prio)) dp = drain_piece(outs) if dp: outs.append(('PWR-VIN_SW-Q1', 'VIN_SW', L_TOP, dp, dict(POWER_NETS)['VIN_SW'])) for k, r in enumerate(B.bus_rects()): outs.append(('PWR-12V-%d' % k, '+12V', L_TOP, r, 4)) # `+3V3` 五块槽墙(几何与判据见 `pcb_base.wall3v3_rects` / `wall_check`)。 # 与 12V 同为**硬铺铜**:喂给布线器当障碍,且脚从 A* 里摘掉。 for k, r in enumerate(B.wall3v3_rects()): outs.append(('PWR-3V3-%d' % k, '+3V3', L_TOP, r, 4)) # 低压轨的软铺铜:排在硬铺铜之后、GND 之前 for name, net, r in RAILS: outs.append((name, net, L_TOP, r, 5)) # ⚠ `PWR-12V-*` 里 0=输入带母线 1=横脊 2~7=五根齿(3 号槽那根是两块拼的 L) ko = keepouts() d = INSET for tag, lay in (('TOP', L_TOP), ('BOT', L_BOT)): for k, t in enumerate(tiles(d, d, B.BW - d, B.BH - d, ko)): outs.append(('GND-%s-%d' % (tag, k), 'GND', lay, t, 9)) return outs def rrect_src(x0, y0, x1, y1): pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)] # ⚠ 必须显式闭合 a = [mil(pts[0][0]), mil(pts[0][1])] for p in pts[1:]: a += ['L', mil(p[0]), mil(p[1])] return a def wall_check(): u"""`+3V3` 槽墙的判据(几何算不出来,只有拿实测焊盘表比才算数)。 ⛔ 三条都要过,缺一条墙就是白做: ① **15 只槽脚全被盖住** —— 与 12V 那条「10 排焊盘全被盖住」同理: 少盖一只,铺铜管理器和 DRC 都只说「有铜」,而那只脚是断的。 ② **一只异网针都不许压到**(`NC` 空网也算异网):南排 NC(73)、北排 NC(71/72) 就贴在 3V3 针旁边,方块一大就压上去,铺铜会绕开它们**把墙在针排上切碎**。 ③ **桥的过孔要落在墙里**,否则过孔与铜面之间靠铺铜「碰巧」相接。 """ bad = [] rects = B.wall3v3_rects() ps = pads() c = B.W3V3_CLR - 0.05 # 判据比构造留的净距松 0.05 def inside(x0, y0, x1, y1): return any(r[0] - 1e-6 <= x0 and x1 <= r[2] + 1e-6 and r[1] - 1e-6 <= y0 and y1 <= r[3] + 1e-6 for r in rects) own = 0 for p in ps: px0, py0 = p[2] - p[4] / 2, p[3] - p[5] / 2 px1, py1 = p[2] + p[4] / 2, p[3] + p[5] / 2 if p[7] == '+3V3': if inside(px0, py0, px1, py1): own += 1 continue for r in rects: if px0 - c < r[2] and r[0] < px1 + c and py0 - c < r[3] and r[1] < py1 + c: bad.append(u'3V3 槽墙压到异网针 %s.%s(%s) @%.2f,%.2f' % (p[0], p[1], p[7] or 'NC', p[2], p[3])) break slots = set(PL.SLOT_ORDER) slot_own = sum(1 for p in ps if p[7] == '+3V3' and p[0] in slots) if own != slot_own: bad.append(u'3V3 槽脚 %d 只,墙只盖住 %d 只' % (slot_own, own)) _segs, vias = B.wall3v3_bridges() for (vx, vy) in vias: r = VIA_D / 2.0 if not inside(vx - r, vy - r, vx + r, vy + r): bad.append(u'3V3 桥的过孔 (%.2f, %.2f) 不在墙里' % (vx, vy)) return bad, own, slot_own def bus_check(): u"""12V 干道/铜墙必须覆盖全部槽脚,且不能压到异网焊盘。""" rects = B.bus_rects() ps = pads() slots = set(B.USB_SLOT_REF.values()) clear = CLR - 0.05 def inside(p): x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2 x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2 return any(r[0] - 1e-6 <= x0 and x1 <= r[2] + 1e-6 and r[1] - 1e-6 <= y0 and y1 <= r[3] + 1e-6 for r in rects) own = [p for p in ps if p[0] in slots and p[7] == '+12V'] missed = [p for p in own if not inside(p)] bad = [u'12V 铜墙没盖住 %s.%s @%.2f,%.2f' % (p[0], p[1], p[2], p[3]) for p in missed] for p in ps: if p[7] == '+12V': continue px0, py0 = p[2] - p[4] / 2, p[3] - p[5] / 2 px1, py1 = p[2] + p[4] / 2, p[3] + p[5] / 2 if any(px0 - clear < r[2] and r[0] < px1 + clear and py0 - clear < r[3] and r[1] < py1 + clear for r in rects): bad.append(u'12V 干道/铜墙压到异网针 %s.%s(%s) @%.2f,%.2f' % (p[0], p[1], p[7] or 'NC', p[2], p[3])) return bad, len(own) - len(missed), len(own) def selfcheck(ps): bad = [] ko = [k for k in keepouts() if k not in B.bus_rects()] for name, net, lay, r, _p in ps: if r[2] - r[0] < 0.6 or r[3] - r[1] < 0.6: bad.append(u'%s 太小: %s' % (name, r)) if r[0] < INSET - 1e-6 or r[1] < INSET - 1e-6 or \ r[2] > B.BW - INSET + 1e-6 or r[3] > B.BH - INSET + 1e-6: bad.append(u'%s 出了内缩线: %s' % (name, r)) for k in ko: if r[0] < k[2] - 1e-6 and k[0] < r[2] - 1e-6 and \ r[1] < k[3] - 1e-6 and k[1] < r[3] - 1e-6: bad.append(u'%s 压进净空区 %s' % (name, tuple(round(v, 1) for v in k))) # 入口链各段不许互相重叠(同层,靠 F1/Q1/Rs6 断开)。 # ⚠ **只判不同网**:`+12V` 自己那几块(母线 / 横脊)是故意相接的,重叠才对。 pw = [(n, net, r) for (n, net, _l, r, _p) in ps if n.startswith('PWR-') or n.startswith('RAIL-')] for i, (na, na_net, a) in enumerate(pw): for (nb, nb_net, b) in pw[i + 1:]: if na_net == nb_net: continue if a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]: bad.append(u'%s(%s) 与 %s(%s) 在顶层重叠 —— 串联的两段铜搭上就把器件短路了' % (na, na_net, nb, nb_net)) return bad # ── 断面校核 ───────────────────────────────────────────────────────────── def cut_height(net, rect, x): u"""在 x 处竖切一刀,`rect` 这块铜面上属于 `net` 的**最宽一段**连续净铜高(mm)。 别人的焊盘 / 过孔 / 导线按 `CLR` 膨胀之后从这条线上扣掉。 ⚠ 取「最宽一段」而不是总和:被挖成两条 4 mm 的铜带,不等于一条 8 mm 的。 """ if not (rect[0] <= x <= rect[2]): return 0.0 spans = [(rect[1], rect[3])] blocks = [] for p in pads(): if p[7] == net or p[6] not in (1, 12): continue if p[2] - p[4] / 2 - CLR <= x <= p[2] + p[4] / 2 + CLR: blocks.append((p[3] - p[5] / 2 - CLR, p[3] + p[5] / 2 + CLR)) segs, vias = routes() for s in segs: if s[6] == net or s[0] != 1: continue x0, x1 = sorted((s[1], s[3])) hw = s[5] / 2.0 + CLR if x0 - hw <= x <= x1 + hw: y0, y1 = sorted((s[2], s[4])) blocks.append((y0 - hw, y1 + hw)) for v in vias: if len(v) > 2 and v[2] == net: continue r = VIA_D / 2.0 + CLR if abs(v[0] - x) <= r: blocks.append((v[1] - r, v[1] + r)) for (b0, b1) in blocks: nxt = [] for (a0, a1) in spans: if b1 <= a0 or b0 >= a1: nxt.append((a0, a1)) continue if b0 > a0: nxt.append((a0, b0)) if b1 < a1: nxt.append((b1, a1)) spans = nxt return max([q[1] - q[0] for q in spans] or [0.0]) def cut_width_y(net, rect, y): u"""在 y 处横切一刀,最宽一段连续净铜宽(mm)。""" if not (rect[1] <= y <= rect[3]): return 0.0 spans = [(rect[0], rect[2])] blocks = [] for p in pads(): if p[7] == net or p[6] not in (1, 12): continue if p[3] - p[5] / 2 - CLR <= y <= p[3] + p[5] / 2 + CLR: blocks.append((p[2] - p[4] / 2 - CLR, p[2] + p[4] / 2 + CLR)) segs, vias = routes() for q in segs: if q[6] == net or q[0] != 1: continue y0, y1 = sorted((q[2], q[4])) hw = q[5] / 2.0 + CLR if y0 - hw <= y <= y1 + hw: x0, x1 = sorted((q[1], q[3])) blocks.append((x0 - hw, x1 + hw)) for v in vias: if len(v) > 2 and v[2] == net: continue rr = VIA_D / 2.0 + CLR if abs(v[1] - y) <= rr: blocks.append((v[0] - rr, v[0] + rr)) for (b0, b1) in blocks: nxt = [] for (a0, a1) in spans: if b1 <= a0 or b0 >= a1: nxt.append((a0, a1)); continue if b0 > a0: nxt.append((a0, b0)) if b1 < a1: nxt.append((b1, a1)) spans = nxt return max([q[1] - q[0] for q in spans] or [0.0]) def series_windows(net): u"""这个网自己串联元件占的坐标窗口(本体的焊盘外框)。 ⛔ 断面校核**必须跳过这些窗口**:最窄的一刀永远落在串联元件身上 (2512 分流电阻自己就只有 3.2 mm 宽、DC 座三只脚挤在 5 mm 里), 量到的是元件的宽度不是铜的宽度,判据就没意义了。 元件本身的载流由选型保证(`Rs6` 是 2512 3 W、`F1` 是 12 A 保险), 这里要查的是**元件之间那段铜**。 """ # ⛔ 窗口要按**整颗器件**的焊盘外框算,不是只按它属于本网的那只脚。 # 只按那只脚的话,`D1`(TVS:一端 VIN_SW、一端 GND)只有阴极那半边被跳过, # 阳极的 GND 盘照样把断面掐断 —— 实测 `VIN_SW` 被判成 4.35 mm。 # 物理上电流是**穿过器件**的,整颗器件占的那一段本来就不该算铜宽。 ps = pads() own = set(p[0] for p in ps if p[7] == net) by = {} for p in ps: if p[0] not in own: continue b = by.setdefault(p[0], [1e9, 1e9, -1e9, -1e9]) b[0] = min(b[0], p[2] - p[4] / 2); b[2] = max(b[2], p[2] + p[4] / 2) b[1] = min(b[1], p[3] - p[5] / 2); b[3] = max(b[3], p[3] + p[5] / 2) return by def check_cuts(ps): print(u'') print(u'载流断面校核(逐刀切,取**最窄**一刀,跳过串联元件本身):') bad = [] byname = {} for (n, _net, _l, r, _p) in ps: byname.setdefault(n, []).append(r) for key, label, net, axis, need, must in CUTS: # 挖了 GATE 缺口之后一段铜会被切成几块(`PWR-X`、`PWR-X-1`…):每一刀取包含它的那块 rs = [r for n, v in byname.items() if n == key or n.startswith(key + '-') for r in v] if not rs: print(u' %-18s %-9s [X] 这一段没有铜面' % (label, net)) bad.append(label) continue r = (min(q[0] for q in rs), min(q[1] for q in rs), max(q[2] for q in rs), max(q[3] for q in rs)) skip = [] for ref, b in series_windows(net).items(): if b[0] > r[2] or b[2] < r[0] or b[1] > r[3] or b[3] < r[1]: continue skip.append((ref, (b[0] - CLR, b[2] + CLR) if axis == 'x' else (b[1] - CLR, b[3] + CLR))) lo, hi = (r[0], r[2]) if axis == 'x' else (r[1], r[3]) hs = [] t = lo + 0.25 while t <= hi - 0.25: if not any(a <= t <= b for _ref, (a, b) in skip): f = cut_height if axis == 'x' else cut_width_y hs.append((max(f(net, q, t) for q in rs), t)) t += 0.25 worst = min(hs) if hs else (0.0, 0.0) ok = worst[0] >= need print(u' %-18s %-9s 铜面 %5.1f×%5.1f 最窄 %5.2f mm (%s=%.2f,要 %.1f) %s' % (label, net, r[2] - r[0], r[3] - r[1], worst[0], axis, worst[1], need, u'[OK]' if ok else (u'[X] 不够' if must else u'[~] 瓶颈在连接器,不强制'))) if skip: print(u' (跳过串联元件 %s)' % ' '.join(sorted(ref for ref, _w in skip))) if not ok and must: bad.append(u'%s %s 最窄只有 %.2f mm' % (label, net, worst[0])) return bad # ── 缝合过孔 ───────────────────────────────────────────────────────────── def board_tracks(): return call(OPEN + "return (await eda.pcb_PrimitiveLine.getAll()||[])" ".filter(l=>l.layer===1||l.layer===2)" ".map(l=>[l.startX/%s,l.startY/%s,l.endX/%s,l.endY/%s," "l.lineWidth/%s,String(l.net||'')]);" % (MIL, MIL, MIL, MIL, MIL), timeout=300) def _seg_d(px, py, x0, y0, x1, y1): dx, dy = x1 - x0, y1 - y0 L = dx * dx + dy * dy t = 0.0 if L < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L)) return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy)) def stitch_points(tracks=None): u"""GND 缝合过孔的坐标。 ⛔ **顶层的地铜被信号切成很多块岛,每块岛至少要有一个缝合过孔**, 否则「移除孤岛」把它整块删掉 —— 而 `rebuildCopperRegion()` / 铺铜管理器 / DRC **三个地方都不报错**(刀片 §24.4)。 4 mm 网格是为了让最小的岛也有机会摊上一个。 """ d = pads() if tracks is None: tracks = board_tracks() r = VIA_D / 2.0 + STITCH_CLR ko = keepouts() # ⛔ **硬功率铜面里不打缝合孔**:那里顶层没有地铜,GND 孔只会在 9 A 铜上掏洞 # (4 mm 网格一刀能掏掉 3~4 mm 净铜),而断面校核看不到缝合孔。 ko += [rr for (n, _net, _l, rr, _p) in plan() if n.startswith('PWR-')] # 专用 GND 过孔(地岛 / 缺口里的地脚)永远在,网格点让开它们 own = gnd_pad_vias()[1] out = list(own) ko += [(vx - VIA_D, vy - VIA_D, vx + VIA_D, vy + VIA_D) for (vx, vy) in own] y = INSET + 2.0 while y <= B.BH - INSET - 2.0: x = INSET + 2.0 while x <= B.BW - INSET - 2.0: ok = True for k in ko: if k[0] - r < x < k[2] + r and k[1] - r < y < k[3] + r: ok = False break if ok: for p in d: # ⛔ **通孔地脚不能当「自己人」跳过**:缝合过孔落在它身上, # DRC 报「钻孔到钻孔 0 mil」(实测 J1_3 / C2_2 / J33_1 三处)。 # 同网也要留钻孔间距,这是工艺规则不是电气规则。 pass if p[2] - p[4] / 2 - r < x < p[2] + p[4] / 2 + r and \ p[3] - p[5] / 2 - r < y < p[3] + p[5] / 2 + r: ok = False break if ok: for t in tracks: if t[5] == 'GND': continue if _seg_d(x, y, t[0], t[1], t[2], t[3]) < r + t[4] / 2.0: ok = False break if ok: out.append((round(x, 3), round(y, 3))) x += STITCH_PITCH y += STITCH_PITCH return out def stitch(): pts = stitch_points() print(u'GND 缝合过孔 %d 个(%g mm 网格)' % (len(pts), STITCH_PITCH)) JS = OPEN + u""" const MIL = %s, P = %s, D = %s, H = %s; let old = 0; for (const v of (await eda.pcb_PrimitiveVia.getAll()||[])) if (String(v.net) === 'GND') { await eda.pcb_PrimitiveVia.delete(v.primitiveId); old++; } let n = 0; for (const p of P) { try { await eda.pcb_PrimitiveVia.create('GND', p[0]*MIL, p[1]*MIL, H*MIL, D*MIL); n++; } catch (e) {} } await eda.pcb_Document.save(); return {deleted: old, made: n, now: (await eda.pcb_PrimitiveVia.getAll()||[]).length}; """ % (MIL, json.dumps(pts), VIA_D, VIA_HOLE) print(call(JS, timeout=900)) # ── 下发铺铜 ───────────────────────────────────────────────────────────── JS_DEL = OPEN + u""" let n = 0; for (const old of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; } await eda.pcb_Document.save(); return {deleted: n}; """ JS_MAKE = OPEN + u""" const PLAN = %s; for (const p of PLAN) { const poly = await eda.pcb_MathPolygon.createComplexPolygon([p.src]); if (!poly) continue; try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false, p.name, p.prio, 10); } catch (e) {} } return {made: (await eda.pcb_PrimitivePour.getAll()) .map(x => String(x.pourName || '').toUpperCase())}; """ JS_REBUILD = OPEN + u""" const sleep = ms => new Promise(r => setTimeout(r, ms)); const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; }; const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t); if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; }; const leaf = (txt) => { for (const el of document.querySelectorAll('*')) if (el.children.length===0 && (el.textContent||'').trim()===txt) return el; return null; }; const btn = (txt) => { for (const b of document.querySelectorAll('button')) if ((b.innerText||'').trim()===txt && b.offsetParent!==null) return b; return null; }; if (!fire(leaf('工具'))) return {err: 'no 工具'}; await sleep(900); if (!fire(leaf('铺铜管理器...'))) return {err: 'no 铺铜管理器'}; await sleep(2500); const b = btn('重建所有'); if (!b) return {err: 'no 重建所有'}; return {fired: fire(b)}; """ JS_POLL = u"return {poured: (await eda.pcb_PrimitivePoured.getAll()).length};" JS_DONE = u""" const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; }; const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t); if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; }; let ok = null; for (const b of document.querySelectorAll('button')) if ((b.innerText||'').trim()==='确认' && b.offsetParent!==null) { ok = b; break; } if (ok) fire(ok); await new Promise(r => setTimeout(r, 1500)); await eda.pcb_Document.save(); return {poured: (await eda.pcb_PrimitivePoured.getAll()).length, pour: (await eda.pcb_PrimitivePour.getAll()).length}; """ def rebuild(): print(u'重建所有铺铜(走 GUI —— `rebuildCopperRegion()` 是坏的,刀片 §24 已查清)…') print(call(JS_REBUILD, timeout=180)) last, same = -1, 0 for _ in range(40): time.sleep(6) n = call(JS_POLL, timeout=120)['poured'] print(u' 已填 %d 块' % n) same = same + 1 if n == last else 0 last = n if same >= 2 and n > 0: break print(call(JS_DONE, timeout=180)) def push(ps): print(call(JS_DEL, timeout=300)) arr = [{'name': n, 'net': net, 'layer': l, 'prio': p, 'src': rrect_src(*r)} for (n, net, l, r, p) in ps] made = [] for k in range(0, len(arr), 12): made += call(JS_MAKE % json.dumps(arr[k:k + 12], ensure_ascii=False), timeout=600)['made'] want = set(n.upper() for (n, _net, _l, _r, _p) in ps) got = set(made) print(u'铜面建了 %d / %d 块' % (len(want & got), len(want))) if want - got: print(u' [!] 没建上: %s' % ' '.join(sorted(want - got))) rebuild() if __name__ == '__main__': # 铜面按 ROUTE_FIX 的计划位置算(见 pads());板端还没挪件就铺,铜和件对不上。 if ('--push' in sys.argv or '--stitch' in sys.argv) and PL.route_fix_moves(): sys.exit(u'!! ROUTE_FIX 还有 %d 件没下发到板端(%s);先 place_base --push 再 ' u'--adopt-live --dump,然后才能铺铜' % (len(PL.route_fix_moves()), ' '.join(sorted(PL.route_fix_moves())))) if '--clear' in sys.argv: print(call(JS_DEL, timeout=300)) sys.exit(0) ps = plan() bad = selfcheck(ps) print(u'计划建 %d 块铜面:' % len(ps)) for name, net, lay, r, prio in ps: if not name.startswith('GND-') or name.endswith('-0'): print(u' %-12s net=%-9s L%d x %6.2f~%6.2f y %6.2f~%6.2f prio=%d' % (name, net, lay, r[0], r[2], r[1], r[3], prio)) print(u' (GND 瓦片 %d 块,绕开天线净空与四个 M3 孔)' % sum(1 for p in ps if p[0].startswith('GND-'))) if bad: print(u'[X] 自检 %d 条不过:' % len(bad)) for t in bad: print(u' ' + t) else: print(u'[OK] 自检全过(不出内缩线 / 不压净空 / 入口链四段互不重叠)') wbad, wown, wtot = wall_check() bs, bv = B.wall3v3_bridges() print(u'3V3 槽墙:%d 块矩形,盖住 %d/%d 只槽脚;走廊底层桥 %d 段 / 过孔 %d' % (len(B.wall3v3_rects()), wown, wtot, len(bs), len(bv))) if wbad: print(u'[X] 3V3 槽墙自检 %d 条不过:' % len(wbad)) for t in wbad: print(u' ' + t) bad += wbad else: print(u'[OK] 3V3 槽墙:槽脚全盖住 / 不压异网针 / 桥的过孔都在墙里') bbad, bown, btot = bus_check() print(u'12V 干道/铜墙:盖住 %d/%d 只插槽电源脚' % (bown, btot)) if bbad: print(u'[X] 12V 干道/铜墙自检 %d 条不过:' % len(bbad)) for t in bbad: print(u' ' + t) bad += bbad else: print(u'[OK] 12V 干道/铜墙:槽脚全盖住 / 不压异网针') cbad = check_cuts(ps) bad += cbad if '--stitch' in sys.argv: stitch() if '--push' in sys.argv: if bad: sys.exit(u'!! 自检不过,不下发') push(ps) elif '--rebuild' in sys.argv: rebuild()