文件
phonerail/.claude/pcb1_ch_reroute.py

303 行
14 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""下板 PCB1:**在一个通道的窗口里,把布线器自己画的铜拆光、按指定顺序重布**(纯本地实验,不下发)。
/c/Python314/python .claude/pcb1_ch_reroute.py --ch 1 # CH1 的 C 类五条
/c/Python314/python .claude/pcb1_ch_reroute.py --ch 1 --also-efuse # 连 EFUSE_FLT/PG/EN 一起拆了重布
/c/Python314/python .claude/pcb1_ch_reroute.py --ch 1 --move R112:0:0:180 --move R113:0:0:180
# 先在本地模型里把件挪/转了再试
/c/Python314/python .claude/pcb1_ch_reroute.py --ch 1 --json out.json # 把布出来的路径写成文件
## 为什么要有这一支
§26.22 的 C 类(`$*N676/680/698/727`:eFuse 东侧四个编程脚 → 0402 电阻阵)在五路
同时布不通。布线器日志只说「布不通」,分不清是**顺序 / 拆线额度**的事,还是
**母版几何**根本装不下。这里把窗口里本脚本(`pcb1_route_sig`)自己画过的铜全部摘掉,
只留扇出 / 功率级 / 焊盘,然后按给定顺序、用引擎同一套 `connect()` 逐条布:
· 全通 ⇒ 是顺序问题,方案就是「按这个顺序、这些路径手写下发」(`--json` 给几何)
· 不通 ⇒ 母版几何缺口;再用 `--move` 在本地模型里试挪件 / 转件,通了就是母版该改的位置
⚠️ 只读本地缓存,`eda.run` 被换成抛异常的桩,永远不会碰桥接。
"""
import io
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import eda # noqa: E402
def _blocked(*a, **k):
raise RuntimeError(u'pcb1_ch_reroute.py 是纯本地实验,禁止碰桥接')
eda.run = _blocked
eda.port = _blocked
import pcb1_route_sig as R # noqa: E402
E, D, NL = R.E, R.D, R.NL
L_TOP, L_BOT = R.L_TOP, R.L_BOT
KSEP = R.KSEP
# 每路 C 类网络的**功能**(母版 CH1 的自动网名;别的通道按同一签名对齐)
# eFuse U%14:15 OVP / 17 ILIM / 18 dVdT / 19 IMON / 20 FLT# / 3 PGTH / 2 PGOOD / 14 EN
# 列表顺序 = ILIM, dVdT, IMON, OVP, PGTH, 放电联锁(727);由焊盘签名跨五路对齐算出
C_NETS = {
1: ["$2N685", "$2N687", "$2N680", "$2N676", "$2N698", "$2N727"],
2: ["$8N685", "$8N687", "$8N680", "$8N676", "$8N698", "$8N727"],
3: ["$10N685", "$10N687", "$10N680", "$10N676", "$10N698", "$10N727"],
4: ["$12N864", "$12N866", "$12N859", "$12N855", "$12N877", "$12N906"],
5: ["$14N685", "$14N687", "$14N680", "$14N676", "$14N698", "$14N727"],
}
EFUSE_NETS = {n: ['EFUSE_FLT%d' % n, 'EFUSE_PG%d' % n, 'EFUSE_EN%d' % n] for n in range(1, 6)}
# 窗口:母版局部 (x 4~21, y 24.5~41.5) 按 CH_PLACE 变换;这里直接给绝对坐标
WIN = {
1: (12.0, 40.0, 29.0, 57.0),
4: (43.5, 40.0, 60.5, 57.0),
5: (121.0, 40.0, 138.0, 57.0),
2: (37.5, 68.5, 54.5, 85.5),
3: (126.0, 68.5, 143.0, 85.5),
}
def inwin(x, y, w):
return w[0] <= x <= w[2] and w[1] <= y <= w[3]
def move_pads(pads, spec):
u"""`--move DES:dx:dy[:drot]`:把这个元件的焊盘在本地模型里挪 / 绕元件中心转。"""
des, rest = spec.split(':', 1)
parts = rest.split(':')
dx, dy = float(parts[0]), float(parts[1])
drot = int(parts[2]) if len(parts) > 2 else 0
mine = [q for q in pads if q['ref'] == des]
if not mine:
sys.exit(u'--move:位号 %s 板上没有' % des)
cx = sum(q['x'] for q in mine) / len(mine)
cy = sum(q['y'] for q in mine) / len(mine)
a = math.radians(drot)
ct, st = math.cos(a), math.sin(a)
for q in mine:
rx, ry = q['x'] - cx, q['y'] - cy
q['x'] = round(cx + rx * ct - ry * st + dx, 4)
q['y'] = round(cy + rx * st + ry * ct + dy, 4)
if drot in (90, 270):
q['hw'], q['hh'] = q['hh'], q['hw']
q.pop('rects', None)
print(u' 本地模型:%s 挪 (%+.2f,%+.2f) 转 %d° -> %s' % (
des, dx, dy, drot, ' '.join('%s(%.2f,%.2f)' % (q['num'], q['x'], q['y']) for q in mine)))
def collision_report(pads, moved, lines, vias, lown_by_id, vown_by_id, reach=6.0):
u"""挪完件之后:6 mm 内哪些线 / 孔被新焊盘压到(异网净距 < 0.20)—— 下发时要删;
哪些同网短线原本搭在老焊盘上 —— 扇出要重做。两类都从本地模型里摘掉再布。"""
mine = [q for q in pads if q['ref'] in moved]
def far(x, y):
return all(math.hypot(x - q['x'], y - q['y']) > reach for q in mine)
kill_l, kill_v, refan = [], [], []
for l in lines:
if far(l['x0'], l['y0']) and far(l['x1'], l['y1']):
continue
for q in mine:
if l['lay'] not in R.pad_layers(q):
continue
d = D.seg_rect((l['x0'], l['y0']), (l['x1'], l['y1']), q['x'], q['y'], q['hw'], q['hh']) - l['w'] / 2.0
if l['net'] != q['net'] and d < R.CLR - 1e-6:
kill_l.append((l, q['tag'], d))
break
if l['net'] == q['net'] and d < 0.0:
refan.append((l, q['tag']))
break
for v in vias:
if far(v['x'], v['y']):
continue
for q in mine:
d = D.seg_rect((v['x'], v['y']), (v['x'], v['y']), q['x'], q['y'], q['hw'], q['hh']) - v['r']
if v['net'] != q['net'] and d < R.CLR - 1e-6:
kill_v.append((v, q['tag'], d))
break
print(u'\n挪件善后(%s,%.0f mm 内):' % (' '.join(sorted(moved)), reach))
print(u' 被压到、下发时要删的异网铜:%d 线 / %d 孔' % (len(kill_l), len(kill_v)))
bynet = {}
for l, tag, d in kill_l:
bynet.setdefault(l['net'], []).append(u'L%d (%.2f,%.2f)-(%.2f,%.2f) w%.2f ← %s %+.3f%s' % (
l['lay'], l['x0'], l['y0'], l['x1'], l['y1'], l['w'], tag, d, u'(自己画的)' if lown_by_id.get(id(l)) else u'(扇出/功率级)'))
for v, tag, d in kill_v:
bynet.setdefault(v['net'], []).append(u'过孔 (%.2f,%.2f) ← %s %+.3f%s' % (
v['x'], v['y'], tag, d, u'(自己画的)' if vown_by_id.get(id(v)) else u'(扇出/功率级)'))
for n in sorted(bynet):
print(u' %s' % n)
for t in bynet[n]:
print(u' %s' % t)
print(u' 原本搭在老焊盘上的同网短线(扇出要按新位置重做):%d' % len(refan))
for l, tag in refan:
print(u' %-10s L%d (%.2f,%.2f)-(%.2f,%.2f) w%.2f ← %s' % (l['net'], l['lay'], l['x0'], l['y0'], l['x1'], l['y1'], l['w'], tag))
gone = set(id(l) for l, _t, _d in kill_l) | set(id(l) for l, _t in refan)
gone_v = set(id(v) for v, _t, _d in kill_v)
return [l for l in lines if id(l) not in gone], [v for v in vias if id(v) not in gone_v]
def frags_of(net, pads, lines, vias, pgrids):
nl_pads = [dict(net=q['net'], x=q['x'], y=q['y'], hw=q['hw'], hh=q['hh'],
th=(q['lay'] == 12), ref=q['ref'], num=q['num'],
plane=(1 if q['lay'] == 12 else q['lay']), tag=q['tag']) for q in pads]
items, groups = NL.analyze(net, nl_pads, vias, lines, pgrids)
real = [gp for gp in groups if any(items[i][0] != 'line' for i in gp)]
bytag = {q['tag']: q for q in pads}
frags = []
for gp in real:
fp, fv, fl = [], [], []
for i in gp:
k, _lay, o, _lab = items[i]
if k == 'pad':
q = bytag.get(o['tag'])
if q:
fp.append(q)
elif k == 'via':
fv.append(o)
elif k == 'line':
fl.append(o)
if fp or fv or fl:
frags.append(dict(pads=fp, vias=fv, lines=fl))
return frags
def main():
argv = sys.argv[1:]
ch = int(argv[argv.index('--ch') + 1]) if '--ch' in argv else 1
also_efuse = '--also-efuse' in argv
moves = [argv[i + 1] for i, a in enumerate(argv) if a == '--move']
outjson = argv[argv.index('--json') + 1] if '--json' in argv else None
order = None
win = WIN[ch]
nets = list(C_NETS[ch]) + (EFUSE_NETS[ch] if also_efuse else [])
if order:
nets = order
# 任意窗口 / 任意网络(给 A 类挪件实验用):--win x0,y0,x1,y1 --nets A B C
if '--win' in argv:
win = tuple(float(v) for v in argv[argv.index('--win') + 1].split(','))
if '--nets' in argv:
nets = []
for a in argv[argv.index('--nets') + 1:]:
if a.startswith('--'):
break
nets.append(a)
if '--order' in argv:
order = []
for a in argv[argv.index('--order') + 1:]:
if a.startswith('--'):
break
order.append(a)
nets = order
drop_vias = [tuple(float(v) for v in argv[i + 1].split(',')) for i, a in enumerate(argv) if a == '--drop-via']
pads, vias, lines = R.board(False)
pours = R.pour_rects()
pall, src = R.plan_all()
lown, vown = R.own_of(pall, lines, vias, verbose=False)
print(u'板上 %d 焊盘 / %d 孔 / %d 线;累计计划 %d 条(%s)' % (
len(pads), len(vias), len(lines), len(pall), src))
# ── 拆:窗口里、这些网络的、布线器自己画的线和孔 ──
keep_l, keep_v, rip_l, rip_v = [], [], [], []
# ⚠️ 这些网络的自己人铜**全拆、不限窗口**:逃逸段(ESC_R 4 mm)常常伸到窗外,
# 只拆窗内那半截,窗外那半截就成了一个悬空碎片,布线器还得去接它(实测 $2N698 就这样)。
# `--rip-in-win`:只拆窗内的(给 SDA/SCL 这种全板大网用,别把整条干线拆了)
rip_in_win = '--rip-in-win' in argv
for i, l in enumerate(lines):
if l['net'] in nets and lown[i] and (not rip_in_win or inwin(l['x0'], l['y0'], win) or inwin(l['x1'], l['y1'], win)):
rip_l.append(l)
else:
keep_l.append(l)
for i, v in enumerate(vias):
if v['net'] in nets and vown[i] and (not rip_in_win or inwin(v['x'], v['y'], win)):
rip_v.append(v)
else:
keep_v.append(v)
# `--add-via NET:x,y`:在本地模型里预先打一个孔 + 从最近的同网焊盘中心拉一根 0.3 的短线(模拟扇出)
for spec in [argv[i + 1] for i, a in enumerate(argv) if a == '--add-via']:
net_, xy = spec.split(':')
vx, vy = (float(v) for v in xy.split(','))
keep_v.append(dict(net=net_, x=vx, y=vy, r=R.VIA_D / 2.0))
q = min((q for q in pads if q['net'] == net_), key=lambda q: math.hypot(q['x'] - vx, q['y'] - vy))
keep_l.append(dict(net=net_, lay=(q['lay'] if q['lay'] != 12 else 1), x0=q['x'], y0=q['y'], x1=vx, y1=vy, w=0.3))
print(u' --add-via:%s 孔 (%.2f,%.2f) + 短线 <- %s (%.2f,%.2f)' % (net_, vx, vy, q['tag'], q['x'], q['y']))
if drop_vias:
before = len(keep_v)
keep_v = [v for v in keep_v if not any(math.hypot(v['x'] - x, v['y'] - y) < 0.05 for x, y in drop_vias)]
print(u' --drop-via:从本地模型里摘掉 %d 个孔(扇出要重做的那几个)' % (before - len(keep_v)))
print(u'窗口 %s:拆掉 %d 线 / %d 孔(按网络:%s)' % (
win, len(rip_l), len(rip_v),
' '.join('%s×%d' % (n, sum(1 for l in rip_l if l['net'] == n)) for n in nets)))
left = [(l['net'], l['lay'], l['x0'], l['y0'], l['x1'], l['y1']) for l in keep_l
if l['net'] in nets and (inwin(l['x0'], l['y0'], win) or inwin(l['x1'], l['y1'], win))]
if left:
print(u' 窗口里这些网络**没拆**的铜(不是布线器画的,多半是扇出):')
for t in left:
print(u' %-10s L%d (%.2f,%.2f)-(%.2f,%.2f)' % t)
moved = set()
for spec in moves:
move_pads(pads, spec)
moved.add(spec.split(':')[0])
if moved:
keep_l, keep_v = collision_report(pads, moved, keep_l, keep_v, lown_by_id={id(l): lown[i] for i, l in enumerate(lines)},
vown_by_id={id(v): vown[i] for i, v in enumerate(vias)})
obstacles = R.obstacles_of(pads, keep_v, keep_l, pours)
pgrids = NL.pour_grids(False)
# ── 布:照 main() 的流程,出线段 → 逃逸段 → 逐网络 ──
js = []
for net in nets:
fr = frags_of(net, pads, keep_l, keep_v, pgrids)
if len(fr) > 1:
js.append((net, fr))
need_tags = set()
for net, frags in js:
for f in frags:
if len(f['pads']) == 1 and not f['vias'] and not f['lines']:
need_tags.add(f['pads'][0]['tag'])
stubs, alts = R.plan_stubs([n for n, _f in js], pads, obstacles, need_tags)
escapes, stuck = R.plan_escapes(pads, obstacles, need_tags, stubs)
print(u'任务 %d 个网络 / %d 条连接;出线段 %d,逃逸 %d,几何上出不去 %d' % (
len(js), sum(len(f) - 1 for _n, f in js), len(stubs), len(escapes), len(stuck)))
routes, nfail = {}, 0
for net, frags in js:
res = R.route_net(net, frags, pads, obstacles, stubs, alts, escapes)
if not res:
continue
paths, fails = res
nfail += fails
for i, (path, _shp) in enumerate(paths):
routes[net + KSEP + str(i)] = path
ln = sum(math.hypot(b[0] - a[0], b[1] - a[1])
for p, _s in paths for a, b in zip(p, p[1:]))
print(u' %-10s %s %d 条路径 / %.1f mm%s' % (
net, u'✔' if not fails else u'✘', len(paths), ln,
u'' if not fails else u' ← 布不通 %d' % fails))
# 校核(同 main)
chk_pads = R.expand_pads(pads) + R.pour_pads(pours)
routes = D.chamfer_routes(routes, chk_pads, keep_v, keep_l, smooth=True)
bad = D.check(routes, chk_pads, keep_v, keep_l, verbose=True)
hb = R.hole_check(routes, keep_v)
pb = R.pour_check(routes, pours)
print(u'\n结果:布不通 %d;净距 %d / 钻孔 %d / 铺铜 %d' % (nfail, len(bad), len(hb), len(pb)))
if outjson:
io.open(outjson, 'w', encoding='utf-8').write(json.dumps(
{k.replace(KSEP, '#'): v for k, v in routes.items()}, ensure_ascii=False, indent=1))
print(u'-> %s' % outjson)
return 1 if nfail else 0
if __name__ == '__main__':
sys.exit(main())