# -*- coding: utf-8 -*- u"""载板 PCB2-CARRIER 的扇出 —— 复用下板 `pcb_fanout.py` 的规划器,只换掉板子相关的常量。 /c/Python314/python .claude/dump_pads.py 942ed1560d81c1e7 pcb2-copper.json # 1. 抓障碍表 /c/Python314/python .claude/pcb2_fanout.py # 2. 只规划 /c/Python314/python .claude/pcb2_fanout.py --push # 3. 下发 ⚠️ 顺序同 `pcb_fanout.py` 文件头那条:**布局一动就要 `--clean` → 重新 dump → `--push`**, 三步不能省也不能并(并了会拿旧障碍表规划,或者板上剩两套过孔)。 ## 为什么现在只扇 GND Inner2 是整板地平面,已经铺好了;但载板上 242 个 GND 焊盘里只有 **20 个是直插** (五路的 C8n1 负极 + TP8n2、J23 的四个屏蔽脚、6 个 M3), 其余两百多个贴片焊盘**根本没连到平面上**。 Inner1 的电源岛还没划(卡在排母选型),所以 `+5V` / `+3V3_HUB` / `VOn_P` 现在扇过去就是一堆悬空过孔 —— 等 Inner1 铺完再一起扇。 ## 和下板的四处不同 | | 下板 PCB1 | 载板 PCB2 | |---|---|---| | 板框 | 160 × 125 | **220 × 135** | | 元件面 | 全在顶层 | **两面都是**(J31/J32 + 整个 Hub 在底层)→ `FANOUT_LAYERS = (1, 2)` | | 天线禁铺区 | ESP32 在下板 | **没有** → 挪到板外,等于不生效 | | 目标平面层 | GND→Inner1(15) | **GND→Inner2(16)**(层角色是镜像的,§25.6 ①)| ~~⛔ J31/J32 的 33 个 GND 脚故意不扇~~ —— **2026-09-05 这条整个作废**(`SKIP_HDR = False`)。 排母换成直插的 `C5307344`(§22.3f)之后,那 80 个脚**本身就是通孔**, 直接落到 Inner1/Inner2 上,根本不需要扇出 —— 而且 `pcb_fanout.one_net()` 的 `todo` 本来就带 `not p['hole']`,通孔焊盘一律不进扇出清单。 所以这里只要把 `SKIP_HDR` 关掉,`_pour_filter` 返回 None,规划器自己会跳过它们。 `HDR_BOXES` 留着当历史记录(那是**旧的贴片**焊盘区,坐标已经作废)。 """ import importlib.util import io import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) _spec = importlib.util.spec_from_file_location( 'pcb_fanout', os.path.join(HERE, 'pcb_fanout.py')) F = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(F) # ── 换掉板子相关的常量 ──────────────────────────────────────────────── F.PCB = '942ed1560d81c1e7' # PCB2-CARRIER F.SRC = os.path.join(HERE, 'pcb2-copper.json') F.BOARD = (0.0, 0.0, 220.0, 135.0) F.R_CORNER = 5.0 F.ANT = (-100.0, -100.0, -99.0, -99.0) # 载板没有天线禁铺区,挪到板外 F.FANOUT_LAYERS = (F.L_TOP, F.L_BOT) # 两面都是元件面 # ⚠️ 载板的层角色和下板是**镜像的**:Inner1(15)=电源面 / Inner2(16)=完整地平面。 # 照抄下板的 `TARGET['GND'] = L_GND(15)` 会把地过孔打到电源面上。 # 下板 L_GND=15 是它的地平面,载板的地平面是 **Inner2 = 16**(= 下板的 L_PWR 常量)。 F.TARGET = {'GND': 16} F.N_VIA = {'GND': 1} F.TRK_W_MAX = {'GND': F.TRK_W} F.TRK_W_NET = F.TRK_W_MAX # ── J31/J32 的焊盘区:现在不扇(排母未定,换件后整体移位)────────────── SKIP_HDR = False # 2026-09-05 换成直插排母,80 个脚自己就是过孔 HDR_BOXES = [(36.5, 63.5, 87.5, 71.5), # J31,实测焊盘 x 37.87~86.13 / y 65.1~69.9 (132.5, 63.5, 183.5, 71.5)] # J32,实测 x 133.87~182.13 def _pour_filter(net): u"""借 `pcb_fanout.one_net()` 里现成的钩子来**排除**焊盘(原本是用来筛铜面覆盖的)。 返回的谓词为 True 才扇出。这里对 GND 返回「不在 J31/J32 焊盘区里」。 """ if not SKIP_HDR or net != 'GND': return None def keep(x, y): return not any(b[0] <= x <= b[2] and b[1] <= y <= b[3] for b in HDR_BOXES) return keep F._pour_filter = _pour_filter # ── v5 差分布完之后加的三条硬约束(PROGRESS「接下来按顺序 · 2」)──────── # ⚠️ 前提:**这一轮不许 `--clean`**。板上那 178 条线(11 对差分 + 3 条 +3V3 车道) # 是上一轮的成果,`clean2()` 会把信号层的线连同过孔一起删光。 # 规划器现在会把它们当障碍(`pcb_fanout.EXIST_LAYERS`,同层比线 / 过孔不分层)。 # U32(Hub)西缘 pin13 GND —— **不往西挤了,改往 QFN 肚子里打**。 # # 上一轮留的那条「孔位钉在 (94.6, 71.4)」实测**画不出来**: # 西边的走廊确实只剩 x ∈ [94.59, 94.625](DP5 车道东沿 94.09 + 0.5,U32 西排焊盘西沿 95.125 − 0.5), # 但 pin13 的**引线出不去**:pin13(y 70.10~70.40) 与 pin14(70.60~70.90) 之间只有 0.20 mm, # 0.3 的线要斜着往西北走,一定切进 pin14 的净距圈;而留在 y=70.25 平着往西, # 过孔又躲不开 USB5_DP 的横段(y 69.75,只差 0.5 < 0.64)。 # # 正解是往**东**:QFN 的引脚环与散热盘之间有一整圈空地 # (西排东沿 95.975 ←→ 散热盘西沿 97.600,净宽 1.625), # 自动搜索已经把 pin13 放在 (96.50, 70.25)、pin19 放在 (97.25, 71.50),都在这一圈里。 # 那里到差分车道有 2 mm 以上,比西边的走廊宽裕得多。⇒ 不再钉死孔位。 F.FORCE_VIA = {} # C747 / C748 的 GND 脚(都是 pin2,在各自 +3V3 脚的正北): # 南边是留给 +3V3_HUB 的进线,过孔一律往北打。 F.ANG_RANGE = {('C747', '2'): (45, 135), ('C748', '2'): (45, 135)} # ── J7n(尾插 BM24,五个站各 13 个 GND 脚)整批不扇 ──────────────────── # 这 65 个脚是 `pcb2_route_sig.py` 的 `bm24_pre()` **手写死的两条梳子 + pin52 单独出线** # (§25.8:0.7 mm 间距里根本打不下 Ø0.61 的孔,只能逃逸出来再并)。 # 让自动扇出去碰它们有两个后果: # ① 十个脚必然失败(实测 50 个),失败清单被这些噪声塞满,真正的问题看不见; # ② pin 1 / 2 / 52 又「成功」了 —— 打出来的孔正好落在梳子和车道上, # route_sig 还得写一个删除框把它们收回去(那个删除框就是这么来的)。 def _skip(p): return p['net'] == 'GND' and (p['des'] or '').startswith('J7') F.SKIP_PAD = _skip # ── 载板专用的 clean ────────────────────────────────────────────────── # ⚠️⚠️ **不能用 `F.clean()`**:下板那个 CLEAN_JS 删的是「layer !== 11 的所有导线」。 # 下板上除了板框没有别的线,所以没出事;载板上却有 # layer 3 丝印 50 条(5 个手机位外框 + 接口边加粗) # layer 9 顶层装配 100 条 # layer 13 文档层 20 条 # 照抄会把这 170 条一起删掉,而且 DRC / 几何检查一条都不会报。 # 这里只删**过孔**和**信号层**(1/2/15/16)的导线。 CLEAN_JS2 = u""" await eda.dmt_EditorControl.openDocument('__PCB__'); await new Promise(r => setTimeout(r, 1500)); const SIG = [1, 2, 15, 16]; const vs = await eda.pcb_PrimitiveVia.getAllPrimitiveId(); if (vs.length) await eda.pcb_PrimitiveVia.delete(vs); const ls = (await eda.pcb_PrimitiveLine.getAll()).filter(l => SIG.includes(l.layer)); if (ls.length) await eda.pcb_PrimitiveLine.delete(ls.map(l => l.primitiveId)); await eda.pcb_Document.save(); const left = {}; for (const l of await eda.pcb_PrimitiveLine.getAll()) left[l.layer] = (left[l.layer] || 0) + 1; // ⚠ 板框是 layer 11 上的一条**多段线**(pcb_PrimitivePolyline),不是 Line —— // 所以「剩余导线」里永远没有 11,别拿它当板框还在的证据。 const outline = (await eda.pcb_PrimitivePolyline.getAll()).filter(p => p.layer === 11).length; return {vias: vs.length, lines: ls.length, outline, leftVia: (await eda.pcb_PrimitiveVia.getAll()).length, leftByLayer: left}; """ def clean2(): r = F.run(CLEAN_JS2.replace('__PCB__', F.PCB), timeout=600) if not r.get('success'): sys.exit(u'清理失败: ' + json.dumps(r, ensure_ascii=False)[:400]) x = r['result'] print(u'清掉:过孔 %d / 信号层导线 %d;剩余过孔 %d' % (x['vias'], x['lines'], x['leftVia'])) print(u' 剩余导线按层: %s(3=丝印 / 9=装配 / 13=文档,都必须还在);板框多段线 %d 条' % (x['leftByLayer'], x['outline'])) for lay in ('3', '9', '13'): if not x['leftByLayer'].get(lay): sys.exit(u'!! layer %s 的导线被清光了,说明用错了 clean' % lay) if x['outline'] != 1: sys.exit(u'!! layer 11 的板框多段线有 %d 条,应为 1' % x['outline']) def main(): if not os.path.exists(F.SRC): sys.exit(u'先抓障碍表:/c/Python314/python .claude/dump_pads.py ' u'942ed1560d81c1e7 pcb2-copper.json') print(u'载板扇出:板框 %.0f × %.0f,元件面 %s,GND -> layer %d' % (F.BOARD[2], F.BOARD[3], F.FANOUT_LAYERS, F.TARGET['GND'])) if SKIP_HDR: print(u'⛔ J31/J32 的焊盘区不扇(排母未定,换件后 80 个焊盘会整体移位)') if '--clean' in sys.argv: clean2() if '--push' not in sys.argv: return d = json.load(io.open(F.SRC, encoding='utf-8')) F.one_net('GND', d) if __name__ == '__main__': main()