# -*- coding: utf-8 -*- u"""R2 刀片 PCB 的**自建布线器**(M4c-4)。 /c/Python314/python .claude/r2/router.py # 只算 + 打印,不下发 /c/Python314/python .claude/r2/router.py --push # 下发导线 + 过孔 /c/Python314/python .claude/r2/router.py --clear # 清掉本脚本布的线(保留 GND 缝合过孔) ## 为什么要自己写 嘉立创 EDA 的自动布线在这台客户端上**跑不出一根铜**(方案 §24.6): `pcb_Document.autoRouting` 这个 API 根本不存在;GUI 的对话框能开能配, 点「运行」之后立刻关闭、铜线数 0,不报错不弹进度。⇒ 只能自己布。 ## 做法 **0.25 mm 网格 + 8 邻接 A\\***,两层(L1 顶 / L4 底),换层收过孔代价。 - 网格选 0.25 是因为**金手指 1.0 mm 间距正好落在格点上**(32.5 / 0.25 = 130), 64 根指的扇出不用做特殊处理。 - 障碍按「导线半宽 + 净距」膨胀 ⇒ 一条 0.2 mm 的线会把 0.25 外的相邻格点也占掉, 自动保证平行线中心距 ≥ 0.5 mm。**不用另外写间距检查**。 - 焊盘中心大多不在格点上(摆位是 0.001 精度),所以每个焊盘先出一小段 **引出脚**(pad 中心 → 最近的可用格点),再交给 A\\*。 ## ⛔ 三条硬约束(写死在代码里,不是注释) 1. **只准布 L1 / L4**。L2 是「完整地平面,不许割断」(§7.1),L3 是 `VO_P` / `+12V` 的岛。 2. **金手指区(y < 6.0)除了 J1 自己的引出脚,一根线都不许有** —— 那一带外层铜的邻居是 0.3 mm 间距的指,蹭上就是短路,而 DRC 只报间距不报"接错了指"。 3. **缺口(防呆键)四周 0.5 mm 内不许有铜**,缺口本身是铣穿的。 ## `GND` / `VO_P` / `+12V` 不走 A\\*,走铜面 - `GND`:顶/底/L2/L3 四块铜面 + 196 个缝合过孔已经接好;**只有 J1 的 39 根地指** 在铜面下沿(y=11.5)以下够不着,给它们各出一段引出脚 + 一个过孔。 - `VO_P`:L3 上有整块 `VO-L3` 岛(4 A 走平面,比走线好)。每个 `VO_P` 焊盘 出一小段引出脚 + 一个过孔扎进岛里。 - `+12V`:`V12-L3` 岛在功率列上半段,够不着金手指。金手指那五根 + F1 之间 用底层一条 0.8 mm 的走廊连过去,两端各一个过孔。 """ import heapq import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) # ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place), # 先插 .claude 再插 HERE 的话,`import pcb_pour` 会拿到 R1 那一份, # 而且**不报错**,只在调到 R2 专有的函数时才炸。 sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 import pcb_blade as B # noqa: E402 import fp_edge # noqa: E402 import pcb_pour as P # noqa: E402 import b4_edge # noqa: E402 针位表的唯一事实来源 PCB = B.PCB MIL = B.MIL PADS = os.path.join(HERE, '_blade_pads.json') G = 0.25 # 网格(mm) NX = int(round(B.BW / G)) + 1 NY = int(round(B.BH / G)) + 1 # 布线用的净距。设计规则是 0.15;这里留一点余量给网格吸附。 # ⚠ 0.18 -> 0.16:通道宽度 = 线宽 + 2×净距,0.2+0.36=0.56 在这块板上处处卡壳, # 0.15+0.32=0.47 才挤得过去。仍高于规则的 0.15。 CLR = 0.16 STUB_CLR = 0.15 # 引出脚(缩窄段)用规则值本身,见 pad_nodes EDGE_CLR = 0.35 # 到板边 / 缺口 # ⚠ M6(2026-09-09)0.6/0.3 -> 0.5/0.25:细间距扇出的过孔要能塞进 0.5 mm 节距的 # 引出脚之间(0.5 − 0.25 − 0.075 = 0.175 ≥ 0.15)。规则下限 0.3/0.2,嘉立创四层做得出。 VIA_D, VIA_HOLE = 0.6, 0.3 # `--viafan` 那一版用过 0.5/0.25,默认仍 0.6/0.3(与缓存一致) # 换层代价(格)。⚠ 2026-09-08 器件翻面之后这条注释反过来了: # 现在 **73 件在底层、顶层几乎全空**,换层代价定高了等于逼所有线挤在底层。 # 12 格 = 3 mm 还是太贵,降到 8(= 2 mm)。 VIA_COST = 8 # 焊盘周围的「过路费」:半径(mm)与代价(格)。见 build_grid 末尾的说明。 PAD_PEN_R = 0.55 PAD_PEN = 10 LAY = (1, 2) # 只准这两层 FINGER_KEEP_Y = 6.0 # 金手指区上沿:A* 不许进 # 线宽(mm)—— 和 pcb_rules.py 的规则表对齐 # 信号线宽。⚠ 0.2 -> 0.15:这块板的信号全是逻辑 / I²C,0.15 载流绰绰有余, # 而 0.05 的差在 0.5~0.65 mm 间距的封装周围就是「过得去」和「过不去」的分界。 # 规则里 PWR1A 的下限就是 0.15,嘉立创四层能做到 0.09。 W_DEFAULT = 0.15 WIDTH = {'+12V': 0.8, '+12V_F': 0.8, 'VO': 1.5, 'VO_P': 1.5, '+5V': 0.6, 'VBUS': 0.6, '+3V3': 0.4} WIDTH_AUTO = {'SW': 2.0, 'VO_F': 1.5} # 自动命名网,按 pcb_rules 的探针反查 # ⛔ **全局最优先布的网**(跨线宽档,比宽线还先)。 # `priority` 只在同一档宽度里排序,对「这一档整个被前面几档挤没了」无能为力; # `FIRST_NETS` 是真的第一个布。BOOT 当初就是靠它解决的。 # ⇒ 把**已知难布的**全放进来:难的先挑路,容易的绕着走。 FIRST_NETS = ( '$1N138', '$1N126', # BOOT / FB:Buck 的命门 # ⛔ M6(2026-09-09):这五个网的焊盘会被**自家邻脚的引出脚**封死 —— # U2.15 被 EFUSE_EN 的孔 (89.5,31) 卡到只剩 0.04 mm、U2.17 被 $1N158 的斜线拦、 # U2.2 被 $1N164 的孔拦;R16.1 / R8.2 四面是别的网的过孔。手工绕都绕不出来 # (+3V3 的 y=30.75 母线加缝合孔那一排把口袋封死)。只能让它们**最先**布。 # ⇒ 试过把它们排到最前面:这五个通了,但换来 20 处别的没通(EFUSE_EN/$1N164/ # $1N158 的 U2 脚、KEY_VUP/VDN 的 U4 脚反过来被封死)。**不是顺序问题,是 U2/U4 # 周围一共就那么几个出口**。默认不排前(留着这行注释别再试一遍)。 # ⚠ M7:`KEY_PWR/VUP/VDN` 三根挨着排最前面。它们接的是 `U4`(TSSOP-24) 北排 # **相邻的三只脚**(p4/p5/p6,0.65 mm 间距,不是 0.25 网格的整数倍), # 三根引出脚互相挤,谁后布谁出不来(实测「KEY_VUP: U4.5 没布通」)。 'KEY_PWR', 'KEY_VUP', 'KEY_VDN', 'VBUS_EN', 'EFUSE_EN', '$1N164', 'USB_DPJ', '$1N151', 'SCL', 'INT#', 'PRSNT#', '+3V3', 'VBUS', 'EFUSE_FLT', 'EFUSE_PG', 'SDA', 'INA_ALERT', 'BUCK_PG', 'USB_DP', 'USB_THERM', 'EDL_EN', '$1N147', '$1N158', '$1N173', '$1N136', ) PLANE_VIA_NETS = ('VO_P',) # 靠 L3 的岛成网,只需要每个焊盘一个过孔 ISLAND_NETS = ('VO_P', '+12V') # 连通性判据里不按「一整块铜」要求的网(靠 L3 岛) # ⛔ **Kelvin 取样**(方案 §7.2 第四条):INA226 的两个取样脚必须用**专线**落到 # `Rs1` 的两个焊盘上,不许「就近扎个孔进 VO_P 平面」—— # 走平面的话,取样点变成了平面上的某一处,量到的是「分流电阻 + 一段铜箔」的压降, # 电流读数会随铜箔温度漂,而**DRC / 连通性检查全过**,只有实测才发现。 # 所以这两条在所有网络之前单独布,布完再让 VO_P 的平面过孔接管大电流。 KELVIN = [(('U3', '9'), ('Rs1', '1'), 0.3), (('U3', '10'), ('Rs1', '2'), 0.3)] KELVIN_PADS = set() for _a, _b, _w in KELVIN: KELVIN_PADS.add(_a) KELVIN_PADS.add(_b) SKIP = ('GND',) # 地靠铜面,只单独处理 J1 的地指 # ⛔ **不按焊盘封顶线宽**的网。默认空(刀片一直是全封顶的)。 # 底板的七对 D± 必须保持 1.2 mm 的干线口径 —— 阻抗是按线宽算的(方案 §6.3), # 被 U32 那颗 0.5 mm 间距 QFN 的 0.30 焊盘封顶就等于把阻抗方案作废, # 而 DRC / 连通性 / 几何四道判据**一条都不会响**。缩颈交给 `stub_w`(只缩引出脚)。 NO_CAP = () def call(js, timeout=300): r = run(js, timeout=timeout) if not r.get('success'): print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:600]) sys.exit(1) return r['result'] def load_pads(): d = json.load(io.open(PADS, encoding='utf-8')) for p in d: if not p[4]: p[4] = 2.2 if not p[5]: p[5] = 2.2 return d # ── 网格 ────────────────────────────────────────────────────────────── def gx(x): return int(round(x / G)) def gy(y): return int(round(y / G)) def wx(i): return round(i * G, 4) def wy(j): return round(j * G, 4) class Grid(object): u"""两层占用图。每个格点记**一个字节的归属**:0=空闲,1..250=只被这个网占着,255=被两个以上的网占着。 ⛔ **第一版是「blk 位图 + own 集合」,判据写错了**: `free()` 里「本网占过」优先于「被别人占着」,而**膨胀区也算本网占过** —— 于是一个离本网焊盘 0.93 mm、同时**落在别人焊盘正中**的格点被判成可进, A* 就在 `U2` 的散热焊盘上打了个过孔。自算净距 −0.300,DRC 报「过孔到焊盘」。 换成「归属字节」之后,只要有第二个网碰过这个格点就永久变 255,谁也进不去。 两套:`blk/own` 按导线半宽膨胀,`blkv/ownv` 按过孔半径膨胀(换层判据)。 ⚠ 只有一套的话,A* 会在「线过得去、过孔过不去」的格点上换层, DRC 报「钻孔到焊盘」,而路径本身看着完全正常。 """ def __init__(self): self.own = {l: bytearray(NX * NY) for l in LAY} self.ownv = {l: bytearray(NX * NY) for l in LAY} # 「过路费」层:不封死,只是让 A* 别贴着焊盘走(见 build_grid 的 PAD_PEN) self.pen = {l: bytearray(NX * NY) for l in LAY} # 协商式拥塞(PathFinder)用:occ = 当前有几个网压在这一格,hist = 历史拥塞代价。 # 两者**跨宽度档共享**(它们只跟格点有关,跟线宽无关),所以是外面挂进来的引用。 self.occ = None self.hist = None self._id = {} def nid(self, net): u"""网名 -> 1..250 的归属号。超出就并到 250(当成同一个「其它」,偏保守)。""" if net is None: net = '#' v = self._id.get(net) if v is None: v = len(self._id) + 1 # ⛔ 饱和是**静默**的:溢出之后不同的网共用一个号, # 彼此判成「同一个网」,可以随便叠着走,而检查器之外没人会报。 if v > 250: raise RuntimeError(u'归属号超过 250 个(%d 个不同的网/障碍),' u'占用图会静默失效' % v) self._id[net] = v return v def idx(self, i, j): return j * NX + i def _mark(self, arr, l, x0, y0, x1, y1, k, exact=False): # ⚠ 边界默认用 floor/ceil,不用 round:round 最坏会**少封半格(0.125 mm)**, # 那正好是 1.5 mm 宽的两根线互相压 0.02~0.19 mm 的来源。 # # ⛔ 但 `exact=True`(**只给焊盘用**)必须按「格点中心落在框里才算被占」: # floor/ceil 最坏多封**一整格 0.25 mm**,而 0.65 mm 间距的 TSSOP # 邻脚之间本来就只剩 0.34 mm 的缝 —— 多封一格这条缝就没了, # 于是「空板上出口数 = 0」,报「U4.5 出不来」,看着像板子太挤。 # 焊盘框传进来时**已经膨胀了 halfw(半线宽 + 净距)**,所以 # 「中心在框外」= 线放这儿合法,不需要再留保守量。 # 走线 / 过孔 / 板边仍然保守 —— 那几类是沿途扫描的,少封半格会真的压上。 if exact: i0 = max(0, int(math.ceil(x0 / G - 1e-9))) i1 = min(NX - 1, int(math.floor(x1 / G + 1e-9))) j0 = max(0, int(math.ceil(y0 / G - 1e-9))) j1 = min(NY - 1, int(math.floor(y1 / G + 1e-9))) a = arr[l] for j in range(j0, j1 + 1): base = j * NX for i in range(i0, i1 + 1): o = base + i v = a[o] a[o] = k if v == 0 else (v if v == k else 255) return i0 = max(0, int(math.floor(x0 / G))) i1 = min(NX - 1, int(math.ceil(x1 / G))) j0 = max(0, int(math.floor(y0 / G))) j1 = min(NY - 1, int(math.ceil(y1 / G))) a = arr[l] for j in range(j0, j1 + 1): base = j * NX for i in range(i0, i1 + 1): cur = a[base + i] if cur == 0: a[base + i] = k elif cur != k: a[base + i] = 255 def block_rect(self, l, x0, y0, x1, y1, net=None, via=False): self._mark(self.ownv if via else self.own, l, x0, y0, x1, y1, self.nid(net)) def block_both(self, l, x0, y0, x1, y1, extra, net=None, exact=False): u"""同一个障碍,分别按导线口径和过孔口径打进两张图。""" k = self.nid(net) self._mark(self.own, l, x0, y0, x1, y1, k, exact) self._mark(self.ownv, l, x0 - extra, y0 - extra, x1 + extra, y1 + extra, k, exact) def penalize(self, l, x0, y0, x1, y1, val): u"""给一片格点加「过路费」(不是封死)。""" i0 = max(0, int(math.floor(x0 / G))) i1 = min(NX - 1, int(math.ceil(x1 / G))) j0 = max(0, int(math.floor(y0 / G))) j1 = min(NY - 1, int(math.ceil(y1 / G))) a = self.pen[l] for j in range(j0, j1 + 1): base = j * NX for i in range(i0, i1 + 1): o = base + i if a[o] < val: a[o] = val def reset_pen(self): for l in LAY: self.pen[l] = bytearray(NX * NY) def cost(self, l, i, j): o = j * NX + i c = self.pen[l][o] if self.occ is not None: c += PF_PRESENT[0] * self.occ[l][o] + PF_HIST_W * self.hist[l][o] return c def free(self, l, i, j, net): if i < 0 or j < 0 or i >= NX or j >= NY: return False v = self.own[l][self.idx(i, j)] return v == 0 or v == self.nid(net) def free_via(self, i, j, net): u"""两层都要按过孔口径让得开,才准在这里换层。""" if i < 0 or j < 0 or i >= NX or j >= NY: return False k = self.nid(net) for l in LAY: v = self.ownv[l][self.idx(i, j)] if v != 0 and v != k: return False return True def load_vias(): u"""读回板上已有的过孔(主要是 196 个 GND 缝合过孔)。 ⚠ **不读的话它们就不在占用图里**,A* 会直接从过孔身上压过去 —— DRC 报「导线到过孔」,而路径看上去完全正常。 """ return call("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2500));" "return (await eda.pcb_PrimitiveVia.getAll()||[])" " .map(v=>[v.x/%s, v.y/%s, String(v.net||''), (v.diameter||%s)/%s]);" % (PCB, MIL, MIL, VIA_D * MIL, MIL), timeout=300) def build_grid(pads, halfw, vias=()): u"""halfw = 本次布线的「线半宽 + 净距」,障碍按它膨胀; 同时按过孔口径(VIA_D/2 + CLR)再膨胀一份,供换层判据用。""" g = Grid() extra = max(0.0, VIA_D / 2.0 + CLR - halfw) # 板边(四条带)+ 金手指区,都归到 '#edge' 这个归属号上 for l in LAY: m = EDGE_CLR + halfw for (a, b, c, d) in ((-1, -1, B.BW + 1, m), (-1, B.BH - m, B.BW + 1, B.BH + 1), (-1, -1, m, B.BH + 1), (B.BW - m, -1, B.BW + 1, B.BH + 1), (-1, -1, B.BW + 1, FINGER_KEEP_Y)): g.block_both(l, a, b, c, d, extra, net='#edge') # 缺口 nx0, _a, nx1, ny1 = P.notch_cut() for l in LAY: g.block_both(l, nx0 - halfw, 0, nx1 + halfw, ny1 + halfw, extra) # 焊盘 for p in pads: ref, num, x, y, w, h, lay, net = p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7] if lay not in LAY: continue # ⛔ 只按**网**标归属,别按「网+位号+脚号」—— # 361 个焊盘会把 250 个归属号占满,之后所有网并到同一号上, # 互相判成「同一个网」,整块板的线就叠着走了(自算净距一次报 319 条)。 g.block_both(lay, x - w / 2 - halfw, y - h / 2 - halfw, x + w / 2 + halfw, y + h / 2 + halfw, extra, net=net, exact=True) # ⛔ 排线穿板槽:铣穿的内槽,两层都不许进(还要给铜到板边的净距) for (a, b, c2, d2) in B.mill_cuts(): k = EDGE_CLR + halfw for l in LAY: g.block_both(l, a - k, b - k, c2 + k, d2 + k, extra, net='#edge') # ⛔ 主板定位孔(NPTH):两层都不许压。孔壁到铜按 HOLE_D/2 + 净距。 for (hx, hy) in B.phone_holes().values(): r = B.HOLE_D / 2.0 + halfw + 0.15 for l in LAY: g.block_both(l, hx - r, hy - r, hx + r, hy + r, extra) # 已有过孔(通孔,两层都占) for (vx, vy, vnet, vd) in vias: r = vd / 2.0 + halfw for l in LAY: g.block_both(l, vx - r, vy - r, vx + r, vy + r, extra, net=vnet) # ⛔ **焊盘周围收「过路费」**。这是 28 处接不上的真正解法: # 把失败那一刻的占用图打出来看,每个「出不来」的脚周围**全是别的网的铜** —— # 先布的干线贴着焊盘走,顺手把后面那些脚的出口全占了。封死不行(有些线 # 本来就得从焊盘边过),所以改成加钱:贴着焊盘走要多花 PAD_PEN, # A* 于是优先绕开焊盘阵,把出口留给它们自己的脚。 pen_pads(g, pads, halfw) return g def pen_pads(g, pads, halfw): u"""给这些焊盘周围收「过路费」(先清空再打)。""" g.reset_pen() for p in pads: if p[6] not in LAY: continue r = PAD_PEN_R + halfw g.penalize(p[6], p[2] - p[4] / 2 - r, p[3] - p[5] / 2 - r, p[2] + p[4] / 2 + r, p[3] + p[5] / 2 + r, PAD_PEN) def block_track(g, l, x0, y0, x1, y1, halfw, net, extra=0.0): u"""把一段已布的线打进占用图(两张都打)。""" steps = int(max(abs(x1 - x0), abs(y1 - y0)) / (G / 2)) + 1 for k in range(steps + 1): t = k / float(steps) x, y = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t g.block_both(l, x - halfw, y - halfw, x + halfw, y + halfw, extra, net=net) _NB8 = [(1, 0, 1.0), (-1, 0, 1.0), (0, 1, 1.0), (0, -1, 1.0), (1, 1, 1.414), (1, -1, 1.414), (-1, 1, 1.414), (-1, -1, 1.414)] # 每层的**优选走线方向**:'v' = 纵向(沿 y),'h' = 横向(沿 x) LAYER_DIR = {2: 'v', 1: 'h'} # 2 = 底层 = 器件面(纵向);1 = 顶层(横向) CROSS_COST = 3.0 # 逆着优选方向走的倍率(斜线按分量算) def _along(l, di, dj): u"""这一步是不是顺着本层的优选方向。斜线两个分量各占一半,算「半顺」。""" d = LAYER_DIR.get(l) if d is None: return True if di and dj: # 斜线:一半顺、一半逆 return False return (dj != 0) if d == 'v' else (di != 0) def astar(g, starts, goals, net, halfw): u"""8 邻接 A*,两层。starts/goals 是 [(layer, i, j)]。返回 [(layer,i,j)] 路径。""" goalset = set(goals) if not goalset: return None gj = [(l, i, j) for (l, i, j) in goals] def h(n): _l, i, j = n return min(max(abs(i - a[1]), abs(j - a[2])) + 0.414 * min(abs(i - a[1]), abs(j - a[2])) for a in gj) open_ = [] gsc = {} came = {} for s in starts: if not g.free(s[0], s[1], s[2], net): continue gsc[s] = 0.0 heapq.heappush(open_, (h(s), s)) # ⛔ **两层正交**:器件面(底层 L4,73 件都在这儿)**纵向**、 # 另一面(顶层 L1,几乎空着)**横向**。2026-09-08 用户定的, # 和 R1「顶层横向、底层纵向」是同一条约定。 # 做法是**加钱不是封死** —— 逆着方向走要贵 `CROSS_COST` 倍, # 于是拐弯自然变成「换层拐」,两层互补,拥塞大幅下降; # 但引出脚那种非横即竖走不了的地方仍然出得去。 nb = {l: [(di, dj, c * (1.0 if _along(l, di, dj) else CROSS_COST)) for (di, dj, c) in _NB8] for l in LAY} seen = set() while open_: _f, cur = heapq.heappop(open_) if cur in seen: continue seen.add(cur) if cur in goalset: path = [cur] while path[-1] in came: path.append(came[path[-1]]) return path[::-1] l, i, j = cur base = gsc[cur] for di, dj, c in nb[l]: n = (l, i + di, j + dj) if n in seen or not g.free(l, i + di, j + dj, net): continue ng = base + c + g.cost(l, i + di, j + dj) if ng < gsc.get(n, 1e18): gsc[n] = ng came[n] = cur heapq.heappush(open_, (ng + h(n), n)) other = LAY[1] if l == LAY[0] else LAY[0] n = (other, i, j) if n not in seen and g.free(other, i, j, net) and g.free_via(i, j, net): ng = base + VIA_COST if ng < gsc.get(n, 1e18): gsc[n] = ng came[n] = cur heapq.heappush(open_, (ng + h(n), n)) return None def to_segments(path): u"""路径 -> [(layer, x0,y0, x1,y1)] + 过孔 [(x,y)],合并共线段。""" segs, vias = [], [] run_start = path[0] prev = path[0] for cur in path[1:]: if cur[0] != prev[0]: # 换层 if run_start != prev: segs.append((prev[0], wx(run_start[1]), wy(run_start[2]), wx(prev[1]), wy(prev[2]))) vias.append((wx(prev[1]), wy(prev[2]))) run_start = cur prev = cur continue d1 = (prev[1] - run_start[1], prev[2] - run_start[2]) d2 = (cur[1] - prev[1], cur[2] - prev[2]) if run_start != prev and (d1[0] * d2[1] - d1[1] * d2[0] != 0 or (d1[0] * d2[0] + d1[1] * d2[1]) <= 0): segs.append((prev[0], wx(run_start[1]), wy(run_start[2]), wx(prev[1]), wy(prev[2]))) run_start = prev prev = cur if run_start != prev: segs.append((prev[0], wx(run_start[1]), wy(run_start[2]), wx(prev[1]), wy(prev[2]))) return segs, vias # ── 焊盘引出脚 ──────────────────────────────────────────────────────── def pad_rect(p, inf=0.0): return (p[2] - p[4] / 2 - inf, p[3] - p[5] / 2 - inf, p[2] + p[4] / 2 + inf, p[3] + p[5] / 2 + inf) def seg_hits_rect(x0, y0, x1, y1, r): u"""线段是否与矩形相交(粗判:采样)。""" n = max(2, int(math.hypot(x1 - x0, y1 - y0) / 0.05)) for k in range(n + 1): t = k / float(n) x, y = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t if r[0] <= x <= r[2] and r[1] <= y <= r[3]: return True return False def pad_nodes(g, p, halfw, others, stub_half=None, gt=None): # ⚠ `others` 必须按 **CLR + 引出脚半宽** 膨胀过再传进来。第一版按 0.05 膨胀, # 于是引出脚可以贴着隔壁焊盘 0.05 mm 走过去 —— DRC「贴片焊盘到导线」一次报 158 条。 u"""这个焊盘可以起步的格点,按「离盘心近 + 引出脚不蹭别的盘」排序。""" lay, net = p[6], p[7] # ⛔ `others` 传进来的是**未膨胀**的焊盘框,在这里按**引出脚**的口径膨胀。 # 用干线口径(1.5 mm 线 → 0.93)膨胀的话,隔壁焊盘的膨胀区会把 # 0.5 mm 间距 QFN 的所有出口堵死 —— 报「出不来」,看着像拥塞, # 其实是拿干线的宽度去量那一小段缩窄的引出脚。 if stub_half is None: stub_half = W_DEFAULT / 2.0 # ⚠ 引出脚用**规则净距 0.15**,不用干线那个留了余量的 CLR=0.18。 # 那 0.03 的余量是给 A* 干线的;最后 0.5 mm 缩窄段上再留, # 0.35 mm 间距的连接器就一个出口都没有了。router_check 判的也是 0.15。 inf = STUB_CLR + stub_half # ⛔ **自身焊盘按「同一个矩形」排除,不能按「谁包住我的盘心」排除。** # 膨胀之后,0.35 mm 间距的 `J4` 隔壁脚的框正好把本脚盘心吞进去 # (50.01 那只脚膨胀后是 49.64~50.38,本脚盘心 49.66 在里面), # 于是**隔壁脚被当成自己跳过**,引出脚就贴着它走了 —— 净距 0.047, # 而 pad_nodes 全程不报错。这一条 2026-09-08 实测踩到两次(USB_THERM / CC1)。 own = pad_rect(p) others = [(r[0] - inf, r[1] - inf, r[2] + inf, r[3] + inf) for r in others if max(abs(r[0] - own[0]), abs(r[1] - own[1]), abs(r[2] - own[2]), abs(r[3] - own[3])) > 1e-9] for ring in range(0, 5): # 一圈找不到就往外扩,最多 4 圈(1 mm) out = _pad_nodes_ring(g, p, halfw + ring * G, others, lay, net, gt, stub_half) if out: return out return [] def _oth_inflated(p, others, stub_half): u"""把同层焊盘框按**引出脚口径**膨胀,并剔掉本盘自己那一个(按矩形本身,不是按包住盘心)。""" inf = STUB_CLR + stub_half own = pad_rect(p) return [(r[0] - inf, r[1] - inf, r[2] + inf, r[3] + inf) for r in others if max(abs(r[0] - own[0]), abs(r[1] - own[1]), abs(r[2] - own[2]), abs(r[3] - own[3])) > 1e-9] def _axis_exit(p, stub_half): u"""沿焊盘**长轴**向外的两个出口点(本盘自己的「车道」,两侧没有邻盘)。""" d = STUB_CLR + stub_half if p[5] >= p[4]: r = p[5] / 2.0 + d return [(p[2], p[3] + r), (p[2], p[3] - r)] r = p[4] / 2.0 + d return [(p[2] + r, p[3]), (p[2] - r, p[3])] def stub_path(p, X, Y, oth, stub_half, g=None, net=None): u"""盘心 → 落点的引出脚折线;走不通返回 None。 ⛔ **先直线,走不通再走 L 形**:`J4`(0.35 mm 间距、盘 0.18 宽)盘心到邻盘边 只有 0.26 mm,任何斜着出去的引出脚都会蹭上邻盘(实测 0.047 mm), 于是「一个出口都找不到」。但**沿着焊盘长轴直着出去**是安全的 —— 那是本盘自己的车道,两侧的邻盘在左右不在前后。出了盘阵再拐就没事了。 `U1`/`U2`/`U7` 那几只内侧脚同理。 ⛔ **候选还要过占用图那一关**(`g` / `net`,2026-09-09 M7 补): 这里原来只判「蹭没蹭到别的**焊盘**」,不判过孔和已布的铜 —— 于是 L 形的拐点可以正好贴在别人的过孔边上:实测 `U2.15`($1N147) 的引出脚 离 `EFUSE_EN` 的孔净距 **−0.204**、`U2.2`(EFUSE_PG) 离 `$1N164` 的孔同样, DRC 报「导线到过孔 0 mil」四条,而 A*、线与线、引出脚自己那三关全过。 """ def ok(pts, strict): for a, b in zip(pts, pts[1:]): if any(seg_hits_rect(a[0], a[1], b[0], b[1], r) for r in oth): return False if strict and g is not None and not _stub_clear( g, p[6], a[0], a[1], b[0], b[1], net if net is not None else p[7], pad_rect(p)): return False return True cand = [[(p[2], p[3]), (X, Y)]] for (ex, ey) in _axis_exit(p, stub_half): cand.append([(p[2], p[3]), (ex, ey), (X, Y)]) # ⛔ 占用图那一关只用来**排序**,不用来否决:拿它当硬条件的话, # 「落点挑得出来(`pad_nodes` 也用 `gt`)、引出脚却发不出来」, # 实测多报 3~4 个脚「一根铜都没有」——**丢一只脚比蹭 0.05 mm 严重得多**。 # ⇒ 先要「既不蹭盘也不蹭孔」的;没有的话退回老判据(只不蹭盘)。 for strict in (True, False): for pts in cand: if ok(pts, strict): return pts return None def stub_emit(p, X, Y, others, stub_half, w, net, g=None): u"""发引出脚的线段(1 段或 L 形 2 段)。判据和 `stub_path` 是同一个,不许各写一份。 ⚠ 宽度不能低于**规则下限**:引出脚缩窄是为了从细间距里挤出来,但 DRC 不认这个理由 (`SWNODE` 下限 0.3,而 `U1` 的 SW 脚只有 0.279,按脚缩就报「物理错误」)。 比焊盘宽一点点只是稍微出头,间距还够。 ⚠ `g` 传进来就顺带过一遍占用图(避过孔 / 已布的铜),不传就退回「只避焊盘」。 **真正落到板上的那一次调用一定要传**,见 `plan_all` 里的注释。 """ w = max(w, rule_min(net, AUTO_CACHE)) pts = stub_path(p, X, Y, _oth_inflated(p, others, stub_half), stub_half, g, net) if pts is None: pts = [(p[2], p[3]), (X, Y)] out = [] for a, b in zip(pts, pts[1:]): if math.hypot(b[0] - a[0], b[1] - a[1]) > 1e-6: out.append((p[6], a[0], a[1], b[0], b[1], w, net)) return out def _stub_clear(g, lay, px, py, qx, qy, net, own=None): u"""引出脚沿途的格点必须对本网可进。 ⛔ `own` = 本焊盘自己的框:**落在自己盘里的采样点要跳过**。 引出脚从盘心出发,而盘心那一格常常被隔壁那根线的避让区盖住 (`U1.13` 旁边 0.5 mm 就是 2 mm 宽的 SW 直连段)—— 于是**第一个采样点** 就判死,报「出不来」,而它左边明明整片是空的。 焊盘里本来就有铜,这一段不该参与净距判断。 """ n = max(2, int(math.hypot(qx - px, qy - py) / (G / 2.0))) for k in range(n + 1): t = k / float(n) x, y = px + (qx - px) * t, py + (qy - py) * t if own is not None and own[0] <= x <= own[2] and own[1] <= y <= own[3]: continue if not g.free(lay, gx(x), gy(y), net): return False return True def _pad_nodes_ring(g, p, halfw, others, lay, net, gt=None, stub_half=0.1): x0, y0, x1, y1 = pad_rect(p, halfw) out = [] for j in range(max(0, gy(y0)), min(NY - 1, gy(y1)) + 1): for i in range(max(0, gx(x0)), min(NX - 1, gx(x1)) + 1): if not g.free(lay, i, j, net): continue X, Y = wx(i), wy(j) # ⚠ 自己的焊盘框已经在 pad_nodes 里**按矩形本身**剔掉了。 # 直线走不通还要试 L 形(见 stub_path),否则细间距的脚全报「出不来」。 if stub_path(p, X, Y, others, stub_half) is None: continue # ⛔ 引出脚还要躲**已经布好的铜**。只查焊盘的话,引出脚会从先布的 # 导线/过孔身上压过去(自算净距报 `USB_DP` 的指脚 vs `VBUS` −0.097)。 # ⚠ 只拿**已布的铜**那张图查,不能拿主占用图查: # 细间距器件上相邻焊盘的膨胀区互相重叠,格点会变成 # 255(多网占用),连自己的引出脚都进不去,整网报「出不来」。 # 焊盘那一层已经由上面的 `others` 按引出脚口径管过了。 if gt is not None and not _stub_clear(gt, lay, p[2], p[3], X, Y, net, pad_rect(p)): continue out.append((math.hypot(X - p[2], Y - p[3]), (lay, i, j))) out.sort() return [n for _d, n in out] def stub_w(p, width): u"""引出脚宽度按焊盘尺寸封顶。 ⛔ 不封顶的话,`SWNODE` 2.0 mm 的线要接进 `U1`(0.5 mm 间距 QFN)和 `C5`(0402), 引出脚直接盖住隔壁两个脚 —— 自算净距 −0.73,DRC 报「贴片焊盘到导线」。 干线仍然是全宽(A* 的路径离别人的焊盘有 halfw 的余量),**只有最后接进焊盘 那一小段缩窄**,这也是手工布板的做法。 ⚠ 下限 **0.15,不是 0.2**:`J4`(BM24-50DS)下排 0.35 mm 间距、焊盘 0.18 宽, 盘心到隔壁盘边只有 0.26 mm。要 0.15 的净距,引出脚半宽就得 ≤ 0.11 —— 0.2 宽的脚(半宽 0.1 + 规则 0.15 = 0.25)**只剩 0.01 余量**, grid 一吸附就蹭上(实测 USB_THERM 对 J4.16 只有 0.047)。 0.15 宽 ⇒ 0.15 + 0.075 = 0.225,余 0.035。嘉立创四层线宽下限 0.09,做得出来。 """ return round(max(0.15, min(width, p[4], p[5])), 3) # 「已经被铜连在一起」的虚拟落点:位号是这些的,`route_net` 把它们**全部当种子** # (多源 Prim),彼此之间不布线。目前只有底板的 `+3V3` 槽墙(方案 R2 §30)。 PRE_WIRED = ('WALL',) def _is_launch(p): u"""扇出/金手指扇出留下的**虚拟焊盘**(落点)。""" return str(p[1]).startswith('fan:') or p[1] in ('launch', 'wall') def route_net(g, net, pads_of_net, halfw, width, others_by_layer, log, gt=None, mutate=True): u"""对一个网络做 Prim:逐个把还没连上的焊盘接进来。 ⚠ **落点(虚拟焊盘)不做「引出脚别蹭邻盘」那道检查**:落点就在焊盘阵的边上, 邻盘膨胀后的框必然把它包住,于是每个候选格点都被判死 —— 报「出不来」, 而它明明已经是扇出阶段验证过的合法落点了。 """ def _oth(p): return [] if _is_launch(p) else others_by_layer.get(p[6], []) segs, vias, stubs = [], [], [] if len(pads_of_net) < 2: return segs, vias, stubs todo = list(pads_of_net) # 种子挑**出口最多**的那个焊盘(最自由),不是第一个碰到的 cand = [] for k, p in enumerate(todo): ns = pad_nodes(g, p, halfw, _oth(p), stub_w(p, width) / 2.0, gt) if ns: cand.append((len(ns), k, ns)) if not cand: log.append(u'%s: 一个焊盘都出不来' % net) return segs, vias, stubs cand.sort(reverse=True) _n, seed, starts = cand[0] seedpad = todo.pop(seed) # ⛔ **种子的引出脚必须画到 A* 真正起步的那个出口上**,不能画到 `starts[0]`。 # 下面 `src` 放的是种子的**全部**出口(那是对的,只放一个会自锁); # 可引出脚只画一条 —— A* 从别的出口起步时,铜就和焊盘**断开**了, # 而布线器只看「路找到没有」,**一个字都不会报**。 # `KPAD_PWR` 就是这么断的:K1 的引出脚停在 (7.75,34.25), # 干线却从 (8.75,33.25) 起步,中间 1.4 mm 是空的。 # ⇒ 改成**惰性**:谁真被用上了,就给谁补一条引出脚。 seed_nodes = set(starts) stubbed = set() def note_seed(node): if node not in seed_nodes or node in stubbed: return stubbed.add(node) stubs.append((seedpad, node, stub_w(seedpad, width))) if mutate: for (_l, ax, ay, bx, by, sw, _n) in stub_emit( seedpad, wx(node[1]), wy(node[2]), _oth(seedpad), stub_w(seedpad, width) / 2.0, stub_w(seedpad, width), net): block_track(g, seedpad[6], ax, ay, bx, by, halfw + sw / 2.0, net) # ⛔ 种子集合要放**全部出口**,不能只放第一个。 # 只放一个的话,后面每个焊盘都得 A* 到那一个格点;它一旦被先布的线围住, # 整个网就全线报「没布通」(`+3V3` 14 个脚里 11 个失败就是这么来的), # 而看上去像是板子太挤。 src = set(starts) # ⛔ **多源种子**:`PRE_WIRED` 的落点落在**同一块已经存在的铜**上(3V3 槽墙那种), # 它们彼此之间不需要一根线。全部一次性进 `src` 并从 `todo` 里摘掉。 # 不这么做的话,Prim 会把「落点」当普通节点,逼 A* 在铜上再布一条线把它们串起来 # —— 实测 `+3V3` 因此从最北那个落点一路蜿蜒到板子最南(100 mm,横穿五条走廊, # 正好压在 I²C 的出脚道上),而**布线器只报「布通了」**。 # ⇒ 一只脚就近接到墙上,这才是「铜墙」的本意。 for p in list(todo): if p[0] in PRE_WIRED: for n in pad_nodes(g, p, halfw, _oth(p), stub_w(p, width) / 2.0, gt): src.add(n) todo.remove(p) todo.sort(key=lambda q: math.hypot(q[2] - seedpad[2], q[3] - seedpad[3])) done, fail = set(), {} # 走两遍:第二遍时 src 已经长大,第一遍够不着的往往就通了 for p in todo + [q for q in todo]: if id(p) in done: continue goals = pad_nodes(g, p, halfw, _oth(p), stub_w(p, width) / 2.0, gt) if not goals: fail[id(p)] = u'%s: %s.%s 出不来' % (net, p[0], p[1]) continue path = astar(g, list(src), goals[:24], net, halfw) if not path: fail[id(p)] = u'%s: %s.%s 没布通' % (net, p[0], p[1]) continue s, v = to_segments(path) note_seed(path[0]) # 这条路真正用的是哪个种子出口 segs += [(l, a, b, c, d, width, net) for (l, a, b, c, d) in s] vias += [(x, y, net) for (x, y) in v] stubs.append((p, path[-1], stub_w(p, width))) # ⚠ 回填占用图要按 **halfw + 本线半宽**,不能只按 halfw: # halfw 已经含了「后来那根线的半宽 + 净距」,但**这根线自己也有宽度**。 # 只按 halfw 填的话,同一档宽度里两根 1.5 mm 的线会被排到中心距 1.5, # 实际净距 0 —— DRC 一口气报出 598 条,全是宽线互相压。 keep = halfw + width / 2.0 done.add(id(p)) for n in path: src.add(n) if mutate: g.block_rect(n[0], wx(n[1]) - keep, wy(n[2]) - keep, wx(n[1]) + keep, wy(n[2]) + keep, net=net) for (l, a, b, c, d) in s: if mutate: block_track(g, l, a, b, c, d, keep, net, extra=max(0.0, VIA_D / 2.0 + CLR - keep)) if gt is not None: block_track(gt, l, a, b, c, d, CLR + W_DEFAULT / 2.0 + width / 2.0, net) for (vx, vy) in v: # 自己刚打的过孔 # ⛔ 净距那一项不能漏:`VIA_D/2 + halfw` 只保证「铜刚好挨上」, # 后布的线就能贴到过孔边(DRC:导线到过孔 0 mil,实测 12 条)。 r = VIA_D / 2.0 + CLR + halfw if mutate: for l2 in LAY: g.block_both(l2, vx - r, vy - r, vx + r, vy + r, max(0.0, VIA_D / 2.0 + CLR - halfw), net=net) # ⛔ **刚打的过孔也要进 `gt`(引出脚校验图)**。`gt` 是每一轮开头建一次的, # 只含开轮时已有的过孔 —— 同一轮里先布的网打的孔,对后布的网的引出脚 # 就是隐形的。实测两处:`U2.15`($1N147) 的引出脚贴到 `EFUSE_EN` 的孔上 # (净距 −0.204),`U2.2`(EFUSE_PG) 贴到 `$1N164` 的孔上。 # 线与线、A* 那几关全过,只有 DRC 的「导线到过孔」报得出来。 if gt is not None: rt = VIA_D / 2.0 + CLR + W_DEFAULT / 2.0 for l2 in LAY: gt.block_rect(l2, vx - rt, vy - rt, vx + rt, vy + rt, net=net) # 引出脚(盘心 -> 第一个格点,可能是 L 形)也要占位, # 否则后面的线会从它身上压过去 if mutate: for (_l, ax, ay, bx, by, sw, _n) in stub_emit( p, wx(path[-1][1]), wy(path[-1][2]), _oth(p), stub_w(p, width) / 2.0, stub_w(p, width), net, g): block_track(g, p[6], ax, ay, bx, by, halfw + sw / 2.0, net) # ⛔ **失败必须往外报**。这个 `fail` 字典以前收完就扔了 —— 于是 # 「某个脚没布通」全程静默:网上只剩一小段引出脚(`BUCK_PG` 0.02 mm), # 脚本打印「0 条没搞定」,`getNetLength()` 也非 0(有那一小段), # 只有 EasyEDA 的连接性 DRC 会报,而那一支平时被当成「还没布线」忽略掉。 # 2026-09-08 就是这么漏掉 20 多个连接的。 for p in todo: if id(p) in done: continue log.append(fail.get(id(p), u'%s: %s.%s 没接上' % (net, p[0], p[1]))) return segs, vias, stubs # ── J1 金手指扇出(不走 A*,几何是确定的)─────────────────────────────── BUS_Y = {1: 7.0, 2: 8.2} # 顶层地母线 / 底层地母线(错开,过孔互不挡) LAUNCH_Y = 11.75 # 信号指爬到这一行,交给 A*(要落在 0.25 网格上) STUB_Y0 = 4.0 # 引出脚从指的中段起(压在焊盘上) BUS_W, STUB_W = 0.4, 0.2 FINGER_W = {'+12V': 0.35, '+5V': 0.3} def finger_geom(): u"""返回 (pin -> (x, layer, net))。x 直接用封装的事实来源。""" out = {} # ⚠ 针位表在 `b4_edge.PIN_NET`(原理图那一份),几何在 `fp_edge`(封装那一份)。 # 两份各是各的事实来源,**不许在这里抄第三份**。 for pin, net in b4_edge.PIN_NET.items(): row = pin[0] out[pin] = (round(B.J1_X + fp_edge.PIN_X[pin], 4), fp_edge.LAYER[row], net) return out def gnd_runs(fg): u"""每一排里连续的地指分段。""" runs = {} for row, lay in (('A', 2), ('B', 1)): cur = [] for n in range(1, 33): pin = '%s%d' % (row, n) if fg[pin][2] == 'GND': cur.append(pin) else: if cur: runs.setdefault(lay, []).append(cur) cur = [] if cur: runs.setdefault(lay, []).append(cur) return runs def finger_escape(): u"""地指走母线 + 过孔;信号指直上到 LAUNCH_Y。 ⚠ **过孔只能打在「另一排同 x 是地或悬空」的位置**:过孔是通孔, 打在信号指正上方会把另一层那根信号的引出脚堵死。 这一条不写进代码的话,`B14/B15`(USB D±)和 `A16`(VDAC)会被地过孔挡住, 而 DRC 只会报「这根线没布通」,指不到根因。 """ fg = finger_geom() other = {} for n in range(1, 33): other['A%d' % n] = fg['B%d' % n][2] other['B%d' % n] = fg['A%d' % n][2] segs, vias, launch = [], [], {} # 地指 for lay, runs in sorted(gnd_runs(fg).items()): y = BUS_Y[lay] for run in runs: xs = [fg[p][0] for p in run] x0, x1 = min(xs), max(xs) ok = [fg[p][0] for p in run if other[p] in ('GND', 'NC')] if not ok: # 整段都被对排的信号占着 -> 母线往外伸 1 mm x1 = x1 + 1.0 ok = [x1] segs.append((lay, x0, y, x1, y, BUS_W, 'GND')) for p in run: segs.append((lay, fg[p][0], STUB_Y0, fg[p][0], y, STUB_W, 'GND')) for k, vx in enumerate(ok): if k % 2 == 0 or len(ok) <= 2: # 每隔一根打一个,够用 vias.append((vx, y, 'GND')) # 信号指:爬到 LAUNCH_Y 之后**立刻换到空的那一面**。 # ⛔ M5 时代这句是「换到**底层**」,理由是「83 件全在 A 面,底层是空的」—— # **M6 把 83 件全翻到了 B 面(底层)之后这句就反了**,而注释没跟着改: # 于是 19 根金手指信号全被灌进器件面,跟 78 个件的焊盘抢那几条通道, # A* 布不出来就退化成「直连段」——直接一条铜横穿金手指区, # 压在 16 根地指的引出脚上(DRC:`PRSNT#` 到 `GND` 净距 0,16 条)。 # ⚠ **试过反过来(换到顶层):更差**,20 条没搞定(原来 6 条)—— # 78 个件的焊盘全在底层,信号换到顶层之后每根线都要再换回来, # 每根多两个过孔,扇出区先堵死。⇒ 仍然换到底层, # `PRSNT#` 那条横穿金手指区的铜另找原因(它是 A* 失败后的退化直连)。 # ⚠ 例外:同一个 x 上 A/B 两排都是**不同网的信号**时不能换 —— # 通孔会把另一排那根线在底层的去路堵死。这样的位置只有两个 # (1 号针 A1=PRSNT# / B1=+12V,11 号针 A11=EFUSE_EN / B11=INT#), # 让 B 排那根留在顶层,交给 A* 自己想办法。 sig = lambda n: n not in ('GND', 'NC') for pin, (x, lay, net) in sorted(fg.items()): if not sig(net): continue w = FINGER_W.get(net, STUB_W) segs.append((lay, x, STUB_Y0, x, LAUNCH_Y, w, net)) onet = other[pin] clash = sig(onet) and onet != net if lay == 1 and not clash: vias.append((x, LAUNCH_Y, net)) launch.setdefault(net, []).append((2, x, LAUNCH_Y)) else: launch.setdefault(net, []).append((lay, x, LAUNCH_Y)) return segs, vias, launch # ── 主流程 ──────────────────────────────────────────────────────────── def rule_width(net, auto): if net in WIDTH: return WIDTH[net] for k, n in auto.items(): if n == net: return WIDTH_AUTO.get(k, W_DEFAULT) return W_DEFAULT # 各线宽规则的**下限**(和 pcb_rules.TRACK_RULES 的 strokeWidthMin 对齐)。 # ⛔ 低于下限 DRC 报「物理错误:线宽 x mil,应该在 y ~ z」—— 直连段按焊盘取宽 # (`U1` 的 SW 脚只有 0.279)和过孔合并的连接段(固定 0.2)都会踩到。 RULE_MIN = {'PWR12V': 0.25, 'SWNODE': 0.30, 'PWR4A': 0.30, 'PWR5V': 0.20, 'PWR1A': 0.15} def rule_min(net, auto): u"""这个网的线宽下限。查不到就按默认 0.15。""" import pcb_rules as R cls = R.NET_TRACK.get(net) if cls is None: for name, (c, probe, chk) in R.AUTO_TRACK.items(): if auto.get(name) == net: cls = c break return RULE_MIN.get(cls, 0.15) def width_of(net, auto, pads_of_net=()): u"""线宽 = 规则宽度,但**被这个网最小的焊盘封顶**。 ⛔ 不封顶的后果不是「线太粗」,是**根本出不来**:找出口那张占用图是按干线宽度 建的,1.5 mm 的网去接 `U2`(0.5 mm 间距 WQFN,焊盘 0.3×0.9)时, 隔壁焊盘按 0.93 mm 膨胀,把所有出口全堵死 —— 报的是「出不来」不是「没布通」, 一眼看上去像拥塞,其实是自己把自己关在焊盘里了。 这块板上这么封顶不损失载流:几个宽网要么本身就是**多脚并联的短连接** (`$1N165` 是 eFuse 五个 OUT 脚 → `Rs1`,每脚 0.8 A,跨度 4 mm), 要么走的是 L3 的铜面(`VO_P` / `+12V`)。 """ # ⚠ 2026-09-08:**撤掉了按焊盘封顶**。那是绕开「出不来」的权宜之计, # 代价是把 4 A 的 `SWNODE`/`PWR4A` 干路压到 0.48 mm(外层 1 oz 只带 ~2.2 A), # 拿载流换布通率,不能这么换。真正的根因在 `pad_nodes` 的 `others` 膨胀口径, # 已经修好;干线保持规则宽度,只有**引出脚**按焊盘缩窄(`stub_w`)。 return rule_width(net, auto) def route_groups(bypad, pads, segs, vias, board_vias, auto, priority=(), only=None): u"""按线宽分档把 `bypad` 里的网布完;`only` 给拆线重布用(只重布这几个网)。 直接**原地**往 `segs` / `vias` 里追加,返回没搞定的日志。 """ logs = [] # 其余按线宽分档,宽的先布 groups = {} capped = [] for net, ps in bypad.items(): if only is not None and net not in only: continue w = width_of(net, auto, ps) # ⚠ 直连之后**才**按焊盘封顶:4 A 的干线已经由直连段承担(宽度=焊盘本身, # IC 的脚有多宽就有多宽,再宽也没意义),剩下要 A* 的都是小脚上的支路 # (BOOT 电容、FB 抽头)。不封顶的话,2 mm 的口径在 0.5 mm 间距的 # QFN 上一个出口都找不到 —— 报「出不来」,看着像拥塞。 small = w if net in NO_CAP else min([min(q[4], q[5]) for q in ps if q[0] != 'J1'] or [w]) # ⚠ 封顶不能低于**规则下限**,否则 DRC 报「物理错误:线宽不足」。 # `SWNODE` 下限 0.3,而 `U1` 的 SW 脚只有 0.279 —— 按脚封顶就踩线了。 w = max(W_DEFAULT, rule_min(net, auto), min(w, small)) if w < rule_width(net, auto) - 1e-9: capped.append(u'%s %.2f→%.2f' % (net, rule_width(net, auto), w)) groups.setdefault(w, []).append(net) if capped: print(u'线宽被焊盘封顶: ' + ' '.join(capped)) for w in sorted(groups, reverse=True): halfw = w / 2.0 + CLR g = build_grid(pads, halfw, board_vias) # ⚠ 已布的铜要**按它自己的网名**打进占用图,不能统一记成 '#done': # 记成 '#done' 的话,一个网自己的金手指引出脚会把自己的落脚点堵死, # 现象是「J1.launch 出不来」——,而且看上去像是拥塞,其实是自锁。 for (l, a, b, c, d, ww, nn) in segs: # 已布的铜 # ⛔ `extra` 不能用默认的 0:`block_both` 的第二张图(`ownv`)是**换层判据**, # 不给额外膨胀的话,别人的过孔可以直接落在这根线上 —— # DRC 报「导线到过孔 0 mil」,实测两处(`EFUSE_EN` 的孔压 `$1N147`、 # `$1N164` 的孔压 `EFUSE_PG`),而线与线那一支干干净净,很难看出来是谁的错。 block_track(g, l, a, b, c, d, halfw + ww / 2.0, nn, extra=max(0.0, VIA_D / 2.0 + CLR - (halfw + ww / 2.0))) for (vx, vy, nn) in vias: # ⛔ 同上:少了 `CLR` 的话,后布的线会贴着别人的过孔走。 rv = VIA_D / 2.0 + CLR + halfw for l in LAY: g.block_rect(l, vx - rv, vy - rv, vx + rv, vy + rv, net=nn) # 只装已布铜的占用图,供引出脚校验用(按引出脚口径 + 那根线自己的半宽膨胀) gt = Grid() stub_half = W_DEFAULT / 2.0 for (l, a, b, c, d, ww, nn) in segs: block_track(gt, l, a, b, c, d, CLR + stub_half + ww / 2.0, nn) for (vx, vy, nn) in vias: r = VIA_D / 2.0 + CLR + stub_half for l in LAY: gt.block_rect(l, vx - r, vy - r, vx + r, vy + r, net=nn) others = {1: [], 2: []} for p in pads: if p[6] in others: others[p[6]].append(pad_rect(p)) # 跨度大的先布:短线随便绕,长线一旦被占住走廊就真的没路了 def span(n): ps = bypad[n] return -(max(q[2] for q in ps) - min(q[2] for q in ps) + max(q[3] for q in ps) - min(q[3] for q in ps)) # ⛔ 但**排在跨度前面的是「引出脚有多少条路」**。 # 只按跨度排,细间距 IC 的**内侧引脚**必然吃亏:邻脚是长线、先布, # 两边一占,内侧那只脚就再也出不来了。实测现象是 # 「INA_ALERT: 一个焊盘都出不来」(U4 TSSOP-24 底排第 15 脚 + U3 MSOP-10 第 3 脚), # 看着像拥塞,其实是**顺序**问题 —— 同样的摆位,先布它就通。 # 判据用「在这一档刚建好的占用图上,这个网最紧的那只脚有几个可起步格点」, # 少的先布。空 = 0,直接排最前面。 def escapes(n): m = 99 for q in bypad[n]: if q[0] == 'J1': continue m = min(m, len(pad_nodes(g, q, halfw, others.get(q[6], []), w / 2.0, gt))) return m seq = sorted(groups[w], key=lambda n: (0 if n in priority else 1, escapes(n), span(n), n)) # ⛔ **过路费只对「还没布的脚」收**。静态收费(所有焊盘一视同仁)已经把 # 28 条降到 16 条;但布完的网还继续收费,等于白白把已经用不着的出口 # 也围起来,后面的线被迫绕远、反而去挤别人的出口。 # 改成每布一个网之前重算一次:**只围还没轮到的那些脚**。 todo_nets = list(seq) + [n for ww2 in groups if ww2 != w for n in groups[ww2]] for k, net in enumerate(seq): left = set(todo_nets[k:]) pen_pads(g, [q for nn in left for q in bypad.get(nn, []) if q[0] != 'J1'], halfw) s, v, st = route_net(g, net, bypad[net], halfw, w, others, logs, gt) segs += s vias += v for (q, node, ww) in st: # ⚠ 这里发的才是**真正落到板上的**引出脚(上面那两处只是往占用图里占位)。 # `g` 一定要传:不传就退回「只避焊盘」的老判据, # L 形拐点会贴到别人的过孔上(见 stub_path 的说明)。 # ⚠ 传的是 `gt`(引出脚口径的占用图)不是 `g`(干线口径): # `pad_nodes` 挑落点时用的就是 `gt`,两边判据必须**同一张图** —— # 用 `g` 更严,会出现「落点挑得出来、引出脚发不出来」, # 实测多报 4 个脚「一根铜都没有」(U2.2 / U2.15 / U2.20 / C9.1)。 segs += stub_emit(q, wx(node[1]), wy(node[2]), [] if _is_launch(q) else others.get(q[6], []), ww / 2.0, ww, net, gt) return logs # 拆线半径(mm)。⚠ 2.5 太小:实测挡住失败焊盘的网**不都在它旁边** —— # 空占用图上这些脚全都出得来,一旦别的网从三五毫米外压过来就出不来了。 RIP_R = 6.0 RIP_MAX = 15 # 一次最多拆几个网 RIP_ROUNDS = 4 def _seg_pt_dist(sg, x, y): return _pt_seg(x, y, sg[1], sg[2], sg[3], sg[4]) def _pt_seg(px, py, ax, ay, bx, by): dx, dy = bx - ax, by - ay L = dx * dx + dy * dy t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L)) return math.hypot(px - (ax + t * dx), py - (ay + t * dy)) def _msg_net(m): u"""从日志里取网名。⚠ 连通性那几条带 `[连通性] ` 前缀,不剥掉的话 优先队列里塞的是「[连通性] +12V_F」这种鬼名字,等于**这些网从来没被重试过**。""" if m.startswith(u'[连通性] '): m = m[len(u'[连通性] '):] return m.split(':', 1)[0].strip() def repair(bypad, pads, segs, vias, logs, board_vias, auto, fixed_s=0, fixed_v=0): u"""**拆线重布**:把挡在失败焊盘出口附近的几个网拆掉,先布失败的那个,再把拆掉的补回去。 ⛔ 到这一步为止试过的所有招(顺序、扇出、过路费、L 形引出脚、正交方向)都 只能把失败数从 42 压到 13 —— 因为**剩下的全是「轮到它时出口已经被占了」**, 而这个布线器没有回退。把占位的那几根拆掉重来,是唯一还剩的办法。 判据是**总数变少才接受**:重布之后如果失败反而更多,整批回滚。 """ padmap = {} for q in pads: padmap[(q[0], str(q[1]))] = q for rnd in range(RIP_ROUNDS): if not logs: break before = len(logs) for msg in list(logs): if msg not in logs: continue net = _msg_net(msg) if net not in bypad: continue q = None try: ref_pin = msg.split(':', 1)[1].strip().split()[0] q = padmap.get(tuple(ref_pin.split('.'))) except Exception: q = None if q is None: # 「铜分成 N 块」这种没指名焊盘:拿这个网自己的焊盘当中心 cand2 = [x for x in bypad[net] if x[0] != 'J1'] if not cand2: continue q = cand2[0] near = {} for sg in segs: if sg[6] == net or sg[6] in SKIP or sg[6] not in bypad: continue d = _seg_pt_dist(sg, q[2], q[3]) if d < RIP_R: near[sg[6]] = min(near.get(sg[6], 99), d) victims = [n for _d, n in sorted((d, n) for n, d in near.items())][:RIP_MAX] if not victims: continue redo = set(victims) | {net} # ⛔ **前 fixed_s / fixed_v 条是前置阶段(Kelvin / VO_P 过孔 / 直连)的铜, # 一根都不许拆** —— `route_groups` 只跑 A*,拆了它**重建不回来**, # 而且拆掉的那些焊盘早已从 `bypad` 里摘掉了,于是**失败计数看不出来**: # 板子断了,脚本还报「好了」。`$1N138` 只剩一段 0.06 mm 的残桩就是这么来的。 keep_s = segs[:fixed_s] + [sg for sg in segs[fixed_s:] if sg[6] not in redo] keep_v = vias[:fixed_v] + [v for v in vias[fixed_v:] if v[2] not in redo] keep_logs = [m for m in logs if _msg_net(m) not in redo] ns, nv = list(keep_s), list(keep_v) new_logs = route_groups(bypad, pads, ns, nv, board_vias, auto, priority=(net,), only=redo) # ⚠ 重布完要**按几何**再查一遍:A* 说「布通了」不等于铜真的连上 # (见 route_net 里种子引出脚那条注释)。 for m2 in check_connected(pads, ns, nv): if _msg_net(m2) in redo: new_logs.append(u'[连通性] ' + m2) # ⚠ **打平也接受**:不接受打平就会一直卡在同一个平台上 # (12~16 来回跳)。换一组同样多的失败出来,下一轮就可能解开。 if len(keep_logs) + len(new_logs) <= len(logs): segs[:] = ns vias[:] = nv logs[:] = keep_logs + new_logs print(u' 拆线重布第 %d 轮:%d -> %d' % (rnd + 1, before, len(logs))) if len(logs) >= before: break return segs, vias, logs def check_connected(pads, segs, vias): u"""**真正的连通性判据**:按几何做并查集,看每个网的焊盘是不是真的连成一片。 ⛔ 为什么非要有:布线器报「0 条没搞定」只说明**它自己**觉得布完了。 拆线重布一旦把前置阶段的铜(直连 / Kelvin / VO_P 过孔)删掉, 那些焊盘早就不在 `bypad` 里,于是**没人再检查它们** —— 板子断了、脚本报好。 这里不看布线器的账本,只看铜。 """ par = {} def find(a): while par.get(a, a) != a: par[a] = par.get(par[a], par[a]) a = par[a] return a def uni(a, b): ra, rb = find(a), find(b) if ra != rb: par[ra] = rb def key(l, x, y): return (l, round(x, 2), round(y, 2)) for sg in segs: l, x0, y0, x1, y1 = sg[0], sg[1], sg[2], sg[3], sg[4] par.setdefault(key(l, x0, y0), key(l, x0, y0)) par.setdefault(key(l, x1, y1), key(l, x1, y1)) uni(key(l, x0, y0), key(l, x1, y1)) for (vx, vy, _n) in vias: a1, b1 = key(LAY[0], vx, vy), key(LAY[1], vx, vy) par.setdefault(a1, a1) par.setdefault(b1, b1) uni(a1, b1) # 焊盘:盘框内的所有端点并到一起 bad = [] bynet = {} for q in pads: bynet.setdefault(q[7], []).append(q) ends = {} for k in list(par): ends.setdefault(k[0], []).append(k) for net, ps in bynet.items(): if not net or net in SKIP: continue roots, naked = [], [] for q in ps: if q[0] == 'J1': # 金手指自己有扇出,另算 continue r = pad_rect(q, 0.05) hit = [k for k in ends.get(q[6], []) if r[0] <= k[1] <= r[2] and r[1] <= k[2] <= r[3]] if not hit: naked.append('%s.%s' % (q[0], q[1])) continue for h2 in hit[1:]: uni(hit[0], h2) roots.append(find(hit[0])) if naked: bad.append(u'%s: %s 一根铜都没有' % (net, ' '.join(naked[:4]))) elif len(set(roots)) > 1 and net not in ISLAND_NETS: # ⚠ `VO_P` / `+12V` 靠 L3 的整块铜岛成网(每个焊盘扎一个过孔进去), # 铜在几何上**本来就是分开的几块**,不是断线。 bad.append(u'%s: 铜分成 %d 块' % (net, len(set(roots)))) return bad def plan_all(priority=()): u"""`priority` 里的网络在同一档宽度里**排最前面** —— 给失败重试用。""" import pcb_rules as R auto = R.resolve_auto() pads = load_pads() board_vias = load_vias() print(u'板上已有过孔 %d 个' % len(board_vias)) fsegs, fvias, launch = finger_escape() segs = list(fsegs) vias = list(fvias) logs = [] # J1 的焊盘不再参与 A*(已经爬到 LAUNCH_Y),改成「虚拟焊盘」 bypad = {} for p in pads: if p[0] == 'J1': continue if not p[7] or p[7] in SKIP: continue bypad.setdefault(p[7], []).append(p) for net, pts in launch.items(): for (lay, x, y) in pts: bypad.setdefault(net, []).append(['J1', 'launch', x, y, 0.3, 0.3, lay, net, 'RECT']) # ── Kelvin 专线(必须在所有别的线之前)── halfk = 0.3 / 2.0 + CLR gk = build_grid(pads, halfk, board_vias) for (l, a, b, c, d, ww, nn) in segs: block_track(gk, l, a, b, c, d, halfk + ww / 2.0, nn) padmap = {(q[0], q[1]): q for q in pads} othersk = {1: [pad_rect(q) for q in pads if q[6] == 1], 2: [pad_rect(q) for q in pads if q[6] == 2]} for (aa, bb, w) in KELVIN: pa, pb = padmap.get(aa), padmap.get(bb) if not pa or not pb: logs.append(u'Kelvin: 找不到 %s / %s' % (aa, bb)) continue na = pad_nodes(gk, pa, halfk, othersk.get(pa[6], []), w / 2.0) nb = pad_nodes(gk, pb, halfk, othersk.get(pb[6], []), w / 2.0) path = astar(gk, na[:12], nb[:12], pa[7], halfk) if not path: logs.append(u'Kelvin: %s.%s -> %s.%s 没布通' % (aa + bb)) continue sg, vv = to_segments(path) segs += [(l, x0, y0, x1, y1, w, pa[7]) for (l, x0, y0, x1, y1) in sg] vias += [(x, y, pa[7]) for (x, y) in vv] segs += stub_emit(pa, wx(path[0][1]), wy(path[0][2]), othersk.get(pa[6], []), w / 2.0, w, pa[7]) segs += stub_emit(pb, wx(path[-1][1]), wy(path[-1][2]), othersk.get(pb[6], []), w / 2.0, w, pb[7]) # ⚠ Kelvin 那两条已经把 `U3.9/10` 接到 `Rs1` 上了,**要把 U3 那两只脚从 # bypad 里摘掉** —— 不摘的话 A* 会再去接一遍(`$1N165: U3.10 出不来`), # 而它本来就已经连上了。 for (aa, _bb, _w) in KELVIN: for net in list(bypad): bypad[net] = [q for q in bypad[net] if (q[0], q[1]) != aa] # VO_P:靠 L3 的岛,每个焊盘扎一个过孔就够(4 A 走平面,比走线好)。 # ⚠ **过孔位置必须过占用图**:第一版是「往右 0.9 mm 找个点就打」, # 于是 `U3.8`(0.5 mm 间距的 MSOP-10)的 1.5 mm 引出脚直接压住隔壁两个脚, # DRC 一口气报 400 条「导线到导线 0 mil」。判据换成 pad_nodes + 岛的范围。 halfv = VIA_D / 2.0 + CLR gv = build_grid(pads, halfv, board_vias) for (l, a, b, c, d, ww, nn) in segs: block_track(gv, l, a, b, c, d, halfv + ww / 2.0, nn) othersv = {1: [pad_rect(q) for q in pads if q[6] == 1], 2: [pad_rect(q) for q in pads if q[6] == 2]} isls = (P.VO_ISLAND, P.VO_LOBE) # M6:岛是 L 形,两块矩形任一块里都能落孔 vo = [q for q in bypad.pop('VO_P', []) if (q[0], q[1]) not in KELVIN_PADS] for p in vo: if p[0] == 'J1': continue # ⚠ 下限是 **0.3 = PWR4A 规则的最小线宽**,不是 0.25。 # `U3` 的 8/9 脚(VBUS 采样 / IN+)只有 0.28 宽,按焊盘缩到 0.28 就 # 低于规则下限,DRC 报「物理错误:线宽 11mil,应该 >= 11.8mil」。 # 0.3 的线接 0.28 的盘只是稍微出头,间距还剩 0.36-0.30=0.06,是够的。 w = min(WIDTH['VO_P'], max(0.30, min(p[4], p[5]))) # ⚠ 落的是**通孔**,两层都要让得开 —— 只查自己那一层的话会撞上另一面的焊盘 # (实测 VO_P 的孔压在顶层 `C60` 的 GND 盘上,净距 0.125,DRC 报「焊盘到过孔」)。 cand = [n for n in pad_nodes(gv, p, halfv, othersv.get(p[6], []), w / 2.0) if any(isl[0] + 0.6 < wx(n[1]) < isl[2] - 0.6 and isl[1] + 0.6 < wy(n[2]) < isl[3] - 0.6 for isl in isls) and gv.free_via(n[1], n[2], 'VO_P')] if not cand: logs.append(u'VO_P: %s.%s 找不到落过孔的地方' % (p[0], p[1])) continue n = cand[0] vx, vy = wx(n[1]), wy(n[2]) segs += stub_emit(p, vx, vy, othersv.get(p[6], []), w / 2.0, w, 'VO_P') vias.append((vx, vy, 'VO_P')) keep = halfv + w / 2.0 block_track(gv, p[6], p[2], p[3], vx, vy, keep, 'VO_P') for l in LAY: gv.block_rect(l, vx - keep, vy - keep, vx + keep, vy + keep, net='VO_P') board_vias.append([vx, vy, 'VO_P', VIA_D]) # ── 直连:**焊盘对焊盘、中间没别人**的短连接,不走 A* ──────────────── AUTO_CACHE.clear() AUTO_CACHE.update(auto) dsegs, merged = direct_links(pads, bypad, segs, vias) segs += dsegs if dsegs: print(u'直连 %d 段(焊盘贴焊盘,A* 的干线口径进不去)' % len(dsegs)) for net, groups in merged.items(): drop = set() for gset in groups: drop |= set(list(gset)[1:]) # 每一簇只留一个代表 bypad[net] = [q for q in bypad[net] if (q[0], q[1]) not in drop] # ── 过孔扇出(M6,`--viafan`):细间距 IC 每只信号脚先打一个过孔上顶层 ── if '--viafan' in sys.argv: fs, fv, fl = via_fanout(pads, bypad, segs, vias, auto) segs += fs vias += fv n_fan = 0 for net, items in fl.items(): for (q, vx, vy) in items: bypad[net] = [o for o in bypad[net] if o is not q] bypad[net].append([q[0], 'fan:%s' % q[1], vx, vy, 0.3, 0.3, 1, net, 'RECT']) n_fan += 1 print(u'过孔扇出 %d 只脚(%s),落点全在顶层' % (n_fan, ' '.join(FANOUT_REFS))) # ── 关键网络优先:在**所有**别的网之前布 ──────────────────────────── # ⛔ `$1N126`(FB) 和 `$1N138`(BOOT) 是 Buck 的命门:少了 BOOT 高侧管不开, # 少了 FB 环路不闭合。它们都在 `U1` 最挤的那一圈上,按线宽分档的话排在 # 最后一档、又被别的网占完出口 —— 所以给它们一条绝对优先的通道。 for net in FIRST_NETS: if net not in bypad or not bypad[net]: continue ps = bypad[net] w = max(W_DEFAULT, rule_min(net, auto), min([min(q[4], q[5]) for q in ps if q[0] != 'J1'] or [W_DEFAULT])) halfw = w / 2.0 + CLR g = build_grid(pads, halfw, board_vias) for (l, a1, b1, c1, d1, ww, nn) in segs: block_track(g, l, a1, b1, c1, d1, halfw + ww / 2.0, nn) gt = Grid() for (l, a1, b1, c1, d1, ww, nn) in segs: block_track(gt, l, a1, b1, c1, d1, CLR + W_DEFAULT / 2.0 + ww / 2.0, nn) others = {1: [], 2: []} for q in pads: if q[6] in others: others[q[6]].append(pad_rect(q)) s2, v2, st2 = route_net(g, net, ps, halfw, w, others, logs, gt) segs += s2 vias += v2 for (q, node, ww) in st2: segs += stub_emit(q, wx(node[1]), wy(node[2]), [] if _is_launch(q) else others.get(q[6], []), ww / 2.0, ww, net, gt) bypad.pop(net, None) # ⚠ **独立的扇出阶段试过,反而更差**(30 -> 40 条没接上),代码留在 `fanout()` # 里备查,默认不跑。原因:提前把每只脚的引出脚铺成铜,等于在还不知道干线 # 往哪走的时候先占地盘;更要命的是干线从此**只能从那一个落点起步**, # 而原来 `pad_nodes` 能给 A* 24 个候选起点。少了这份自由度,得不偿失。 fixed_s, fixed_v = len(segs), len(vias) if '--pf' in sys.argv: # 协商式拥塞布线:所有干线交给它,不再走「一次布死」那条路 logs += pf_solve(bypad, pads, segs, vias, board_vias, auto, fixed_s, fixed_v) else: logs += route_groups(bypad, pads, segs, vias, board_vias, auto, priority) if logs: segs, vias, logs = repair(bypad, pads, segs, vias, logs, board_vias, auto, fixed_s, fixed_v) segs, vias = dedupe_vias(segs, vias) for m in check_connected(pads, segs, vias): if m not in logs: logs.append(u'[连通性] ' + m) return segs, vias, logs PUSH_JS = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const MIL = %s, S = %s, V = %s, H = %s, D = %s; let ls = 0, vs = 0; for (const s of S) { try { await eda.pcb_PrimitiveLine.create(s[6], s[0], s[1]*MIL, s[2]*MIL, s[3]*MIL, s[4]*MIL, s[5]*MIL); ls++; } catch (e) {} } for (const v of V) { try { await eda.pcb_PrimitiveVia.create(v[2], v[0]*MIL, v[1]*MIL, H*MIL, D*MIL); vs++; } catch (e) {} } return {ls, vs}; """ def push(segs, vias): n = 0 for k in range(0, len(segs), 120): r = call(PUSH_JS % (PCB, MIL, json.dumps(segs[k:k + 120]), json.dumps(vias if k == 0 else []), VIA_HOLE, VIA_D), timeout=600) n += r['ls'] print(u' 下发 %d 段 / %d 过孔' % (r['ls'], r['vs'])) call("await eda.pcb_Document.save(); return 1;") print(u' 共 %d 段' % n) def clear_mine(): print(call("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,2500));" "const ls = (await eda.pcb_PrimitiveLine.getAll()||[])" " .filter(l=>l.layer===1||l.layer===2).map(l=>l.primitiveId);" "if (ls.length) await eda.pcb_PrimitiveLine.delete(ls);" "const vs = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>v.primitiveId);" "if (vs.length) await eda.pcb_PrimitiveVia.delete(vs);" "await eda.pcb_Document.save();" "return {lines: ls.length, vias: vs.length};" % PCB, timeout=600)) FAN_RING = 10 # 扇出找落点最多往外扫几圈(× G = 2.5 mm) FAN_PITCH = 1.0 # 只给「邻脚中心距 < 这个值」的密脚做扇出 def _pad_axis(pads_of_comp, q): u"""焊盘的**出线方向**:沿长轴、背对本封装的重心。""" cx = sum(p[2] for p in pads_of_comp) / float(len(pads_of_comp)) cy = sum(p[3] for p in pads_of_comp) / float(len(pads_of_comp)) if q[5] >= q[4]: # 竖着的盘 -> 上下出 return (0.0, 1.0 if q[3] >= cy else -1.0) return (1.0 if q[2] >= cx else -1.0, 0.0) FANOUT_REFS = ('U2', 'U4', 'U3') # WQFN-20 0.5 / TSSOP-24 0.65 / MSOP-10 0.5 FAN_NEAR, FAN_FAR = 0.55, 1.15 # 过孔中心离焊盘端的距离,相邻脚交替(两排) FAN_MAX_W = 0.4 # 只扇信号(含 +3V3);VO/$1N165 那种粗线靠直连的铜条 def via_fanout(pads, bypad, segs, vias, auto): u"""**交错两排的过孔扇出**(手工布细间距 IC 的标准做法)。 每只信号脚沿焊盘长轴直着伸出一段 0.15 的引出脚,末端打一个 Ø0.5 的过孔上顶层; 相邻脚一近一远交替(0.55 / 1.15),于是同一排过孔的节距是脚距的两倍、 远排的引出脚正好从近排两个过孔中间穿过(0.5 节距:0.5 − 0.25 − 0.075 = 0.175)。 干线从此全在**几乎是空的顶层**起步,细间距 IC 周围的底层不再是抢出口的战场。 ⚠ 与 `fanout()`(同层落点、已废)的区别就在「换层」:同层落点只是把地盘 提前占了,干线还是要从那一堆脚中间挤出去。 落点冲突(别的网的焊盘 / 已有铜 / 已有过孔)就换另一排;两排都不行的脚原样交给 A*。 返回 (引出脚线段, 过孔, {网: [(原焊盘, vx, vy)]})。 """ bycomp = {} for q in pads: bycomp.setdefault(q[0], []).append(q) laid = list(segs) fan_vias = list(vias) # (x, y, net) out_s, out_v, out_l = [], [], {} r_via = VIA_D / 2.0 want = {} for net, ps in bypad.items(): if rule_width(net, auto) > FAN_MAX_W + 1e-9: continue for q in ps: if q[0] in FANOUT_REFS and not _is_launch(q) and (q[0], q[1]) not in KELVIN_PADS: want[(q[0], q[1])] = (q, net) for ref in FANOUT_REFS: comp = bycomp.get(ref, []) sides = {} for q in comp: if (q[0], q[1]) not in want: continue ax, ay = _pad_axis(comp, q) key = (int(round(ax)), int(round(ay))) sides.setdefault(key, []).append((q, ax, ay)) for key, items in sides.items(): # 沿着这一边排序:竖排按 y,横排按 x items.sort(key=lambda t: t[0][3] if key[0] else t[0][2]) for k, (q, ax, ay) in enumerate(items): net = want[(q[0], q[1])][1] half_len = (q[5] if abs(ay) > 0.5 else q[4]) / 2.0 sw = stub_w(q, W_DEFAULT) order = (FAN_NEAR, FAN_FAR) if k % 2 == 0 else (FAN_FAR, FAN_NEAR) placed = None for dist in order: vx = q[2] + ax * (half_len + dist) vy = q[3] + ay * (half_len + dist) ok = True # 别的网的焊盘(两层都要让:通孔) for o in pads: if o[7] == net: continue r = pad_rect(o) d = math.hypot(max(r[0] - vx, 0.0, vx - r[2]), max(r[1] - vy, 0.0, vy - r[3])) if d < r_via + CLR: ok = False break # 引出脚本身也不许蹭到同层别的盘 if o[6] == q[6] and seg_hits_rect(q[2], q[3], vx, vy, (r[0] - CLR - sw / 2, r[1] - CLR - sw / 2, r[2] + CLR + sw / 2, r[3] + CLR + sw / 2)): ok = False break if not ok: continue for (l, a1, b1, c1, d1, ww, nn) in laid + out_s: if nn == net: continue if _pt_seg_d(vx, vy, a1, b1, c1, d1) < r_via + ww / 2.0 + CLR: ok = False break if l == q[6] and _seg_seg_d((q[2], q[3], vx, vy), (a1, b1, c1, d1)) < sw / 2.0 + ww / 2.0 + CLR: ok = False break if not ok: continue for (ox, oy, on) in fan_vias: if on == net: continue if math.hypot(ox - vx, oy - vy) < 2 * r_via + CLR + 0.05: ok = False break if _pt_seg_d(ox, oy, q[2], q[3], vx, vy) < r_via + sw / 2.0 + CLR: ok = False break if ok: placed = (vx, vy) break if placed is None: continue vx, vy = placed out_s.append((q[6], q[2], q[3], round(vx, 3), round(vy, 3), sw, net)) out_v.append((round(vx, 3), round(vy, 3), net)) fan_vias.append((vx, vy, net)) out_l.setdefault(net, []).append((q, round(vx, 3), round(vy, 3))) return out_s, out_v, out_l def _pt_seg_d(px, py, x0, y0, x1, y1): dx, dy = x1 - x0, y1 - y0 L2 = dx * dx + dy * dy t = 0.0 if L2 == 0 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L2)) return math.hypot(px - (x0 + dx * t), py - (y0 + dy * t)) def _seg_seg_d(a, b): (x0, y0, x1, y1), (u0, v0, u1, v1) = a, b r = (x1 - x0, y1 - y0); s2 = (u1 - u0, v1 - v0) den = r[0] * s2[1] - r[1] * s2[0] if abs(den) > 1e-12: t = ((u0 - x0) * s2[1] - (v0 - y0) * s2[0]) / den u = ((u0 - x0) * r[1] - (v0 - y0) * r[0]) / den if 0 <= t <= 1 and 0 <= u <= 1: return 0.0 return min(_pt_seg_d(x0, y0, *b), _pt_seg_d(x1, y1, *b), _pt_seg_d(u0, v0, *a), _pt_seg_d(u1, v1, *a)) def fanout(pads, bypad, segs, vias, board_vias, auto): u"""**扇出阶段**:布任何干线之前,先把每只焊盘的引出脚铺成真铜。 ⛔ 为什么必须独立成一阶段:出口是**抢**的。谁先布谁把邻脚的格点占掉, 细间距 IC 的内侧脚必然最后一个布、也必然抢不到 —— 一轮下来 29~42 处 报「出不来」,而同样的摆位单独看每只脚**都是出得来的**(空占用图上 `U4.5` 有 7 个出口,`U2.14` 有 4 个)。这就是手工布板「先做扇出、再拉干线」 的顺序,不是优化,是**必须**。 两段式引出脚(细间距连接器靠它):**先沿焊盘长轴直着出去**到本盘自己的 「车道」尽头,再拐到格点。`J4`(0.35 mm 间距、盘 0.18 宽)只有这一条路 —— 盘心到邻盘边只有 0.26 mm,任何斜着走的引出脚都会蹭上邻盘(实测 0.047 mm)。 返回 (引出脚线段, {网: [落点虚拟焊盘]}, 没出来的日志)。 """ bycomp = {} for q in pads: bycomp.setdefault(q[0], []).append(q) laid = list(segs) # 已经有铜的(金手指扇出 / Kelvin / 直连) launched, logs = {}, [] # 宽的先扇出:它的落点要求最苛刻 order = sorted(bypad, key=lambda n: -width_of(n, auto, bypad[n])) for net in order: ps = [q for q in bypad[net] if q[0] != 'J1'] if not ps: continue w = width_of(net, auto, ps) w = max(W_DEFAULT, min(w, min(min(q[4], q[5]) for q in ps))) halfw = w / 2.0 + CLR g = build_grid(pads, halfw, board_vias) for (l, a1, b1, c1, d1, ww, nn) in laid: block_track(g, l, a1, b1, c1, d1, halfw + ww / 2.0, nn) others = [pad_rect(q) for q in pads] out = [] # ⚠ **只扇密脚**。把每只脚都提前铺一段引出脚,等于在还不知道干线往哪走的时候 # 就占地盘 —— 实测反而从 30 处涨到 40 处没接上。真正抢不到出口的只有 # 细间距 IC 的内侧脚(邻脚中心距 < 1 mm),其余交给 A* 自己找更好。 for q in ps: near = min([math.hypot(q[2] - o[2], q[3] - o[3]) for o in bycomp[q[0]] if o is not q] or [99]) if near >= FAN_PITCH: out.append(q) # 原样交给 A* continue sw = stub_w(q, w) inf = STUB_CLR + sw / 2.0 own = pad_rect(q) oth = [(r[0] - inf, r[1] - inf, r[2] + inf, r[3] + inf) for r, p2 in zip(others, pads) if p2[6] == q[6] and p2[7] != net and max(abs(r[0] - own[0]), abs(r[1] - own[1]), abs(r[2] - own[2]), abs(r[3] - own[3])) > 1e-9] oth_pads = [o for o in ps if o is not q] tx = (sum(o[2] for o in oth_pads) / len(oth_pads)) if oth_pads else q[2] ty = (sum(o[3] for o in oth_pads) / len(oth_pads)) if oth_pads else q[3] ax, ay = _pad_axis(bycomp[q[0]], q) reach = (q[5] if ay else q[4]) / 2.0 + inf ex, ey = q[2] + ax * reach, q[3] + ay * reach best = None for ring in range(1, FAN_RING + 1): r = ring * G cand = [] for j in range(gy(ey - r), gy(ey + r) + 1): for i in range(gx(ex - r), gx(ex + r) + 1): X, Y = wx(i), wy(j) if not g.free(q[6], i, j, net): continue if any(seg_hits_rect(q[2], q[3], ex, ey, o) for o in oth): continue if any(seg_hits_rect(ex, ey, X, Y, o) for o in oth): continue if not _stub_clear(g, q[6], q[2], q[3], X, Y, net): continue # ⚠ 落点**朝目的地挑**,不是挑离焊盘最近的: # 引出脚指错方向,后面干线要绕回来,还把路占了。 cand.append((math.hypot(X - tx, Y - ty), math.hypot(X - ex, Y - ey), i, j)) if cand: cand.sort() best = cand[0] break if best is None: logs.append(u'%s: %s.%s 扇不出来' % (net, q[0], q[1])) continue _d, _d2, i, j = best X, Y = wx(i), wy(j) if math.hypot(ex - q[2], ey - q[3]) > 1e-6: laid.append((q[6], q[2], q[3], ex, ey, sw, net)) laid.append((q[6], ex, ey, X, Y, sw, net)) block_track(g, q[6], q[2], q[3], ex, ey, halfw + sw / 2.0, net) block_track(g, q[6], ex, ey, X, Y, halfw + sw / 2.0, net) out.append([q[0], 'fan:' + str(q[1]), X, Y, 0.3, 0.3, q[6], net, 'RECT']) if out: launched[net] = out return laid[len(segs):], launched, logs LINK_MAX = 2.2 # 直连允许的最大焊盘净距 def _pad_gap(a, b): dx = max(abs(a[2] - b[2]) - (a[4] + b[4]) / 2.0, 0.0) dy = max(abs(a[3] - b[3]) - (a[5] + b[5]) / 2.0, 0.0) return math.hypot(dx, dy) AUTO_CACHE = {} def direct_links(pads, bypad, segs, vias, blocked_rects=()): u"""**焊盘对焊盘**的短连接直接画一段铜,不交给 A*。 ⛔ 为什么非要有这一步:A* 的占用图是按**干线宽度**建的。`SWNODE` 是 2.0 mm ⇒ 半宽 + 净距 = 1.18 mm,于是 `U1` 的 SW 脚(0.5 mm 间距)周围**一个格点都不空**, 报「$1N137: U1.11 出不来」。可这条连接根本不需要 A* —— 摆位时就是 **焊盘正对焊盘、净距 0.65 mm**,人手画就是一段直铜。 同一类的还有 `VO`(L1→C6)、`$1N165`(U2→Rs1)、`+12V_F`(Cin→U1)。 宽度取**两个焊盘在垂直于连线方向上的较小者** —— 再宽也没用, 电流的瓶颈就是 IC 自己的脚。 判据:同层、同网、净距 ≤ `LINK_MAX`,且连线走廊里没有第三个焊盘、 没有已布的铜、没有过孔。 """ out, merged = [], {} others = {} for q in pads: others.setdefault(q[6], []).append(q) for net, ps in sorted(bypad.items()): cand = [q for q in ps if q[0] != 'J1'] groups = [] for i in range(len(cand)): for j in range(i + 1, len(cand)): a, b = cand[i], cand[j] if a[6] != b[6]: continue if _pad_gap(a, b) > LINK_MAX: continue # 垂直于连线方向的可用宽度 dx, dy = abs(a[2] - b[2]), abs(a[3] - b[3]) wa = a[5] if dx >= dy else a[4] wb = b[5] if dx >= dy else b[4] w = round(max(rule_min(net, AUTO_CACHE), min(wa, wb)), 3) if w < 0.15: continue inf = CLR + w / 2.0 bad = False for owner, rr in blocked_rects: if owner == net: continue r = (rr[0] - inf, rr[1] - inf, rr[2] + inf, rr[3] + inf) if seg_hits_rect(a[2], a[3], b[2], b[3], r): bad = True break for q in others.get(a[6], []): # ⚠ **同网的焊盘不算障碍** —— 压上去也是同一个节点。 # 漏了这一条,`U1` 的两只 SW 脚会互相判死:11 脚要接 L1, # 12 脚(同网、隔 0.5 mm)挡在旁边,于是「U1.11 出不来」。 if q[7] == net or (q[0], q[1]) in ((a[0], a[1]), (b[0], b[1])): continue r = pad_rect(q, inf) if seg_hits_rect(a[2], a[3], b[2], b[3], r): bad = True break if not bad: for (l, x0, y0, x1, y1, ww, nn) in segs: if l != a[6] or nn == net: continue if seg_seg_dist(a[2], a[3], b[2], b[3], x0, y0, x1, y1) < inf + ww / 2.0: bad = True break if not bad: for (vx, vy, nn) in vias: if nn == net: continue if seg_hits_rect(a[2], a[3], b[2], b[3], (vx - VIA_D / 2 - inf, vy - VIA_D / 2 - inf, vx + VIA_D / 2 + inf, vy + VIA_D / 2 + inf)): bad = True break if bad: continue out.append((a[6], a[2], a[3], b[2], b[3], w, net)) ka, kb = (a[0], a[1]), (b[0], b[1]) ga = next((g for g in groups if ka in g), None) gb = next((g for g in groups if kb in g), None) if ga is None and gb is None: groups.append({ka, kb}) elif ga is None: gb.add(ka) elif gb is None: ga.add(kb) elif ga is not gb: ga |= gb groups.remove(gb) if groups: merged[net] = groups return out, merged def seg_seg_dist(x1, y1, x2, y2, x3, y3, x4, y4): u"""两条线段的最小距离(够用的粗算:端点到线段 + 采样)。""" def pt_seg(px, py, ax, ay, bx, by): dx, dy = bx - ax, by - ay L = dx * dx + dy * dy t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L)) return math.hypot(px - (ax + t * dx), py - (ay + t * dy)) return min(pt_seg(x1, y1, x3, y3, x4, y4), pt_seg(x2, y2, x3, y3, x4, y4), pt_seg(x3, y3, x1, y1, x2, y2), pt_seg(x4, y4, x1, y1, x2, y2)) VIA_MIN_D = VIA_D # 同网过孔的最小中心距 = 钻孔间距 0.3 + 钻孔径 0.3 def dedupe_vias(segs, vias): u"""同一个网上**挨得太近的过孔**合成一个。 ⛔ 占用图是「按网可进」的,所以同一个网的两条支路可以各自在几乎同一个点换层 —— 几何上不冲突,但**钻孔到钻孔**的规则不认网:DRC 报 「钻孔 (SCL) 到 钻孔 (SCL) 距离为 0 mil,应该 >= 11.8 mil」。 自算净距那一支也抓不到(`router_check` 只判铜与铜,钻孔那一支归 EasyEDA)。 合并判据:同网、中心距 < `VIA_MIN_D`(=0.6 mm)。这时两个过孔的**焊盘本来就重叠**, 所以补一小段两层导线把被删的那个点接回保留点,不会伸出原来的铜之外。 """ keep, drop = [], {} for (x, y, net) in vias: hit = None for (kx, ky, kn) in keep: if kn == net and math.hypot(kx - x, ky - y) < VIA_MIN_D - 1e-9: hit = (kx, ky) break if hit is None: keep.append((x, y, net)) else: drop[(round(x, 4), round(y, 4), net)] = hit if drop: print(u'同网过孔合并 %d 个(中心距 < %.2f mm)' % (len(drop), VIA_MIN_D)) for (x, y, net), (kx, ky) in drop.items(): w = max(0.2, rule_min(net, AUTO_CACHE)) for l in LAY: segs.append((l, x, y, kx, ky, w, net)) return segs, keep PASSES = 4 # 失败重试轮数(拆线重布接手之后不必太多) CACHE = os.path.join(HERE, '_blade_routes.json') # ══════════════════════════════════════════════════════════════════════ # 协商式拥塞布线(PathFinder) # ══════════════════════════════════════════════════════════════════════ # ⛔ 为什么非要换算法:这个「一次布死、不回退」的 A* 到 23 条就不动了。 # 试过顺序、扇出、过路费、L 形引出脚、正交方向、拆线重布、难的先布 —— 全在 # 12~23 之间打转。根因是**它没有让路机制**:谁先占住走廊,后来的就只能报失败。 # # PathFinder 的做法(Nair 1987 / McMurchie-Ebeling 1995): # 1. 每一轮**所有网都重新布一遍**,允许暂时压在别人身上(不是硬障碍,是加钱); # 2. 一轮结束后,把**被多个网压着的格点**的「历史代价」加一档; # 3. 下一轮大家再抢,历史贵的地方自然有人让开 —— 谁绕路代价小谁让。 # 反复若干轮直到没有格点被两个网同时占。这就是「协商」。 # ⚠ 调度是 PathFinder 的命门。第一版「每轮翻倍、封顶 64」**不收敛** —— # 冲突 1790→1062 之后在 1200 附近震荡:当前代价涨得太快,网被推得来回横跳, # 历史代价还没来得及把「谁该让」这件事沉淀下来。 # 改成教科书的慢涨(×1.25),并让历史按**超用量**累加。 PF_PRESENT = [1.0] # 当前拥塞权重,每轮 ×PF_GROW PF_GROW = 1.25 PF_HIST_W = 4 # 历史拥塞权重 PF_ITERS = 30 def _route_cells(segs_of_net, halfw): u"""一条线占的格点。 ⛔ 膨胀量是 **线半宽 + 净距/2**,不是 `halfw + w/2`(= 线宽 + 净距)。 两条线各膨胀「半宽 + 净距/2」之后**刚好在净距处相切** —— 这才是 「再近一点就违规」的判据。按 `halfw + w/2` 膨胀的话,两根**正好隔着 合法净距**的线也会共占格点,于是 PF 一直在追逐根本不存在的冲突, 冲突数卡在 800 上下永远收敛不了(实测 2678→823 就不动了)。 """ out = set() for (l, x0, y0, x1, y1, w, _n) in segs_of_net: r = w / 2.0 + CLR / 2.0 steps = int(max(abs(x1 - x0), abs(y1 - y0)) / (G / 2)) + 1 for k in range(steps + 1): t = k / float(steps) x, y = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t for j in range(gy(y - r), gy(y + r) + 1): for i in range(gx(x - r), gx(x + r) + 1): if 0 <= i < NX and 0 <= j < NY: out.add((l, i, j)) return out def _pf_mark(occ, cells, d): for (l, i, j) in cells: o = j * NX + i v = occ[l][o] + d occ[l][o] = max(0, min(250, v)) def _exact_conflicts(cur, pads): u"""按**精确几何**找出真正压在一起的网(判据同 router_check:净距 < MIN_CLR)。 ⚠ **过孔那一支不能漏**:只查线与线的话,会漏掉「过孔压在另一面的焊盘上」 和「两个不同网的过孔叠在一起」—— 实测漏掉了 `PRSNT#` 的孔压在 `J2.5`(GND) 盘上(净距 −0.300)和 `+12V_F` / `$1N131` 两个孔叠了 0.041。 通孔是穿两层的,所以焊盘那一支**两层都要查**。 """ MIN = 0.15 items = [] allv = [] for net, (s2, v2, _c) in cur.items(): for t in s2: items.append((t[0], t[1], t[2], t[3], t[4], t[5], net)) for (vx, vy, _q) in v2: allv.append((vx, vy, net)) # 按 y 粗分桶,避免 O(n^2) 全比 bad = set() n = len(items) for i in range(n): a = items[i] ay0, ay1 = min(a[2], a[4]), max(a[2], a[4]) ax0, ax1 = min(a[1], a[3]), max(a[1], a[3]) for j in range(i + 1, n): b = items[j] if b[6] == a[6] or b[0] != a[0]: continue if min(b[2], b[4]) > ay1 + 1.0 or max(b[2], b[4]) < ay0 - 1.0: continue if min(b[1], b[3]) > ax1 + 1.0 or max(b[1], b[3]) < ax0 - 1.0: continue d = seg_seg_dist(a[1], a[2], a[3], a[4], b[1], b[2], b[3], b[4]) if d - a[5] / 2.0 - b[5] / 2.0 < MIN - 1e-9: bad.add(a[6]) bad.add(b[6]) # 过孔 vs 焊盘(两层都查,通孔穿透) for (vx, vy, net) in allv: for q in pads: if q[7] == net: continue dx = max(q[2] - q[4] / 2 - vx, 0.0, vx - (q[2] + q[4] / 2)) dy = max(q[3] - q[5] / 2 - vy, 0.0, vy - (q[3] + q[5] / 2)) if math.hypot(dx, dy) - VIA_D / 2.0 < MIN - 1e-9: bad.add(net) break # 过孔 vs 过孔(异网) for i in range(len(allv)): for j in range(i + 1, len(allv)): if allv[i][2] == allv[j][2]: continue if math.hypot(allv[i][0] - allv[j][0], allv[i][1] - allv[j][1]) - VIA_D < MIN - 1e-9: bad.add(allv[i][2]) bad.add(allv[j][2]) return bad def pf_solve(bypad, pads, segs, vias, board_vias, auto, fixed_s, fixed_v): u"""对 `bypad` 里所有网做协商式拥塞布线。原地改 segs/vias,返回没搞定的日志。""" occ = {l: bytearray(NX * NY) for l in LAY} hist = {l: bytearray(NX * NY) for l in LAY} base_s, base_v = segs[:fixed_s], vias[:fixed_v] # 每个网一份线宽 / 半宽;硬障碍图按宽度分档建一次就够 wof, hard = {}, {} for net, ps in bypad.items(): w = width_of(net, auto, ps) small = w if net in NO_CAP else min([min(q[4], q[5]) for q in ps if q[0] != 'J1'] or [w]) wof[net] = max(W_DEFAULT, rule_min(net, auto), min(w, small)) for w in sorted(set(wof.values())): halfw = w / 2.0 + CLR g = build_grid(pads, halfw, board_vias) for (l, a, b, c, d, ww, nn) in base_s: # 前置阶段的铜是硬障碍 # ⛔ `extra` 不能用默认的 0:`block_both` 的第二张图(`ownv`)是**换层判据**, # 不给额外膨胀的话,别人的过孔可以直接落在这根线上 —— # DRC 报「导线到过孔 0 mil」,实测两处(`EFUSE_EN` 的孔压 `$1N147`、 # `$1N164` 的孔压 `EFUSE_PG`),而线与线那一支干干净净,很难看出来是谁的错。 block_track(g, l, a, b, c, d, halfw + ww / 2.0, nn, extra=max(0.0, VIA_D / 2.0 + CLR - (halfw + ww / 2.0))) for (vx, vy, nn) in base_v: for l in LAY: g.block_rect(l, vx - VIA_D / 2 - halfw, vy - VIA_D / 2 - halfw, vx + VIA_D / 2 + halfw, vy + VIA_D / 2 + halfw, net=nn) g.occ, g.hist = occ, hist hard[w] = g others = {1: [], 2: []} for p in pads: if p[6] in others: others[p[6]].append(pad_rect(p)) cur = {} # net -> (segs, vias, cells) order = sorted(bypad, key=lambda n: -wof[n]) logs = [] best = None for it in range(PF_ITERS): logs = [] for net in order: old = cur.pop(net, None) if old: _pf_mark(occ, old[2], -1) w = wof[net] g = hard[w] halfw = w / 2.0 + CLR s2, v2, st2 = route_net(g, net, bypad[net], halfw, w, others, logs, None, mutate=False) for (q, node, ww) in st2: s2 = s2 + stub_emit(q, wx(node[1]), wy(node[2]), [] if _is_launch(q) else others.get(q[6], []), ww / 2.0, ww, net) cells = _route_cells(s2, halfw) _pf_mark(occ, cells, +1) cur[net] = (s2, v2, cells) # 一轮结束:给被多个网压着的格点按**超用量**加历史代价 bad = 0 for l in LAY: oc, hi = occ[l], hist[l] for o in range(NX * NY): if oc[o] > 1: bad += 1 hi[o] = min(250, hi[o] + oc[o] - 1) print(u' PF 第 %d 轮:冲突格点 %d,没布通 %d(present=%.1f)' % (it + 1, bad, len(logs), PF_PRESENT[0])) if best is None or (bad, len(logs)) < best[0]: best = ((bad, len(logs)), {n: (list(a2), list(b2), set(c2)) for n, (a2, b2, c2) in cur.items()}, list(logs)) if bad == 0 and not logs: break PF_PRESENT[0] *= PF_GROW # 取**冲突最少的那一轮**,不是最后一轮 if best is not None: cur, logs = best[1], list(best[2]) # ⛔ **格点冲突 ≠ 真违规**。0.25 mm 的格子表示不了 0.35 mm 间距的分离, # 所以 PF 的冲突数里混着大量**量化假象**(卡在 ~895 下不去就是这个原因)。 # 真正的判据是**精确几何**(和 router_check 同一套)。这里按精确距离筛一遍, # 只把**真的**压在一起的网拿去重布。 real = _exact_conflicts(cur, pads) if real: print(u' 精确几何:真违规涉及 %d 个网 —— %s' % (len(real), ' '.join(sorted(real)[:10]))) base_cells = {} for n2, (s2, v2, c2) in cur.items(): if n2 not in real: base_cells[n2] = (s2, v2, c2) ns = list(base_s) + [t for n2 in base_cells for t in base_cells[n2][0]] nv = list(base_v) + [(x, y, n2) for n2 in base_cells for (x, y, _q) in base_cells[n2][1]] sub = route_groups(bypad, pads, ns, nv, board_vias, auto, priority=tuple(real), only=set(real)) logs += sub del segs[fixed_s:] del vias[fixed_v:] segs += ns[len(base_s):] vias += nv[len(base_v):] return logs # ⛔ **绝不能把还有冲突的解下发**(那是真短路)。还剩冲突就逐个丢掉 # 「压着别人最多」的那个网,直到干净为止,丢掉的如实报成没布通。 while True: occ2 = {l: bytearray(NX * NY) for l in LAY} for _n, (_s, _v, c2) in cur.items(): _pf_mark(occ2, c2, +1) blame = {} for net, (_s, _v, c2) in cur.items(): k = sum(1 for (l, i, j) in c2 if occ2[l][j * NX + i] > 1) if k: blame[net] = k if not blame: break worst = max(blame, key=lambda n: (blame[n], wof[n])) del cur[worst] logs.append(u'%s: 让路(协商后仍有冲突,整条丢弃)' % worst) del segs[fixed_s:] del vias[fixed_v:] for net, (s2, v2, _c) in cur.items(): segs += s2 vias += [(x, y, net) for (x, y, _n) in v2] return logs if __name__ == '__main__': if '--clear' in sys.argv: clear_mine() sys.exit(0) # 算一次要好几分钟,结果落盘;--push 默认吃缓存,加 --replan 才重算。 if '--replan' not in sys.argv and os.path.exists(CACHE): c = json.load(io.open(CACHE, encoding='utf-8')) segs, vias, logs = c['segs'], c['vias'], c['logs'] print(u'(用缓存 _blade_routes.json)') else: # ⛔ **一轮布不完是正常的**:谁先布谁占走廊,后布的就没路。 # 没有 rip-up 的话,最有效的补救是「**这一轮没接上的,下一轮排最前面**」, # 跑几轮取最好的一轮。实测 30 -> 个位数。 best, prio = None, set() for it in range(1 if '--pf' in sys.argv else PASSES): segs, vias, logs = plan_all(prio) print(u' 第 %d 轮:%d 条没搞定' % (it + 1, len(logs))) if best is None or len(logs) < len(best[2]): best = (segs, vias, logs) if not logs: break prio |= set(_msg_net(m) for m in logs) segs, vias, logs = best io.open(CACHE, 'w', encoding='utf-8').write( json.dumps({'segs': segs, 'vias': vias, 'logs': logs}, ensure_ascii=False)) tot = sum(math.hypot(s[3] - s[1], s[4] - s[2]) for s in segs) print(u'导线 %d 段 / 总长 %.1f mm / 过孔 %d' % (len(segs), tot, len(vias))) nets = {} for s in segs: nets[s[6]] = nets.get(s[6], 0) + 1 print(u'涉及 %d 个网络' % len(nets)) if logs: print(u'[!] %d 条没搞定:' % len(logs)) for m in logs[:40]: print(u' ' + m) if '--push' in sys.argv: push(segs, vias)