文件
phonerail/.claude/r2/archive_base2L/rules_base.py
T
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

360 行
17 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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. **两层板**,当前配置是 `JLCPCB Capability(Two Layers Board)`:
`Track` / `Safe Spacing` 下只有 `copperThickness1oz` / `2oz` 两条,
**没有内层条目**,所以刀片那段「补 0.5 oz 内层」整块删掉。
2. **入口链是 9 A**(方案 §6.4),不是刀片的 1.45 A。线宽规则给到
`strokeWidthMax = 12`,真正的载流靠 `pour_base` 的断面校核保证,不靠 DRC。
3. **七对差分**:五个槽 + ESP32 原生 USB(D13)+ 上行 USB-C。
刀片那组 `W 0.28 / S 0.18` 是**四层薄介质**的数,两层 1.6 mm 板照搬会变成 ~137 Ω。
按方案 §6.3 取 **W 1.2 / S 0.2 ⇒ ~94 Ω**。
⚠ `min` 必须留到 0.25:1.2 的线接不进 U32 那颗 0.5 mm 间距 QFN-64 的焊盘,
末端一定要缩颈,min 卡在 1.2 会把整条线判成违规。
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 = 2
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 = {
'VIN_RAW': 'PWR9A', # J1 中心极 → F1
'VIN_FUSED': 'PWR9A', # F1 → Q1 反接保护
'VIN_SW': 'PWR9A', # Q1 → 储能 → Rs6 上游
'+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', 'VIN_SW', '+12V']
# ── 差分对(mm)── 方案 §6.3:两层 1.6 mm 微带,W 1.2 / S 0.2 ⇒ ~94 Ω ──
DIFF = {'strokeWidthDefault': 1.2, 'strokeWidthMin': 0.25, 'strokeWidthMax': 1.6,
'differentailPairSpacingDefault': 0.20, 'differentailPairSpacingMin': 0.15,
'differentailPairLenTolerMax': 1.0} # intra-pair skew 目标 ≤1 mm(§6.3)
PAIR_RULE = 'USBPAIR'
PAIR_CLEARANCE = 0.20
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.2,不是刀片的 0.15。两层板走 JLC 标准工艺(0.2/0.2)最便宜,
# 而底板上最紧的封装是 U32 QFN-64(0.5 间距)与 J23 USB-C,实测都过得去。
# 真刷出「贴片焊盘到贴片焊盘」的假错(全落在某个买来的封装内部)再降到 0.15。
MIN_CLEARANCE = 0.20
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 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.127, 0.254, 2.54);
Object.assign(c['Physics']['Via Size']['viaSize'].form,
{viaOuterdiameterMin: 0.4, viaInnerdiameterMin: 0.3});
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 push():
check_names()
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)))