原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
1578 行
79 KiB
Python
1578 行
79 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""载板电源 + 杂散信号布线:**栅格迷宫布线器**(`pcb2_route_dp.py` 的姊妹件)。
|
||
|
||
/c/Python314/python .claude/pcb2_route_sig.py # 全部算 + 本地校核,不下发
|
||
/c/Python314/python .claude/pcb2_route_sig.py --net +5V # 只算这几个网络(可多个)
|
||
/c/Python314/python .claude/pcb2_route_sig.py --push # 下发(只删本脚本负责的网络)
|
||
/c/Python314/python .claude/pcb2_route_sig.py --plot out.png # 画一张自己算的图
|
||
|
||
## 为什么这里用布线器,差分对却是手写坐标
|
||
|
||
差分对只有 11 对,但每一对都要管耦合、参考面、偏斜、换层窗口 —— 是**几何设计**,
|
||
写死坐标才说得清理由。剩下这 60 来个网络是另一回事:三百多个端点、
|
||
大部分是「从 A 走到 B、别撞上东西」,逐条手写既慢又容易漏。
|
||
所以这里搭一个**能被同一套 `check()` 验的**布线器:路径怎么来的不重要,
|
||
下发前一律过 `pcb2_route_dp.check()`(和差分对同一份净距判据)。
|
||
|
||
## 障碍模型(三条都实测过,别改成"看着差不多"的写法)
|
||
|
||
1. **栅格 0.1 mm,量化误差按最坏算**。障碍按"格心落在铜里"栅格化,
|
||
再做欧氏距离变换(`scipy.ndimage.distance_transform_edt`)。
|
||
真实铜边最多比"最近的被标记格心"再近 `grid·√2/2 = 0.0708 mm`,
|
||
所以判据一律是 `edt >= 净距 + 半线宽 + QERR`,**QERR 不许省**。
|
||
2. **本轮自己画的线立刻进障碍表**。网络是一个一个布的,
|
||
后布的必须看得见先布的 —— 否则本地校核会在最后一起爆。
|
||
3. **通孔焊盘(layer 12)两层都算障碍**,过孔也是。板框按 220×135 R5 内缩 1 mm。
|
||
|
||
## BM24 的出线(`STUB`)不交给布线器
|
||
|
||
`J7n` 是 0.35 mm 间距,相邻焊盘净距只有 0.17 mm —— 0.1 mm 的栅格根本分辨不出来,
|
||
而且出线宽度要在 0.11 / 0.20 / 0.28 三档之间切换(算式见 `pcb2_route_dp.W_ARR`)。
|
||
所以这一小段**手写**:前排一律往南直出、后排一律往北直出(§25.8 的硬约束),
|
||
出了阵列再让布线器接手。GND 脚另外在阵列外并成一条梳子、共用过孔。
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
import numpy as np
|
||
from scipy import ndimage
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from pcb2_route_dp import (geom, check, expand, netof, push, MIL, PCB,
|
||
L_TOP, L_BOT, CLR, VIA_D, VIA_H, seg_rect, seg_pt, seg_seg,
|
||
chamfer_routes, sharp_corners)
|
||
from pcb2_place import SITES, to_board, BW, BH, PW, PH
|
||
|
||
GRID = 0.1 # 栅格步长 mm
|
||
QERR = GRID * math.sqrt(2) / 2 # 栅格量化误差上界 0.0708 mm,判据里必须加上
|
||
EDGE = 1.0 # 铜离板边的最小距离
|
||
CORNER_R = 5.0
|
||
VIA_R = VIA_D / 2.0
|
||
HOLE_CLR = 0.30 # 钻孔到钻孔(规则 otherClearance.hole2Hole),**不分网络**
|
||
VIA_COST = 2.5 # 换一次层折算成多少 mm 路程(压住"没事就打孔")
|
||
TURN_COST = 0.30 # 拐一次弯折算成多少 mm(换来长直段,少几百条碎线)
|
||
BBOX_PAD = 10.0 # 每次连线的搜索窗口比两端包围盒大多少
|
||
ASTAR_W = 1.3 # 加权 A*:h 乘这个数。放弃严格最优换速度
|
||
MAX_CELLS = 1200000 # 单层格数超过这个才把栅格放粗一档(只有跨半块板的网络会碰到)
|
||
|
||
W_ARR = 0.11 # 还在 BM24 焊盘阵列里那一段
|
||
W_ESC = 0.20 # 刚出阵列
|
||
W_SIG = 0.20 # 一般信号。**不是 0.25** —— Hub 的卫星圈是 1.2 mm 网格上的
|
||
# 0402,缝只有 1.2 − 0.541 = 0.659 mm,
|
||
# 0.25 的线要 2×(0.20+0.125) = 0.65,量化余量一进去就过不去;
|
||
# 0.20 要 0.60,留 0.06 的余量,实测这一档才布得通。
|
||
# XTAL1/2 必须 0.20:Hub 南侧竖井只有 2.16 mm,要并排过 4 根
|
||
# (XTAL ×2 + UP_DP/DM ×2)。0.25 的话 0.2 净距下正好塞不进去 —— 实测布不通。
|
||
# `+5V` 0.60(不是 0.80):§25.10 重排逐针分配之后它是**10 针分散取电**,
|
||
# 每个入口只喂本站的 U2n + C70n、段电流 <= 0.6 A,0.60 mm 外层 1 oz 可载 ~2 A。
|
||
# 0.80 在排母到服务带那条已经站满 EN/POGO 线的走廊里挤不过去(实测五处布不通)。
|
||
WIDTH = {'+5V': 0.60, '+3V3_HUB': 0.35, 'XTAL1': 0.20, 'XTAL2': 0.20}
|
||
for _n in range(1, 6):
|
||
WIDTH['VBUS%d' % _n] = 0.50
|
||
|
||
|
||
def width_of(net):
|
||
return WIDTH.get(net, W_SIG)
|
||
|
||
|
||
# ══════════════════ BM24 出线(手写,站内坐标 u/v)══════════════════
|
||
#
|
||
# ⚠️ **2026-09-06(第十三轮)整段按 v5 重写。** 老的一版是 v3/v4 的坐标,
|
||
# v5 把 J7n 从 rot 180 改成 rot 0 又挪了位置,站内坐标整体差一个仿射映射
|
||
# (`u' = u − 5.00`,`v' = 32.00 − v`,v 轴是**镜像**的)。
|
||
# 照抄映射结果也不行 —— v 一镜像,「前排南侧那条缝」跑到了北侧,
|
||
# 而北侧现在站着 R76n/R77n;净距必须拿 v5 的实际障碍重算。下面全部是实测值。
|
||
#
|
||
# 焊盘几何(`fp_bm24.py`,`--dump` 回读逐站核过,五个站完全同构):
|
||
# 后排(奇数脚,**全是 GND**)v = 33.940,u 48.30~56.70
|
||
# 中间两个大脚 v = 35.000,pin51(VBUS) u 46.47 / pin52(GND) u 58.53
|
||
# 前排(偶数脚) v = 36.060,u 48.30~56.70
|
||
# 两排都是 hh = 0.24;信号脚 hw = 0.0902,脚距 0.35 ⇒ 相邻焊盘边到边 0.1696
|
||
J7_BACK_V, J7_MID_V, J7_FRONT_V = 33.940, 35.000, 36.060
|
||
J7_PAD_HH = 0.24
|
||
# 出线换挡的 v:离焊盘外沿 0.35,0.20 的线到相邻脚的焊盘角还有余量
|
||
J7_FRONT_ESC = J7_FRONT_V + J7_PAD_HH + 0.35 # 36.65,往**外**(+v)
|
||
J7_BACK_ESC = J7_BACK_V - J7_PAD_HH - 0.35 # 33.35,往**外**(−v)
|
||
J7_FRONT_IN = J7_FRONT_V - J7_PAD_HH - 0.35 # 35.47,前排往**内**(进中央通道)
|
||
|
||
# GND 脚的 u(`--check` 会逐站断言)
|
||
J7_GND_FRONT = [48.30, 49.35, 50.05, 50.75, 52.15] # pin 2/8/12/16/24
|
||
J7_GND_BACK = [48.30, 51.45, 52.15, 53.20, 54.60, 55.30, 56.35] # pin 1/19/23/29/37/41/47
|
||
J7_GND_BIG = 58.53 # pin 52,0.49×0.76 大焊盘
|
||
J7_THERM_U, J7_CC1_U = 50.40, 51.80 # pin 14 口温 / pin 22 CC1
|
||
|
||
# ══ v5 的车道分配(和 v4 换了个方向,理由在下面)══
|
||
#
|
||
# ┌ 前排外侧 v > 36.30 ── **只给 THERM / CC1**(R76n/R77n 就在正北 v 38.0)
|
||
# │ 中央通道 v ∈ [34.18, 35.82],净高 1.64 ── **前排 5 个 GND 的梳子**
|
||
# └ 后排外侧 v < 33.70 ── **后排 7 个 GND 的梳子**,一路空到 v 26,最宽敞
|
||
#
|
||
# 为什么和 v4 相反:v5 的**后排 25 个脚全是 GND**(没有信号脚要往外挤),
|
||
# 而 THERM(pin14)/CC1(pin22) 都在前排、目标 R76n/R77n 也在前排外侧 ——
|
||
# 让它们各走各的最短路,中央通道就整条空出来给前排 GND,不再有
|
||
# v4 那个「东行车道被竖着穿过通道的 GND 线切断」的死结。
|
||
#
|
||
# 中央通道梳子 v = 35.00(正中):到两排焊盘内沿各 0.82
|
||
# 0.30 的线:0.82 − 0.15 = 0.67 ✔ 过孔:0.82 − 0.305 = 0.515 ✔(要 0.50)
|
||
# ⚠️ 梳子只走到 u 52.15(最东那个前排 GND 脚),离 pin52 焊盘西沿 58.285 还有 6 mm,
|
||
# 所以通道里这条梳子和 pin51/pin52 两个大脚互不相干。
|
||
J7_MID_COMB = J7_MID_V # 35.00
|
||
J7_MID_VIA_U = [48.30, 49.35, 50.75, 52.15] # 相邻间距 1.05/1.40/1.40 >= 0.81 ✔(跳过 50.05)
|
||
#
|
||
# 后排外侧梳子 v = 33.10:到后排焊盘外沿 33.70 有 0.60
|
||
# 0.30 的线余 0.45 ✔ 过孔余 0.295 ✔(要 0.20;离焊盘 0.60 − 0.305 = 0.295)
|
||
# ⚠️ 这一带在**尾插排线车道 `TAIL_LANE` 里**(u 44.6~62.6 / v −1.2~36.3)。
|
||
# 车道的约束是「不放元件」,走线和过孔是允许的(排线平贴着盖过去)。
|
||
J7_BACK_COMB = 33.10
|
||
J7_BACK_VIA_U = [48.30, 52.15, 53.20, 54.60, 56.35] # 间距 3.85/1.05/1.40/1.75 >= 0.81 ✔
|
||
#
|
||
# pin52(GND 大焊盘,u 58.53 / v 35.00):往**东**平着出去打孔,
|
||
# 离 J7n 最东那列焊盘 (u 56.79) 2.8、离手机定位柱 PnH3 (u 61.90, v 33.70, Ø2.5) 1.05 ✔
|
||
J7_PIN52_VIA_U = 59.60
|
||
|
||
W_COMB = 0.30
|
||
W_LANE = 0.20
|
||
|
||
# ── THERM / CC1:都往前排外侧出,THERM 走顶层跳过 CC1 ──
|
||
#
|
||
# 两个脚一个要往东(THERM → R77n.1 u 54.367)、一个要往北(CC1 → R76n.1 u 51.800),
|
||
# **在 u ≈ 51.8 必然交叉**,同层无解。CC1 是最短的那根,让它全程底层直上;
|
||
# THERM 打两个孔从顶层跨过去。
|
||
# 西孔 (50.95, 37.20):离 `*_DPJ` 的换层孔 (50.00, 37.00) **0.97 ≥ 0.81**(孔到孔);
|
||
# 离 J7n.16/18 焊盘 0.86;离 R76n.1 焊盘框 0.78 ✔
|
||
# 东孔 (54.37, 37.20):离 J7n.38 焊盘 0.90;R77n.1 是同网,不设限 ✔
|
||
# 顶层那一段 v = 37.20:顶层在 J7n 这一带**一根线都没有**(`*_DPJ` 的顶层小跳线在 u ≤ 50.0)
|
||
# R76n 的中心特意取 52.233,好让 pin1 正落在 u 51.800 = J7n.22 正北 ⇒ CC1 是一根直线。
|
||
# ⚠️ THERM 出阵列后**第一次拐弯的 v 只有 0.053 mm 的窗口**,而且那一段必须一直保持 0.11:
|
||
# 往下:离前排焊盘外沿 36.30 要 w/2 + 0.20 ⇒ v >= 36.555
|
||
# 往上:离 `*_DPJ` 的换层孔 (50.00, 37.00) 要 0.305 + w/2 + 0.20 = 0.560,
|
||
# 而它在 u 上只差 0.40 ⇒ |v − 37.00| >= 0.392 ⇒ v <= 36.608
|
||
# 取窗口正中 36.58(上下各余 0.027)。**换成 0.15 的线窗口只剩 0.005,0.20 直接无解** ——
|
||
# 2026-09-06 第一版写 0.20 / 拐点 36.65,五个站一起报「线-过孔 净距 0.127」。
|
||
J7_THERM_TURN_V = 36.58
|
||
J7_THERM_VIA = [(50.80, 37.30), (54.367, 37.30)]
|
||
J7_THERM_KINK = (50.80, 36.98) # 45° 斜段的终点,之后竖着上到换层孔
|
||
J7_THERM_LANE_V = 37.30
|
||
J7_R76_PIN1_U, J7_R77_PIN1_U = 51.800, 54.367
|
||
J7_R_PAD_V = 38.000 # R76n/R77n 焊盘中心 v(`--check` 会核)
|
||
J7_R_ENTER_V = 37.90 # 进焊盘的落点(焊盘 v 37.730~38.270)
|
||
|
||
|
||
def _stub(u, v_pad, v_esc, w_out=W_ESC):
|
||
u"""BM24 一个脚的直出段(站内坐标):焊盘 -> 阵列外。"""
|
||
return [(u, v_pad, L_TOP, W_ARR), (u, v_esc, L_TOP, w_out)]
|
||
|
||
|
||
def bm24_stub(n, u, v_pad, v_esc, w_out=W_ESC):
|
||
u"""同上,映射到第 n 站的板坐标。"""
|
||
out = []
|
||
for u_, v_, lay, ww in _stub(u, v_pad, v_esc, w_out):
|
||
bx, by, _ = to_board(n, u_, v_, 0)
|
||
out.append((round(bx, 3), round(by, 3), lay, ww))
|
||
return out
|
||
|
||
|
||
# ══════════════════ 板框 ══════════════════
|
||
_CORNERS = ((CORNER_R, CORNER_R), (BW - CORNER_R, CORNER_R),
|
||
(CORNER_R, BH - CORNER_R), (BW - CORNER_R, BH - CORNER_R))
|
||
|
||
|
||
def board_mask(XX, YY, m=EDGE):
|
||
u"""格心在板内(含 R5 圆角、内缩 m)的布尔数组。"""
|
||
ok = (XX >= m) & (XX <= BW - m) & (YY >= m) & (YY <= BH - m)
|
||
for cx, cy in _CORNERS:
|
||
sx = 1.0 if cx < BW / 2 else -1.0
|
||
sy = 1.0 if cy < BH / 2 else -1.0
|
||
inc = ((XX - cx) * sx < 0) & ((YY - cy) * sy < 0)
|
||
ok &= ~(inc & (np.hypot(XX - cx, YY - cy) > CORNER_R - m))
|
||
return ok
|
||
|
||
|
||
# ══════════════════ 形状(障碍 / 本网铜都用这一套)══════════════════
|
||
def sh_rect(x, y, hw, hh, layers):
|
||
return ('rect', x, y, hw, hh, layers)
|
||
|
||
|
||
def sh_via(x, y, r=VIA_R):
|
||
return ('via', x, y, r, 0, (L_TOP, L_BOT))
|
||
|
||
|
||
def sh_seg(x0, y0, x1, y1, w, lay):
|
||
return ('seg', x0, y0, x1, y1, w / 2.0, (lay,))
|
||
|
||
|
||
def shapes_of(pads, vias, lines):
|
||
u"""把 geom() 的三张表变成障碍表:`[(形状, 网络), ...]`。
|
||
|
||
⚠️ **网络必须跟着形状走**:布某个网络时它自己的铜不能算障碍,
|
||
否则连焊盘都出不来(第一版就是漏了这条,25 个种子格一个邻居都扩不出去)。
|
||
"""
|
||
out = []
|
||
for p in pads:
|
||
lay = (L_TOP, L_BOT) if p['lay'] == 12 else (p['lay'],)
|
||
out.append((sh_rect(p['x'], p['y'], p['hw'], p['hh'], lay), p['net']))
|
||
for v in vias:
|
||
out.append((sh_via(v['x'], v['y'], v['r']), v['net']))
|
||
for l in lines:
|
||
out.append((sh_seg(l['x0'], l['y0'], l['x1'], l['y1'], l['w'], l['lay']), l['net']))
|
||
return out
|
||
|
||
|
||
# ══════════════════ 栅格场 ══════════════════
|
||
class Field(object):
|
||
u"""一个搜索窗口里的两层障碍距离场。
|
||
|
||
`edt[L][j, i]` = 该格心到**最近的异网铜**的距离(mm,含量化误差 QERR)。
|
||
判据统一是 `edt >= 净距 + 半线宽 + QERR`。
|
||
"""
|
||
|
||
#: 搜索窗口外面还要多留一圈**只标障碍、不许走线**的边(mm)。见 __init__ 里的注释。
|
||
RIM = 1.2
|
||
|
||
def __init__(self, obstacles, bbox, skip_net=None, grid=GRID):
|
||
x0, y0, x1, y1 = bbox
|
||
self.g = grid
|
||
self.qerr = grid * math.sqrt(2) / 2
|
||
# ⚠️ **窗口边上必须留一圈墙。** 障碍是裁到窗口里再栅格化的,
|
||
# 完全落在窗口外的铜一格都不标 —— 于是贴着窗口边走的线看不见它。
|
||
# 2026-09-06 实测:`SCL` 的搜索窗口从 x=44.02 起,J31.6 的焊盘框
|
||
# (42.10~43.80) 整个在窗外,布线器就在 (44.10, 67.40) 打了个孔,
|
||
# 离那个焊盘只有 0.134 —— 栅格判据全过,`check()` 才报出来。
|
||
# 做法:数组往外扩 RIM,障碍照标,**再把外面这一圈整体标成占用**。
|
||
# 可走的区域和原来一模一样,但现在看得见窗外 RIM 以内的铜。
|
||
rim = self.RIM
|
||
x0 -= rim; y0 -= rim; x1 += rim; y1 += rim
|
||
self.x0 = math.floor(x0 / grid) * grid
|
||
self.y0 = math.floor(y0 / grid) * grid
|
||
self.nx = int(round((x1 - self.x0) / grid)) + 2
|
||
self.ny = int(round((y1 - self.y0) / grid)) + 2
|
||
# ⚠️ **层集合不能写死 (L_TOP, L_BOT)**。下板从 2026-09-06 起在 **Inner2(16)**
|
||
# 上有真走线了(`pcb1_i2c_l3.py` 的 I²C 干线),障碍表里就会出现 layer 16 ——
|
||
# 写死两层的话这里直接 `KeyError: 16`;就算滤掉,**新过孔也不会对内层那些线
|
||
# 做净距判断**(`viafree` 是 min(edt[TOP], edt[BOT])),孔会直接压在内层干线上。
|
||
# 这里按障碍表里真正出现过的层建数组;只有 1/2 层时行为和以前一模一样。
|
||
self.lays = sorted(set([L_TOP, L_BOT])
|
||
| set(L for s, _n in obstacles for L in s[-1]))
|
||
occ = {L: np.zeros((self.ny, self.nx), bool) for L in self.lays}
|
||
# ⚠️ 障碍要**先胀 QERR 再栅格化**,见 `_mark` 的注释。
|
||
for s, net in obstacles:
|
||
if net == skip_net:
|
||
continue
|
||
for L in s[-1]:
|
||
self._mark(occ[L], s, grow=self.qerr)
|
||
# 板框外一律不许有铜
|
||
xs = self.x0 + np.arange(self.nx) * grid
|
||
ys = self.y0 + np.arange(self.ny) * grid
|
||
XX, YY = np.meshgrid(xs, ys)
|
||
out = ~board_mask(XX, YY)
|
||
# 外圈那一圈墙(障碍已经标完了,这里只是禁止走线)
|
||
nb = max(1, int(round(rim / grid)))
|
||
wall = np.zeros((self.ny, self.nx), bool)
|
||
wall[:nb, :] = True; wall[-nb:, :] = True
|
||
wall[:, :nb] = True; wall[:, -nb:] = True
|
||
out |= wall
|
||
for L in self.lays:
|
||
occ[L] |= out
|
||
self.occ = occ
|
||
self.edt = {L: ndimage.distance_transform_edt(~occ[L], sampling=grid).astype(np.float32)
|
||
for L in self.lays}
|
||
# 过孔是通孔 ⇒ **每一层**都要让开(含 Inner2 上的真走线)
|
||
self.viafree = np.minimum.reduce([self.edt[L] for L in self.lays])
|
||
# ⚠️ **钻孔间距不分网络**。铜可以重叠,孔不行 —— 规则 otherClearance.hole2Hole = 0.30。
|
||
# `viafree` 是「到**异网**铜的距离」,同网的过孔在它眼里根本不存在,
|
||
# 于是布线器会紧挨着 `pcb2_dp_long` 已经打好的孔再打一个:
|
||
# 2026-09-06 实测 (92.500, 70.000) 与 (92.400, 70.500) 中心只隔 0.510,
|
||
# DRC 报「钻孔 到 钻孔 8.1mil,应 >= 11.8mil」,而净距校核一声不吭。
|
||
occh = np.zeros((self.ny, self.nx), bool)
|
||
for s, _net in obstacles:
|
||
if s[0] == 'via':
|
||
self._mark(occh, ('via', s[1], s[2], VIA_H / 2.0, 0, s[5]), grow=self.qerr)
|
||
occh |= out
|
||
self.holefree = ndimage.distance_transform_edt(
|
||
~occh, sampling=grid).astype(np.float32)
|
||
|
||
# ── 栅格化 ──
|
||
def _idx(self, x, y):
|
||
return (int(round((x - self.x0) / self.g)), int(round((y - self.y0) / self.g)))
|
||
|
||
def _win(self, xa, xb, ya, yb):
|
||
i0 = max(0, int(math.ceil((xa - self.x0) / self.g)))
|
||
i1 = min(self.nx - 1, int(math.floor((xb - self.x0) / self.g)))
|
||
j0 = max(0, int(math.ceil((ya - self.y0) / self.g)))
|
||
j1 = min(self.ny - 1, int(math.floor((yb - self.y0) / self.g)))
|
||
return i0, i1, j0, j1
|
||
|
||
def _mark(self, mask, s, grow=0.0):
|
||
u"""把一个形状打进布尔栅格。
|
||
|
||
⚠️ **`grow` 不是保险丝,是判据的一部分。** 只标"格心落在铜里"的格时,
|
||
铜的边界到最近的被标记格心可以差到将近一整格(实测:一个 Ø0.61 的过孔
|
||
边缘在 x=52.781,最近的格心在 52.7,差 0.081 > √2/2·0.1),
|
||
于是 `edt` 会**高估** 0.08 mm,净距校核就在下发前一刻才炸。
|
||
把障碍先胀 `QERR = g·√2/2` 再栅格化,任何铜边界点周围
|
||
半径 QERR 内一定有被标记的格 ⇒ `真实净距 >= edt − QERR` 严格成立,
|
||
判据 `edt >= 净距 + 半线宽 + QERR` 才真的是充分条件。
|
||
"""
|
||
k = s[0]
|
||
if k == 'rect':
|
||
_, x, y, hw, hh, _ = s
|
||
hw, hh = hw + grow, hh + grow
|
||
i0, i1, j0, j1 = self._win(x - hw, x + hw, y - hh, y + hh)
|
||
if i0 <= i1 and j0 <= j1:
|
||
mask[j0:j1 + 1, i0:i1 + 1] = True
|
||
return
|
||
if k == 'via':
|
||
_, x, y, r, _, _ = s
|
||
r = r + grow
|
||
i0, i1, j0, j1 = self._win(x - r, x + r, y - r, y + r)
|
||
if i0 > i1 or j0 > j1:
|
||
return
|
||
xs = self.x0 + np.arange(i0, i1 + 1) * self.g
|
||
ys = self.y0 + np.arange(j0, j1 + 1) * self.g
|
||
XX, YY = np.meshgrid(xs, ys)
|
||
mask[j0:j1 + 1, i0:i1 + 1] |= ((XX - x) ** 2 + (YY - y) ** 2) <= r * r
|
||
return
|
||
_, xa, ya, xb, yb, h, _ = s
|
||
h = h + grow
|
||
i0, i1, j0, j1 = self._win(min(xa, xb) - h, max(xa, xb) + h,
|
||
min(ya, yb) - h, max(ya, yb) + h)
|
||
if i0 > i1 or j0 > j1:
|
||
return
|
||
xs = self.x0 + np.arange(i0, i1 + 1) * self.g
|
||
ys = self.y0 + np.arange(j0, j1 + 1) * self.g
|
||
XX, YY = np.meshgrid(xs, ys)
|
||
dx, dy = xb - xa, yb - ya
|
||
L2 = dx * dx + dy * dy
|
||
if L2 < 1e-12:
|
||
d2 = (XX - xa) ** 2 + (YY - ya) ** 2
|
||
else:
|
||
t = np.clip(((XX - xa) * dx + (YY - ya) * dy) / L2, 0.0, 1.0)
|
||
d2 = (XX - (xa + t * dx)) ** 2 + (YY - (ya + t * dy)) ** 2
|
||
mask[j0:j1 + 1, i0:i1 + 1] |= d2 <= h * h
|
||
|
||
def mask_of(self, shapes):
|
||
m = {L: np.zeros((self.ny, self.nx), bool) for L in self.lays}
|
||
for s in shapes:
|
||
for L in s[-1]:
|
||
self._mark(m[L], s)
|
||
return m
|
||
|
||
# ── 判据 ──
|
||
# `relax` = **本网自己的焊盘**所占的格。焊盘里本来就有铜,
|
||
# 线从焊盘中心出来的那一小段落在焊盘内部,不该拿净距去卡它
|
||
# (0402 的两个焊盘只隔 0.5 mm,卡下去等于每个器件都出不了线)。
|
||
# 只放宽焊盘,不放宽本网的**导线** —— 导线本身是合法的,它所在的格 edt 也够。
|
||
def set_relax(self, shapes):
|
||
self.relax = self.mask_of(shapes)
|
||
|
||
def free(self, lay, w):
|
||
f = self.edt[lay] >= CLR + w / 2.0 + self.qerr
|
||
if getattr(self, 'relax', None) is not None:
|
||
f = f | self.relax[lay]
|
||
return f
|
||
|
||
def via_ok(self):
|
||
return ((self.viafree >= CLR + VIA_R + self.qerr)
|
||
& (self.holefree >= VIA_H / 2.0 + HOLE_CLR + self.qerr))
|
||
|
||
def clear_seg(self, x0, y0, x1, y1, lay, w):
|
||
u"""一段任意角度的线是否安全(沿线采样,EDT 是 1-Lipschitz 所以可加采样余量)。"""
|
||
need = CLR + w / 2.0 + self.qerr
|
||
L = math.hypot(x1 - x0, y1 - y0)
|
||
n = max(2, int(L / (self.g / 2.0)) + 1)
|
||
step = L / (n - 1) if n > 1 else 0.0
|
||
need += step / 2.0
|
||
rl = getattr(self, 'relax', None)
|
||
for k in range(n):
|
||
t = k / float(n - 1) if n > 1 else 0.0
|
||
i, j = self._idx(x0 + t * (x1 - x0), y0 + t * (y1 - y0))
|
||
if not (0 <= i < self.nx and 0 <= j < self.ny):
|
||
return False
|
||
if self.edt[lay][j, i] < need and not (rl is not None and rl[lay][j, i]):
|
||
return False
|
||
return True
|
||
|
||
|
||
# ══════════════════ A* ══════════════════
|
||
DIRS = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
|
||
|
||
|
||
def _edges(ok, dx, dy, nx, ny, base):
|
||
u"""某一层上、沿 (dx, dy) 这个方向的所有合法走步 -> (起点索引, 终点索引)。
|
||
|
||
斜着走要**额外**要求两个正交邻格也是通的 —— 否则线会从两个障碍的夹角里"钻"过去。
|
||
"""
|
||
xs0, xs1 = max(0, -dx), nx - max(0, dx)
|
||
ys0, ys1 = max(0, -dy), ny - max(0, dy)
|
||
if xs1 <= xs0 or ys1 <= ys0:
|
||
e = np.empty(0, np.int64)
|
||
return e, e
|
||
m = ok[ys0:ys1, xs0:xs1] & ok[ys0 + dy:ys1 + dy, xs0 + dx:xs1 + dx]
|
||
if dx and dy:
|
||
m = m & ok[ys0:ys1, xs0 + dx:xs1 + dx] & ok[ys0 + dy:ys1 + dy, xs0:xs1]
|
||
jj, ii = np.nonzero(m)
|
||
jj = jj.astype(np.int64) + ys0
|
||
ii = ii.astype(np.int64) + xs0
|
||
return base + jj * nx + ii, base + (jj + dy) * nx + (ii + dx)
|
||
|
||
|
||
def route_grid(f, src, tgt, w):
|
||
u"""多源多汇最短路。`src`/`tgt` 是 {层: 布尔数组}。返回 [(i, j, 层), ...] 或 None。
|
||
|
||
代价 = 路程(mm) + 每次换层 `VIA_COST`。
|
||
|
||
## 为什么是 scipy 的 Dijkstra,不是自己写的 A*
|
||
|
||
第一版是纯 Python 的加权 A*(还带拐弯惩罚)。**跨半块板的网络跑不动** ——
|
||
`VBUS_UP`(J23 → Hub,95 mm)在 0.1 mm 栅格上是 140 万个节点,
|
||
两分钟没跑完,放粗到 0.2 mm 又因为净距判据跟着变严而布不通。
|
||
改成把整个栅格建成稀疏图交给 `scipy.sparse.csgraph.dijkstra`(C 实现、多源、
|
||
带前驱),同一个网络降到秒级。
|
||
|
||
代价是**丢了拐弯惩罚**(Dijkstra 表达不了"到达方向"),
|
||
最短路会退化成楼梯。所以 `polish()` 必须做「最远可视 L 形合并」把楼梯收回来 ——
|
||
这两件事是配套的,只改一半会得到几百条碎线。
|
||
"""
|
||
from scipy.sparse import coo_matrix
|
||
from scipy.sparse.csgraph import dijkstra
|
||
|
||
nx, ny, g = f.nx, f.ny, f.g
|
||
LAY = [L_TOP, L_BOT]
|
||
ok = [f.free(L, w) for L in LAY]
|
||
N = nx * ny
|
||
rows, cols, vals = [], [], []
|
||
for li in (0, 1):
|
||
for dx, dy in ((1, 0), (0, 1), (1, 1), (1, -1)):
|
||
fr, to = _edges(ok[li], dx, dy, nx, ny, li * N)
|
||
if len(fr):
|
||
rows.append(fr)
|
||
cols.append(to)
|
||
vals.append(np.full(len(fr), g * (math.sqrt(2) if dx and dy else 1.0),
|
||
np.float32))
|
||
vm = f.via_ok() & ok[0] & ok[1]
|
||
jj, ii = np.nonzero(vm)
|
||
if len(jj):
|
||
idx = jj.astype(np.int64) * nx + ii.astype(np.int64)
|
||
rows.append(idx)
|
||
cols.append(idx + N)
|
||
vals.append(np.full(len(idx), VIA_COST, np.float32))
|
||
if not rows:
|
||
return None
|
||
G = coo_matrix((np.concatenate(vals),
|
||
(np.concatenate(rows), np.concatenate(cols))),
|
||
shape=(2 * N, 2 * N)).tocsr()
|
||
|
||
def flat(mask):
|
||
out = []
|
||
for li in (0, 1):
|
||
jj, ii = np.nonzero(mask[LAY[li]] & ok[li])
|
||
out.append(jj.astype(np.int64) * nx + ii.astype(np.int64) + li * N)
|
||
return np.concatenate(out)
|
||
|
||
si, ti = flat(src), flat(tgt)
|
||
if not len(si) or not len(ti):
|
||
return None
|
||
D, pred, _srcs = dijkstra(G, directed=False, indices=si, min_only=True,
|
||
return_predecessors=True)
|
||
d = D[ti]
|
||
k = int(np.argmin(d))
|
||
if not np.isfinite(d[k]):
|
||
return None
|
||
out = []
|
||
idx = int(ti[k])
|
||
seen = set()
|
||
while idx >= 0 and idx not in seen:
|
||
seen.add(idx)
|
||
li, r = divmod(idx, N)
|
||
j, i = divmod(r, nx)
|
||
out.append((i, j, LAY[li]))
|
||
idx = int(pred[idx])
|
||
out.reverse()
|
||
return out
|
||
|
||
|
||
# ══════════════════ 路径整形 ══════════════════
|
||
def _runs(cells):
|
||
u"""8 邻域格路 -> 顶点表(同层的共线段合并;换层处断开)。"""
|
||
pts = [cells[0]]
|
||
for k in range(1, len(cells) - 1):
|
||
i0, j0, l0 = cells[k - 1]
|
||
i1, j1, l1 = cells[k]
|
||
i2, j2, l2 = cells[k + 1]
|
||
if l0 != l1 or l1 != l2:
|
||
pts.append(cells[k])
|
||
continue
|
||
if (i1 - i0, j1 - j0) != (i2 - i1, j2 - j1):
|
||
pts.append(cells[k])
|
||
pts.append(cells[-1])
|
||
return pts
|
||
|
||
|
||
def _l_shapes(a, b):
|
||
u"""A->B 的 45°/90° 两段折线(两种拐法);本来就是 8 方向的直接返回一段。"""
|
||
(x0, y0), (x1, y1) = a, b
|
||
dx, dy = x1 - x0, y1 - y0
|
||
if abs(dx) < 1e-9 or abs(dy) < 1e-9 or abs(abs(dx) - abs(dy)) < 1e-9:
|
||
return [[a, b]]
|
||
m = min(abs(dx), abs(dy))
|
||
sx = math.copysign(1.0, dx)
|
||
sy = math.copysign(1.0, dy)
|
||
return [[a, (x0 + sx * m, y0 + sy * m), b], # 先斜后正
|
||
[a, (x1 - sx * m, y1 - sy * m), b]] # 先正后斜
|
||
|
||
|
||
def polish(f, pts, w):
|
||
u"""顶点表 -> 少而长的 45°/90° 段。逐层做,换层点原样保留。"""
|
||
out = []
|
||
seg_start = 0
|
||
for k in range(len(pts)):
|
||
last = (k == len(pts) - 1)
|
||
if not last and pts[k + 1][2] == pts[k][2]:
|
||
continue
|
||
chunk = pts[seg_start:k + 1]
|
||
lay = chunk[0][2]
|
||
xy = [(f.x0 + i * f.g, f.y0 + j * f.g) for i, j, _ in chunk]
|
||
out.append((lay, _polish_one(f, xy, lay, w)))
|
||
seg_start = k + 1
|
||
return out
|
||
|
||
|
||
def _polish_one(f, xy, lay, w):
|
||
if len(xy) <= 2:
|
||
return xy
|
||
def try_j(i, j):
|
||
for cand in _l_shapes(xy[i], xy[j]):
|
||
if all(f.clear_seg(a[0], a[1], b[0], b[1], lay, w)
|
||
for a, b in zip(cand, cand[1:])):
|
||
return cand
|
||
return None
|
||
|
||
res = [xy[0]]
|
||
i = 0
|
||
n = len(xy)
|
||
while i < n - 1:
|
||
# 先指数外推找到"最后一个还通的 j"的量级,再二分收敛。
|
||
# 逐个从远往近试是 O(n²) 次几何校核,长线(上千个格)会卡住。
|
||
lo, best = i + 1, try_j(i, i + 1)
|
||
step = 1
|
||
while lo + step <= n - 1:
|
||
c = try_j(i, lo + step)
|
||
if c is None:
|
||
break
|
||
lo, best = lo + step, c
|
||
step *= 2
|
||
hi = min(n - 1, lo + step)
|
||
while lo + 1 < hi:
|
||
mid = (lo + hi) // 2
|
||
c = try_j(i, mid)
|
||
if c is None:
|
||
hi = mid
|
||
else:
|
||
lo, best = mid, c
|
||
if best is None:
|
||
res.append(xy[i + 1])
|
||
i += 1
|
||
continue
|
||
res.extend(best[1:])
|
||
i = lo
|
||
# 去掉共线冗余点
|
||
out = [res[0]]
|
||
for k in range(1, len(res) - 1):
|
||
ax, ay = out[-1]
|
||
bx, by = res[k]
|
||
cx, cy = res[k + 1]
|
||
if abs((bx - ax) * (cy - by) - (by - ay) * (cx - bx)) > 1e-9:
|
||
out.append(res[k])
|
||
out.append(res[-1])
|
||
return out
|
||
|
||
|
||
# ══════════════════ 网络布线 ══════════════════
|
||
class Comp(object):
|
||
u"""一个网络里已经连通的一坨铜。
|
||
|
||
`center` 只在"这一坨就是一个焊盘"时非空,值是**焊盘中心**。
|
||
⚠️ 这一条不是锦上添花:`half()` 给圆焊盘返回的是**外接矩形**,
|
||
路径若停在矩形角上(Ø1.7 的排母脚差 0.32 mm)实际上根本没搭到铜。
|
||
所以单焊盘的起终点一律钉在圆心,A* 的种子/目标也只取圆心那一格。
|
||
"""
|
||
|
||
def __init__(self, shapes, anchor, center=None, layers=None):
|
||
self.shapes = list(shapes)
|
||
self.anchor = anchor # (x, y) 只用来算"离谁最近"
|
||
self.center = center
|
||
self.layers = layers
|
||
|
||
def merge(self, other, extra):
|
||
self.shapes += other.shapes + extra
|
||
self.center = None
|
||
self.layers = None
|
||
|
||
|
||
FINE_PAD = 0.31 # 焊盘短边半长 < 这个数就算"细间距",出线要先直出
|
||
STUB_EXT = (0.30, 0.20, 0.12) # 直出到焊盘外多少(依次退让)
|
||
_CENTROID = {}
|
||
|
||
|
||
def _centroid(ref, pads_all):
|
||
if ref not in _CENTROID:
|
||
ps = [p for p in pads_all if p['ref'] == ref]
|
||
_CENTROID[ref] = (sum(p['x'] for p in ps) / len(ps),
|
||
sum(p['y'] for p in ps) / len(ps)) if ps else None
|
||
return _CENTROID[ref]
|
||
|
||
|
||
def clear_exact(x0, y0, x1, y1, lay, w, net, obstacles):
|
||
u"""一段线到**异网铜**的真实最小净距(不走栅格,逐个形状精确算)。"""
|
||
h = w / 2.0
|
||
best = 1e9
|
||
a, b = (x0, y0), (x1, y1)
|
||
for s, n in obstacles:
|
||
if n == net or lay not in s[-1]:
|
||
continue
|
||
if s[0] == 'rect':
|
||
d = seg_rect(a, b, s[1], s[2], s[3], s[4]) - h
|
||
elif s[0] == 'via':
|
||
d = seg_pt(x0, y0, x1, y1, s[1], s[2]) - s[3] - h
|
||
else:
|
||
d = seg_seg(a, b, (s[1], s[2]), (s[3], s[4])) - s[5] - h
|
||
if d < best:
|
||
best = d
|
||
return best
|
||
|
||
|
||
ELONGATED = 1.3 # 长短边之比超过这个,就认为焊盘"有方向"
|
||
|
||
|
||
def entry_stub(q, pads_all, obstacles, net, w, all_cands=False):
|
||
u"""细间距焊盘的**直出段**:从焊盘中心直着走出焊盘外一小截,路由器从这一截接手。
|
||
|
||
不这么做的话最短路会从焊盘的**侧面**或**角上**离开:
|
||
QFN(0.5 脚距) 和 0402 的邻脚就在 0.2~0.4 mm 外,一出来就违规。
|
||
上一版 `XTAL1` 进 U32.61 是这么卡死的(布不通),
|
||
`+3V3_HUB` 进 U32.64 更糟 —— 它从**西侧**横着挤进去,离 U32.63 只剩 0.10 mm。
|
||
|
||
## 方向怎么定(两条规则,缺一不可)
|
||
|
||
1. **焊盘明显是长条的(长短边 ≥ 1.3)⇒ 沿长轴出**。这是封装设计者的意思:
|
||
长轴指向元件外面。QFN 南边一排是 0.30×0.851,J23 的信号脚是 1.30×0.30。
|
||
2. **接近方的(0402 焊盘 0.50×0.541、晶振 1.40×1.20)⇒ 沿"背离元件形心"那根轴出**。
|
||
|
||
只按第 2 条会错得很难看:`J23.A5` 上下各有一个焊盘、左右空着,
|
||
而形心被四个屏蔽脚拉到东边 ⇒ 判成"往南出",**正好撞在 B8 焊盘上**,
|
||
于是没有出线段,两个 2 mm 远的焊盘反而布不通。
|
||
"""
|
||
if q['lay'] == 12 or min(q['hw'], q['hh']) >= FINE_PAD:
|
||
return [] if all_cands else None
|
||
cen = _centroid(q['ref'], pads_all)
|
||
if cen is None:
|
||
return [] if all_cands else None
|
||
dx, dy = q['x'] - cen[0], q['y'] - cen[1]
|
||
if q['hw'] >= q['hh'] * ELONGATED:
|
||
axes = ('x', 'y')
|
||
elif q['hh'] >= q['hw'] * ELONGATED:
|
||
axes = ('y', 'x')
|
||
else:
|
||
axes = ('x', 'y') if abs(dx) >= abs(dy) else ('y', 'x')
|
||
# 候选:主轴背离形心 -> 次轴背离形心 -> 主轴朝向形心(万不得已)
|
||
cands = []
|
||
for ax in axes:
|
||
d = dx if ax == 'x' else dy
|
||
cands.append((ax, 1.0 if d >= 0 else -1.0))
|
||
cands.append((axes[0], -cands[0][1]))
|
||
ws = min(w, 2 * min(q['hw'], q['hh']))
|
||
out = []
|
||
for ax, sgn in cands:
|
||
for ext in STUB_EXT:
|
||
if ax == 'x':
|
||
end = (q['x'] + sgn * (q['hw'] + ext), q['y'])
|
||
else:
|
||
end = (q['x'], q['y'] + sgn * (q['hh'] + ext))
|
||
if clear_exact(q['x'], q['y'], end[0], end[1], q['lay'], ws, net, obstacles) >= CLR:
|
||
out.append((end, ws))
|
||
break # 同一个方向只留最长的那一档
|
||
if not out:
|
||
return [] if all_cands else None
|
||
return out if all_cands else out[0]
|
||
|
||
|
||
def plan_stubs(order, pads_all, obstacles, pre_pads):
|
||
u"""**先给所有网络的所有焊盘留好出线段,再开始布线。**
|
||
|
||
这一步不是优化,是必需的。第一版是"轮到哪个网络才现画它的出线段",
|
||
于是先布的网络会把后布的网络**出焊盘的那 0.6 mm 占掉**:
|
||
|
||
· `VBUS2` 抄近路斜穿 U25 东侧 ⇒ `+5V` 出 U25.1 只剩 −0.02 mm,
|
||
出线段只好掉头往**西**(钻进器件内部),整条 `+5V` 就此布不通;
|
||
· `+3V3_HUB` 从西边竖着切进 U42.5 ⇒ 隔壁 U42.6 的 `EDL_EN2` 出不来,
|
||
而这两个焊盘只隔 0.95 mm。
|
||
|
||
先把出线段全部落地当障碍,等于给每个焊盘**预留了逃逸通道**,
|
||
这是布线器的常规做法(escape routing 先行)。
|
||
"""
|
||
stubs, alts = {}, {}
|
||
for net in order:
|
||
w = width_of(net)
|
||
for q in pads_all:
|
||
key = (q['ref'], q['num'])
|
||
if q['net'] != net or key in pre_pads:
|
||
continue
|
||
cand = entry_stub(q, pads_all, obstacles, net, w, all_cands=True)
|
||
if not cand:
|
||
continue
|
||
alts[key] = cand
|
||
end, ws = cand[0]
|
||
stubs[key] = (end, ws)
|
||
# 只**预留**首选方向。备选方向不占位 —— 它们只在首选走不通时才拿出来试,
|
||
# 那时候本来就是"能捡到什么算什么"。
|
||
obstacles.append((sh_seg(q['x'], q['y'], end[0], end[1], ws, q['lay']), net))
|
||
return stubs, alts
|
||
|
||
|
||
def route_net(net, pads_all, obstacles, w=None, pre=None, verbose=True,
|
||
stubs=None, alts=None):
|
||
u"""把一个网络布通。返回 [路径, ...](每条是 `expand()` 认的点表)。
|
||
|
||
`pre` 是手写前置段(BM24 出线 / GND 梳子),它们**先算进本网铜**,
|
||
然后端点参与后面的最小生成树。
|
||
`stubs` 是 `plan_stubs()` 预留好的出线段(已经在障碍表里了,这里只取用)。
|
||
`alts` 是同一个焊盘的**备选出线方向**:首选方向布不通时换一个再试。
|
||
|
||
⚠️ 为什么需要备选:出线方向是按「背离元件形心」定的,那只是个启发式。
|
||
`R709.1`(VBUS_UP)在 Hub 卫星圈 A/B 两圈之间,背离形心 = **朝东**,
|
||
正好扎进 A/B 圈之间那条 0.268 mm 的死胡同 —— 出线段本身是合法的(只有 0.59 mm 长),
|
||
所以 `plan_stubs` 挑中了它,然后整个网络就再也接不上这个焊盘。
|
||
实测:只布这一个网络、整块板别的线都不存在,照样布不通 ⇒ 不是拥挤,是方向选错了。
|
||
"""
|
||
w = w or width_of(net)
|
||
paths = list(pre or [])
|
||
comps = []
|
||
used = set()
|
||
for p in pre or []:
|
||
shp = []
|
||
for x0, y0, x1, y1, lay, ww in expand(p)[0]:
|
||
shp.append(sh_seg(x0, y0, x1, y1, ww, lay))
|
||
for x, y in expand(p)[1]:
|
||
shp.append(sh_via(x, y))
|
||
# ⚠️ 手写前置段也必须进障碍表。漏了这一条的后果是**后面的网络看不见它们** ——
|
||
# BM24 那五组车道是最先落的铜,`POGO2_PWR` 却从中间穿了过去,
|
||
# 而且只有最后一步 `check()` 才报出来(−0.200,实打实压在一起)。
|
||
obstacles.extend((sp, net) for sp in shp)
|
||
for q in pads_all:
|
||
if q['net'] != net:
|
||
continue
|
||
if seg_rect((p[0][0], p[0][1]), (p[0][0], p[0][1]),
|
||
q['x'], q['y'], q['hw'], q['hh']) < 1e-6:
|
||
used.add((q['ref'], q['num']))
|
||
comps.append(Comp(shp, (p[-1][0], p[-1][1])))
|
||
for q in pads_all:
|
||
if q['net'] != net or (q['ref'], q['num']) in used:
|
||
continue
|
||
lay = (L_TOP, L_BOT) if q['lay'] == 12 else (q['lay'],)
|
||
rect = sh_rect(q['x'], q['y'], q['hw'], q['hh'], lay)
|
||
st = (stubs or {}).get((q['ref'], q['num']))
|
||
if st is None:
|
||
comps.append(Comp([rect], (q['x'], q['y']), center=(q['x'], q['y']), layers=lay))
|
||
continue
|
||
end, ws = st
|
||
paths.append([(round(q['x'], 4), round(q['y'], 4), q['lay'], ws),
|
||
(round(end[0], 4), round(end[1], 4), q['lay'], ws)])
|
||
seg = sh_seg(q['x'], q['y'], end[0], end[1], ws, q['lay'])
|
||
c = Comp([rect, seg], end, center=end, layers=lay)
|
||
c.pad, c.rect, c.stub_i = q, rect, len(paths) - 1 # 换出线方向时要用
|
||
c.stub_alts = [a for a in (alts or {}).get((q['ref'], q['num']), [])[1:]]
|
||
comps.append(c)
|
||
if len(comps) < 2:
|
||
return paths
|
||
|
||
# `relax`(允许贴着走的自网焊盘)**只放没有出线段的那些**。
|
||
# 有出线段的焊盘已经有了合法的进出口,再把整个焊盘放开的话,
|
||
# 最短路会**从焊盘的另一头钻进来**:实测 `+3V3_HUB` 就这么顺着 R715.2 的
|
||
# 焊盘北沿爬进去,那一小段铜离 R715.1 只有 0.175 mm(0402 自己两个焊盘才隔 0.30)。
|
||
relax = [sh_rect(q['x'], q['y'], q['hw'], q['hh'],
|
||
(L_TOP, L_BOT) if q['lay'] == 12 else (q['lay'],))
|
||
for q in pads_all
|
||
if q['net'] == net and (q['ref'], q['num']) not in (stubs or {})]
|
||
|
||
# 生成树用 **Prim**:每一步挑「离**已连上的整坨**最近的那个」。
|
||
# 第一版是"离上一个接上的点最近"(最近邻链),在 33 个焊盘的 `+3V3_HUB` 上
|
||
# 会串成一条长蛇,好几段被逼着绕过整个 Hub 卫星圈。
|
||
cur = comps[0]
|
||
rest = comps[1:]
|
||
anchors = [cur.anchor]
|
||
while rest:
|
||
rest.sort(key=lambda c: min(math.hypot(c.anchor[0] - a[0], c.anchor[1] - a[1])
|
||
for a in anchors))
|
||
tgt = rest.pop(0)
|
||
# 宽度阶梯:正常宽度过不去就收窄再试。Hub 卫星圈 / 排母缝这些地方
|
||
# 差的往往就是几十微米,收到 0.15 就通了,而载流上这些都是信号线。
|
||
seg = None
|
||
for wtry in _widths(w, net):
|
||
seg = _connect(net, cur, tgt, obstacles, wtry, relax=relax)
|
||
if seg is not None:
|
||
break
|
||
# 首选出线方向走不通 -> 换个方向再试(见文件头那段说明)
|
||
if seg is None and getattr(tgt, 'stub_alts', None):
|
||
q = tgt.pad
|
||
for end2, ws2 in tgt.stub_alts:
|
||
t2 = Comp([tgt.rect, sh_seg(q['x'], q['y'], end2[0], end2[1], ws2, q['lay'])],
|
||
end2, center=end2, layers=tgt.layers)
|
||
for wtry in _widths(w, net):
|
||
seg = _connect(net, cur, t2, obstacles, wtry, relax=relax)
|
||
if seg is not None:
|
||
break
|
||
if seg is not None:
|
||
paths[tgt.stub_i] = [
|
||
(round(q['x'], 4), round(q['y'], 4), q['lay'], ws2),
|
||
(round(end2[0], 4), round(end2[1], 4), q['lay'], ws2)]
|
||
tgt = t2
|
||
if verbose:
|
||
print(u' %s.%s 改从 %s 出线' % (q['ref'], q['num'], _fmt(end2)))
|
||
break
|
||
if seg is None:
|
||
if verbose:
|
||
print(u' [!!] %s 布不通:%s -> %s' % (net, _fmt(cur.anchor), _fmt(tgt.anchor)))
|
||
cur.merge(tgt, [])
|
||
anchors.append(tgt.anchor)
|
||
continue
|
||
path, shp = seg
|
||
paths.append(path)
|
||
obstacles.extend((sp, net) for sp in shp)
|
||
cur.merge(tgt, shp)
|
||
anchors.append(tgt.anchor)
|
||
return paths
|
||
|
||
|
||
W_MIN = 0.15 # 一般信号收窄的下限(嘉立创四层工艺下限 0.102,留足余量)
|
||
# ⚠️ **功率网络的下限是设计规则给的,不是工艺给的。**
|
||
# `pcb2_rules.TRACK_RULES` 里 PWR1A 的 `strokeWidthMin` 是 0.25、PWR5V 是 0.30、
|
||
# PWR4A 是 0.30;收窄到 0.15 会被 DRC 记成「物理错误·导线」——
|
||
# 实测第一版就这么刷出 **40 条**,而净距校核一条都不报(线宽不是净距问题)。
|
||
W_MIN_NET = {'+5V': 0.30, '+3V3_HUB': 0.15} # PWR1A 的 min 已降到 0.15,见 pcb2_rules
|
||
for _n in range(1, 6):
|
||
W_MIN_NET['VBUS%d' % _n] = 0.25
|
||
W_MIN_NET['VO%d_P' % _n] = 0.30
|
||
|
||
|
||
def _widths(w, net=None):
|
||
lo = W_MIN_NET.get(net, W_MIN)
|
||
out = [w]
|
||
for v in (round(w * 0.7, 3), lo):
|
||
if v < out[-1] - 1e-6 and v >= lo - 1e-6:
|
||
out.append(v)
|
||
return out
|
||
|
||
|
||
def _fmt(p):
|
||
return u'(%.2f,%.2f)' % (p[0], p[1])
|
||
|
||
|
||
def _bbox(shapes, pad):
|
||
xs, ys = [], []
|
||
for s in shapes:
|
||
if s[0] == 'rect':
|
||
xs += [s[1] - s[3], s[1] + s[3]]
|
||
ys += [s[2] - s[4], s[2] + s[4]]
|
||
elif s[0] == 'via':
|
||
xs += [s[1] - s[3], s[1] + s[3]]
|
||
ys += [s[2] - s[3], s[2] + s[3]]
|
||
else:
|
||
xs += [s[1] - s[5], s[3] + s[5]]
|
||
ys += [s[2] - s[5], s[4] + s[5]]
|
||
return (min(xs) - pad, min(ys) - pad, max(xs) + pad, max(ys) + pad)
|
||
|
||
|
||
def _connect(net, a, b, obstacles, w, pad=BBOX_PAD, relax=None):
|
||
u"""把两坨铜连起来。窗口先小后大、栅格先粗后细,找到就返回。
|
||
|
||
⚠️ **细栅格那一档不是可有可无的。** 排母 2.54 间距 / Ø1.699 焊盘 ⇒ 缝宽 0.841 mm,
|
||
0.25 的线要 2×(0.20+0.125) = 0.65 —— 明明过得去,但 0.1 mm 栅格上
|
||
离缝心最近的格心可能偏 0.04,再加 QERR 0.071 就差那么一点点,
|
||
于是"从排母针缝里穿过去"这件事在粗栅格上**整片不可达**。
|
||
0.05 mm 栅格把这两项都减半,缝就通了。
|
||
"""
|
||
# ⚠️ 窗口先**以目标为中心**开小的,再退到「整坨 + 目标」的大窗口(2026-09-06)。
|
||
# 原来一上来就取 a.shapes + b.shapes 的包围盒:`+3V3_HUB` 已连上的那坨横跨全板,
|
||
# 窗口一大栅格就被放粗到 0.2 以上,QFN 东侧 0.71 mm 的走廊(U32.46 的出线端)
|
||
# 在粗栅格上整片不可达 —— 单独布它能通、全量布就不通,根子在这里不在拥挤。
|
||
# 小窗口里只有离目标近的那部分 `a` 当种子,别的部分对这一段本来也没用。
|
||
tries = [('tgt', 0, 1), ('tgt', 0, 2), ('tgt', 0, 4), ('tgt', 1, 2), ('tgt', 1, 4),
|
||
('all', 0, 1), ('all', 0, 2), ('all', 0, 4), ('all', 1, 1), ('all', 1, 2),
|
||
('all', 1, 4), ('all', 2, 1), ('all', 2, 4)]
|
||
for mode, grow, fine in tries:
|
||
base = b.shapes if mode == 'tgt' else a.shapes + b.shapes
|
||
x0, y0, x1, y1 = _bbox(base, pad + grow * 20.0)
|
||
x0, y0 = max(0.0, x0), max(0.0, y0)
|
||
x1, y1 = min(BW, x1), min(BH, y1)
|
||
if mode == 'tgt':
|
||
a_in = [s_ for s_ in a.shapes if _bbox([s_], 0.0)[2] > x0 and _bbox([s_], 0.0)[0] < x1
|
||
and _bbox([s_], 0.0)[3] > y0 and _bbox([s_], 0.0)[1] < y1]
|
||
if not a_in:
|
||
continue # 这一窗里没有种子,直接开大窗
|
||
a_use = Comp(a_in, a.anchor, center=a.center, layers=a.layers)
|
||
else:
|
||
a_use = a
|
||
# 窗口太大就把栅格放粗一档:判据里的 QERR 跟着变,仍然是**充分条件**,
|
||
# 只是长距离那几根线会保守一点(走廊本来就宽,损失不了什么)。
|
||
g = GRID / float(fine)
|
||
while ((x1 - x0) / g + 2) * ((y1 - y0) / g + 2) > MAX_CELLS:
|
||
g *= 2
|
||
f = Field(obstacles, (x0, y0, x1, y1), skip_net=net, grid=g)
|
||
if relax:
|
||
f.set_relax(relax)
|
||
src = _mask(f, a_use)
|
||
tgt = _mask(f, b)
|
||
cells = route_grid(f, src, tgt, w)
|
||
if cells is None:
|
||
continue
|
||
pts = _runs(cells)
|
||
chunks = polish(f, pts, w)
|
||
path = []
|
||
for lay, xy in chunks:
|
||
for x, y in xy:
|
||
path.append((round(float(x), 3), round(float(y), 3), lay, w))
|
||
path = _dedup(path)
|
||
# 端点钉回焊盘圆心(见 Comp 的注释)。
|
||
# ⚠️ 端点处可能有**一串重合点**(换层),要整串一起挪,不能只挪最后一个。
|
||
if a.center:
|
||
_snap_end(path, a.center, False)
|
||
if b.center:
|
||
_snap_end(path, b.center, True)
|
||
path = _end_via(path)
|
||
segs, vs = expand(path)
|
||
shp = [sh_seg(sx0, sy0, sx1, sy1, sw, sl) for sx0, sy0, sx1, sy1, sl, sw in segs]
|
||
shp += [sh_via(vx, vy) for vx, vy in vs]
|
||
return path, shp
|
||
return None
|
||
|
||
|
||
def _mask(f, c):
|
||
u"""A* 的种子 / 目标格。单焊盘只取圆心那一格,其余按整坨铜。"""
|
||
if c.center:
|
||
m = {L: np.zeros((f.ny, f.nx), bool) for L in f.lays}
|
||
i, j = f._idx(c.center[0], c.center[1])
|
||
if 0 <= i < f.nx and 0 <= j < f.ny:
|
||
for L in c.layers:
|
||
m[L][j, i] = True
|
||
return m
|
||
return f.mask_of(c.shapes)
|
||
|
||
|
||
def _dedup(path):
|
||
out = []
|
||
for p in path:
|
||
if out and abs(out[-1][0] - p[0]) < 1e-9 and abs(out[-1][1] - p[1]) < 1e-9 \
|
||
and out[-1][2] == p[2]:
|
||
continue
|
||
out.append(p)
|
||
return out
|
||
|
||
|
||
def _same(a, b):
|
||
return abs(a[0] - b[0]) < 1e-9 and abs(a[1] - b[1]) < 1e-9
|
||
|
||
|
||
def _snap_end(path, xy, at_end):
|
||
u"""把路径某一端**所有重合的点**一起钉到 `xy`(焊盘圆心)。
|
||
|
||
换层处路径里是两个同坐标不同层的点;只挪其中一个的话,
|
||
过孔就跟导线错开 0.05 mm,`expand()` 认不出换层,孔直接消失。
|
||
"""
|
||
if at_end:
|
||
k = len(path) - 1
|
||
while k > 0 and _same(path[k - 1], path[k]):
|
||
k -= 1
|
||
rng = range(k, len(path))
|
||
else:
|
||
k = 0
|
||
while k + 1 < len(path) and _same(path[k], path[k + 1]):
|
||
k += 1
|
||
rng = range(0, k + 1)
|
||
for i in rng:
|
||
path[i] = (round(xy[0], 4), round(xy[1], 4), path[i][2], path[i][3])
|
||
|
||
|
||
def _end_via(path):
|
||
u"""路径**最后一个点上的过孔**要补一个重复点,`expand()` 才认得出来。
|
||
|
||
⚠️ 这是本脚本抓到的最阴的一个 bug。`expand()` 的语义是
|
||
「第 i 点的层 = 从这一点走到下一点那一段的层」,换层的过孔打在第 i+1 点上,
|
||
**但它显式跳过最后一个点**(`i + 1 < n - 1`)——最后一点的层被当成占位符。
|
||
于是"走到目标焊盘正上方再换层落下去"这种路径,**过孔被静默丢掉**:
|
||
两层的铜各画各的,中间断开。
|
||
|
||
更糟的是三道校核全都抓不到:净距校核只看有没有撞上(断开当然不撞),
|
||
DRC 的连接性又要 `location.reload()` 才刷新。
|
||
实测 `$25N40` / `SDA` / `VBUS_DET` / `XTAL2` / `VBUS_EN3` 五个网络就是这么断的。
|
||
起点侧不用补 —— 起点的重合点对 `expand()` 是"第 0 段长度为 0 + 第 1 点换层",本来就认得出。
|
||
"""
|
||
if len(path) >= 2 and _same(path[-1], path[-2]) and path[-1][2] != path[-2][2]:
|
||
return path + [path[-1]]
|
||
return path
|
||
|
||
|
||
# ══════════════════ 本轮要布的网络 ══════════════════
|
||
def _to_site(n, seq):
|
||
return [(round(to_board(n, u_, v_, 0)[0], 3), round(to_board(n, u_, v_, 0)[1], 3), lay, ww)
|
||
for u_, v_, lay, ww in seq]
|
||
|
||
|
||
def bm24_pre(n):
|
||
u"""第 n 站 BM24 的 13 个 GND 脚怎么接到地平面(手写,站内坐标,**全部底层**)。
|
||
|
||
§25.8 判定「51 个脚旁边打不下 Ø0.61 的孔、只能逃逸出来再并」是对的。
|
||
v5 的具体走法(和 v4 反过来,理由见上面那段车道分配):
|
||
|
||
· **前排** pin 2/8/12/16/24 → 往**内**(进中央通道),在 v=35.00 并成一条 0.30 梳子,
|
||
梳子上打 4 个孔(跳过 u 50.05,它和 50.75 只隔 0.70 < 0.81);
|
||
· **后排** pin 1/19/23/29/37/41/47 → 往**外**(v−),在 v=33.10 并成第二条梳子,
|
||
打 5 个孔(那一带一路空到 v 26,是全站最宽敞的地方);
|
||
· **pin 52** 单独往东平出到 u=59.60 打一个孔。
|
||
|
||
⚠️ 这 65 个脚**已经从 `pcb2_fanout.py` 里整批剔除**(`SKIP_PAD`),
|
||
全靠这里画出来 —— 漏一个就是一条连接性 DRC。
|
||
"""
|
||
B = lambda seq: _to_site(n, seq)
|
||
out = []
|
||
# ── 前排:往内进中央通道 ──
|
||
# 最西那根出线段和梳子写成**同一条路径**,西端那个 90° 才切得成 45°;
|
||
# 其余几根是 T 形接头(线本身不拐弯),保持原样。
|
||
out.append(B([(J7_GND_FRONT[0], J7_FRONT_V, L_BOT, W_ARR),
|
||
(J7_GND_FRONT[0], J7_FRONT_IN, L_BOT, W_COMB),
|
||
(J7_GND_FRONT[0], J7_MID_COMB, L_BOT, W_COMB),
|
||
(J7_GND_FRONT[-1], J7_MID_COMB, L_BOT, W_COMB)]))
|
||
for u_ in J7_GND_FRONT[1:]:
|
||
out.append(B([(u_, J7_FRONT_V, L_BOT, W_ARR),
|
||
(u_, J7_FRONT_IN, L_BOT, W_COMB),
|
||
(u_, J7_MID_COMB, L_BOT, W_COMB)]))
|
||
for u_ in J7_MID_VIA_U:
|
||
out.append(B([(u_, J7_MID_COMB, L_BOT, W_COMB), (u_, J7_MID_COMB, L_TOP, W_COMB)]))
|
||
# ── 后排:往外并成第二条梳子 ──
|
||
out.append(B([(J7_GND_BACK[0], J7_BACK_V, L_BOT, W_ARR),
|
||
(J7_GND_BACK[0], J7_BACK_ESC, L_BOT, W_COMB),
|
||
(J7_GND_BACK[0], J7_BACK_COMB, L_BOT, W_COMB),
|
||
(J7_GND_BACK[-1], J7_BACK_COMB, L_BOT, W_COMB)]))
|
||
for u_ in J7_GND_BACK[1:]:
|
||
out.append(B([(u_, J7_BACK_V, L_BOT, W_ARR),
|
||
(u_, J7_BACK_ESC, L_BOT, W_COMB),
|
||
(u_, J7_BACK_COMB, L_BOT, W_COMB)]))
|
||
for u_ in J7_BACK_VIA_U:
|
||
out.append(B([(u_, J7_BACK_COMB, L_BOT, W_COMB), (u_, J7_BACK_COMB, L_TOP, W_COMB)]))
|
||
# ── pin 52:往东平出打孔 ──
|
||
out.append(B([(J7_GND_BIG, J7_MID_V, L_BOT, W_COMB),
|
||
(J7_PIN52_VIA_U, J7_MID_V, L_BOT, W_COMB),
|
||
(J7_PIN52_VIA_U, J7_MID_V, L_TOP, W_COMB)]))
|
||
return out
|
||
|
||
|
||
def bm24_lane(n, kind):
|
||
u"""THERM / CC1 从前排往**外**出,直接进正北的 R77n.1 / R76n.1(v5)。
|
||
|
||
v4 那条「走中央通道往东绕出来」的路线在 v5 已经没有必要 ——
|
||
后排整排都是 GND,通道判给了前排 GND 的梳子,而 R76n/R77n 就在前排外侧。
|
||
|
||
· `CC1_n`:R76n 的中心特意取 52.233,pin1 正落在 u 51.800 = J7n.22 正北
|
||
⇒ **一根直线上去**,全程底层。
|
||
· `THERM_n`:目标在 u 54.367,要往东跨过 CC1 那根竖线 ——
|
||
**打两个孔走顶层过去**(顶层在 J7n 这一带一根线都没有)。
|
||
西孔离 `*_DPJ` 的换层孔 0.97 ≥ 0.81,是这一段最紧的一处。
|
||
"""
|
||
if kind == 'CC1':
|
||
return _to_site(n, [(J7_CC1_U, J7_FRONT_V, L_BOT, W_ARR),
|
||
(J7_CC1_U, J7_FRONT_ESC, L_BOT, W_LANE),
|
||
(J7_R76_PIN1_U, J7_R_ENTER_V, L_BOT, W_LANE)])
|
||
(wu, wv), (eu, ev) = J7_THERM_VIA
|
||
ku, kv = J7_THERM_KINK
|
||
# ⚠️ 第 i 点的 width 管的是「这一点到下一点」那一段。
|
||
# 前两段(直出 + 45° 斜出)**必须都是 0.11** —— 理由见 J7_THERM_TURN_V 那一段注释。
|
||
return _to_site(n, [(J7_THERM_U, J7_FRONT_V, L_BOT, W_ARR),
|
||
(J7_THERM_U, J7_THERM_TURN_V, L_BOT, W_ARR), # 直出到 36.58,仍 0.11
|
||
(ku, kv, L_BOT, W_LANE), # 45° 斜出,仍 0.11 -> 之后放宽
|
||
(wu, wv, L_BOT, W_LANE), # 竖上到西孔(离 DPJ 孔 0.80)
|
||
(wu, wv, L_TOP, W_LANE), # ↑ 换层
|
||
(eu, ev, L_TOP, W_LANE), # 顶层东行(顶层这一带是空的)
|
||
(eu, ev, L_BOT, W_LANE), # ↓ 换层
|
||
(J7_R77_PIN1_U, J7_R_ENTER_V, L_BOT, W_LANE)])
|
||
|
||
|
||
# ── 布线顺序(挤的先布、宽的先布)─────────────────────────────────────
|
||
# Hub 卫星圈是 1.2 mm 网格上的 0402,缝只有 0.66 mm;服务带里 0402 也是密排。
|
||
# 所以次序是:**Hub 局部 -> 上游 -> 站内局部 -> 电源 -> 长距离控制线**。
|
||
ALL_NETS = set()
|
||
PADS_ALL = [] # main() 装进来,`_pogo_by_length()` 要用
|
||
lanes0 = (['THERM_%d' % n for n in range(1, 6)] + ['CC1_%d' % n for n in range(1, 6)])
|
||
|
||
|
||
def _pogo_by_length():
|
||
u"""十五根 POGO,按站分组、组内按两端直线距离从短到长。"""
|
||
out = []
|
||
for n in range(1, 6):
|
||
row = []
|
||
for k in ('PWR', 'VUP', 'VDN'):
|
||
net = 'POGO%d_%s' % (n, k)
|
||
ps = [p for p in PADS_ALL if p['net'] == net]
|
||
d = (math.hypot(ps[0]['x'] - ps[1]['x'], ps[0]['y'] - ps[1]['y'])
|
||
if len(ps) == 2 else 0.0)
|
||
row.append((d, net))
|
||
out += [net for _d, net in sorted(row)]
|
||
return out
|
||
|
||
|
||
def net_order():
|
||
# GND 与 THERM/CC1 的车道排最前:BM24 周围那三条缝是**手写死的几何**,
|
||
# 后面所有网络都必须看得见它们、绕开走。
|
||
#
|
||
# ⚠️ 2026-09-06 试过把 `+3V3_HUB` 提到这一组最前(想让它先抢卫星圈的缝),
|
||
# **结果更糟**:它有 33 个焊盘,先布就把 Hub 周围铺满,
|
||
# `XTAL1`/`CFGSEL0`/`PRTDIS6`/`PRTDIS7`/`HUB_RST` 五根配置线全布不通
|
||
# (没布通的从 4 个涨到 7 个)。⇒ 大网络必须排在点对点的小网络之后。
|
||
hub = ['GND'] + lanes0 + ['VBUS_UP'] + [
|
||
'RBIAS', 'VDD18', 'VDD18PLL', 'XTAL1', 'XTAL2',
|
||
'CFGSEL0', 'CFGSEL1', 'NONREM0', 'NONREM1', 'PRTDIS6', 'PRTDIS7',
|
||
'HUB_RST', '+3V3_HUB', 'VBUS_DET']
|
||
# `EDL_ENn` / `VBUS_ENn` 在 U4n / U2n 上是**紧挨着 +3V3 脚的那一针**,
|
||
# 排在 `+3V3_HUB` 之前,否则 3V3 的长支干会从这 0.95 mm 的缝里横穿过去。
|
||
en = ['EDL_EN%d' % n for n in range(1, 6)] + ['VBUS_EN%d' % n for n in range(1, 6)]
|
||
# `+3V3_HUB` 排在 Hub 局部之后、其它一切之前:它有 33 个焊盘、
|
||
# 一半挤在 Hub 的卫星圈里,晚一步就没缝了。
|
||
# `+5V` 紧跟 `+3V3_HUB`:它要从排母穿过已经站满 EN/POGO 线的走廊过来,
|
||
# 排在后面的话 site2/site5 那两段就没缝了。
|
||
# 2026-09-06(v5)实测调过一次次序:`+3V3_HUB` 和 `VBUS_UP` 单独跑都能通
|
||
# (45 s / 2 s),一起跑就都断 —— 说明是抢地盘,不是没有路。
|
||
# · `+3V3_HUB` 提到 EN 组**之前**(但仍在 Hub 局部之后,那五根配置线的教训还在);
|
||
# · `VBUS_UP` 提到 `+5V` **之前** —— 它要横穿大半块板去 J23,
|
||
# 而 `+5V` 是 0.60 的宽线,先布就把那条走廊占死了。
|
||
up = en + ['VBUS_UP', 'UP_VBUS_DET', '+5V', '$16N222', '$16N224']
|
||
# `$` 开头的是原理图自动命名的局部网(J6n<->U4n、C72n<->U2n),
|
||
# 编号不成规律(site1 是 $21N57/58,其余是 $2xN39/40),所以从网表里捞。
|
||
site = sorted(n for n in ALL_NETS if n.startswith('$'))
|
||
power = ['VBUS%d' % n for n in range(1, 6)]
|
||
# 每个站的三根 POGO 按**两端直线距离从短到长**排:短的那根余地最小,
|
||
# 让它先挑路;长的绕得起。site4/site5 上这条规则把 `VDN` 提到最前 ——
|
||
# 它的源在最东、靶点却在最西(J59 比 J57/J58 靠里),后布就被另外两根夹死
|
||
# (`POGO5_VDN` 原来就是这么布不通的)。
|
||
ctrl = [n for n in _pogo_by_length()] + ['SDA', 'SCL']
|
||
out, seen = [], set()
|
||
for n in hub + up + site + power + ctrl:
|
||
if n not in seen:
|
||
seen.add(n)
|
||
out.append(n)
|
||
return out
|
||
|
||
|
||
# ⚠️ 手写的前置段也**必须**过一遍 `_end_via()`。
|
||
# `bm24_pre()` 里那些「走到位置 -> 换层」的路径最后两个点是同坐标不同层,
|
||
# 而 `expand()` 显式跳过最后一个点上的过孔 —— 五个站 **50 个 GND 过孔全部丢掉**,
|
||
# 两条梳子和 pin52 的连线整个悬空,DRC 报出 J7n 的 pin 1/2/8/12/47/52 断开。
|
||
# 净距校核和 `inner1_verify` 都不会响:悬空的铜不违反任何净距。
|
||
PRE = {}
|
||
for _n in range(1, 6):
|
||
PRE.setdefault('GND', []).extend(_end_via(p) for p in bm24_pre(_n))
|
||
PRE['THERM_%d' % _n] = [_end_via(bm24_lane(_n, 'THERM'))]
|
||
PRE['CC1_%d' % _n] = [_end_via(bm24_lane(_n, 'CC1'))]
|
||
|
||
PRE_ONLY = {'GND'} # 只落前置段、不做最小生成树的网络
|
||
OUT = os.path.join(HERE, '_pcb2_sig_routes.json')
|
||
|
||
|
||
# ══════════════════ 下发 ══════════════════
|
||
# ⚠️ `pcb2_route_dp.push()` 的做法是「把这些网络上现有的线和过孔全删掉再重画」。
|
||
# 对差分对没问题,对 **GND 是灾难** —— 一按就把 §25.8 那 210 个扇出过孔和
|
||
# 126 条出线全扫掉,而几何校核和 DRC 都只会说"连接性错误变多了"。
|
||
# 所以 GND 走**外科式删除**:只删落在五个 BM24 逃逸框里的 GND 铜。
|
||
# v5 的框取站内 u ∈ [47.5, 60.3]、v ∈ [32.5, 36.5]:
|
||
# · 装得下两条梳子(v 35.00 / 33.10)、13 根出线和 pin52 往东那一截;
|
||
# · **不碰**框外那些 `pcb2_fanout.py` 画的孔 —— R77n.2 在 v 38.0、U5n.2 的孔在 (48.80, 39.45),
|
||
# 两者都在 v > 36.5;手机定位柱 PnH3 在 u 61.90 > 60.3。
|
||
# · J7n 那 65 个 GND 脚本来就被 `pcb2_fanout.SKIP_PAD` 剔除了,框里除了本脚本自己画的没有别人的铜。
|
||
GND_BOX_UV = (47.5, 32.5, 60.3, 36.5)
|
||
|
||
|
||
def gnd_boxes():
|
||
out = []
|
||
for n in SITES:
|
||
xs, ys = [], []
|
||
for u_, v_ in ((GND_BOX_UV[0], GND_BOX_UV[1]), (GND_BOX_UV[2], GND_BOX_UV[3])):
|
||
bx, by, _ = to_board(n, u_, v_, 0)
|
||
xs.append(bx)
|
||
ys.append(by)
|
||
out.append([round(min(xs), 3), round(min(ys), 3), round(max(xs), 3), round(max(ys), 3)])
|
||
return out
|
||
|
||
|
||
JS_PUSH_SIG = u"""
|
||
const NETS = %s, BOXES = %s, SEGS = %s, VIAS = %s, MIL = %f;
|
||
const S = new Set(NETS);
|
||
const inBox = (x, y) => BOXES.some(b => x >= b[0] && x <= b[2] && y >= b[1] && y <= b[3]);
|
||
let dl = 0, dv = 0;
|
||
for (const o of await eda.pcb_PrimitiveLine.getAll()) {
|
||
const L = o.getState_Layer(); if (L !== 1 && L !== 2) continue;
|
||
const n = o.getState_Net();
|
||
const keep = n === 'GND'
|
||
? !(inBox(o.getState_StartX()/MIL, o.getState_StartY()/MIL) &&
|
||
inBox(o.getState_EndX()/MIL, o.getState_EndY()/MIL))
|
||
: !S.has(n);
|
||
if (!keep) { await eda.pcb_PrimitiveLine.delete(o.getState_PrimitiveId()); dl++; }
|
||
}
|
||
for (const o of await eda.pcb_PrimitiveVia.getAll()) {
|
||
const n = o.getState_Net();
|
||
const keep = n === 'GND' ? !inBox(o.getState_X()/MIL, o.getState_Y()/MIL) : !S.has(n);
|
||
if (!keep) { await eda.pcb_PrimitiveVia.delete(o.getState_PrimitiveId()); dv++; }
|
||
}
|
||
let nl = 0, nv = 0;
|
||
for (const [net, lay, x0, y0, x1, y1, w] of SEGS) {
|
||
await eda.pcb_PrimitiveLine.create(net, lay, x0*MIL, y0*MIL, x1*MIL, y1*MIL, w*MIL); nl++;
|
||
}
|
||
for (const [net, x, y] of VIAS) {
|
||
await eda.pcb_PrimitiveVia.create(net, x*MIL, y*MIL, %f*MIL, %f*MIL); nv++;
|
||
}
|
||
await eda.pcb_Document.save();
|
||
await new Promise(r => setTimeout(r, 2500));
|
||
return {del_lines: dl, del_vias: dv, lines: nl, vias: nv};
|
||
"""
|
||
|
||
|
||
def push_sig(routes):
|
||
segs, vs = [], []
|
||
for key, path in routes.items():
|
||
net = netof(key)
|
||
s_, v_ = expand(path)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
segs.append([net, lay, round(x0, 4), round(y0, 4), round(x1, 4), round(y1, 4), w])
|
||
for x, y in v_:
|
||
vs.append([net, round(x, 4), round(y, 4)])
|
||
nets = sorted(set(netof(k) for k in routes) - {'GND'})
|
||
js = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||
"await new Promise(r=>setTimeout(r,1800));" % PCB) + (
|
||
JS_PUSH_SIG % (json.dumps(nets), json.dumps(gnd_boxes()), json.dumps(segs),
|
||
json.dumps(vs), MIL, VIA_H, VIA_D))
|
||
from eda import run
|
||
r = run(js, timeout=1800)
|
||
if not r.get('success'):
|
||
sys.exit(u'下发失败: ' + json.dumps(r, ensure_ascii=False)[:600])
|
||
d = r['result']
|
||
print(u'下发完成:先删 %d 线 / %d 孔,再画 %d 线 / %d 孔'
|
||
% (d['del_lines'], d['del_vias'], d['lines'], d['vias']))
|
||
|
||
|
||
def _to_site_uv(n, x, y):
|
||
u"""板坐标 -> 站内 (u, v)。`to_board` 的逆。"""
|
||
x0, y0, rho = SITES[n]
|
||
if rho == 0:
|
||
return (x - x0, y - y0)
|
||
return (PW - (x - x0), PH - (y - y0))
|
||
|
||
|
||
def j7_assert(pads):
|
||
u"""**把上面那一堆 J7_* 常量和板上的实际焊盘对一遍**(五个站逐一)。
|
||
|
||
⚠️ 这个断言是 2026-09-06 补的,起因是它不在的时候踩的坑:
|
||
v5 把 J7n 从 rot 180 改成 rot 0 又挪了位置,站内坐标整体差
|
||
`u' = u − 5.00 / v' = 32.00 − v`,而脚本照旧用 v4 的常量 ——
|
||
`bm24_pre()` 于是在**空地上**画了五套梳子,净距校核全过(空地当然不撞),
|
||
只有 DRC 的连接性会含糊地多出几十条。⇒ 手写坐标必须有断言兜底。
|
||
"""
|
||
bad = []
|
||
for n in SITES:
|
||
by = {}
|
||
for p in pads:
|
||
if p['ref'] != 'J7%d' % n:
|
||
continue
|
||
u_, v_ = _to_site_uv(n, p['x'], p['y'])
|
||
by[p['num']] = (round(u_, 3), round(v_, 3), p['net'], p['lay'])
|
||
if not by:
|
||
bad.append(u'J7%d 一个焊盘都没找到' % n)
|
||
continue
|
||
want = ([(str(k), J7_GND_FRONT[i], J7_FRONT_V) for i, k in enumerate((2, 8, 12, 16, 24))]
|
||
+ [(str(k), J7_GND_BACK[i], J7_BACK_V) for i, k in enumerate((1, 19, 23, 29, 37, 41, 47))]
|
||
+ [('52', J7_GND_BIG, J7_MID_V)])
|
||
for num, uu, vv in want:
|
||
got = by.get(num)
|
||
if not got:
|
||
bad.append(u'J7%d 没有 pin%s' % (n, num)); continue
|
||
if got[2] != 'GND':
|
||
bad.append(u'J7%d.pin%s 的网络是 %s,不是 GND' % (n, num, got[2]))
|
||
if abs(got[0] - uu) > 0.02 or abs(got[1] - vv) > 0.02:
|
||
bad.append(u'J7%d.pin%s 实测 (%.3f, %.3f),常量写的是 (%.2f, %.2f)'
|
||
% (n, num, got[0], got[1], uu, vv))
|
||
for num, uu, net in (('14', J7_THERM_U, 'THERM_%d' % n), ('22', J7_CC1_U, 'CC1_%d' % n)):
|
||
got = by.get(num)
|
||
if not got:
|
||
bad.append(u'J7%d 没有 pin%s' % (n, num)); continue
|
||
if got[2] != net:
|
||
bad.append(u'J7%d.pin%s 的网络是 %s,应为 %s' % (n, num, got[2], net))
|
||
if abs(got[0] - uu) > 0.02 or abs(got[1] - J7_FRONT_V) > 0.02:
|
||
bad.append(u'J7%d.pin%s 实测 u=%.3f v=%.3f,常量 u=%.2f v=%.2f'
|
||
% (n, num, got[0], got[1], uu, J7_FRONT_V))
|
||
if by.get('2') and by['2'][3] != L_BOT:
|
||
bad.append(u'J7%d 不在底层(lay=%s)—— bm24_pre 的路径层写死了 L_BOT' % (n, by['2'][3]))
|
||
# THERM / CC1 的靶子:R76n.1 / R77n.1
|
||
for ref, uu, tag in (('R76%d' % n, J7_R76_PIN1_U, 'CC1'), ('R77%d' % n, J7_R77_PIN1_U, 'THERM')):
|
||
hit = [p for p in pads if p['ref'] == ref and p['num'] == '1']
|
||
if not hit:
|
||
bad.append(u'找不到 %s.1' % ref); continue
|
||
u_, v_ = _to_site_uv(n, hit[0]['x'], hit[0]['y'])
|
||
if abs(u_ - uu) > 0.02 or abs(v_ - J7_R_PAD_V) > 0.02:
|
||
bad.append(u'%s.1(%s)实测 (%.3f, %.3f),常量 (%.3f, %.3f)'
|
||
% (ref, tag, u_, v_, uu, J7_R_PAD_V))
|
||
if bad:
|
||
for t in bad[:20]:
|
||
print(u' !! ' + t)
|
||
sys.exit(u'\nBM24 的手写常量和板上对不上(%d 条)—— 先改常量再布线。' % len(bad))
|
||
print(u'BM24 手写常量与板上实测一致(五个站 × 13 个 GND 脚 + THERM/CC1 + R76n/R77n)✔')
|
||
|
||
|
||
def main():
|
||
only = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||
pads, vias, lines = geom()
|
||
ALL_NETS.update(p['net'] for p in pads if p['net'])
|
||
PADS_ALL[:] = pads
|
||
j7_assert(pads)
|
||
# ⚠️ 下发时要删掉五个 BM24 框里的旧 GND 铜(§25.8 自动扇出的产物),
|
||
# 所以**算的时候就得当它们已经没了** —— 否则手写的车道会跟一堆
|
||
# 马上就要消失的过孔"撞车",而真正的冲突反而被淹没。
|
||
boxes = gnd_boxes()
|
||
inbox = lambda x, y: any(b[0] <= x <= b[2] and b[1] <= y <= b[3] for b in boxes)
|
||
mine = set(net_order()) - {'GND'}
|
||
n0 = len(vias) + len(lines)
|
||
# ① 本脚本负责的网络:下发时整网删掉重画,所以**算的时候就当它们不存在**。
|
||
# 不摘的话,第二次跑会把自己上一轮的线当成障碍 —— 结果既不幂等,
|
||
# 还会在校核里刷出一堆「和马上要删掉的那条线撞了」的假冲突(实测 3 条)。
|
||
# ② GND 只摘五个 BM24 框里的(§25.8 自动扇出的产物,下发时按框删)。
|
||
vias = [v for v in vias
|
||
if v['net'] not in mine and not (v['net'] == 'GND' and inbox(v['x'], v['y']))]
|
||
lines = [l for l in lines
|
||
if l['net'] not in mine and not (l['net'] == 'GND' and inbox(l['x0'], l['y0'])
|
||
and inbox(l['x1'], l['y1']))]
|
||
print(u'障碍表:焊盘 %d / 过孔 %d / 铜层导线 %d(摘掉本脚本自己负责的旧铜 %d 件)'
|
||
% (len(pads), len(vias), len(lines), n0 - len(vias) - len(lines)))
|
||
order = [n for n in net_order() if not only or n in only]
|
||
obs = shapes_of(pads, vias, lines)
|
||
have = set(p['net'] for p in pads)
|
||
missing = [n for n in order if n not in have]
|
||
if missing:
|
||
sys.exit(u'网络表里没有这些网络:%s' % missing)
|
||
|
||
# ⚠️ `+3V3_HUB` 上**已经有铜**:`pcb2_dp_long.py` 在 Hub 西缘画了三条
|
||
# (pin5 / pin10 / 顶层干线,15 段 + 4 个孔)。它们被 D± 夹在中间,
|
||
# 是差分那一轮算出来的、后面的布线器再也挤不进去的位置。
|
||
# 而 `push_sig()` 的做法是「把本脚本负责的网络整网删掉重画」——
|
||
# 不管的话这三条会被静默抹掉,只在 DRC 里冒出几条连接性错误。
|
||
# ⇒ 从**同一个事实来源**(`pcb2_dp_long.build`)取回来当本网的前置段:
|
||
# 既让最小生成树知道这些焊盘已经连上了,下发时也原样重画。
|
||
from pcb2_dp_long import build as _dp_long_build
|
||
_kept = [q for k, q in _dp_long_build(pads).items() if netof(k) == '+3V3_HUB']
|
||
if len(_kept) != 3:
|
||
sys.exit(u'pcb2_dp_long 里的 +3V3_HUB 前置段应有 3 条,实际 %d 条' % len(_kept))
|
||
# U32 的 7 个 +3V3 脚里,**pin46 在东排、四周被 VBUS_DET / NONREM0 围死**,
|
||
# 它唯一的出路是往 QFN **肚子里**走:南排焊盘上沿 63.975 与散热盘南沿 65.60
|
||
# 之间有一条 1.6 mm 的死带(芯片底下,没有别人要用)。
|
||
# 单独跑 `--net +3V3_HUB` 时布线器自己就是这么走的;但排在 Hub 那几根配置线之后跑,
|
||
# 那个内角已经被占掉 ⇒ 干脆手写成前置段,和 BM24 的梳子一样**先占住**。
|
||
# (102.25, 63.975) 是 pin52 焊盘的北沿;45° 斜段离 pin51 焊盘框 0.50、
|
||
# 东行段离 pin47/pin45 焊盘框各 0.35(0.24 的线要 0.32)✔
|
||
PRE['+3V3_HUB'] = _kept + [[(102.250, 63.550, L_BOT, 0.24),
|
||
(102.250, 64.125, L_BOT, 0.24),
|
||
(103.375, 65.250, L_BOT, 0.24),
|
||
(104.450, 65.250, L_BOT, 0.24)],
|
||
# R716.2(HUB_RST 的上拉)是最东边那颗 +3V3 焊盘,
|
||
# 夹在 R711.2 / R720.2 两个 GND 脚之间(南北各 1.2 mm),
|
||
# 只能**翻顶层**从 C750.1 斜拉过来。同样先占住。
|
||
# 两个孔位都是卡着算的:
|
||
# 西孔 (105.95, 63.825):离 R710.2 那条 0.15 扇出线 0.624(要 0.58)
|
||
# —— 原来放在 105.88 只有 0.566,差 14 µm;
|
||
# 东孔 (109.60, 66.45):离 `USB5_DP` 竖段 (x 110.30) 0.70(要 0.645)
|
||
# —— 原来放在 109.72 只有 0.58,差 65 µm。
|
||
[(105.950, 63.450, L_BOT, 0.35),
|
||
(105.950, 63.825, L_BOT, 0.35),
|
||
(105.950, 63.825, L_TOP, 0.35),
|
||
(106.275, 64.150, L_TOP, 0.35),
|
||
(107.300, 64.150, L_TOP, 0.35),
|
||
(109.600, 66.450, L_TOP, 0.35),
|
||
(109.600, 66.450, L_BOT, 0.35),
|
||
(109.333, 66.450, L_BOT, 0.35)]] + PRE.get('+3V3_HUB', [])
|
||
print(u'+3V3_HUB 保留 pcb2_dp_long 画的 3 条前置段(%d 段导线 / %d 个孔)'
|
||
% (sum(len(expand(q)[0]) for q in _kept), sum(len(expand(q)[1]) for q in _kept)))
|
||
|
||
# `HUB_RST`:U32.43 (104.45, 66.75) 到 R716.1 (108.467, 66.45) 之间只有
|
||
# R709(y 65.85) 与 R712(y 67.05) 之间那条 **0.66 mm** 的缝。
|
||
# 0.20 的线走它是够的(上下各 0.23 ≥ 0.20),但**栅格布线器进不去**:
|
||
# 它的判据是 `edt >= 0.20 + 0.10 + QERR(0.0708) = 0.371`,而半缝只有 0.33。
|
||
# ⇒ 这一段也手写。(不是布线器的 bug —— 0.1 mm 栅格就是分辨不出 0.03 的余量。)
|
||
# ⚠️ 这一段**必须 0.15 不能 0.20**,两处都是算死的:
|
||
# 转折点 a 的窗口:往西受 U32.44 焊盘框(东沿 104.875)挡,a >= 104.875 + (w/2+0.20);
|
||
# 往东受 R712.1 焊盘框西南角 (105.584, 66.780) 挡(45° 斜段的垂距)。
|
||
# w=0.20 时窗口只有 15 µm(105.175~105.190),w=0.15 时有 75 µm ⇒ 取 105.19。
|
||
# 东行段 y=66.45 离 (107.624, 67.050) 那个 GND 扇出孔 0.600:
|
||
# w=0.20 要 0.605(差 5 µm 不过),w=0.15 要 0.58 ✔
|
||
PRE['HUB_RST'] = [[(104.450, 66.750, L_BOT, 0.15),
|
||
(105.190, 66.750, L_BOT, 0.15),
|
||
(105.490, 66.450, L_BOT, 0.15), # 45°
|
||
(108.467, 66.450, L_BOT, 0.15)]]
|
||
|
||
# `VBUS_DET`:U32.44 (104.45, 66.25) 到 R709.2 (105.867, 65.85) —— 和 HUB_RST 同一个口袋。
|
||
# HUB_RST 的手写段占了 y 66.45 那条线之后,这一小段的余量掉到栅格分辨不出来的量级,
|
||
# 所以一起手写。45° 斜段离 HUB_RST 斜段 0.4525(要 0.375)、离 U32.45 焊盘框 0.371(要 0.30)。
|
||
PRE['VBUS_DET'] = [[(104.450, 66.250, L_BOT, W_SIG),
|
||
(105.050, 66.250, L_BOT, W_SIG),
|
||
(105.450, 65.850, L_BOT, W_SIG), # 45°
|
||
(105.867, 65.850, L_BOT, W_SIG)]]
|
||
|
||
# `+5V`:J32.39 与 J32.40 是排母上下相邻的两针,中间 2.54 mm。
|
||
# 上一轮布线器给 J32.39 留的出线段末端**正好落在 J32.40 的焊盘圈里**(差 0.077),
|
||
# 本脚本的连通域判定认了,EasyEDA 的连接性检查不认 —— DRC 报
|
||
# 「导线 (+5V) 与其他相同网络的对象断开了连接」+「插件焊盘 J32_39 断开」。
|
||
# ⇒ 两针之间直接连一根顶层短线,端点落在焊盘中心,谁都不会误判。
|
||
PRE['+5V'] = [[(182.1298, 68.770, L_TOP, 0.60),
|
||
(182.1298, 66.230, L_TOP, 0.60)]]
|
||
|
||
pre_pads = set()
|
||
for net, ps in PRE.items():
|
||
for p in ps:
|
||
for q in pads:
|
||
if q['net'] == net and seg_rect((p[0][0], p[0][1]), (p[0][0], p[0][1]),
|
||
q['x'], q['y'], q['hw'], q['hh']) < 1e-6:
|
||
pre_pads.add((q['ref'], q['num']))
|
||
# ⚠️ **所有前置段一开始就进障碍表**(带各自的网络名)。
|
||
# 原来只有 GND 的在循环里补进去,THERM/CC1/+3V3 的要等轮到自己才进 ——
|
||
# 于是排在它们前面的网络看不见这些手写坐标,照样从上面碾过去。
|
||
for _net, _ps in PRE.items():
|
||
for _p in _ps:
|
||
_sg, _vv = expand(_p)
|
||
for _x0, _y0, _x1, _y1, _lay, _w in _sg:
|
||
obs.append((sh_seg(_x0, _y0, _x1, _y1, _w, _lay), _net))
|
||
for _x, _y in _vv:
|
||
obs.append((sh_via(_x, _y), _net))
|
||
stubs, stub_alts = plan_stubs(order, pads, obs, pre_pads)
|
||
print(u'预留出线段 %d 条' % len(stubs))
|
||
|
||
routes, failed = {}, []
|
||
t0 = time.time()
|
||
for net in order:
|
||
t = time.time()
|
||
if net in PRE_ONLY:
|
||
# GND 不做最小生成树 —— 它靠 Inner2 整板地平面 + §25.8 的扇出连通,
|
||
# 这里只补 BM24 那 65 个脚的逃逸段(`bm24_pre`),照原样落盘。
|
||
ps = list(PRE[net])
|
||
for p in ps:
|
||
for x0, y0, x1, y1, lay, w in expand(p)[0]:
|
||
obs.append((sh_seg(x0, y0, x1, y1, w, lay), net))
|
||
for x, y in expand(p)[1]:
|
||
obs.append((sh_via(x, y), net))
|
||
ok = True
|
||
else:
|
||
ps = route_net(net, pads, obs, pre=PRE.get(net), verbose=True,
|
||
stubs=stubs, alts=stub_alts)
|
||
ok = _wire_ok(net, ps, pads)
|
||
if not ok:
|
||
failed.append(net)
|
||
for k, p in enumerate(ps):
|
||
routes[net if k == 0 else '%s#%d' % (net, k + 1)] = p
|
||
print(u' %-12s %2d 段路径 / %3d 导线 / %2d 过孔 %5.1fs %s'
|
||
% (net, len(ps), sum(len(expand(q)[0]) for q in ps),
|
||
sum(len(expand(q)[1]) for q in ps), time.time() - t,
|
||
u'' if ok else u' ← 有端点没连上'))
|
||
print(u'\n%d 个网络 / %d 条路径 / %d 段导线 / %d 个过孔 (%.0f 秒)'
|
||
% (len(order), len(routes), sum(len(expand(p)[0]) for p in routes.values()),
|
||
sum(len(expand(p)[1]) for p in routes.values()), time.time() - t0))
|
||
if failed:
|
||
print(u'⚠️ %d 个网络没布通:%s' % (len(failed), u' '.join(failed)))
|
||
|
||
# ── 45° 化(2026-09-06 用户拍板:全板不许有直角走线)──
|
||
# 放在**布完之后统一做**,而不是每条路径各切各的:
|
||
# 斜切只会拿掉铜,所以拿"未切之前"的其它段做校核是保守且正确的。
|
||
n0 = len(sharp_corners(routes))
|
||
t = time.time()
|
||
routes = chamfer_routes(routes, pads, vias, lines, smooth=True)
|
||
left = sharp_corners(routes)
|
||
print(u'\n45° 化:直角拐点 %d -> %d(%.0f 秒)' % (n0, len(left), time.time() - t))
|
||
for key, i, xy, ang in left[:20]:
|
||
print(u' [!!] 切不动: %-12s (%.2f, %.2f) 转向 %.0f°' % (key, xy[0], xy[1], ang))
|
||
if len(left) > 20:
|
||
print(u' (还有 %d 处)' % (len(left) - 20))
|
||
|
||
print(u'\n净距校核(%.2f mm):' % CLR)
|
||
bad = check(routes, pads, vias, lines)
|
||
if not bad:
|
||
print(u' 全部通过 ✔')
|
||
json.dump({k: [list(map(float, q[:2])) + [int(q[2]), float(q[3])] for q in v]
|
||
for k, v in routes.items()},
|
||
io.open(OUT, 'w', encoding='utf-8'))
|
||
print(u'路径已存 %s' % os.path.basename(OUT))
|
||
|
||
if '--push' in sys.argv:
|
||
if bad:
|
||
sys.exit(u'\n!! 净距还有问题,不下发。')
|
||
if failed and '--force' not in sys.argv:
|
||
sys.exit(u'\n!! 还有 %d 个网络没布通,要下发请加 --force。'
|
||
u'(没布通的网络只会落下几截悬空的出线段,'
|
||
u'下一轮 --push 会把它们连同别的一起删掉重画。)' % len(failed))
|
||
push_sig(routes)
|
||
|
||
|
||
def _wire_ok(net, paths, pads_all):
|
||
u"""这个网络是不是**真的连成一坨**(按实际铜算连通域,不看 DRC)。
|
||
|
||
⚠️ 判据必须是"连通域 == 1",**不能是"每个焊盘都被某条线碰到"**。
|
||
每个焊盘出门时都先画了一小截 `entry_stub`,所以"被碰到"永远成立 ——
|
||
第一版就是这么写的,于是 20 个断开的网络里只报出 5 个,
|
||
另外 15 个各自剩着一截悬空的出线段,几何校核还全绿。
|
||
"""
|
||
items = [] # (层集合, 种类, 数据)
|
||
for q in pads_all:
|
||
if q['net'] == net:
|
||
lay = {L_TOP, L_BOT} if q['lay'] == 12 else {q['lay']}
|
||
items.append((lay, 'pad', (q['x'], q['y'], q['hw'], q['hh'])))
|
||
for p in paths:
|
||
segs, vs = expand(p)
|
||
for x0, y0, x1, y1, lay, w in segs:
|
||
items.append(({lay}, 'seg', (x0, y0, x1, y1, w / 2.0)))
|
||
for x, y in vs:
|
||
items.append(({L_TOP, L_BOT}, 'via', (x, y, VIA_R)))
|
||
n = len(items)
|
||
if n < 2:
|
||
return True
|
||
par = list(range(n))
|
||
|
||
def find(i):
|
||
while par[i] != i:
|
||
par[i] = par[par[i]]
|
||
i = par[i]
|
||
return i
|
||
|
||
def touch(a, b):
|
||
la, ka, da = a
|
||
lb, kb, db = b
|
||
if not (la & lb):
|
||
return False
|
||
if ka > kb:
|
||
return touch(b, a)
|
||
if ka == 'pad' and kb == 'pad':
|
||
return (abs(da[0] - db[0]) <= da[2] + db[2] + 1e-6
|
||
and abs(da[1] - db[1]) <= da[3] + db[3] + 1e-6)
|
||
if ka == 'pad' and kb == 'seg':
|
||
return seg_rect((db[0], db[1]), (db[2], db[3]), da[0], da[1], da[2], da[3]) <= db[4] + 1e-6
|
||
if ka == 'pad' and kb == 'via':
|
||
return seg_rect((db[0], db[1]), (db[0], db[1]), da[0], da[1], da[2], da[3]) <= db[2] + 1e-6
|
||
if ka == 'seg' and kb == 'seg':
|
||
return seg_seg((da[0], da[1]), (da[2], da[3]),
|
||
(db[0], db[1]), (db[2], db[3])) <= da[4] + db[4] + 1e-6
|
||
if ka == 'seg' and kb == 'via':
|
||
return seg_pt(da[0], da[1], da[2], da[3], db[0], db[1]) <= da[4] + db[2] + 1e-6
|
||
return math.hypot(da[0] - db[0], da[1] - db[1]) <= da[2] + db[2] + 1e-6
|
||
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
if find(i) != find(j) and touch(items[i], items[j]):
|
||
par[find(i)] = find(j)
|
||
return len(set(find(i) for i in range(n))) == 1
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|