# -*- coding: utf-8 -*- u"""下板 PCB1 设计规则 + D± 差分对(2026-09-06,两板拆分之后重写)。 取代**单板时代**的 `pcb_rules.py`(那一份留在原地作历史,别再拿来下发 —— 它建 7 对差分、给 `+5V` 挂 `PWR12V`,两条在拆板之后都已经不对)。 做法和载板的 `pcb2_rules.py` 一模一样,**两个坑、两条陷阱全部照抄**, 那两份文件头是唯一的说明书,改规则前先读: · `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装 · `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状**, 两者不能混用(混了会被静默吃掉) · 下发之后**别再去 GUI 换「配置」下拉框**,换一次整套重置、网络指派全部悬空 · 写进去的 mm 会被换算成 mil 再换回来,`0.2` 回读是 `0.19999959999999997`, 任何几何比较都留 1e-4 容差 ## 和载板不同的四处 1. **差分对只剩 1 对**:`DEBUG`(`USB_DP` / `USB_DM`,J22 调试口 → U20 ESP32-S3)。 五路下行 D±、上行 `UP_*` 和五段 `*J` 在 2026-09-04 拆板时**整体搬到载板**, 下板上这些网络已经不存在了 —— 建不上的对不是「没建上」,是本来就不该有。 `strokeWidthMin` 保持 **0.20**(不像载板降到 0.10):这一对两端是 USB-C 和模组, 路上没有 0.35 mm 间距的连接器要钻。 2. **层角色是载板的镜像**:下板 **Inner1(L2) = 完整地平面 / Inner2(L3) = 分割电源面** (§10.1)。D± 走**顶层、以 Inner1 为参考**,所以 W 0.28 / S 0.18 这一组几何 与载板完全同源(嘉立创四层 1.6 mm 标准叠层上下对称,L1-L2 与 L3-L4 都是 0.2104 mm)。 3. **线宽分五档**(载板三档)。多出来的两档是下板独有的东西: | 规则 | 默认 | 网络 | 依据 | |---|---|---|---| | `PWR12V` | 2.0 | `+12V` + 8 A 输入链三段 | §10.1:8 A 走 2.0 mm 温升 35 °C ⇒ **总干必须走灌铜**,2.0 只给分支 | | `SWNODE` | 2.0 | 五路 Buck 的 SW 节点 | §10.1 表:4 A 的 SW 铜箔给 2.0 mm(温升 10.4 °C)| | `PWR4A` | 1.5 | `VOn` / `VOn_F` / `VOn_P` | 每路 4 A(IPC-2221 外层 1 oz,ΔT≈16.7 °C)| | `PWR5V` | 1.2 | `+5V` + U30 的 SW | §9.4 最坏 2.8 A(1.2 mm 可载 3.6 A)| | `PWR1A` | 0.6 | `+3V3`、`SW3V3` | §9.3 辅助 Buck 按 0.5 A 设计 | **`SWNODE` 为什么和 `PWR4A` 分开、而且 max 卡在 4.0**:SW 是开关节点, 铜越宽 I²R 越小但 dV/dt 的辐射面积越大(§10.2-2「压到刚好够载流」)。 `PWR4A` 的 max 6.0 是给灌铜出线用的,SW 不该享受那个上限。 最小值一律 0.15~0.3,理由同载板 §10.1:规则是**按网络**生效的, `+12V` 上挂着 R104 那种 0402 分压抽头(焊盘才 0.57 mm 宽), 卡大线宽会让几十个焊盘扇不出去、刷出一屏假 DRC。 **载流靠灌铜保证,不靠 DRC。** 4. ⭐ **四条 4 A / 8 A 的干路是自动命名网(`$2N699` 一类), 这里按「位号 + 引脚」当场反查,不写死网名。** 拆板那次导入变更之后,同一个节点的自动网名从 `$2N687` 变成了 `$2N699`。 写死网名的后果是**静默失效**:`overwriteNetRules` 只改它认得的行, 认不出的名字**既不报错也不留痕**,DRC 照样干干净净, 而板上那条 4 A 干路的线宽规则已经悬空了。 ⇒ 事实来源是 `pcb1-parts.json` 里每个焊盘带的 `net`, `resolve_auto()` 拿 (位号, 引脚号) 反查,并**逐条断言**: 查出来的必须是 `$` 开头的自动网、必须互不重复、 而且必须同时出现在这条链路的另一端(eFuse / Buck 的对应脚上)。 对不上就直接退出,不下发。 用法: /c/Python314/python .claude/pcb1_rules.py [--push | --check] ⚠️ 跑之前先跑过 `pcb1_parts_dump.py`(导入变更之后必跑), 否则反查用的是旧网名 —— 这正是上面第 4 条要防的那件事。 """ import io import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run PCB = '7e9415a16a909c51' # PCB1(下板) PARTS = os.path.join(HERE, 'pcb1-parts.json') # ── 线宽规则(mm)────────────────────────────────────────────────────── TRACK_RULES = { 'PWR12V': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.3, 'strokeWidthMax': 10.0}, 'SWNODE': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.3, 'strokeWidthMax': 4.0}, 'PWR4A': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0}, 'PWR5V': {'strokeWidthDefault': 1.2, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0}, 'PWR1A': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.15, 'strokeWidthMax': 3.0}, } # ── 有名字的网络 → 线宽规则 ──────────────────────────────────────────── NET_TRACK = {} for _n in range(1, 6): NET_TRACK['VO%d' % _n] = 'PWR4A' # Buck 输出 → eFuse 输入 NET_TRACK['VO%d_P' % _n] = 'PWR4A' # 分流之后 → 排针 → 载板 NET_TRACK['+12V'] = 'PWR12V' NET_TRACK['+5V'] = 'PWR5V' NET_TRACK['+3V3'] = 'PWR1A' NET_TRACK['SW3V3'] = 'PWR1A' # U19 SW → L2,0.5 A # ── 自动命名网 → 线宽规则(按「位号.引脚」反查,见文件头第 4 条)────── # 值是 (探针, 校核端):两端查出来的必须是同一个网,否则说明位号/引脚写错了。 AUTO_TRACK = {} for _n, _u in zip(range(1, 6), (1, 2, 3, 4, 5)): _c = _n * 100 + 1 # SW 节点:Buck 的 SW 脚 → 电感。TPS54622(VQFN-14) 的 SW 是 pin 11/12。 AUTO_TRACK['SW%d' % _n] = ('SWNODE', ('L%d' % _c, '1'), ('U%d' % _u, '11')) # VOn_F:eFuse 输出 → PGTH 分压 / INA226 VIN+ → 分流电阻上游。 # TPS25940(WQFN-20) 的 OUT 是 pin 4~8,Rs*01 的上游脚是 pin 2。 AUTO_TRACK['VO%d_F' % _n] = ('PWR4A', ('Rs%d' % _c, '2'), ('U%d' % (13 + _n), '5')) # 8 A 输入链(§10.2b):J1 → F1 → Q1 → C2/C3/D1 → Rs6。分叉之前全程 8 A。 AUTO_TRACK['VIN_RAW'] = ('PWR12V', ('J1', '2'), ('F1', '1')) AUTO_TRACK['VIN_FUSED'] = ('PWR12V', ('F1', '2'), ('Q1', '1')) AUTO_TRACK['VIN_SW'] = ('PWR12V', ('Rs6', '1'), ('C2', '1')) # 第二路辅助 Buck 的 SW(§9.4,12V→5V,最坏 2.8 A) AUTO_TRACK['SW5V'] = ('PWR5V', ('L3', '1'), ('U30', '2')) # 母线泄放链(2026-09-06 R9 拆成 5 × 2Ω 2512):Q3 漏极 → R9A → … → R9E → GND。 # 13.3 V 全压加在 10 Ω 上是 **1.33 A**,默认线宽扛不住;`PWR1A`(0.6 mm) 外层 1 oz # 可带 2.16 A(ΔT 20 °C),够。四个串联节点全是自动命名网,同样按位号反查。 for _i, (_a, _b) in enumerate(zip('ABCD', 'BCDE')): AUTO_TRACK['BLEED%d' % (_i + 1)] = ('PWR1A', ('R9%s' % _a, '2'), ('R9%s' % _b, '1')) AUTO_TRACK['BLEED0'] = ('PWR1A', ('Q3', '5'), ('R9A', '1')) # ── 铺铜的焊盘连接方式(Plane / Copper Zone)──────────────────────────── # 默认 `connectMode = 0` 是十字花(辐条 0.254 mm)。给「有通孔焊盘、且电流大」的 # 网络单独挂一条**全连接**,理由和载板 §25.11 那次一样:不是怕十字花烧 # (单条辐条 0.254 mm 长、两端焊在大铜面上,8 A 下 ΔT ≈ 0.3 K,实算见 pcb2_rules.py), # 而是**铺完之后铜面几何要量得准** —— 十字花时铜面多边形把自己的焊盘挖空, # 断面测量会读到一个 0.3 mm 的假瓶颈,真瓶颈和度量假象分不出来。 # # 内层铺铜的全连接**没有手焊代价**(焊接热走的是外层),所以这里比载板放得开一点。 # 三类进来: # · `VOn_P` 每路 3 个通孔焊盘(J24/J25 的排针针脚),一路 4 A 全从这三针进铜面; # · `+5V` 10 个通孔焊盘(J24 四针 + J25 六针),最坏 2.8 A; # · 8 A 输入链三段(`$1N223` / `$1N227` / `$1N236`)—— J1 端子和 C2/C3 两颗直插 # 电解都在这上面,**这才是全板电流最大的通孔焊盘**。 # 不进来的两个,理由都不是「忘了」: # · `GND` ~38 个通孔焊盘、同时贴 Inner1 与 Inner2,留十字花(同载板的取舍); # · `+3V3` / `VOn` 一个通孔焊盘都没有(全是贴片),connectMode 对它们是空操作。 PLANE_RULE = 'PLANE-DIRECT' PLANE_CONNECT_DIRECT = 1 # 0 = 十字花(默认), 1 = 全连接 PLANE_NETS = ['VO%d_P' % _n for _n in range(1, 6)] + ['+5V'] PLANE_AUTO = ['VIN_RAW', 'VIN_FUSED', 'VIN_SW'] # 键,取自 AUTO_TRACK # ── 差分对几何(mm)── 与载板同一组数,理由见文件头第 2 条 ───────────── # ⚠️ 长度容差 5.0 mm(2026-09-04 用户拍板):USB 2.0 HS 的对内偏斜预算按 100 ps # 算、FR4 上约 15 mm;5 mm ≈ 30 ps 仍有大余量。卡 0.5 mm 会逼出没必要的蛇形线。 DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.20, 'strokeWidthMax': 0.60, 'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15, 'differentailPairLenTolerMax': 5.0} # 对内 0.28/0.18 是设计意图,但 DP/DM 是两个网络,通用 0.2 mm 净距规则不认「这是一对」, # 在换层过孔处会误判。建一条 0.15 只挂在这一对自己的两个网络之间。 PAIR_RULE = 'USBPAIR' PAIR_CLEARANCE = 0.15 PAIRS = [('DEBUG', 'USB_DP', 'USB_DM')] MIN_CLEARANCE = 0.2 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)[:600]) sys.exit(1) return r['result'] # ── 自动命名网的反查 ────────────────────────────────────────────────── def resolve_auto(): u"""从 `pcb1-parts.json` 按 (位号, 引脚号) 查出当前的自动网名,并逐条断言。 返回 {逻辑名: 网名}。任何一条对不上就直接 `sys.exit` —— 这是**下发前的闸门**, 宁可不下发,也不要留一批悬空的指派(悬空是静默的,DRC 看不出来)。 """ if not os.path.exists(PARTS): sys.exit(u'没有 %s —— 先跑 pcb1_parts_dump.py' % PARTS) parts = json.load(io.open(PARTS, encoding='utf-8')) pin = {} for p in parts: for pad in p['pads']: pin[(p['d'], str(pad[0]))] = pad[5] out, bad, seen = {}, [], {} for key, (rule, probe, cross) in sorted(AUTO_TRACK.items()): if probe not in pin: bad.append(u'%s: 板上没有 %s.%s' % (key, probe[0], probe[1])) continue if cross not in pin: bad.append(u'%s: 板上没有校核端 %s.%s' % (key, cross[0], cross[1])) continue net, other = pin[probe], pin[cross] if not net or not net.startswith('$'): bad.append(u'%s: %s.%s 上是 “%s”,不是自动命名网 —— 探针写错了' % (key, probe[0], probe[1], net)) continue if net != other: bad.append(u'%s: %s.%s = %s,但校核端 %s.%s = %s —— 两端不是同一个网' % (key, probe[0], probe[1], net, cross[0], cross[1], other)) continue if net in seen: bad.append(u'%s 和 %s 查到同一个网 %s —— 探针重复了' % (key, seen[net], net)) continue seen[net] = key out[key] = net if bad: print(u' [!!] 自动命名网反查失败,**没有下发任何东西**:') for b in bad: print(u' · ' + b) print(u' 多半是 pcb1-parts.json 过期了(导入变更之后要重跑 pcb1_parts_dump.py),') print(u' 也可能是原理图改了拓扑 —— 两种情况都要人看一眼,不能自动放过。') sys.exit(1) return out def net_track_full(auto): u"""有名字的 + 反查出来的,合成最终的「网络 → 线宽规则」表。""" t = dict(NET_TRACK) for key, net in auto.items(): t[net] = AUTO_TRACK[key][0] return t def plane_nets_full(auto): return list(PLANE_NETS) + [auto[k] for k in PLANE_AUTO] 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) 多层板能力集:三个放宽值 + 两条内层条目 ── 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; } // ── 2) 功率线宽规则 ── 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; } // ── 3) 差分对几何 ── 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; // ── 3b) 差分对对内间距条目(只给 net-by-net 用,不设为默认)── 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; } // ── 4) 安全间距全表抬到下限 ── 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] < MINC) { row[i] = MINC; bumped++; } // ── 5) 铺铜焊盘连接方式:给大电流网络单独开一条「全连接」条目 ── 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 push_config(): 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 格,间距条目 %s' % (r['bumped'], ' '.join(r['spacing']))) print(u' 铺铜连接方式条目 %s' % ' '.join(r['plane'])) print(u' 配置名现在是 “%s”' % r['name']) def caveat(n): print(u'') print(u'──── 两条必须记住的事 ────') print(u'⚠️ 别去 GUI 的 设计 → 设计规则… 里换“配置”下拉框。') print(u' 换一下整套重置,五条线宽规则全被冲掉,') print(u' %d 个网络的指派立刻悬空(DRC 不报错,线宽也就不生效)。' % n) print(u'⚠️ 再从原理图导入一次变更之后,自动命名网可能改名。') print(u' 顺序是:pcb1_import --apply → pcb1_parts_dump → **pcb1_rules --push**。') print(u' 少跑中间那一步,反查用的就是旧网名,指派会静默落空。') def _nums(o): if isinstance(o, bool): return [] if isinstance(o, (int, float)): return [float(o)] if isinstance(o, dict): return [v for x in o.values() for v in _nums(x)] if isinstance(o, list): return [v for x in o for v in _nums(x)] return [] def _find(o, key_sub): if isinstance(o, dict): for k, v in o.items(): if key_sub.lower() in str(k).lower() and isinstance(v, (int, float)) \ and not isinstance(v, bool): return float(v) for v in o.values(): r = _find(v, key_sub) if r is not None: return r elif isinstance(o, list): for v in o: r = _find(v, key_sub) if r is not None: return r return None AUDIT_JS = """ await new Promise(r=>setTimeout(r,1500)); 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']), cap: { track1oz: c.config.Physics['Track']['copperThickness1oz'], via: c.config.Physics['Via Size']['viaSize'], half: !!c.config.Physics['Track']['copperThickness0.5oz'], inner: !!c.config.Spacing['Safe Spacing']['copperThicknessInner0.5oz'], }, diff: c.config.Physics['Differential Pair'], spacing: c.config.Spacing['Safe Spacing'], 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(), planeEntries: Object.keys(c.config['Plane']['Copper Zone']), planeMode: ((((c.config['Plane']['Copper Zone'][%s] || {}).form || {}) .multiLayerPadModel || {}).data || {})['1'], 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(auto): track = net_track_full(auto) planes = plane_nets_full(auto) r = call(AUDIT_JS % json.dumps(PLANE_RULE)) 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'──── 审计 ────') chk(r['layers'] == 4, u'铜层数 4', u'铜层数是 %s,应为 4' % r['layers']) cap = r['cap'] tmin = _find(cap['track1oz'], 'minValue') if tmin is None: tmin = _find(cap['track1oz'], 'strokeWidthMin') vout = _find(cap['via'], 'viaOuterdiameterMin') vin = _find(cap['via'], 'viaInnerdiameterMin') okcap = (tmin is not None and abs(tmin - 0.102) < 5e-4 and vout is not None and abs(vout - 0.3) < 5e-4 and vin is not None and abs(vin - 0.2) < 5e-4 and cap['half'] and cap['inner']) chk(okcap, u'四层板能力集已就位(最小线宽 %.3f / 过孔 %.2f-%.2f,含 0.5oz 内层条目)' % (tmin or -1, vout or -1, vin or -1), u'还是两层板能力集:最小线宽 %.3f(应 0.102)/ 过孔 %.2f-%.2f(应 0.30-0.20)' u' / 0.5oz 线宽条目 %s / 内层间距条目 %s' % (tmin if tmin is not None else -1, vout if vout is not None else -1, vin if vin is not None else -1, u'有' if cap['half'] else u'无', u'有' if cap['inner'] else u'无')) print(u' (配置名 “%s”)' % r['cfgName']) for name in sorted(TRACK_RULES): chk(name in r['track'], u'线宽规则 %s 存在' % name, u'线宽规则 %s **不存在** —— 有 %d 个网络指着它,现在是悬空的' % (name, sum(1 for _, t in r['assigned'] if t == name))) w = _find(r['diff'], 'strokeWidthDefault') if w is None: w = _find(r['diff'], 'defaultValue') sp = _find(r['diff'], 'PairSpacingDefault') if sp is None: d2 = (r['diff'] or {}).get('differentialPair', {}).get('form', {}) \ .get('diffPairSpacingTables') sp = _find(d2, 'defaultValue') chk(w is not None and abs(w - DIFF['strokeWidthDefault']) < 5e-3 and sp is not None and abs(sp - DIFF['differentailPairSpacingDefault']) < 5e-3, u'差分对几何 W %.3f / S %.3f mm' % (w or -1, sp or -1), u'差分对几何是 W %.3f / S %.3f,应为 W %.2f / S %.2f mm' % (w if w is not None else -1, sp if sp is not None else -1, DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault'])) # ⚠️ 容差留 1e-4:mm→mil→mm 往返,0.2 回读是 0.19999959999999997 vals = [v for v in _nums(r['spacing']) if 0 < v < 10] mn = min(vals) if vals else -1 chk(mn >= PAIR_CLEARANCE - 1e-4, u'安全间距下限 %.4f mm(对内那条 %.2f)' % (mn, PAIR_CLEARANCE), u'安全间距下限只有 %.4f mm' % mn) others = [v for k in r['spacing'] if k != PAIR_RULE for v in _nums(r['spacing'][k]) if 0 < v < 10] mno = min(others) if others else -1 chk(mno >= MIN_CLEARANCE - 1e-4, u'非差分对间距下限 %.4f mm' % mno, u'非差分对间距下限只有 %.4f mm,应抬到 %.2f mm' % (mno, MIN_CLEARANCE)) want = sorted(n for n, _, _ in PAIRS) chk(r['pairs'] == want, u'差分对 %d 对: %s' % (len(r['pairs']), ' '.join(r['pairs'])), u'差分对对不上:有 %s,应为 %s' % (r['pairs'], want)) pm = r.get('planeMode') or {} chk(PLANE_RULE in (r.get('planeEntries') or []) and pm.get('connectMode') == PLANE_CONNECT_DIRECT, u'铺铜连接方式条目 %s 在(connectMode=%s 全连接)' % (PLANE_RULE, pm.get('connectMode')), u'没有 %s 条目或 connectMode 不是 %d —— 大电流通孔焊盘会退回十字花' % (PLANE_RULE, PLANE_CONNECT_DIRECT)) pa = r.get('planeAssigned') or [] chk(sorted(n for n, _ in pa) == sorted(planes) and all(v == PLANE_RULE for _, v in pa), u'铺铜全连接指派 %d 个网络' % len(pa), u'铺铜全连接指派是 %s,应为 %s 全挂 %s' % (pa, planes, PLANE_RULE)) got = dict(r['assigned']) missing = [(n, t) for n, t in sorted(track.items()) if got.get(n) != t] chk(not missing and len(got) == len(track), u'线宽指派 %d 个网络(含 %d 个自动命名网)' % (len(got), len(auto)), u'线宽指派对不上:板上 %d 个 / 应 %d 个;缺或错的 %s' % (len(got), len(track), missing[:8])) chk(r['nbn'] == len(PAIRS), u'对内间距规则 %d 条' % r['nbn'], u'对内间距规则 %d 条,应为 %d' % (r['nbn'], len(PAIRS))) if bad: caveat(len(track)) return not bad def main(): auto = resolve_auto() track = net_track_full(auto) planes = plane_nets_full(auto) print(u'下板 PCB1 规则:线宽 %d 条(%s),安全间距下限 %.2f mm,四层板能力集' % (len(TRACK_RULES), ' '.join(sorted(TRACK_RULES)), MIN_CLEARANCE)) print(u'差分对:W %.2f / S %.2f / 线长容差 %.1f mm,共 %d 对(%s)' % (DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault'], DIFF['differentailPairLenTolerMax'], len(PAIRS), ' '.join(n for n, _, _ in PAIRS))) print(u'自动命名网反查(事实来源 pcb1-parts.json):') for key in sorted(auto): rule, probe, cross = AUTO_TRACK[key] print(u' %-10s = %-9s -> %-7s (%s.%s / 校核 %s.%s)' % (key, auto[key], rule, probe[0], probe[1], cross[0], cross[1])) print(u'线宽指派共 %d 个网络;铺铜全连接 %d 个(%s)' % (len(track), len(planes), ' '.join(planes))) if '--check' in sys.argv: sys.exit(0 if audit(auto) else 1) if '--push' not in sys.argv: print(u'(未加 --push,没有下发;--check 审计当前状态)') return push_config() 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' [!!] 有对没建上 —— 多半是网络名在这块板上不存在') # ⚠️ `getNetRules()` 回来的是**整张表**,`overwriteNetRules` 整表覆盖 —— # 线宽(Track)和铺铜连接方式(Copper Zone)必须在**同一次读改写**里做完, # 分两次写的话后一次会拿着旧快照把前一次冲掉。 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(track, ensure_ascii=False), json.dumps(planes, ensure_ascii=False), json.dumps(PLANE_RULE))) print(u' 线宽指派 %d / %d 个网络' % (n['track'], len(track))) if n['track'] != len(track): miss = sorted(set(track) - set(n['seen'])) print(u' [!!] 板上找不到这些网络:%s' % ' '.join(miss)) print(u' 悬空的指派 DRC 不报错 —— 先查 pcb1-parts.json 是不是过期了') print(u' 铺铜全连接指派 %d / %d 个网络(%s)' % (n['plane'], len(planes), ' '.join(planes))) if n['plane'] != len(planes): print(u' [!!] 有网络没指派上 —— 没指派 = 退回十字花,大电流铜面几何就量不准了') # ⚠️ overwriteNetByNetRules 是整表覆盖,先读回再合并。 # ⚠️ 还要**清掉自己的陈迹**:单板时代 `pcb_rules.py` 在这里留下了 7 条 # (USB1~5 / UPSTREAM / DEBUG)。那 6 对的网络已经随 Hub 搬去载板、板上不存在了, # 只加不删的话表里会一直挂着 6 条指向空网络的规则。判据是「rule 名是我们的 # USBPAIR、但不在本轮 PAIRS 里」—— 别人建的条目一律不动。 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)) if n['now'] != len(PAIRS): print(u' [!!] 应为 %d 条' % len(PAIRS)) call("await eda.pcb_Document.save(); return 1;") print(u' 已保存') caveat(len(track)) if __name__ == '__main__': main()