# -*- coding: utf-8 -*- u"""R2 底板 PCB 的设计规则(M8b)。 /c/Python314/python .claude/r2/rules_base.py # 只反查 + 打印,不下发 /c/Python314/python .claude/r2/rules_base.py --dump # 重抓「位号.引脚 → 网名」 /c/Python314/python .claude/r2/rules_base.py --push # 下发规则 + 差分对 + 网络指派 /c/Python314/python .claude/r2/rules_base.py --audit # 读回审计 做法与 `pcb_rules.py`(刀片)同源,**那一份的文件头是唯一的说明书**,四个坑一个没变: · `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装 · `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状** · 下发之后**别再去 GUI 换「配置」下拉框** —— 换一次整套重置,网络指派全部悬空 · `overwriteNetRules` / `overwriteNetByNetRules` 都是**整表覆盖**,先读回再合并 ## 和刀片那份不同的四处 1. **四层板**(D15,2026-09-10 由 2 层改)。建工程时是 2 层,`--push` 先 `setTheNumberOfCopperLayers(4)`(换层数会换基准能力集,方案 §24.1), 再照刀片补 `copperThickness0.5oz` 线宽 + `copperThicknessInner0.5oz` 间距 (内层表从 `JLCPCB Capability(Multiple Layers Board)` 拷)。 叠层:L1 插槽面 / L2 GND / L3 GND / L4 器件面,内层只做地平面、不布线。 2. **入口链是 9 A**(方案 §6.4),不是刀片的 1.45 A。线宽规则给到 `strokeWidthMax = 12`,真正的载流靠 `pour_base` 的断面校核保证,不靠 DRC。 3. **七对差分**:五个槽 + ESP32 原生 USB(D13)+ 上行 USB-C。 D± 走 L4、参考 L3,和刀片同一种薄介质微带 ⇒ 直接用刀片那组 **W 0.28 / S 0.18**(~90 Ω)。 ⛔ 两层版的 W 1.2 / S 0.2 在四层上阻抗严重偏低,别拿回来。 intra-pair skew **150 mil(3.81 mm)**(2026-09-10 用户定,原 1 mm;刀片放到 5)。 USB 2.0 HS 480 Mbps 下 3.8 mm ≈ 25 ps,常用预算即 150 mil;构造 D±(U 形掉头 / 东绕)的 天然长度差 56~136 mil,按 1 mm 卡只能加蛇形,没必要。 4. **自动命名网一个都不指派**:底板上 22 个 `$xNyy` 全是小信号 (泄放电阻串、Buck 反馈、DAC 的 RC、风扇 MOS 栅极),走默认线宽即可。 ⇒ 没有「按位号.引脚反查自动网名」那一步,也就没有那一类静默失效。 """ import io import json import os import sys 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 PCB = B.PCB LAYERS = 4 NETS_FILE = os.path.join(HERE, '_base_pcb_nets.json') # ── 线宽规则(mm,外层 1 oz)──────────────────────────────────────────── # 载流靠灌铜断面保证、不靠 DRC;min 一律压到 0.15~0.3,否则 0402 焊盘扇不出去。 TRACK_RULES = { 'PWR9A': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.30, 'strokeWidthMax': 12.0}, 'PWR12V': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.30, 'strokeWidthMax': 12.0}, 'FAN12V': {'strokeWidthDefault': 0.8, 'strokeWidthMin': 0.25, 'strokeWidthMax': 3.0}, 'PWR5V': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.20, 'strokeWidthMax': 3.0}, 'PWR3V3': {'strokeWidthDefault': 0.5, 'strokeWidthMin': 0.15, 'strokeWidthMax': 3.0}, } # ── 有名字的网络 → 线宽规则 ──────────────────────────────────────────── # ⚠ `+12V` 是**一个网**:从 Rs6 下游一路到 5 个槽的 12V 针(43 只脚)。 # 干路 9 A 那一段和「母线 → 单槽」1.5 A 那一段线宽不同,DRC 只能按一条规则判 ⇒ # 取干路的 `PWR9A`(default 2.0 只是画线的起手宽度),断面由 `pour_base` 逐段校核。 NET_TRACK = { # D16(2026-09-11):入口改 USB-C PD —— 20V 侧 ≤5 A,降压后 12V 侧 ≤8 A;载流仍由 pour_base 的铜块保证 'VIN_RAW': 'PWR9A', # J1 VBUS → F1(20V) 'VIN_FUSED': 'PWR9A', # F1 → U37 / U36 / 输入电容(20V);L1 上另有小岛连通 U37 两侧 VIN 'SW12': 'PWR9A', # U37 SW → L4(开关节点,≤8 A,只有 1.6 mm 长) 'VIN_SW': 'PWR9A', # L4 → 储能 → Rs6 上游(12V) '+12V': 'PWR9A', # Rs6 下游 → 母线 → 5 槽 + 两颗 Buck + 风扇 'FAN_12V_SW': 'FAN12V', # 风扇下管开关点(R1 §22.6 定的 0.8) '+5V': 'PWR5V', # U30 → 5 槽(每槽给刀片 U5 TPS22918) '+3V3': 'PWR3V3', # U19 → ESP32 / mux / DAC / 5 槽逻辑 '+3V3_HUB': 'PWR3V3', # U31 AMS1117 → U32(与 +3V3 只共地) } # ── 铺铜焊盘全连接(理由同刀片:不是怕十字花烧,是**铺完之后断面要量得准**)── PLANE_RULE = 'PLANE-DIRECT' PLANE_CONNECT_DIRECT = 1 PLANE_NETS = ['VIN_RAW', 'VIN_FUSED', 'SW12', 'VIN_SW', '+12V'] # ── 差分对(mm)── L4 微带参考 L3,同刀片:W 0.28 / S 0.18 ⇒ ~90 Ω ── DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.20, 'strokeWidthMax': 0.60, 'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15, 'differentailPairLenTolerMax': 3.81} # intra-pair skew ≤ 150 mil(§6.3,2026-09-10 由 1 mm 放宽) PAIR_RULE = 'USBPAIR' PAIR_CLEARANCE = 0.15 # 必须 ≤ 对内间距 0.18,否则整对自己判违规 PAIRS = [('USB1', 'USB1_DP', 'USB1_DM'), ('USB2', 'USB2_DP', 'USB2_DM'), ('USB3', 'USB3_DP', 'USB3_DM'), ('USB4', 'USB4_DP', 'USB4_DM'), ('USB5', 'USB5_DP', 'USB5_DM'), ('USB6', 'USB6_DP', 'USB6_DM'), # D13:Hub 第 6 口 → ESP32 原生 USB ('USBUP', 'UP_DP', 'UP_DM')] # J23 USB-C → U32 上行 # 0.15,同刀片。两层版取 0.2 是为了走 JLC 两层标准工艺;四层能力 0.09,0.15 不加价, # 而 133 件挤在一面上,布线通道比两层版更紧。 MIN_CLEARANCE = 0.15 OPEN = ("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2000));" % PCB) def call(body, timeout=300): r = run(OPEN + body, timeout=timeout) if not r.get('success'): print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:600]) sys.exit(1) return r['result'] def dump_nets(): u"""重抓「位号.引脚 → 网名」。**每次导入变更之后必跑**。""" r = call("const out = {};" "for (const c of await eda.pcb_PrimitiveComponent.getAll()) {" " const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(" " c.getState_PrimitiveId());" " out[c.designator] = ps.map(p=>[p.padNumber, p.net]); }" "return out;") io.open(NETS_FILE, 'w', encoding='utf-8').write(json.dumps(r, ensure_ascii=False)) print(u'已写 _base_pcb_nets.json:%d 件' % len(r)) return r def nets(): if not os.path.exists(NETS_FILE): return dump_nets() return json.load(io.open(NETS_FILE, encoding='utf-8')) def check_names(): u"""下发前先确认:要指派的网名在板上真的存在(写错名字 = 规则悬空,DRC 不报)。""" have = set() for _ref, pads in nets().items(): for _pn, net in pads: if net: have.add(net) miss = [n for n in sorted(set(list(NET_TRACK) + PLANE_NETS)) if n not in have] pmiss = [p for t in PAIRS for p in t[1:] if p not in have] if miss or pmiss: print(u' [!!] 板上没有这些网,**不下发**:线宽/铺铜 %s;差分 %s' % (miss, pmiss)) sys.exit(1) return True CAP_JS = u""" const D = eda.pcb_Drc; const cur = await D.getCurrentRuleConfiguration(); const c = JSON.parse(JSON.stringify(cur.config)); const multi = (await D.getRuleConfiguration( 'JLCPCB Capability(Multiple Layers Board)')).config; const TRACKS = %s, DW = %s, MINC = %s, PAIRRULE = %s, PAIRC = %s; const PLANERULE = %s, PLANEMODE = %s; const setTrack = (e, mn, df, mx) => { for (const k of Object.keys(e.form.data)) Object.assign(e.form.data[k], {minValue: mn, defaultValue: df, maxValue: mx}); }; const T = c['Physics']['Track'], SS = c['Spacing']['Safe Spacing']; // 四层:外层 1 oz + 内层 0.5 oz。这两段照抄刀片 pcb_rules.py(§24.1)。 setTrack(T['copperThickness1oz'], 0.102, 0.254, 2.54); if (!T['copperThickness0.5oz']) { const e = JSON.parse(JSON.stringify(T['copperThickness1oz'])); e.editName = 'copperThickness0.5oz'; e.isSetDefault = false; setTrack(e, 0.089, 0.254, 2.54); T['copperThickness0.5oz'] = e; } Object.assign(c['Physics']['Via Size']['viaSize'].form, {viaOuterdiameterMin: 0.3, viaInnerdiameterMin: 0.2}); if (!SS['copperThicknessInner0.5oz']) { const e = JSON.parse(JSON.stringify(SS['copperThickness1oz'])); e.editName = 'copperThicknessInner0.5oz'; e.isSetDefault = false; const src = multi['Spacing']['Safe Spacing']['copperThicknessInner0.5oz'].table; for (const k of Object.keys(e.tables)) e.tables[k].content = JSON.parse(JSON.stringify(src)); SS['copperThicknessInner0.5oz'] = e; } for (const name of Object.keys(TRACKS)) { const f = TRACKS[name]; const e = JSON.parse(JSON.stringify(T['copperThickness1oz'])); e.editName = name; e.isSetDefault = false; e.unit = 'mm'; setTrack(e, f.strokeWidthMin, f.strokeWidthDefault, f.strokeWidthMax); T[name] = e; } const dp = c['Physics']['Differential Pair']['differentialPair'].form; for (const k of Object.keys(dp.strokeWidthTables.data)) Object.assign(dp.strokeWidthTables.data[k], {minValue: DW.strokeWidthMin, defaultValue: DW.strokeWidthDefault, maxValue: DW.strokeWidthMax}); for (const k of Object.keys(dp.diffPairSpacingTables.data)) Object.assign(dp.diffPairSpacingTables.data[k], {minValue: DW.differentailPairSpacingMin, defaultValue: DW.differentailPairSpacingDefault}); dp.differentailPairLenTolerMax = DW.differentailPairLenTolerMax; if (!SS[PAIRRULE]) { const e = JSON.parse(JSON.stringify(SS['copperThickness1oz'])); e.editName = PAIRRULE; e.isSetDefault = false; for (const tk of Object.keys(e.tables || {})) for (const row of (e.tables[tk].content || [])) for (let i = 0; i < row.length; i++) if (typeof row[i] === 'number') row[i] = PAIRC; SS[PAIRRULE] = e; } // 安全间距全表**归一到 MINC**:≤0.2 的格子一律赋值成 MINC(不是「只抬不降」); // 0.254 / 0.3 那几格是孔/板边相关的,不动。 let bumped = 0; for (const key of Object.keys(SS)) if (key !== PAIRRULE) for (const tk of Object.keys(SS[key].tables || {})) for (const row of (SS[key].tables[tk].content || [])) for (let i = 0; i < row.length; i++) if (typeof row[i] === 'number' && row[i] <= 0.2001 && Math.abs(row[i] - MINC) > 1e-6) { row[i] = MINC; bumped++; } const CZ = c['Plane']['Copper Zone']; if (!CZ[PLANERULE]) { const e = JSON.parse(JSON.stringify(CZ['copperRegion'])); e.editName = PLANERULE; e.isSetDefault = false; CZ[PLANERULE] = e; } for (const m of ['singleLayerPadModel', 'multiLayerPadModel']) for (const k of Object.keys(CZ[PLANERULE].form[m].data)) CZ[PLANERULE].form[m].data[k].connectMode = PLANEMODE; const ok = await D.overwriteCurrentRuleConfiguration(c); return {ok: ok === true, bumped: bumped, name: cur.name, track: Object.keys(T), spacing: Object.keys(SS), plane: Object.keys(CZ)}; """ def set_layers(): u"""铜层数 → LAYERS。**必须在下发规则之前**:换层数会换基准能力集,先下规则再换等于白下。""" r = call("const was = await eda.pcb_Layer.getTheNumberOfCopperLayers();" "if (was !== %d) await eda.pcb_Layer.setTheNumberOfCopperLayers(%d);" "await new Promise(r=>setTimeout(r,1500));" "return {was: was, now: await eda.pcb_Layer.getTheNumberOfCopperLayers()," " cfg: (await eda.pcb_Drc.getCurrentRuleConfiguration()).name};" % (LAYERS, LAYERS)) print(u' 铜层数 %s → %s(当前配置 "%s")' % (r['was'], r['now'], r['cfg'])) if r['now'] != LAYERS: print(u' [!!] 铜层数没改成 %d,不下发规则' % LAYERS) sys.exit(1) def push(): check_names() set_layers() r = call(CAP_JS % (json.dumps(TRACK_RULES), json.dumps(DIFF), MIN_CLEARANCE, json.dumps(PAIR_RULE), PAIR_CLEARANCE, json.dumps(PLANE_RULE), PLANE_CONNECT_DIRECT)) if not r.get('ok'): print(u' [!!] overwriteCurrentRuleConfiguration 返回非 true,没生效') sys.exit(1) print(u' 规则配置已下发:线宽条目 %s' % ' '.join(r['track'])) print(u' 安全间距归一 %d 格 → %.2f mm;配置名 "%s"' % (r['bumped'], MIN_CLEARANCE, r['name'])) n = call(""" const P = %s; let k = 0; for (const d of await eda.pcb_Drc.getAllDifferentialPairs()) await eda.pcb_Drc.deleteDifferentialPair(d.name); for (const [name, p, m] of P) { if (await eda.pcb_Drc.createDifferentialPair(name, p, m)) k++; } return k; """ % json.dumps(PAIRS)) print(u' 差分对 %d / %d 对' % (n, len(PAIRS))) if n != len(PAIRS): print(u' [!!] 有对没建上 —— 网络名在这块板上不存在') n = call(""" const T = %s, PN = %s, PR = %s; const pset = new Set(PN); const rules = await eda.pcb_Drc.getNetRules(); let k = 0, p = 0; const seen = []; for (const r of rules) { if (r.type !== 'net') continue; if (T[r.name]) { r['Track'] = T[r.name]; k++; seen.push(r.name); } if (pset.has(r.name)) { r['Copper Zone'] = PR; p++; } } await eda.pcb_Drc.overwriteNetRules(rules); return {track: k, plane: p, seen: seen}; """ % (json.dumps(NET_TRACK, ensure_ascii=False), json.dumps(PLANE_NETS, ensure_ascii=False), json.dumps(PLANE_RULE))) print(u' 线宽指派 %d / %d 个网络' % (n['track'], len(NET_TRACK))) if n['track'] != len(NET_TRACK): print(u' [!!] 板上找不到: %s' % ' '.join(sorted(set(NET_TRACK) - set(n['seen'])))) print(u' 铺铜全连接指派 %d / %d(%s)' % (n['plane'], len(PLANE_NETS), ' '.join(PLANE_NETS))) n = call(""" const PAIRS = %s, RULE = %s; const cur = await eda.pcb_Drc.getNetByNetRules(); const old = (cur && cur['Safe Spacing']) ? cur['Safe Spacing'] : []; const key = e => [e.net1.name, e.net2.name].sort().join('|'); const want = new Set(PAIRS.map(([n, dp, dm]) => [dp, dm].sort().join('|'))); const list = []; let dropped = 0; for (const e of old) { if (e.rule === RULE && !want.has(key(e))) { dropped++; continue; } list.push(e); } const have = new Set(list.map(key)); let k = 0; for (const [name, dp, dm] of PAIRS) { const e = {net1: {type: 'net', name: dp}, net2: {type: 'net', name: dm}, rule: RULE, copperRuleName: null, planeRuleName: null}; if (have.has(key(e))) continue; list.push(e); k++; } const ok = await eda.pcb_Drc.overwriteNetByNetRules( {'Safe Spacing': list, 'Creepage Distance': (cur && cur['Creepage Distance']) || []}); const back = await eda.pcb_Drc.getNetByNetRules(); return {added: k, dropped: dropped, ok: ok === true, now: (back['Safe Spacing'] || []).length}; """ % (json.dumps(PAIRS), json.dumps(PAIR_RULE))) print(u' 对内间距:新增 %d / 清掉过期 %d,现共 %d 条(%s = %.2f mm)' % (n['added'], n['dropped'], n['now'], PAIR_RULE, PAIR_CLEARANCE)) call("await eda.pcb_Document.save(); return 1;") print(u' 已保存') print(u'') print(u'⚠ 别去 GUI 的 设计 → 设计规则… 里换「配置」下拉框:换一次整套重置,') print(u' %d 个网络的线宽指派立刻悬空,而 DRC 不会报错。' % len(NET_TRACK)) print(u'⚠ 再导入一次变更之后顺序是:--dump → --push,少跑 --dump 就是拿旧网名指派。') AUDIT_JS = u""" const c = await eda.pcb_Drc.getCurrentRuleConfiguration(); const rules = await eda.pcb_Drc.getNetRules(); const nbn = await eda.pcb_Drc.getNetByNetRules(); return { cfgName: c && c.name, layers: await eda.pcb_Layer.getTheNumberOfCopperLayers(), track: Object.keys(c.config.Physics['Track']), pairs: (await eda.pcb_Drc.getAllDifferentialPairs()).map(d => d.name).sort(), assigned: rules.filter(r => r['Track'] && r['Track'] !== 'default') .map(r => [r.name, r['Track']]).sort(), planeAssigned: rules.filter(r => r['Copper Zone'] && r['Copper Zone'] !== 'default') .map(r => [r.name, r['Copper Zone']]).sort(), nbn: ((nbn && nbn['Safe Spacing']) || []).length, }; """ def audit(): r = call(AUDIT_JS) bad = [] def chk(cond, good, msg): print((u' [OK] ' if cond else u' [!!] ') + (good if cond else msg)) if not cond: bad.append(msg) print(u'──── 审计(配置 "%s")────' % r['cfgName']) chk(r['layers'] == LAYERS, u'铜层数 %d' % LAYERS, u'铜层数是 %s,应为 %d' % (r['layers'], LAYERS)) miss = [k for k in TRACK_RULES if k not in r['track']] chk(not miss, u'%d 条线宽条目都在' % len(TRACK_RULES), u'缺线宽条目 %s' % miss) got = dict(r['assigned']) wrong = [(n, got.get(n), v) for n, v in sorted(NET_TRACK.items()) if got.get(n) != v] chk(not wrong, u'%d 个网络的线宽指派全对' % len(NET_TRACK), u'指派不对: %s' % wrong[:6]) chk(sorted(r['pairs']) == sorted([p[0] for p in PAIRS]), u'差分对 %d 对: %s' % (len(r['pairs']), ' '.join(r['pairs'])), u'差分对是 %s,应为 %s' % (r['pairs'], [p[0] for p in PAIRS])) chk(r['nbn'] == len(PAIRS), u'对内间距 %d 条' % r['nbn'], u'对内间距 %d 条,应为 %d' % (r['nbn'], len(PAIRS))) gotp = dict(r['planeAssigned']) missp = [n for n in PLANE_NETS if gotp.get(n) != PLANE_RULE] chk(not missp, u'铺铜全连接 %d 个网络' % len(PLANE_NETS), u'没指派上: %s' % missp) print(u' ' + (u'[OK] 全部通过' if not bad else u'[X] %d 条不过' % len(bad))) return bad if __name__ == '__main__': print(u'底板 PCB=%s(%d 层)' % (PCB, LAYERS)) if '--dump' in sys.argv: dump_nets() if '--push' in sys.argv: push() audit() elif '--audit' in sys.argv: audit() elif '--dump' not in sys.argv: check_names() print(u'线宽规则:') for k, v in sorted(TRACK_RULES.items()): print(u' %-8s 默认 %.2f 最小 %.2f 最大 %.2f' % (k, v['strokeWidthDefault'], v['strokeWidthMin'], v['strokeWidthMax'])) print(u'网络指派:') for k, v in sorted(NET_TRACK.items()): print(u' %-12s → %s' % (k, v)) print(u'差分对 %d:%s' % (len(PAIRS), ' '.join(p[0] for p in PAIRS))) print(u'安全间距 %.2f;铺铜全连接 %s' % (MIN_CLEARANCE, ' '.join(PLANE_NETS)))