# -*- coding: utf-8 -*- u"""下板 PCB1:**还没布通的每一条连接,卡在哪、差多少**(纯本地,不碰桥接)。 /c/Python314/python .claude/pcb1_unrouted.py # 全部 /c/Python314/python .claude/pcb1_unrouted.py --net SDA SCL # 只看这几个网络 /c/Python314/python .claude/pcb1_unrouted.py --json out.json ## 它回答的是布线器日志回答不了的问题 `pcb1_route_sig.py` 报「[!!] X 布不通:(x,y) -> (x,y)」时,看不出是 **几何上根本过不去**(要挪件 / 改层)还是**被先布的线挤掉了**(多跑几轮 --ripup)。 这里对每一条还欠的连接,在两端包围盒开一个窗,按引擎同一套判据 (`Field` / `route_grid`,CLR 0.20 + 半线宽 + QERR)从宽到窄试线宽、从粗到细试栅格: · 0.1 栅格 + 默认线宽就通 ⇒ **E 单发拥堵**(布线顺序 / 窗口 / 拆线额度的事) · 0.05 栅格才通 ⇒ 栅格量化余量不够(§26.22b 那一类,差的是 QERR 那 0.07) · 只有更细的线才通 ⇒ 几何缺口:报**瓶颈点**、两侧是谁、缝多宽、差多少 mm · 0.03 的线都过不去 ⇒ 被围死:报两端焊盘 1.5 mm 内的全部异网铜 ⚠️ **只读本地缓存**:`_pcb1_place_now.json`(板上铜快照)/ `pcb1-parts.json` / `_pcb1_poured.json` / `_pcb1_polypads.json`。`eda.run` 在 import 之前就被换成抛异常的桩, 任何路径想碰桥接都会当场炸,不会画到别的板上去。 """ 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_unrouted.py 是纯本地工具,禁止碰桥接') eda.run = _blocked eda.port = _blocked import numpy as np # noqa: E402 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 CLR = R.CLR NEED_W = 0.20 # 一般信号线宽 PAD = 2.0 # 搜索窗比两端包围盒大多少(mm) LADDER = (0.15, 0.10, 0.06, 0.03) # 默认线宽之后依次试的线宽 # ══════════════════ 小工具 ══════════════════ def shbox(s): if s[0] == 'rect': return (s[1] - s[3], s[2] - s[4], s[1] + s[3], s[2] + s[4]) if s[0] == 'via': return (s[1] - s[3], s[2] - s[3], s[1] + s[3], s[2] + s[3]) h = s[5] return (min(s[1], s[3]) - h, min(s[2], s[4]) - h, max(s[1], s[3]) + h, max(s[2], s[4]) + h) def bbox(shapes, pad): bs = [shbox(s) for s in shapes] return (min(b[0] for b in bs) - pad, min(b[1] for b in bs) - pad, max(b[2] for b in bs) + pad, max(b[3] for b in bs) + pad) def sh_dist(s, x, y, lay): u"""点到形状的净距(负 = 在里面)。不在这一层返回 None。""" if lay not in s[-1]: return None if s[0] == 'rect': return D.seg_rect((x, y), (x, y), s[1], s[2], s[3], s[4]) if s[0] == 'via': return math.hypot(x - s[1], y - s[2]) - s[3] return D.seg_pt(s[1], s[2], s[3], s[4], x, y) - s[5] def describe(s): if s[0] == 'rect': return u'盘/铜 (%.2f,%.2f) %.2fx%.2f' % (s[1], s[2], s[3] * 2, s[4] * 2) if s[0] == 'via': return u'过孔 (%.2f,%.2f)' % (s[1], s[2]) return u'线 (%.2f,%.2f)-(%.2f,%.2f) w%.2f L%s' % (s[1], s[2], s[3], s[4], s[5] * 2, s[6][0]) def near(obstacles, x, y, lay, net, r, pads_by_xy): rows = [] for s, n in obstacles: if n == net: continue d = sh_dist(s, x, y, lay) if d is None or d > r: continue what = describe(s) if s[0] == 'rect': tag = pads_by_xy.get((round(s[1], 2), round(s[2], 2))) if tag: what = tag + u' ' + what rows.append((round(d, 3), n or u'(空)', what)) rows.sort() return rows def frag_desc(f): tags = [q['tag'] for q in f['pads']] return u'%s%s +%d孔 +%d线' % (' '.join(tags[:4]), u'…' if len(tags) > 4 else '', len(f['vias']), len(f['lines'])) def frag_pt(f): xs, ys = [], [] for q in f['pads']: xs.append(q['x']); ys.append(q['y']) for v in f['vias']: xs.append(v['x']); ys.append(v['y']) for l in f['lines']: xs += [l['x0'], l['x1']]; ys += [l['y0'], l['y1']] return sum(xs) / len(xs), sum(ys) / len(ys) def frag_pt_safe(f): u"""只由铺铜构成的碎片(GND 的内层平面)没有焊盘 / 孔 / 线 —— 拿铺铜格的中心顶上。""" g = f.get('pour_xy') return g if g else (0.0, 0.0) def frag_gap(fa, fb): u"""两个碎片的铜之间最近的距离(按焊盘中心 / 孔心 / 线端点粗算)。""" def pts(f): out = [(q['x'], q['y']) for q in f['pads']] + [(v['x'], v['y']) for v in f['vias']] for l in f['lines']: out += [(l['x0'], l['y0']), (l['x1'], l['y1'])] return out A, B = pts(fa), pts(fb) if not A: A = [frag_pt_safe(fa)] if not B: B = [frag_pt_safe(fb)] best = None for ax, ay in A: for bx, by in B: d = math.hypot(ax - bx, ay - by) if best is None or d < best[0]: best = (d, (ax, ay), (bx, by)) return best def is_stubby(f): u"""「一个焊盘 + 只有自己的出线段」也算孤立焊盘(上一轮把出线段下发了)。""" if len(f['pads']) != 1 or f['vias']: return False q = f['pads'][0] for l in f['lines']: if max(math.hypot(l['x0'] - q['x'], l['y0'] - q['y']), math.hypot(l['x1'] - q['x'], l['y1'] - q['y'])) > 1.2: return False return True # ══════════════════ 一条连接:试栅格 × 线宽 ══════════════════ def probe(net, ca, cb, obstacles, relax, wdef, wmin): u"""返回 dict(grid, w, cells, field) —— **最宽的线、最粗的栅格**里第一个走得通的组合; 全不通返回 None;窗太大(栅格被放粗到 > 0.1)返回 dict(coarse=g)。""" shapes = ca.shapes + cb.shapes x0, y0, x1, y1 = bbox(shapes, PAD) x0, y0 = max(0.0, x0), max(0.0, y0) x1, y1 = min(R.BW, x1), min(R.BH, y1) widths = [wdef] + [w for w in (wmin,) + LADDER if w < wdef - 1e-9] widths = sorted(set(widths), reverse=True) fields = {} for g0 in (0.1, 0.05): g = g0 while ((x1 - x0) / g + 2) * ((y1 - y0) / g + 2) > E.MAX_CELLS: g *= 2 if g > g0 + 1e-9: # 窗太大:照引擎的 'tgt' 模式 —— 只在**小的那一坨**周围开 8 mm 的窗, # 大的那一坨落在窗里的部分当种子(一条都不在就放弃这一档) small, big = (cb, ca) if len(cb.shapes) <= len(ca.shapes) else (ca, cb) bx0, by0, bx1, by1 = bbox(small.shapes, 8.0) bx0, by0 = max(0.0, bx0), max(0.0, by0) bx1, by1 = min(R.BW, bx1), min(R.BH, by1) inside = [s for s in big.shapes if shbox(s)[2] > bx0 and shbox(s)[0] < bx1 and shbox(s)[3] > by0 and shbox(s)[1] < by1] if not inside: if not fields: return dict(coarse=g) break g = g0 while ((bx1 - bx0) / g + 2) * ((by1 - by0) / g + 2) > E.MAX_CELLS: g *= 2 if g > g0 + 1e-9: if not fields: return dict(coarse=g) break f = E.Field(obstacles, (bx0, by0, bx1, by1), skip_net=net, grid=g) f.set_relax(relax) bigc = E.Comp(inside, big.anchor, center=big.center, layers=big.layers) ma, mb = (E._mask(f, bigc), E._mask(f, small)) if big is ca else (E._mask(f, small), E._mask(f, bigc)) fields[g] = (f, ma, mb) continue f = E.Field(obstacles, (x0, y0, x1, y1), skip_net=net, grid=g) f.set_relax(relax) fields[g] = (f, E._mask(f, ca), E._mask(f, cb)) for w in widths: for g in sorted(fields, reverse=True): f, src, tgt = fields[g] cells = E.route_grid(f, src, tgt, w) if cells is not None: return dict(grid=g, w=w, cells=cells, field=f, box=(x0, y0, x1, y1)) return None def path_blockers(res, obstacles, own_keys, net): u"""在「拆光自己人」之后找到的格路上,把**原本挡在路上的自己人铜**按网络列出来。""" f, cells = res['field'], res['cells'] need = CLR + res['w'] / 2.0 + f.qerr pts = [(f.x0 + i * f.g, f.y0 + j * f.g, lay) for i, j, lay in cells] hit = {} for (s, n), key in own_keys: if n == net: continue for x, y, lay in pts[::2]: d = sh_dist(s, x, y, lay) if d is not None and d < need: hit.setdefault(n, set()).add(describe(s)) break return hit def bottleneck(res): u"""沿找到的格路找 edt 最小的格:那就是这条连接的瓶颈。""" f, cells = res['field'], res['cells'] best = None for i, j, lay in cells: e = float(f.edt[lay][j, i]) if best is None or e < best[0]: best = (e, f.x0 + i * f.g, f.y0 + j * f.g, lay) return best def escape_status(q, obstacles, bare): u"""焊盘自己出不出得去:(全铜 0.1 栅格, 全铜 0.025 栅格, 拆光自己人 0.025 栅格)。""" out = [] for obs, g in ((obstacles, E.GRID), (obstacles, R.ESC_GRID), (bare, R.ESC_GRID)): r = R.breathes(q, None, obs, NEED_W, grid=g) out.append(bool(r is not None and r[2] is not None)) return tuple(out) def via_spot(q, obstacles, g=0.05, win=3.0): u"""从这个焊盘出发(本层 0.2 线能走的格),最近的**合法孔位**离焊盘多远。 返回 (距离, x, y) 或 None(3 mm 内一个合法孔位都摸不到)。""" from scipy import ndimage box = (q['x'] - win, q['y'] - win, q['x'] + win, q['y'] + win) f = E.Field(obstacles, box, skip_net=q['net'], grid=g) f.set_relax([E.sh_rect(cx, cy, hw, hh, R.pad_layers(q)) for cx, cy, hw, hh in R.pad_rects(q)]) lay = q['lay'] if q['lay'] != 12 else L_TOP ok = f.free(lay, NEED_W) i, j = f._idx(q['x'], q['y']) seed = np.zeros_like(ok) seed[j, i] = True reach = ndimage.binary_propagation(seed, mask=ok) cand = reach & f.via_ok() jj, ii = np.nonzero(cand) if not len(jj): return None xs = f.x0 + ii * f.g ys = f.y0 + jj * f.g d = np.hypot(xs - q['x'], ys - q['y']) k = int(np.argmin(d)) return (round(float(d[k]), 2), round(float(xs[k]), 2), round(float(ys[k]), 2)) # ══════════════════ 分类 ══════════════════ CH_PAGE = {'$2N': 1, '$8N': 2, '$10N': 3, '$12N': 4, '$14N': 5} def channel_of(net, frags): for k, n in CH_PAGE.items(): if net.startswith(k): return n return None def signature(frags): u"""把网络按「接了哪些件的哪些脚」归一化(去掉通道号),好跨五路对齐。""" sig = set() for f in frags: for q in f['pads']: ref = q['ref'] m = ref.rstrip('0123456789') digits = ref[len(m):] if len(digits) == 3: ref = m + '%' + digits[1:] sig.add(ref + '.' + q['num']) return tuple(sorted(sig)) def classify(net, frags, sigs_by_ch, end_escape): if net in ('USB_DP', 'USB_DM'): return 'B' if net in ('SDA', 'SCL', 'INA_ALERT'): return 'D' if any(not e[2] for e in end_escape.values()): return 'A' if channel_of(net, frags) is not None: return 'C' return 'E' # ══════════════════ 主流程 ══════════════════ def main(): argv = sys.argv[1:] want = None if '--net' in argv: want = set(a for a in argv[argv.index('--net') + 1:] if not a.startswith('--')) outjson = None if '--json' in argv: outjson = argv[argv.index('--json') + 1] pads, vias, lines = R.board(False) pours = R.pour_rects() obstacles = R.obstacles_of(pads, vias, lines, pours) # 「拆光自己人」的障碍表:只留焊盘 / 扇出 / 功率级 / 铺铜(按累计计划的归属表认) pall, pall_src = R.plan_all() lown, vown = R.own_of(pall, lines, vias, verbose=False) bare_l = [l for i, l in enumerate(lines) if not lown[i]] bare_v = [v for i, v in enumerate(vias) if not vown[i]] bare = R.obstacles_of(pads, bare_v, bare_l, pours) own_keys = [((E.sh_seg(l['x0'], l['y0'], l['x1'], l['y1'], l['w'], l['lay']), l['net']), lown[i]) for i, l in enumerate(lines) if lown[i]] own_keys += [((E.sh_via(v['x'], v['y'], v['r']), v['net']), vown[i]) for i, v in enumerate(vias) if vown[i]] print(u'布线器自己画的:%d 线 / %d 孔(归属表 %s);拆光它们之后剩 %d 线 / %d 孔' % ( len(lines) - len(bare_l), len(vias) - len(bare_v), pall_src, len(bare_l), len(bare_v))) pads_by_xy = {} for q in pads: pads_by_xy[(round(q['x'], 2), round(q['y'], 2))] = q['tag'] print(u'板上:%d 焊盘 / %d 过孔 / %d 走线(%s)' % ( len(pads), len(vias), len(lines), os.path.basename(R.SNAP))) js = R.jobs(pads, want, False, skip=()) total = sum(len(f) - 1 for _n, f in js) print(u'还欠:%d 个网络 / %d 条连接\n' % (len(js), total)) # 五路对齐:同一个签名在几个通道里都欠着 sigs_by_ch = {} for net, frags in js: ch = channel_of(net, frags) if ch is not None: sigs_by_ch.setdefault(signature(frags), set()).add(ch) report = [] for net, frags in js: wdef, wmin = R.wclass(net) relax = [E.sh_rect(cx, cy, hw, hh, R.pad_layers(q)) for q in pads if q['net'] == net for cx, cy, hw, hh in R.pad_rects(q)] # 只由铺铜构成的碎片(内层 GND 平面的某一块)没有任何几何可以当种子, # 布线器自己也连不上它 —— 单独列出来,不参与探路。 pour_only = [f for f in frags if not (f['pads'] or f['vias'] or f['lines'])] frags = [f for f in frags if f['pads'] or f['vias'] or f['lines']] if pour_only: print(u'━━ %s 有 %d 个**纯铺铜**碎片(内层平面被切开的块),布线器接不上,' u'要看 pcb1_pour / inner 平面' % (net, len(pour_only))) if len(frags) < 2: continue comps = [R.comp_of(f) for f in frags] for c, f in zip(comps, frags): c.frag = f # 和 route_net 一样:从最大的一坨起头,Prim 最近邻 comps.sort(key=lambda c: -len(c.shapes)) merged, rest = [comps[0]], comps[1:] # 两端焊盘出得出去? end_escape = {} for f in frags: if is_stubby(f): q = f['pads'][0] end_escape[q['tag']] = escape_status(q, obstacles, bare) cls = classify(net, frags, sigs_by_ch, end_escape) print(u'━━ %s [%s] %d 碎片,线宽 %.2f/%.2f' % (net, cls, len(frags), wdef, wmin)) bytag_all = {q['tag']: q for q in pads} for tag, (g1, g2, g3) in sorted(end_escape.items()): vs_full = via_spot(bytag_all[tag], obstacles) vs_bare = via_spot(bytag_all[tag], bare) print(u' 端点 %-9s 出线:全铜 0.1 栅格 %s / 0.025 栅格 %s / **拆光自己人** %s%s;' u'最近合法孔位:全铜 %s / 拆光自己人 %s' % (tag, u'通' if g1 else u'✘', u'通' if g2 else u'✘', u'通' if g3 else u'✘', u'' if g3 else u' ← 摆位几何上就出不去', (u'%.2f mm (%.2f,%.2f)' % vs_full) if vs_full else u'**3 mm 内没有**', (u'%.2f mm (%.2f,%.2f)' % vs_bare) if vs_bare else u'**3 mm 内没有**')) conns = [] while rest: best = None for m in merged: for t in rest: d = math.hypot(m.anchor[0] - t.anchor[0], m.anchor[1] - t.anchor[1]) if best is None or d < best[0]: best = (d, m, t) _d, m, t = best rest.remove(t) merged.append(t) gap = frag_gap(m.frag, t.frag) res = probe(net, m, t, obstacles, relax, wdef, wmin) res_bare = None if res is None or 'coarse' in res: res_bare = probe(net, m, t, bare, relax, wdef, wmin) row = dict(net=net, cls=cls, a=frag_desc(m.frag), b=frag_desc(t.frag), a_pt=[round(v, 2) for v in m.anchor], b_pt=[round(v, 2) for v in t.anchor], gap=round(gap[0], 2), gap_from=[round(v, 2) for v in gap[1]], gap_to=[round(v, 2) for v in gap[2]]) if res is not None and 'coarse' in res: row['verdict'] = u'窗太大' print(u' ? %s ⇢ %s 缝 %.2f mm:跨度太大,0.1 栅格放不下(要放粗到 %.2g),' u'不在这里判 —— 长网络的事交给 --ripup' % (row['a'], row['b'], gap[0], res['coarse'])) if res_bare is not None and 'coarse' not in res_bare: print(u' (拆光自己人之后 w%.2f / %.2g 栅格能通)' % (res_bare['w'], res_bare['grid'])) elif res is None and res_bare is not None and 'coarse' not in res_bare and res_bare['w'] >= wmin - 1e-9: row['verdict'] = u'被自己人挤死' hit = path_blockers(res_bare, obstacles, own_keys, net) row['rip'] = {k: sorted(v) for k, v in hit.items()} print(u' ◆ %s ⇢ %s 缝 %.2f mm:拆光布线器自己画的铜之后 w%.2f / %.2g 栅格就通' u' ⇒ **要拆的是**:%s' % (row['a'], row['b'], gap[0], res_bare['w'], res_bare['grid'], u';'.join(u'%s(%d 段)' % (k, len(v)) for k, v in sorted(hit.items())) or u'(路上没压到谁,是顺序 / 窗口的事)')) elif res is None: row['verdict'] = u'围死' # 两端 1.5 mm 内的异网铜 blk = {} for f in (m.frag, t.frag): for q in f['pads'][:2]: lay = q['lay'] if q['lay'] != 12 else L_TOP blk[q['tag']] = near(obstacles, q['x'], q['y'], lay, net, 1.5, pads_by_xy)[:8] row['blockers'] = blk print(u' ✘ %s ⇢ %s 缝 %.2f mm:**0.03 的线都过不去**' % (row['a'], row['b'], gap[0])) for tag, rows in blk.items(): print(u' %s 周围 1.5 mm:' % tag) for d_, n_, w_ in rows: print(u' %6.3f %-12s %s' % (d_, n_, w_)) else: e, bx, by, blay = bottleneck(res) need = CLR + NEED_W / 2.0 + res['field'].qerr row.update(grid=res['grid'], w=res['w'], bn=[round(bx, 2), round(by, 2), blay], bn_edt=round(e, 3), short=round(max(0.0, need - e), 3)) if res['w'] >= wmin - 1e-9 and res['grid'] >= 0.1 - 1e-9: row['verdict'] = u'可达' print(u' ○ %s ⇢ %s 缝 %.2f mm:0.1 栅格 w%.2f 就通 ⇒ 是布线顺序 / 拆线额度的事' % (row['a'], row['b'], gap[0], res['w'])) elif res['w'] >= wmin - 1e-9: row['verdict'] = u'量化' print(u' △ %s ⇢ %s 缝 %.2f mm:要 %.2g 栅格才通(w%.2f);' u'瓶颈 (%.2f,%.2f) L%d edt %.3f,按 0.1 栅格差 %.3f' % (row['a'], row['b'], gap[0], res['grid'], res['w'], bx, by, blay, e, max(0.0, CLR + NEED_W / 2 + E.QERR - e))) else: row['verdict'] = u'几何缺口' print(u' ✘ %s ⇢ %s 缝 %.2f mm:只有 w%.2f 过得去(%.2g 栅格);' u'瓶颈 (%.2f,%.2f) L%d edt %.3f,0.20 线要 %.3f ⇒ **差 %.3f mm**' % (row['a'], row['b'], gap[0], res['w'], res['grid'], bx, by, blay, e, need, need - e)) rows = near(obstacles, bx, by, blay, net, 1.0, pads_by_xy)[:6] row['bn_near'] = rows for d_, n_, w_ in rows: print(u' %6.3f %-12s %s' % (d_, n_, w_)) conns.append(row) report.extend(conns) print() # 汇总 print(u'\n════ 汇总 ════') by = {} for r in report: by.setdefault(r['cls'], []).append(r) for k in sorted(by): print(u' %s 类 %d 条:%s' % (k, len(by[k]), ' '.join(sorted(set(r['net'] for r in by[k]))))) if outjson: io.open(outjson, 'w', encoding='utf-8').write( json.dumps(report, ensure_ascii=False, indent=1)) print(u'-> %s' % outjson) return 0 if __name__ == '__main__': sys.exit(main())