文件
phonerail/.claude/r2/router.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

2119 行
104 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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)