# -*- 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())