文件
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

623 行
31 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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},
'FAN12V': {'strokeWidthDefault': 0.8, 'strokeWidthMin': 0.3, '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
# 2026-09-08 第三十三轮:风扇高边开关的下游母线。Q7 居中、东西两支**各只走 0.67 A**
# (FAN-R2 两把风扇 2 x 0.67 A),0.8 mm 外层 1 oz 下西支 125 mm -> 51 mV、东支 95 mm -> 39 mV,
# 在 `FAN_CIRCUIT` 的 0.2 V 预算之内。**默认信号线 0.20 mm 扛不住** ——
# 0.20 mm 下同样两支是 204 / 155 mV,单是西支就吃掉全部预算。
# ⚠️ 早年注释里担心的 0.26 V 是「一条 196 mm 走全电流 1.33 A」的算法,
# J26/J27 分到东西两板边、Q7 居中之后不成立。
NET_TRACK['FAN_12V_SW'] = 'FAN12V'
# ── 自动命名网 → 线宽规则(按「位号.引脚」反查,见文件头第 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。
# ⚠️ 2026-09-08 J1 换成圆形 DC 座之后**正极从 pin2 挪到了 pin1**
# (XT30:1=`−` / 2=`+`;DC-005:1=中心正极 / 2、3=两个负极触点)。
# 探针写错时这里是硬失败("J1.2 上是 GND,不是自动命名网"),不会静默放过 —— 这是对的。
AUTO_TRACK['VIN_RAW'] = ('PWR12V', ('J1', '1'), ('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()