原理图/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
612 行
29 KiB
Python
612 行
29 KiB
Python
# -*- 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()
|