文件

418 行
19 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 刀片 PCB 的设计规则(M4c-1)。
/c/Python314/python .claude/r2/pcb_rules.py # 只反查 + 打印,不下发
/c/Python314/python .claude/r2/pcb_rules.py --push # 下发规则 + 差分对 + 网络指派
/c/Python314/python .claude/r2/pcb_rules.py --audit # 读回审计
做法照抄 R1 的 `.claude/pcb1_rules.py`,**那一份的文件头是唯一的说明书**,
四个坑一个都没变:
· `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装
· `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状**
· 下发之后**别再去 GUI 换「配置」下拉框** —— 换一次整套重置,网络指派全部悬空
· `overwriteNetRules` / `overwriteNetByNetRules` 都是**整表覆盖**,必须先读回再合并
## 和 R1 下板不同的四处
1. **刀片只有一路**,所以线宽表比 R1 短:没有 `FAN12V`,`PWR12V` 也从 2.0 降到 0.8
—— 刀片 12V 满载只有 **1.45 A**(§20.7 那颗 2 A 保险就是按它选的),
R1 的 2.0 mm 是给整机 8~10 A 的干路用的,照抄过来只会把功率列挤死。
2. **两对差分**,不是一对:`USB_DP/DM`(金手指 → U7 ESD)和 `USB_DPJ/DMJ`(U7 → J4 尾插)
是被 U7 隔开的两段,各自成对。只建一对的话另一段退回默认 0.2 mm 净距规则,
在换层过孔处会误判 —— 和 R1 §10.1b 同一个理由。
3. **层角色**(§7.1):L1 功率+信号 / **Inner1(L2) = 完整地平面(不许割断)** /
Inner2(L3) = 12V + VO 灌铜 / L4(Bottom) = 控制信号。D± 走 L1、以 L2 为参考。
4. **自动命名网只有两个**(R1 有十几个):SW 节点和 eFuse 输出段。
仍然**按「位号.引脚」当场反查、两端交叉校核**,不写死 `$1N137` 这种名字 ——
再导入一次变更它就可能改名,而写死的后果是**静默失效**(DRC 干干净净,规则悬空)。
"""
import io
import json
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
# ⚠ HERE 必须后插(在 sys.path 最前):`.claude` 下有同名的 R1 脚本。
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
U = json.load(io.open(os.path.join(HERE, 'r2-uuids.json'), encoding='utf-8'))
PCB = U['boards']['BLADE']['pcb']['uuid']
NETS_FILE = os.path.join(HERE, '_blade_pcb_nets.json')
# ── 线宽规则(mm,外层 1 oz)────────────────────────────────────────────
# 载流靠灌铜保证、不靠 DRC;min 一律压到 0.15~0.3,否则 0402 焊盘(0.57 宽)扇不出去。
TRACK_RULES = {
'PWR12V': {'strokeWidthDefault': 0.8, 'strokeWidthMin': 0.25, 'strokeWidthMax': 4.0},
'SWNODE': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.30, 'strokeWidthMax': 4.0},
'PWR4A': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.30, 'strokeWidthMax': 6.0},
'PWR5V': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.20, 'strokeWidthMax': 3.0},
'PWR1A': {'strokeWidthDefault': 0.4, 'strokeWidthMin': 0.15, 'strokeWidthMax': 2.0},
}
# ── 有名字的网络 → 线宽规则 ────────────────────────────────────────────
NET_TRACK = {
'+12V': 'PWR12V', # 金手指 B1/B2/B3 + A2/A3 → F1,1.45 A
'+12V_F': 'PWR12V', # F1 → Buck 输入,同一路电流
'VO': 'PWR4A', # Buck 输出 → eFuse 输入
'VO_P': 'PWR4A', # 检流之后 → J5 电池排线,4 A
'+5V': 'PWR5V', # 金手指 A19/A32 → U5 TPS22918
'VBUS': 'PWR5V', # U5 → J4 尾插,手机 VBUS
'+3V3': 'PWR1A', # 刀片逻辑(INA226 + PCA9555)≈ 3 mA,0.4 纯余量
}
# ── 自动命名网 → (规则, 探针, 校核端):两端查出来必须是同一个网 ─────────
AUTO_TRACK = {
# SW 节点:TPS54622(VQFN-14) 的 SW = pin 11/12 → L1
'SW': ('SWNODE', ('L1', '1'), ('U1', '11')),
# eFuse 输出段:TPS25940A(WQFN-20) 的 OUT = pin 4~8 → Rs1 上游脚
'VO_F': ('PWR4A', ('Rs1', '2'), ('U2', '5')),
}
# ── 铺铜焊盘全连接(理由同 R1:不是怕十字花烧,是**铺完之后断面要量得准**)──
PLANE_RULE = 'PLANE-DIRECT'
PLANE_CONNECT_DIRECT = 1
PLANE_NETS = ['VO_P', 'VO', '+12V', '+12V_F']
PLANE_AUTO = ['VO_F']
# ── 差分对(mm)── 与 R1 同一组数:W 0.28 / S 0.18,以 L2 为参考 ────────
# ⚠ 2026-09-11 下限 0.20 → 0.15:USB_DPJ/DMJ 进 J4(BM24 0.35 节距、焊盘 0.18 宽)只能 0.18,
# 0.20 的下限会让整对 USBPHONE 在 DRC 里刷「差分对线宽」(router.DP_WJ)。
DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.15, 'strokeWidthMax': 0.60,
'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15,
'differentailPairLenTolerMax': 5.0}
PAIR_RULE = 'USBPAIR'
PAIR_CLEARANCE = 0.15
PAIRS = [('USBEDGE', 'USB_DP', 'USB_DM'), # 金手指 → U7 USBLC6
('USBPHONE', 'USB_DPJ', 'USB_DMJ')] # U7 → J4 BM24 尾插
# ⚠ **0.15,不是 R1 的 0.2**。抬到 0.2 会刷出 23 条「贴片焊盘到贴片焊盘」——
# **全部在 `J4` BM24-50DS 内部**:0.4 mm 间距的尾插座,相邻焊盘实测净距 0.168~0.171,
# 这是买来的连接器封装本身的几何,不是摆位问题,改摆位也修不掉。
# 0.15 仍远高于嘉立创四层板 0.09 的能力线,而板上其它最紧的一处是
# `K1~K4` 侧键盘的 0.276(Ø1.5 盘、实测间距 1.78),也过。
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'已写 _blade_pcb_nets.json:%d 件' % len(r))
return r
def resolve_auto(nets=None):
u"""按 (位号, 引脚) 反查自动网名并逐条断言。对不上就**不下发**。"""
if nets is None:
if not os.path.exists(NETS_FILE):
nets = dump_nets()
else:
nets = json.load(io.open(NETS_FILE, encoding='utf-8'))
pin = {}
for ref, pads in nets.items():
for pn, net in pads:
pin[(ref, str(pn))] = net
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 str(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' 多半是 _blade_pcb_nets.json 过期了 —— 导入变更之后要重跑 --dump。')
sys.exit(1)
return out
def net_track_full(auto):
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'];
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 的格子(板厂能力值 0.09~0.127,以及上一版抬到
// 0.2 的那些)一律设成 MINC;0.254 / 0.3 那几格是孔/板边相关的,**不动**。
// ⚠ R1 那份只「抬高不降低」,所以把 MINC 从 0.2 改小是改不动的 —— 这里必须是赋值。
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 push():
auto = resolve_auto()
track = net_track_full(auto)
planes = plane_nets_full(auto)
print(u' 自动网反查: ' + ' '.join(u'%s=%s' % (k, v) for k, v in sorted(auto.items())))
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'], 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(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):
print(u' [!!] 板上找不到: %s' % ' '.join(sorted(set(track) - set(n['seen']))))
print(u' 铺铜全连接指派 %d / %d(%s)' % (n['plane'], len(planes), ' '.join(planes)))
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(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']),
half: !!c.config.Physics['Track']['copperThickness0.5oz'],
inner: !!c.config.Spacing['Safe Spacing']['copperThicknessInner0.5oz'],
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():
auto = resolve_auto()
track = net_track_full(auto)
planes = plane_nets_full(auto)
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'] == 4, u'铜层数 4', u'铜层数是 %s,应为 4' % r['layers'])
chk(r['half'] and r['inner'], u'内层 0.5 oz 的线宽/间距条目都在',
u'内层 0.5 oz 条目缺失')
miss = [k for k in TRACK_RULES if k not in r['track']]
chk(not miss, u'五条线宽条目都在', u'缺线宽条目 %s' % miss)
got = dict(r['assigned'])
wrong = [(n, got.get(n), v) for n, v in sorted(track.items()) if got.get(n) != v]
chk(not wrong, u'%d 个网络的线宽指派全对' % len(track),
u'指派不对: %s' % wrong[:6])
chk(sorted(r['pairs']) == sorted([p[0] for p in PAIRS]),
u'差分对 %s' % 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 planes if gotp.get(n) != PLANE_RULE]
chk(not missp, u'铺铜全连接 %d 个网络' % len(planes), 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' % PCB)
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:
a = resolve_auto()
t = net_track_full(a)
print(u'自动网: ' + ' '.join(u'%s=%s' % (k, v) for k, v in sorted(a.items())))
for n, v in sorted(t.items(), key=lambda kv: (kv[1], kv[0])):
print(u' %-10s -> %s' % (n, v))