# -*- coding: utf-8 -*- u"""R900~R904 一次解完:**位置 + 朝向 + 地过孔 + strap 走线,四件同时成立才算数**。 /c/Python314/python .claude/pcb2_strap_solve.py # 求解 + 权威校核 /c/Python314/python .claude/pcb2_strap_solve.py --push # 摆件 + 下发铜 + 存盘 ## 为什么要合到一起解 分三步走(摆位 → 地扇出 → 布线)连着踩了三次,每次都是「前一步只顾自己那一项」: 1. 只按空地摆(U32 北边 y=79 那条空带)→ 迷宫布线器 **10 条只布通 3 条**; 2. 改成贴着引脚 → 两颗的**地过孔**被围死出不去; 3. 加了「地过孔必须有位置」→ **电阻方向反了**:0402 的 GND 端朝着 U32, strap 线得跨过自己的 GND 焊盘 + 短线 + 过孔,十条里九条过不了净距校核。 ⇒ 分步搜索每一步都「局部最优」,合起来无解。这里对每颗电阻枚举(位置 × 四个朝向), **当场把地过孔和 strap 路径也一并试出来**,四件全过才落定。 ## 两条硬约束 - **QFN 焊盘只能垂直出边**。U32 是 QFN-64 9×9、0.5 mm 间距,焊盘之间只有约 0.2 mm 缝, **穿不过 0.20 mm 的线**。所以路径按「垂直法线出边,出来之后才有自由度」生成,不做迷宫搜索。 - **0402 的 GND 端必须背对 U32**(`pad2` 比 `pad1` 更靠近引脚),否则见上面第 3 条。 ## 性能:搜索用网格索引,权威校核只跑一次 ⚠️ 搜索循环里**不能**调 `D.check()` —— 它是 O(全部障碍) 的(1000 焊盘 / 900 过孔 / 2000 导线), 一个候选位置要试几十条路径,几万次全板扫描,实测单颗电阻 40 分钟出不来。 所以搜索用按 2 mm 分桶的网格索引做**同判据的快速版**, **最后再对整体解跑一次 `D.check()`**(和差分对、整板布线器同一套)作为权威校核。 """ 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 D import pcb2_keypads as K PCB = '942ed1560d81c1e7' MIL = 39.3700787401575 LAY = D.L_BOT WID = 0.20 CLR = D.CLR PW, PH, POFF = 0.5664, 0.5410, 0.433 CELL = 2.0 OUT = os.path.join(HERE, '_pcb2_strap_solved.json') # 位号 -> (U32 脚号, QFN 边)。2026-09-07 用户拍板:只做 PRT_SWP1~5, # BOOST/GANG_EN/已禁用口的 SWP6-7 不装(采到 1 也无功能后果,见设计文档 §21.6a)。 MAP = [('R903', '33', 'E'), ('R902', '47', 'E'), ('R904', '31', 'N'), ('R901', '49', '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 u32_pads(): return call(""" let pid = null; for (const c of await eda.pcb_PrimitiveComponent.getAll()) if (c.getState_Designator() === 'U32') pid = c.getState_PrimitiveId(); const M = %f, out = {}; for (const p of await eda.pcb_PrimitivePad.getAll()) { if ((p.primitiveId || '').indexOf(pid) !== 0) continue; out[String(p.getState_PadNumber ? p.getState_PadNumber() : p.number)] = [Math.round(p.getState_X()/M*1000)/1000, Math.round(p.getState_Y()/M*1000)/1000, p.getState_Net()]; } return out;""" % MIL) class Grid(object): u"""按 2 mm 方格给焊盘/过孔/导线分桶,供搜索期的快速净距判据用。""" def __init__(self, pads, vias, lines): self.p, self.v, self.l = {}, {}, {} for q in pads: self._add(self.p, q['x'] - q['hw'], q['y'] - q['hh'], q['x'] + q['hw'], q['y'] + q['hh'], q) for q in vias: self._add(self.v, q['x'] - q['r'], q['y'] - q['r'], q['x'] + q['r'], q['y'] + q['r'], q) for q in lines: self._add(self.l, min(q['x0'], q['x1']), min(q['y0'], q['y1']), max(q['x0'], q['x1']), max(q['y0'], q['y1']), q) def _add(self, ix, x0, y0, x1, y1, obj): for i in range(int(x0 // CELL) - 1, int(x1 // CELL) + 2): for j in range(int(y0 // CELL) - 1, int(y1 // CELL) + 2): ix.setdefault((i, j), []).append(obj) def _cells(self, ix, x0, y0, x1, y1): out, seen = [], set() for i in range(int(x0 // CELL) - 1, int(x1 // CELL) + 2): for j in range(int(y0 // CELL) - 1, int(y1 // CELL) + 2): for o in ix.get((i, j), ()): k = id(o) if k not in seen: seen.add(k) out.append(o) return out def near(self, ix, x, y, r=1.5): return self._cells(ix, x - r, y - r, x + r, y + r) def near_seg(self, ix, a, b, r=1.5): return self._cells(ix, min(a[0], b[0]) - r, min(a[1], b[1]) - r, max(a[0], b[0]) + r, max(a[1], b[1]) + r) def pad_rects(cx, cy, rot): u"""[pad1(GND), pad2(strap)],每个是 (x, y, hw, hh)。""" r = rot % 360 if r == 0: return [(cx - POFF, cy, PW / 2, PH / 2), (cx + POFF, cy, PW / 2, PH / 2)] if r == 180: return [(cx + POFF, cy, PW / 2, PH / 2), (cx - POFF, cy, PW / 2, PH / 2)] if r == 90: return [(cx, cy - POFF, PH / 2, PW / 2), (cx, cy + POFF, PH / 2, PW / 2)] return [(cx, cy + POFF, PH / 2, PW / 2), (cx, cy - POFF, PH / 2, PW / 2)] def bodies(pads, mine): u"""障碍表里没有元件本体,只有焊盘。用「同 ref 所有焊盘的包围盒」当本体 —— 不加这一条,搜索会把 0402 摆进 U32 那颗 QFN 的本体底下(焊盘确实没撞,物理上装不了)。""" bb = {} for q in pads: if q['ref'] in mine or not q['ref']: continue b = bb.setdefault(q['ref'], [1e9, 1e9, -1e9, -1e9]) b[0] = min(b[0], q['x'] - q['hw']) b[1] = min(b[1], q['y'] - q['hh']) b[2] = max(b[2], q['x'] + q['hw']) b[3] = max(b[3], q['y'] + q['hh']) return list(bb.items()) def pads_clear(cx, cy, rot, G, mine, bods, taken, ex_l, ex_v): for px, py, hw, hh in pad_rects(cx, cy, rot): for ref, b in bods: if b[0] - CLR < px + hw and px - hw < b[2] + CLR and \ b[1] - CLR < py + hh and py - hh < b[3] + CLR: return False for q in G.near(G.p, px, py): if q['ref'] in mine: continue if D.seg_rect((px, py), (px, py), q['x'], q['y'], q['hw'], q['hh']) < hw + hh + CLR: return False for v in list(G.near(G.v, px, py)) + ex_v: if math.hypot(v['x'] - px, v['y'] - py) < max(hw, hh) + v['r'] + CLR: return False for l in list(G.near(G.l, px, py)) + ex_l: if l['lay'] not in (LAY, 12): continue if D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) < max(hw, hh) + l['w'] / 2 + CLR: return False for tx, ty, trot in taken: for a in pad_rects(cx, cy, rot): for b in pad_rects(tx, ty, trot): dx = abs(a[0] - b[0]) - (a[2] + b[2]) dy = abs(a[1] - b[1]) - (a[3] + b[3]) d = math.hypot(max(dx, 0.0), max(dy, 0.0)) if (dx > 0 or dy > 0) else max(dx, dy) if d < CLR: return False return True def via_ok(vx, vy, G, ex_v, ex_l): for q in G.near(G.p, vx, vy, 2.0): need = K.VIA_D / 2 + (0.0 if q['net'] == 'GND' else CLR) if q['lay'] == 12: need = max(need, K.VIA_D / 2 + K.HOLE2HOLE) if D.seg_rect((vx, vy), (vx, vy), q['x'], q['y'], q['hw'], q['hh']) < need: return False for v in list(G.near(G.v, vx, vy, 2.0)) + ex_v: d = math.hypot(v['x'] - vx, v['y'] - vy) if d < K.VIA_D / 2 + v['r'] + K.HOLE2HOLE: return False for l in list(G.near(G.l, vx, vy, 2.0)) + ex_l: if l['net'] == 'GND': continue if D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], vx, vy) < K.VIA_D / 2 + l['w'] / 2 + CLR: return False return True def stub_ok(a, b, G, ex_l, ex_v): h = K.W_STUB / 2.0 for q in G.near_seg(G.p, a, b): if q['net'] == 'GND': continue if D.seg_rect(a, b, q['x'], q['y'], q['hw'], q['hh']) < h + CLR: return False for v in list(G.near_seg(G.v, a, b)) + ex_v: if v['net'] == 'GND': continue if D.seg_pt(a[0], a[1], b[0], b[1], v['x'], v['y']) < h + v['r'] + CLR: return False for l in list(G.near_seg(G.l, a, b)) + ex_l: if l['net'] == 'GND' or l['lay'] not in (LAY, 12): continue if D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) < h + l['w'] / 2 + CLR: return False return True def path_ok(path, net, G, mine, ex_l, ex_v): h = WID / 2.0 for i in range(len(path) - 1): a = (path[i][0], path[i][1]) b = (path[i + 1][0], path[i + 1][1]) for q in G.near_seg(G.p, a, b): if q['net'] == net or q['ref'] in mine or q['lay'] not in (LAY, 12): continue if D.seg_rect(a, b, q['x'], q['y'], q['hw'], q['hh']) < h + CLR: return False for v in list(G.near_seg(G.v, a, b)) + ex_v: if v['net'] == net: continue if D.seg_pt(a[0], a[1], b[0], b[1], v['x'], v['y']) < h + v['r'] + CLR: return False for l in list(G.near_seg(G.l, a, b)) + ex_l: if l['net'] == net or l['lay'] not in (LAY, 12): continue if D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) < h + l['w'] / 2 + CLR: return False return True def strap_paths(a, b, nd): u"""出边 -> L(两种拐序)/ 斜接 / 侧移再折。QFN 只能垂直出边,出来之后才有自由度。""" ax, ay = a bx, by = b nx, ny = nd tx, ty = -ny, nx for d in [v / 100.0 for v in range(30, 201, 10)]: mx, my = round(ax + nx * d, 3), round(ay + ny * d, 3) opts = [[(ax, ay), (mx, my), (round(bx, 3), round(my, 3)), (bx, by)], [(ax, ay), (mx, my), (round(mx, 3), round(by, 3)), (bx, by)], [(ax, ay), (mx, my), (bx, by)]] for k in (0.5, -0.5, 1.0, -1.0, 1.5, -1.5, 2.0, -2.0, 3.0, -3.0): sx, sy = round(mx + tx * k, 3), round(my + ty * k, 3) opts.append([(ax, ay), (mx, my), (sx, sy), (round(bx, 3), round(sy, 3)), (bx, by)]) opts.append([(ax, ay), (mx, my), (sx, sy), (round(sx, 3), round(by, 3)), (bx, by)]) for pts in opts: 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: yield [(x, y, LAY, WID) for x, y in clean] def solve(pads, vias, lines, up): mine = {d for d, _, _ in MAP} bods = bodies(pads, mine) G = Grid(pads, vias, lines) taken, gvias, routes, out = [], [], {}, {} ex_l, ex_v = [], [] for des, pin, side in MAP: px, py, net = up[pin] nd = NORM[side] base = math.degrees(math.atan2(nd[1], nd[0])) angs = sorted(range(-90, 91, 10), key=abs) # 只在引脚的外侧半平面里找 best = None for r in [v / 10.0 for v in range(10, 71, 2)]: for da in angs: th = math.radians(base + da) cx, cy = round(px + r * math.cos(th), 3), round(py + r * math.sin(th), 3) for rot in (0, 90, 180, 270): g, t = pad_rects(cx, cy, rot) # strap 端必须比 GND 端更靠近引脚,否则线要跨过自己的地 if math.hypot(t[0] - px, t[1] - py) >= math.hypot(g[0] - px, g[1] - py): continue if not pads_clear(cx, cy, rot, G, mine, bods, taken, ex_l, ex_v): continue vsel = None for vd in [v / 10.0 for v in range(9, 26)]: for va in range(0, 360, 15): vth = math.radians(va) vx = round(g[0] + math.cos(vth) * vd, 3) vy = round(g[1] + math.sin(vth) * vd, 3) if math.hypot(vx - px, vy - py) <= math.hypot(g[0] - px, g[1] - py): continue # 地过孔往远离引脚的一侧走 if any(math.hypot(vx - ax, vy - ay) < 0.33 + K.VIA_H for ax, ay in gvias): continue if not via_ok(vx, vy, G, ex_v, ex_l): continue if stub_ok((g[0], g[1]), (vx, vy), G, ex_l, ex_v): vsel = (vx, vy) break if vsel: break if not vsel: continue l2 = ex_l + [{'net': 'GND', 'lay': LAY, 'x0': g[0], 'y0': g[1], 'x1': vsel[0], 'y1': vsel[1], 'w': K.W_STUB}] v2 = ex_v + [{'net': 'GND', 'x': vsel[0], 'y': vsel[1], 'r': K.VIA_D / 2}] for path in strap_paths((px, py), (t[0], t[1]), nd): if path_ok(path, net, G, mine, l2, v2): best = (cx, cy, rot, r, vsel, path, (g[0], g[1])) break if best: break if best: break if best: break if best is None: print(u'!! %s (U32.%s) 无解' % (des, pin)) return None cx, cy, rot, r, vsel, path, gp = best routes[net] = path taken.append((cx, cy, rot)) gvias.append(vsel) ex_l.append({'net': 'GND', 'lay': LAY, 'x0': gp[0], 'y0': gp[1], 'x1': vsel[0], 'y1': vsel[1], 'w': K.W_STUB}) ex_v.append({'net': 'GND', 'x': vsel[0], 'y': vsel[1], 'r': K.VIA_D / 2}) for i in range(len(path) - 1): ex_l.append({'net': net, 'lay': LAY, 'x0': path[i][0], 'y0': path[i][1], 'x1': path[i + 1][0], 'y1': path[i + 1][1], 'w': WID}) out[des] = {'pos': [cx, cy, rot], 'pin': pin, 'net': net, 'gnd_pad': list(gp), 'via': list(vsel), 'path': [[p[0], p[1]] for p in path]} L = sum(math.hypot(path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1]) for i in range(len(path) - 1)) print(u' %-5s U32.%-3s %s边 电阻(%7.3f,%6.3f) rot%-4d 离脚 %.1f strap %.2f mm %d 拐点 地孔(%7.3f,%6.3f)' % (des, pin, side, cx, cy, rot, r, L, len(path) - 2, vsel[0], vsel[1])) sys.stdout.flush() return out, routes def main(): if '--nodump' not in sys.argv: D.dump() pads, vias, lines = D.geom() up = u32_pads() got = solve(pads, vias, lines, up) if not got: sys.exit(1) sol, routes = got # 权威校核:把地短线/地过孔也加进障碍,再跑一次和差分对同一套的 check() lines2 = lines + [{'net': 'GND', 'lay': LAY, 'x0': v['gnd_pad'][0], 'y0': v['gnd_pad'][1], 'x1': v['via'][0], 'y1': v['via'][1], 'w': K.W_STUB} for v in sol.values()] vias2 = vias + [{'net': 'GND', 'x': v['via'][0], 'y': v['via'][1], 'r': K.VIA_D / 2} for v in sol.values()] bad = D.check(routes, pads, vias2, lines2, verbose=True) if bad: sys.exit(u'权威校核未过 %d 条' % len(bad)) io.open(OUT, 'w', encoding='utf-8').write(json.dumps(sol, ensure_ascii=False, indent=1)) print(u'\n五颗全部解出,D.check() 权威校核通过;已存 %s' % OUT) print(u'粘到 pcb2_place.GLOBAL:') for des, pin, side in MAP: cx, cy, rot = sol[des]['pos'] print(u" '%s': (%.3f, %.3f, %d), # U32.%s" % (des, cx, cy, rot, pin)) main()