R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 cd01640c40
当前提交 cf7cc9cf90
共修改 759 个文件,包含 76962 行新增和 5299 行删除
+320
查看文件
@@ -0,0 +1,320 @@
# -*- coding: utf-8 -*-
u"""载板 PCB2:把 `CFG_SEL=110` 那次原理图变更(`c2_cfgsel.py`)在板上收尾。
/c/Python314/python .claude/pcb2_cfgsel.py --clean # 删掉变成异网/孤儿的旧铜
/c/Python314/python .claude/pcb2_cfgsel.py --refresh # 重抓障碍表(--clean 之后必须做)
/c/Python314/python .claude/pcb2_cfgsel.py # 算两条新走线 + 净距校核,不下发
/c/Python314/python .claude/pcb2_cfgsel.py --push # 只 create,不删任何东西
## 导入变更之后板上留下什么
`pcb1_import.py --board2 --apply` 只改焊盘的网络归属,**不动铜**。所以:
| 旧铜 | 现在的问题 |
|---|---|
| `U32.13` 往东那一小段 `GND` + 它的过孔 | pin 13 已经是 `+3V3_HUB` ⇒ **异网短路** |
| `R712.2` 往东那一小段 `GND` + 它的过孔 | R712.2 已经是 `+3V3_HUB` ⇒ **异网短路** |
| `PRTDIS6` / `PRTDIS7` 的 10 段线 | R714/R715 删了,网络已不存在 ⇒ 孤儿铜 |
⚠️ 这三类**净距校核一条都不报**(它按 `pad.net` 判同网跳过,而铜自己的 net 字段
还写着旧网名,两边对不上时反而被当成"同网"放过),只有 DRC 的连接性看得见。
所以删除这一步不能省,也不能等 DRC 来提醒。
## 为什么不用 `pcb2_route_sig.py --net +3V3_HUB`
`route_net()` 是**整网重布**:它从焊盘表重新长出生成树,而 `push_sig()` 下发前
先把该网络的铜全删。`+3V3_HUB` 有 33 个焊盘、几十段已经验过的铜,
为了接两个脚把整网推倒重来,风险远大于收益(板子现在停在 DRC 115 条的基线上)。
⇒ 这里只用布线器的**内核** `_connect()`("把两坨铜连起来"),
下发走**只 create 不 delete** 的路子(skill §9v:这种脚本必须自己记账,
所以新画的坐标存进 `_pcb2_cfgsel.json`,重跑 `--push` 先按坐标删掉自己上一次画的)。
## 两条连接
1. **`U32.13` → `+3V3_HUB`**:pin 13 在 QFN 西边 (95.55, 70.25)。
西边 x=96.325 那条走廊已经有一段本网铜(`(96.325,66.45)—(96.325,68.55)`,
U32.5↔U32.10 的内部连接),**顺着它往南延到 pin 13 的高度**即可,不用过孔。
2. **`R712.2` → `+3V3_HUB`**:R712 东边被 `HUB_RST`(y=66.45) 和 `CFGSEL0`(y=67.65)
两道横墙夹住,L2 上出不去,得翻到顶层。这一条交给 `_connect()` 找。
"""
import io
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
import pcb2_route_dp as dp
import pcb2_route_sig as rs
PCB = dp.PCB
MIL = dp.MIL
LEDGER = os.path.join(HERE, '_pcb2_cfgsel.json')
# ---- 要删的旧铜。**按网络 + 坐标匹配**,不用 primitiveId(那个每次导入都会变)。
# 每一项 (net, layer, x0, y0, x1, y1);顺序无关,两端对调也算命中。
DEAD_LINES = [
('GND', 2, 95.926, 70.250, 96.501, 70.250), # U32.13 旧的对地出线
('GND', 2, 106.966, 67.050, 107.624, 67.050), # R712.2 旧的对地出线
('PRTDIS6', 2, 102.450, 62.133, 102.450, 62.716),
('PRTDIS6', 2, 102.450, 62.716, 101.250, 62.825),
('PRTDIS6', 2, 101.250, 63.550, 101.250, 62.825),
('PRTDIS7', 2, 101.360, 62.116, 101.322, 62.133),
('PRTDIS7', 2, 101.821, 62.133, 101.360, 62.116),
('PRTDIS7', 2, 101.322, 62.133, 100.593, 62.443),
('PRTDIS7', 2, 101.250, 62.133, 101.322, 62.133),
('PRTDIS7', 2, 101.322, 62.133, 101.821, 62.133),
('PRTDIS7', 2, 100.593, 62.443, 100.250, 62.825),
('PRTDIS7', 2, 100.250, 63.550, 100.250, 62.825),
# ⚠️ 2026-09-08 补:板上还有一条**跨整段的重叠图元**,上面那两条
# (101.250→101.322 与 101.322→101.821)盖不到它。当时 12/12 全删中、回查也说清零,
# 所以完全看不出来 —— 它跟着进了 `_pcb2_route_geom.json` 快照,
# 重建时又被重放回去,DRC 报「PRTDIS7 游离的导线」。
# ⇒ 教训:**删完要按「该网络还剩几条」回查,不能只数「删中几条」**。
('PRTDIS7', 2, 101.250, 62.133, 101.820, 62.133),
]
DEAD_VIAS = [
('GND', 96.501, 70.250), # U32.13 旧的对地过孔
('GND', 107.624, 67.050), # R712.2 旧的对地过孔
]
NET = '+3V3_HUB'
# ⚠️ 路径的 key 必须是 `网名` 或 `网名 + KSEP + 后缀` —— `dp.netof()` 拿第一段当网络名,
# `check()` 再用它判「同网跳过」。第一版把 key 写成位号('U32.13'),于是
# 「新铜搭上本网旧铜」这件**本来就该发生**的事被报成 8 条净距违规,
# 而真正的异网风险反倒没被检查到。
def _key(tag):
return NET + dp.KSEP + tag
# ---- 连接 1 手写(走廊已经被同网铜证明过,见文件头)。
# 语义见 skill §9r:第 i 点的 layer 是「从这一点走到下一点」那一段的层。
HAND = {
_key('U32.13'): [(95.926, 70.250, dp.L_BOT, 0.24),
(96.325, 70.250, dp.L_BOT, 0.24),
(96.325, 68.550, dp.L_BOT, 0.24)],
}
# ---- 连接 2 交给 _connect():(位号, 焊盘号, 线宽)
AUTO = [('R712', '2', 0.20)]
def call(js, what, timeout=600):
r = run(js, timeout=timeout)
if not r.get('success'):
sys.exit(u'%s 失败: %s' % (what, json.dumps(r, ensure_ascii=False)[:700]))
return r['result']
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1800));" % PCB)
def clean():
js = OPEN + u"""
const MIL=%f, L=%s, V=%s, tol=0.02;
const hit=(a,b,c,d,p,q,r,s)=>
(Math.abs(a-p)<tol&&Math.abs(b-q)<tol&&Math.abs(c-r)<tol&&Math.abs(d-s)<tol)||
(Math.abs(a-r)<tol&&Math.abs(b-s)<tol&&Math.abs(c-p)<tol&&Math.abs(d-q)<tol);
let dl=0, dv=0; const missL=[], missV=[];
const seenL=new Set(), seenV=new Set();
for(const l of await eda.pcb_PrimitiveLine.getAll()){
const n=l.getState_Net()||'', lay=l.getState_Layer();
const a=l.getState_StartX()/MIL,b=l.getState_StartY()/MIL;
const c=l.getState_EndX()/MIL,d=l.getState_EndY()/MIL;
for(let i=0;i<L.length;i++){
const t=L[i];
if(n===t[0]&&lay===t[1]&&hit(a,b,c,d,t[2],t[3],t[4],t[5])){
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++; seenL.add(i); break;}
}
}
for(const v of await eda.pcb_PrimitiveVia.getAll()){
const n=v.getState_Net()||'', x=v.getState_X()/MIL, y=v.getState_Y()/MIL;
for(let i=0;i<V.length;i++){
const t=V[i];
if(n===t[0]&&Math.abs(x-t[1])<tol&&Math.abs(y-t[2])<tol){
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++; seenV.add(i); break;}
}
}
for(let i=0;i<L.length;i++) if(!seenL.has(i)) missL.push(L[i]);
for(let i=0;i<V.length;i++) if(!seenV.has(i)) missV.push(V[i]);
await eda.pcb_Document.save();
// skill §9ad:save() 的返回值不算数,回查一次还剩几条
let leftL=0, leftV=0;
for(const l of await eda.pcb_PrimitiveLine.getAll()){
const n=l.getState_Net()||'', lay=l.getState_Layer();
const a=l.getState_StartX()/MIL,b=l.getState_StartY()/MIL;
const c=l.getState_EndX()/MIL,d=l.getState_EndY()/MIL;
if(L.some(t=>n===t[0]&&lay===t[1]&&hit(a,b,c,d,t[2],t[3],t[4],t[5]))) leftL++;
}
for(const v of await eda.pcb_PrimitiveVia.getAll()){
const n=v.getState_Net()||'', x=v.getState_X()/MIL, y=v.getState_Y()/MIL;
if(V.some(t=>n===t[0]&&Math.abs(x-t[1])<tol&&Math.abs(y-t[2])<tol)) leftV++;
}
return {dl,dv,missL,missV,leftL,leftV};
""" % (MIL, json.dumps([list(x) for x in DEAD_LINES]), json.dumps([list(x) for x in DEAD_VIAS]))
r = call(js, u'删旧铜')
print(u'删除:导线 %d / %d,过孔 %d / %d' % (r['dl'], len(DEAD_LINES), r['dv'], len(DEAD_VIAS)))
if r['missL'] or r['missV']:
print(u'⚠️ 这些没找到(板上可能已经没有了,或坐标写错):')
for t in r['missL'] + r['missV']:
print(u' ' + json.dumps(t, ensure_ascii=False))
if r['leftL'] or r['leftV']:
sys.exit(u'回查还剩 %d 线 / %d 孔没删掉' % (r['leftL'], r['leftV']))
print(u'落盘后回查:目标铜已清零')
def build_routes():
u"""算出两条路径。返回 {key: path}。"""
pads, vias, lines = dp.geom()
rs.PADS_ALL = pads
obs = rs.shapes_of(pads, vias, lines)
routes = dict(HAND)
# 断言手写那条的起点确实落在 U32.13 的焊盘上(skill:手写坐标必须有断言兜底)
p13 = [q for q in pads if q['ref'] == 'U32' and q['num'] == '13']
if len(p13) != 1:
sys.exit(u'找不到 U32.13')
q = p13[0]
if q['net'] != NET:
sys.exit(u'U32.13 的网络是 %r,不是 %s —— 原理图变更没导进来?' % (q['net'], NET))
sx, sy = HAND[_key('U32.13')][0][0], HAND[_key('U32.13')][0][1]
if abs(sx - q['x']) > q['hw'] or abs(sy - q['y']) > q['hh']:
sys.exit(u'手写起点 (%.3f,%.3f) 不在 U32.13 焊盘内 (%.3f,%.3f ±%.3f/%.3f)'
% (sx, sy, q['x'], q['y'], q['hw'], q['hh']))
# 终点必须落在一段**本网**铜上,否则这条线是悬空的
ex, ey = HAND[_key('U32.13')][-1][0], HAND[_key('U32.13')][-1][1]
onnet = [l for l in lines if l['net'] == NET and l['lay'] == dp.L_BOT
and dp.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], ex, ey) < 0.02]
if not onnet:
sys.exit(u'手写终点 (%.3f,%.3f) 没搭在任何一段 %s 底层铜上' % (ex, ey, NET))
print(u'U32.13 手写路径:起点在焊盘内 ✓,终点搭在 %s 底层铜上 ✓' % NET)
# 手写那条立刻进障碍表,后面 _connect 才看得见它
for x0, y0, x1, y1, lay, w in dp.expand(HAND[_key('U32.13')])[0]:
obs.append((rs.sh_seg(x0, y0, x1, y1, w, lay), NET))
# 连接 2:把 R712.2 接到**已有的**本网铜上
have = []
for l in lines:
if l['net'] == NET:
have.append(rs.sh_seg(l['x0'], l['y0'], l['x1'], l['y1'], l['w'], l['lay']))
for v in vias:
if v['net'] == NET:
have.append(rs.sh_via(v['x'], v['y'], v['r']))
for ref, num, w in AUTO:
tgt = [q for q in pads if q['ref'] == ref and q['num'] == num]
if len(tgt) != 1:
sys.exit(u'找不到 %s.%s' % (ref, num))
q = tgt[0]
if q['net'] != NET:
sys.exit(u'%s.%s 的网络是 %r,不是 %s' % (ref, num, q['net'], NET))
lay = (dp.L_TOP, dp.L_BOT) if q['lay'] == 12 else (q['lay'],)
b = rs.Comp([rs.sh_rect(q['x'], q['y'], q['hw'], q['hh'], lay)],
(q['x'], q['y']), center=(q['x'], q['y']), layers=lay)
# `a` 用「离目标最近的那 40 段本网铜」,别把横跨全板的整个 +3V3_HUB 丢进去
# (skill / _connect 注释:窗口一大栅格就被放粗,QFN 边上的窄走廊会整片不可达)
near = sorted(have, key=lambda s: _dist_to(s, q['x'], q['y']))[:40]
a = rs.Comp(near, (q['x'], q['y']))
seg = None
for wtry in rs._widths(w, NET):
seg = rs._connect(NET, a, b, obs, wtry)
if seg is not None:
if wtry != w:
print(u'%s.%s 收窄到 %.2f 才通' % (ref, num, wtry))
break
if seg is None:
sys.exit(u'%s.%s 布不通' % (ref, num))
path, shp = seg
routes[_key('%s.%s' % (ref, num))] = path
obs.extend((s_, NET) for s_ in shp)
print(u'%s.%s 布通:%d 点 / %d 段 / %d 孔'
% (ref, num, len(path), len(dp.expand(path)[0]), len(dp.expand(path)[1])))
return routes, pads, vias, lines
def _dist_to(shape, x, y):
if shape[0] == 'seg':
return dp.seg_pt(shape[1], shape[2], shape[3], shape[4], x, y)
return math.hypot(shape[1] - x, shape[2] - y)
def push(routes):
u"""**只 create,不 delete**(除了本脚本上一次自己画的,按账本删)。"""
segs, vs = [], []
for key, path in routes.items():
s_, v_ = dp.expand(path)
for x0, y0, x1, y1, lay, w in s_:
segs.append([NET, lay, round(x0, 4), round(y0, 4), round(x1, 4), round(y1, 4), w])
for x, y in v_:
vs.append([NET, round(x, 4), round(y, 4)])
old = {'lines': [], 'vias': []}
if os.path.exists(LEDGER):
old = json.load(io.open(LEDGER, encoding='utf-8'))
js = OPEN + u"""
const MIL=%f, OLDL=%s, OLDV=%s, SEG=%s, VIA=%s, tol=0.02;
const hit=(a,b,c,d,p,q,r,s)=>
(Math.abs(a-p)<tol&&Math.abs(b-q)<tol&&Math.abs(c-r)<tol&&Math.abs(d-s)<tol)||
(Math.abs(a-r)<tol&&Math.abs(b-s)<tol&&Math.abs(c-p)<tol&&Math.abs(d-q)<tol);
let dl=0,dv=0;
for(const l of await eda.pcb_PrimitiveLine.getAll()){
const lay=l.getState_Layer();
const a=l.getState_StartX()/MIL,b=l.getState_StartY()/MIL;
const c=l.getState_EndX()/MIL,d=l.getState_EndY()/MIL;
if(OLDL.some(t=>lay===t[1]&&hit(a,b,c,d,t[2],t[3],t[4],t[5]))){
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;}
}
for(const v of await eda.pcb_PrimitiveVia.getAll()){
const x=v.getState_X()/MIL,y=v.getState_Y()/MIL;
if(OLDV.some(t=>Math.abs(x-t[1])<tol&&Math.abs(y-t[2])<tol)){
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++;}
}
// ⚠ create() 的第一个参数就是**网络名**(和 pcb2_route_sig.JS_PUSH_LINES 一致)。
// 先 create 再 setState_Net 会被服务端拒掉:HTTP 500「对象参数不正确,无法应用到画布」。
let nl=0,nv=0;
for(const s of SEG){
await eda.pcb_PrimitiveLine.create(s[0], s[1], s[2]*MIL, s[3]*MIL, s[4]*MIL, s[5]*MIL, s[6]*MIL); nl++;
}
for(const v of VIA){
await eda.pcb_PrimitiveVia.create(v[0], v[1]*MIL, v[2]*MIL, %f*MIL, %f*MIL); nv++;
}
await eda.pcb_Document.save();
return {dl,dv,nl,nv};
""" % (MIL, json.dumps(old['lines']), json.dumps(old['vias']),
json.dumps(segs), json.dumps(vs), dp.VIA_H, dp.VIA_D)
r = call(js, u'下发')
print(u'下发:删自己上一版 %d 线 / %d 孔,新建 %d 线 / %d 孔'
% (r['dl'], r['dv'], r['nl'], r['nv']))
if r['nl'] != len(segs) or r['nv'] != len(vs):
sys.exit(u'建出来的数目对不上:线 %d/%d 孔 %d/%d'
% (r['nl'], len(segs), r['nv'], len(vs)))
json.dump({'lines': segs, 'vias': vs}, io.open(LEDGER, 'w', encoding='utf-8'))
print(u'账本已写 %s' % os.path.basename(LEDGER))
def main():
if '--clean' in sys.argv:
clean()
return
if '--refresh' in sys.argv:
dp.dump()
print(u'障碍表已刷新')
if '--push' not in sys.argv:
return
routes, pads, vias, lines = build_routes()
bad = dp.check(routes, pads, vias, lines)
if bad:
sys.exit(u'净距校核 %d 条,不下发' % len(bad))
print(u'净距校核通过')
if '--push' in sys.argv:
push(routes)
else:
print(u'加 --push 下发')
main()