原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
441 行
22 KiB
Python
441 行
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""PCB1 设计规则 + D± 差分对。
|
||
|
||
做四件事:
|
||
1. **换掉规则配置**。转板时挂上的是 `JLCPCB Capability(Two Layers Board)` ——
|
||
这是块四层板,用两层板的能力集是错的。换成 `Multiple Layers Board`。
|
||
2. 加两条线宽规则并按网络指派:4A 通道输出、12V/5V 母线。
|
||
3. 差分对几何按 90 Ω 设:**W 0.28 / S 0.18 mm**(推导见下),线长容差 0.5 mm。
|
||
4. 建 7 对差分对:USB1~5(Hub 到五路出线座)、UPSTREAM(J23 到 Hub)、
|
||
DEBUG(J22 到 ESP32)。
|
||
|
||
## 线宽哪来的(IPC-2221,外层,ΔT=20 °C)
|
||
|
||
A = (I / (0.048 · ΔT^0.44))^(1/0.725) [mil²], W = A / 铜厚
|
||
|
||
4 A -> A = 72.5 mil² -> 1 oz(1.37 mil) 需 53 mil = 1.34 mm
|
||
8 A -> A = 188 mil² -> 1 oz 需 138 mil = 3.49 mm
|
||
|
||
外层已定 **1 oz**(§10.1)。4A 那条给 **1.5 mm**(温升 16.7 °C);
|
||
12V 分支给 **2.0 mm**,**总干那 8 A 走灌铜**,不当走线画。
|
||
|
||
## 90 Ω 是怎么算的 —— ⚠️ 必须拿板厂叠层复核
|
||
|
||
微带近似,按嘉立创四层 1.6 mm 标准叠层 L1-L2 介质 h=0.2104 mm、εr≈4.3、
|
||
**1 oz 外层(已定稿)**:
|
||
|
||
Z0 = 87/√(εr+1.41) · ln(5.98h / (0.8W+t))
|
||
Zdiff = 2·Z0·(1 − 0.48·e^(−0.96·S/h))
|
||
|
||
W=0.28 mm, S=0.18 mm -> Z0≈57.5 Ω, Zdiff≈90.5 Ω ✓
|
||
|
||
(回到 2 oz 的话同样几何只剩 83 Ω,W 要收到约 0.24 mm —— 铜厚进了 0.8W+t。)
|
||
叠层一变(板厚、prepreg 型号、内层铜厚)这两个数就得重算,
|
||
**打样前一定要用板厂的阻抗计算器复核**,别信这里的近似公式。
|
||
|
||
## 规则配置怎么下发(v3.2.149 实测,2026-09-03 摸清楚)
|
||
|
||
一度以为“只能手动”,**其实是参数形状给错了**。两个坑:
|
||
|
||
1. `overwriteCurrentRuleConfiguration(x)` 要的是 **config 本体**,
|
||
不是 `getRuleConfiguration()` 返回的 `{name, config}` 包装。
|
||
给包装它返回 `undefined` 且什么也不做;给本体返回 `true`。
|
||
2. **两种 config 形状不一样,不能混用。**
|
||
- `getRuleConfiguration('<系统能力集>')` → **平形状**:
|
||
`form.strokeWidthDefault` / `Safe Spacing[*].table`
|
||
- `getCurrentRuleConfiguration()` → **嵌套形状**(文档内部表示):
|
||
`form.data['1'].defaultValue` / `Safe Spacing[*].tables['1'].content`
|
||
|
||
把平形状丢进 `overwriteCurrentRuleConfiguration` 会被默默吃掉。
|
||
**正确做法:拿 `getCurrentRuleConfiguration().config` 改,原形状送回去。**
|
||
|
||
`saveRuleConfiguration(cfg, name, allowOverwrite)` 两种形状都返回 `false`,
|
||
真的不能用——但没关系,我们不需要存命名配置,只需要当前文档的规则对。
|
||
|
||
### “切到多层板能力集”实际就是五件事
|
||
|
||
把两个能力集逐值 diff 过:**只差 3 个数 + 2 条默认不启用的内层条目**,
|
||
其余 300 多个数值一模一样。所以直接把它们搬过来就行:
|
||
|
||
| 项 | Two Layers | Multiple Layers |
|
||
|---|---|---|
|
||
| `Track/copperThickness1oz` 最小线宽 | 0.127 | **0.102** |
|
||
| 过孔外径最小 | 0.5 | **0.3** |
|
||
| 过孔内径最小 | 0.3 | **0.2** |
|
||
| `Track/copperThickness0.5oz` | 无 | 有(min 0.089,不启用)|
|
||
| `Safe Spacing/copperThicknessInner0.5oz` | 无 | 有(不启用)|
|
||
|
||
⚠️ **下完之后别再去 GUI 里换“配置”下拉框** —— 换一下就是整套重置,
|
||
PWR4A / PWR12V 会被冲掉,12 个网络的指派又变成悬空的。
|
||
对话框里**显示的名字依旧是 `JLCPCB Capability(Two Layers Board)`**(名字改不了,
|
||
API 里没有 setCurrentRuleConfigurationName),**下发之后名字会自动变成 “自定义配置”** —— 这是对的,
|
||
说明它已经从系统能力集上摘下来了,以 `--check` 的实测值为准。
|
||
|
||
⚠️ 还有一条:**写进去的 mm 值会被 EasyEDA 换算成 mil 再换回来**,
|
||
0.2 回读是 `0.19999959999999997`。任何几何值都别做精确比较,容差留到 1e-4。
|
||
|
||
用法: /c/Python314/python .claude/pcb_rules.py [--push | --check]
|
||
"""
|
||
import io, json, os, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
|
||
PCB = '7e9415a16a909c51'
|
||
# 多层板能力集只当**数据源**用(取内层 0.5 oz 那张间距表),不切换配置本身
|
||
BASE_CFG = 'JLCPCB Capability(Multiple Layers Board)'
|
||
|
||
# ── 线宽规则(mm)──────────────────────────────────────────────────────
|
||
# 外层 **1 oz**(§10.1,用户 2026-09-03 拍板)。
|
||
# ⚠️ PWR12V 的 2.0 mm 是给**分支**的:12V 总干那 8 A 走 2.0 mm 温升 35 °C,
|
||
# 不能当走线画,必须走灌铜(Inner2-PWR 0.5 oz 需 ≥18 mm 宽)。
|
||
# 最小值不敢设成 3.5 mm —— +12V 上还有一堆进 0402/SOT-23 焊盘的短支线,
|
||
# 卡 3.5 会刷出几百条假 DRC 错误,规则就没人看了。**载流靠灌铜保证,不靠 DRC。**
|
||
TRACK_RULES = {
|
||
# ⚠️ 最小值 1.0 → **0.3**(2026-09-03 扇出时改的,不是放松要求)。
|
||
# 规则是**按网络**生效的,`+12V` 上除了母线还挂着 R104 那种 0402 分压抽头:
|
||
# 0402 焊盘才 0.57 x 0.54 mm,1.0 mm 的线比焊盘还宽,根本引不出来 ——
|
||
# 实测卡 1.0 时有 29 个焊盘扇不出去,硬扇出去的又被判「线宽 11.8mil,应 >= 39.4mil」。
|
||
# 这正是 §10.1 已经写过的那条道理的延伸:**载流靠灌铜保证,不靠 DRC**。
|
||
# 默认值仍是 1.5 / 2.0,交互布线画出来的还是宽线;0.3 只是不再误伤短支线。
|
||
'PWR4A': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0},
|
||
'PWR12V': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.3, 'strokeWidthMax': 10.0},
|
||
}
|
||
NET_TRACK = {}
|
||
for _n in range(1, 6):
|
||
NET_TRACK['VO%d' % _n] = 'PWR4A' # Buck 输出 -> eFuse
|
||
NET_TRACK['VO%d_P' % _n] = 'PWR4A' # 分流之后 -> 出线座
|
||
for _n in ('+12V', '+5V'):
|
||
NET_TRACK[_n] = 'PWR12V'
|
||
|
||
# ── 差分对几何(mm)────────────────────────────────────────────────────
|
||
# ⚠️ 长度容差 0.5 -> **5.0 mm**(2026-09-04 用户拍板)。
|
||
# USB 2.0 HS 的**对内**偏斜预算按 100 ps 算、FR4 上约 15 mm;5 mm ≈ 30 ps 仍有大余量。
|
||
# 卡 0.5 mm 时 USB1/USB2 被判违规(实测偏差 1.64 / 1.88 mm ≈ 10 ps)——
|
||
# 为了凑这个数去绕蛇形线,是拿真实的串扰与占板面积去换一个好看的 DRC 数字。
|
||
DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.20, 'strokeWidthMax': 0.60,
|
||
'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15,
|
||
'differentailPairLenTolerMax': 5.0}
|
||
|
||
# ── 差分对**对内**间距规则(网络与网络之间)──────────────────────────
|
||
# 对内 0.28/0.18 是按 90 Ω 微带算的,紧耦合是设计意图;但 DP 与 DM 是两个不同的网络,
|
||
# 通用的 0.2 mm 净间距规则不认「这两个是一对」,在过孔换层处判了 4 条违规
|
||
# (实测 6~7 mil / 0.15~0.18 mm)。把线拉开阻抗就不是 90 Ω 了,所以**不能拉开**。
|
||
# 正确做法:建一条 0.15 mm 的安全间距条目,只挂在**每一对自己的两个网络之间**
|
||
# (`overwriteNetByNetRules`),对其它网络仍然是 0.2 mm。嘉立创四层工艺下限 0.1 mm。
|
||
PAIR_RULE = 'USBPAIR'
|
||
PAIR_CLEARANCE = 0.15
|
||
PAIRS = [('USB%d' % n, 'USB%d_DP' % n, 'USB%d_DM' % n) for n in range(1, 6)]
|
||
PAIRS += [('UPSTREAM', 'UP_DP', 'UP_DM'), ('DEBUG', 'USB_DP', 'USB_DM')]
|
||
|
||
# 安全间距的**下限**:嘉立创能力集给到 0.102 mm(4 mil),那是工艺极限不是设计目标。
|
||
# 全表抬到 0.2 mm,DRC 才拦得住无意中走太近的线。
|
||
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']
|
||
|
||
|
||
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;
|
||
|
||
// 嵌套形状里一条线宽规则是 form.data{'1':{minValue,defaultValue,maxValue}}
|
||
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) 两条功率线宽规则(名字必须和 NET_TRACK 里指的一模一样)──
|
||
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) // ⚠️ 对内那条 0.15 不能被抬回 0.2
|
||
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++; }
|
||
|
||
const ok = await D.overwriteCurrentRuleConfiguration(c); // ⚠️ 传本体,不传 {name,config}
|
||
return {ok: ok === true, bumped: bumped, name: cur.name,
|
||
track: Object.keys(T), spacing: Object.keys(SS)};
|
||
"""
|
||
|
||
|
||
def push_config():
|
||
u"""把规则配置整套下发到**当前文档**。见文件头的两个坑。"""
|
||
r = call(CAP_JS % (json.dumps(TRACK_RULES), json.dumps(DIFF), MIN_CLEARANCE,
|
||
json.dumps(PAIR_RULE), PAIR_CLEARANCE))
|
||
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”(下发前是系统能力集,改过就自动摘出来了)' % r['name'])
|
||
|
||
|
||
def caveat():
|
||
print(u'')
|
||
print(u'──── 一条必须记住的事 ────')
|
||
print(u'⚠️ 别再去 GUI 的 设计 → 设计规则… 里换“配置”下拉框。')
|
||
print(u' 换一下就是整套重置,PWR4A / PWR12V 两条线宽规则会被冲掉,')
|
||
print(u' 12 个功率网络的指派立刻变成悬空的(DRC 不报错,线宽也就不生效)。')
|
||
print(u' 要改规则就改这个脚本再 --push。')
|
||
print(u' 下发之后配置名会自动变成 “自定义配置” —— 这是对的,说明它不再挂在系统能力集上。')
|
||
|
||
|
||
def _nums(o):
|
||
u"""递归掏出一个子树里所有的数字。规则配置的结构**每个配置都不一样**:
|
||
两层板的差分对是 `form.strokeWidthTables.data['1'].defaultValue`,
|
||
多层板的是 `form.strokeWidthDefault`。别按固定路径取,会静默拿到 undefined。"""
|
||
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):
|
||
u"""按键名子串找第一个数字。"""
|
||
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
|
||
|
||
|
||
def audit():
|
||
JS = """
|
||
await new Promise(r=>setTimeout(r,2000));
|
||
const c = await eda.pcb_Drc.getCurrentRuleConfiguration();
|
||
const rules = await eda.pcb_Drc.getNetRules();
|
||
return {
|
||
cfgName: c && c.name,
|
||
track: Object.keys((c && c.config && c.config.Physics && 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 && c.config && c.config.Physics && c.config.Physics['Differential Pair'],
|
||
spacing: c && c.config && c.config.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(),
|
||
};
|
||
"""
|
||
r = call(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'──── 审计 ────')
|
||
# 配置的**名字**改不了(永远显示 Two Layers Board),所以查的是四层板能力集的数值。
|
||
# 两个能力集逐值 diff 只差这三个数 + 两条不启用的内层条目。
|
||
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, f in sorted(TRACK_RULES.items()):
|
||
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-9:写进去的 mm 会被 EasyEDA 换算成 mil 再换回来,
|
||
# 0.2 回读是 0.19999959999999997(差 4e-7 mm = 0.4 nm)。任何几何值都别做精确比较。
|
||
vals = [v for v in _nums(r['spacing']) if 0 < v < 10]
|
||
mn = min(vals) if vals else -1
|
||
chk(mn >= MIN_CLEARANCE - 1e-4, u'安全间距下限 %.4f mm' % mn,
|
||
u'安全间距下限只有 %.4f mm,应抬到 %.2f mm' % (mn, 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))
|
||
chk(len(r['assigned']) == len(NET_TRACK), u'线宽指派 %d 个网络' % len(r['assigned']),
|
||
u'线宽指派只有 %d 个,应为 %d' % (len(r['assigned']), len(NET_TRACK)))
|
||
|
||
if bad:
|
||
caveat()
|
||
return not bad
|
||
|
||
|
||
def main():
|
||
print(u'目标:线宽规则 %d 条(%s),安全间距下限 %.2f mm,四层板能力集'
|
||
% (len(TRACK_RULES), ' '.join(sorted(TRACK_RULES)), MIN_CLEARANCE))
|
||
print(u'差分对:W %.2f / S %.2f / 线长容差 %.2f mm,共 %d 对'
|
||
% (DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault'],
|
||
DIFF['differentailPairLenTolerMax'], len(PAIRS)))
|
||
if '--check' in sys.argv:
|
||
sys.exit(0 if audit() 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)))
|
||
|
||
n = call("""
|
||
const T = %s;
|
||
const rules = await eda.pcb_Drc.getNetRules();
|
||
let k = 0;
|
||
for (const r of rules) {
|
||
if (r.type === 'net' && T[r.name]) { r['Track'] = T[r.name]; k++; }
|
||
}
|
||
await eda.pcb_Drc.overwriteNetRules(rules);
|
||
return k;
|
||
""" % json.dumps(NET_TRACK, ensure_ascii=False))
|
||
print(u' 线宽指派 %d / %d 个网络' % (n, len(NET_TRACK)))
|
||
|
||
# ── 差分对**对内**间距:只在每一对自己的两个网络之间挂 USBPAIR(0.15 mm)──
|
||
# 形状是从 pcb.js 的 getBetweenNetDataItem() 反出来的:
|
||
# {net1:{type:'net',name}, net2:{type:'net',name}, rule:<安全间距条目名>,
|
||
# copperRuleName:null, planeRuleName:null}
|
||
# ⚠️ overwriteNetByNetRules 是**整表覆盖**,要先把现有的读回来再合并。
|
||
n = call("""
|
||
const PAIRS = %s, RULE = %s;
|
||
const cur = await eda.pcb_Drc.getNetByNetRules();
|
||
const list = (cur && cur['Safe Spacing']) ? cur['Safe Spacing'].slice() : [];
|
||
const key = e => [e.net1.name, e.net2.name].sort().join('|');
|
||
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, ok: ok === true, now: (back['Safe Spacing'] || []).length};
|
||
""" % (json.dumps(PAIRS), json.dumps(PAIR_RULE)))
|
||
print(u' 对内间距规则:新增 %d 条,现共 %d 条(%s = %.2f mm)'
|
||
% (n['added'], 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()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|