# -*- coding: utf-8 -*- u"""U32 十个 strap 脚 -> R900~R909 的连线:**手写短路径**,不用迷宫布线器。 /c/Python314/python .claude/pcb2_strap_route.py # 只算 + 校核 /c/Python314/python .claude/pcb2_strap_route.py --push # 下发 ## 为什么手写 `pcb2_route_sig.py` 对这十条**两次都只布通 2~3 条**,连 R904 那条 0.9 mm 的都报布不通 (`(103.25,73.00) -> (103.89,73.63)`)。原因不是距离,是它从 QFN 焊盘选的**逃逸方向** 不对:pin 49 在南边,它却往东逃(沿着 0.5 mm 间距的焊盘排走),当然出不去。 QFN-64 9×9 是 0.5 mm 间距、焊盘间只有 ~0.2 mm 缝 —— **焊盘之间根本穿不过 0.20 mm 的线**, 唯一的出路是**垂直于所在边直着出来**。这十条又都只有 1~5 mm,属于「几何设计」而不是 「找路」,所以照差分对的办法手写坐标(`pcb2_route_dp.py` 文件头那段的理由同样适用)。 校核用的是**同一套** `pcb2_route_dp.check()`,和差分对、和布线器一个判据。 """ 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 D PCB = '942ed1560d81c1e7' MIL = 39.3700787401575 LAY = D.L_BOT # 元件都在底层 WID = 0.20 # strap 是 DC 采样脚,最细线宽就够 # 位号 -> (U32 脚号, 该脚在 QFN 的哪条边) N/S/E/W = 出线法线方向 MAP = [('R906', '15', 'W'), ('R905', '17', 'N'), ('R904', '31', 'N'), ('R903', '33', 'E'), ('R909', '34', 'E'), ('R902', '47', 'E'), ('R908', '48', 'E'), ('R901', '49', 'S'), ('R907', '50', 'S'), ('R900', '51', 'S')] NORM = {'N': (0.0, 1.0), 'S': (0.0, -1.0), 'E': (1.0, 0.0), 'W': (-1.0, 0.0)} def call(js, timeout=300): r = run("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2000));" % PCB + js, timeout=timeout) if not r.get('success'): sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def endpoints(): u"""读回 U32 十个 strap 焊盘中心 + 十颗电阻的 strap 端焊盘中心(mm)。""" return call(""" const name = {}; for (const c of await eda.pcb_PrimitiveComponent.getAll()) name[c.getState_PrimitiveId()] = c.getState_Designator(); const ids = Object.keys(name).sort((a,b)=>b.length-a.length); const M = %f, u32 = {}, res = {}; for (const p of await eda.pcb_PrimitivePad.getAll()) { const pid = p.primitiveId || ''; let hit = null; for (const id of ids) if (pid.length > id.length && pid.indexOf(id) === 0) { hit = name[id]; break; } if (!hit) continue; const xy = [Math.round(p.getState_X()/M*1000)/1000, Math.round(p.getState_Y()/M*1000)/1000, p.getState_Net()]; if (hit === 'U32') u32[String(p.getState_PadNumber ? p.getState_PadNumber() : p.number)] = xy; else if (/^R9[0-9][0-9]$/.test(hit) && p.getState_Net() !== 'GND') res[hit] = xy; } return {u32, res};""" % MIL) def cands(a, b, nd): u"""从焊盘 a 垂直出边 d,再走 L 形到 b。两种拐序 + 45 度斜接,由短到长试。""" ax, ay = a bx, by = b nx, ny = nd out = [] for d in [v / 100.0 for v in range(30, 161, 10)]: mx, my = round(ax + nx * d, 3), round(ay + ny * d, 3) for order in (0, 1): if order == 0: pts = [(ax, ay), (mx, my), (round(bx, 3), round(my, 3)), (bx, by)] else: pts = [(ax, ay), (mx, my), (round(mx, 3), round(by, 3)), (bx, by)] clean = [pts[0]] for p in pts[1:]: if abs(p[0] - clean[-1][0]) > 1e-6 or abs(p[1] - clean[-1][1]) > 1e-6: clean.append(p) if len(clean) >= 2: out.append(clean) # 直接斜着接过去(出边之后一段 45°) out.append([(ax, ay), (mx, my), (bx, by)]) return out def main(): if '--nodump' not in sys.argv: D.dump() pads, vias, lines = D.geom() ep = endpoints() routes, fail = {}, [] for des, pin, side in MAP: a = ep['u32'][pin][:2] b = ep['res'][des][:2] net = ep['u32'][pin][2] assert net == ep['res'][des][2], u'%s 与 U32.%s 网络不一致' % (des, pin) got = None for pts in cands(a, b, NORM[side]): path = [(x, y, LAY, WID) for x, y in pts] trial = dict(routes) trial[net] = path if not D.check(trial, pads, vias, lines, verbose=False): got = path break if got is None: fail.append((des, pin, a, b)) continue routes[net] = got L = sum(math.hypot(got[i + 1][0] - got[i][0], got[i + 1][1] - got[i][1]) for i in range(len(got) - 1)) print(u' %-5s U32.%-3s %s 边 %d 拐点 %.2f mm' % (des, pin, side, len(got) - 2, L)) if fail: for des, pin, a, b in fail: print(u'!! %s (U32.%s) 手写路径也过不了校核:%s -> %s' % (des, pin, a, b)) sys.exit(1) bad = D.check(routes, pads, vias, lines, verbose=True) if bad: sys.exit(u'净距校核未过:%d 条' % len(bad)) print(u'十条全部找到路径,净距校核(0.20 mm)全过') if '--push' in sys.argv: D.push(routes) main()