文件
phonerail/.claude/r2/archive_base2L/route_base.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

1341 行
66 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 PCB 布线(M8d)。
/c/Python314/python .claude/r2/route_base.py # 算 + 落盘缓存,不下发
/c/Python314/python .claude/r2/route_base.py --replan # 强制重算(默认吃缓存)
/c/Python314/python .claude/r2/route_base.py --dp-only # 只算七对 D±,不改缓存
/c/Python314/python .claude/r2/route_base.py --push # 下发导线 + 过孔
/c/Python314/python .claude/r2/route_base.py --clear # 清掉全部布线
**引擎复用刀片那一份 `router.py`**(0.25 网格 + 8 邻接 A*,见它的文件头),
这里只做三件事:把板级常量换成底板的、加两条两层板独有的约束、写自己的主流程。
## 两层板独有的两条约束(刀片上没有)
1. **底层(L2)是完整地平面 —— 走一格就是在平面上割一刀。**
⇒ 底层全层收「过路费」`BOT_PEN`:不封死(方案 §6.2 允许 D± 走廊之外跳线),
但只有当顶层要绕远 `BOT_PEN` 倍以上时才划算。短跳线照走,长干线不会跑下去。
⛔ 过路费必须**每次 `pen_pads` 之后重新打**:`pen_pads` 头一句就是 `reset_pen()`,
直接写 `g.pen` 会被它清掉,而且**不报错** —— 现象是底层突然多出一堆长线。
2. **D± 走廊下方一刀不许割**(§6.2 第 1 条,这是 2 层方案成立的前提)。
⇒ 分两相布:
**第一相**七对 D± **只准走顶层**(`TOP_ONLY`,底层整层封死);
**第二相**把第一相的铜按 `DIFF_KEEP` 膨胀,在底层**封死**,再布其余所有网。
于是任何跳线都不可能钻到 D± 底下。
## 差分对:靠「贴着走」的过路费成对
`router.py` 是逐网布的,不认识差分对。做法:`DP` 布完之后,给整个顶层打一层
过路费、只把 `DP` 铜周围 `HUG_BAND` 内的格点留成 0,`DM` 自然就贴着 `DP` 走。
实测中心距落在网格的 1.5 mm(= 1.2 线宽 + 0.3 缝),Zdiff 比方案 §6.3 那张表
(1.2/0.2 → 94 Ω)略高,仍在 USB 2.0 的 90 Ω ±15% 窗口里。首板不勾阻抗控制。
## `GND` 不布线
333 只地脚全靠**两层铺铜 + 缝合过孔**(`pour_base.py`)。底层本来就是整块地平面,
顶层的地铜面被信号切成多块岛 —— **每块岛至少要有一个缝合过孔**,
否则会被当孤岛整块删掉,而 `rebuildCopperRegion()` / 铺铜管理器 / DRC **三个都不报错**
(刀片 M4c 的教训,方案 §24.4)。`pour_base --check` 就是查这一条的。
## `+12V` 的 9 A 靠铺铜,不靠这里的线宽
这里给 `+12V` 1.5 mm(= 单槽 1.5 A 的要求)、`VIN_*` 2.0 mm。
入口链那段 9 A 要 ≥8 mm 净铜宽(§6.4),由 `pour_base` 在顶层铺一块 `+12V` 铜面 +
**断面校核**保证(办法沿用 R1 `pcb1_pour.py` 的 `CUTS`)。
⇒ 只看这份脚本的线宽会以为入口链严重欠载流,别慌,看铺铜那一步。
"""
import io
import json
import math
import os
import sys
# `--hash-seed N`:固定字符串哈希(网名集合的遍历顺序 → 直连 / 排网的平局先后)。
# **只对 CPython 有意义**:CPython 的集合按哈希序遍历、字符串哈希按进程随机,
# 固定之后结果可复现,也可以扫几个种子取最好的。
# ⚠ PyPy 的集合一律按**插入序**遍历,哈希值不影响结果 —— 实测 8 个种子输出逐字节相同。
# 哈希种子只能在解释器启动前设,所以带着环境变量重启自己。
if '--hash-seed' in sys.argv:
_seed = sys.argv[sys.argv.index('--hash-seed') + 1]
if os.environ.get('PYTHONHASHSEED') != _seed:
os.execve(sys.executable, [sys.executable] + sys.argv,
dict(os.environ, PYTHONHASHSEED=_seed))
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
import pcb_base as B # noqa: E402
import router as RT # noqa: E402
import rules_base as RB # noqa: E402
# 用户明确要求所有布线运算都放到 10.5.5.13。快照、缓存报告和下发仍可在本机做,
# 但任何会启动 A* / 差分对试算的入口必须由 remote_route.ps1 在服务器上显式解锁。
_COMPUTE_FLAGS = ('--replan', '--dp-only')
if any(flag in sys.argv for flag in _COMPUTE_FLAGS) and os.environ.get('DCDC_ROUTE_SERVER') != '1':
sys.exit(u'!! 布线运算禁止在本机执行;请使用 .claude/r2/remote_route.ps1(服务器 10.5.5.13)')
# ── 把 router 的板级常量换成底板的 ────────────────────────────────────────
RT.B = B
RT.PCB = B.PCB
RT.NX = int(round(B.BW / RT.G)) + 1
RT.NY = int(round(B.BH / RT.G)) + 1
RT.PADS = os.path.join(HERE, '_base_pads.json')
RT.FINGER_KEEP_Y = 0.0 # 底板没有金手指区
RT.KELVIN = []
RT.KELVIN_PADS = set()
RT.CLR = 0.21 # 设计规则是 0.20,留一点给网格吸附
RT.W_DEFAULT = 0.2 # 两层板走 JLC 标准工艺,信号 0.2
RT.EDGE_CLR = 0.4
# ⛔ **两层正交那条约定在底板上要关掉**。刀片上是「器件面纵向、另一面横向」
# (方案 §25.6),做法是逆着方向走贵 `CROSS_COST=3` 倍。底板只有**顶层**能布信号
# (底层是完整地平面),于是「顶层偏好横向」变成「每往北走一格贵 3 倍」——
# 而 D± 全都是南北向的。实测现象:`USB5` 一对布出 **37 段**的锯齿路径,
# 把扇出带占满,紧接着 `USB4` 一段都布不出来。看着像拥塞,其实是**代价函数**。
# `LAYER_DIR` 留空 ⇒ `_along()` 一律返回 True,八个方向同价。
RT.LAYER_DIR = {}
CACHE = os.path.join(HERE, '_base_routes.json')
CACHE_SCHEMA = 7 # v7: live-layout D±, rail bridge, and revised 12V trunk
BOARD_VIAS = os.path.join(HERE, '_base_board_vias.json')
class _NoNotch(object):
u"""`build_grid` 要 `pcb_pour.notch_cut()`(金手指防呆缺口)—— 底板没有。
给一个板外的退化矩形,比在 `router.py` 里加 if 干净(那份是刀片的事实来源)。
"""
@staticmethod
def notch_cut():
return (-10.0, -10.0, -9.0, -9.0)
RT.P = _NoNotch
def _rule_min(net, auto):
u"""线宽下限:查底板自己的 `rules_base.NET_TRACK`,不是刀片那份。"""
cls = RB.NET_TRACK.get(net)
if cls is None:
return 0.15
return RB.TRACK_RULES[cls]['strokeWidthMin']
RT.rule_min = _rule_min
# ⛔ **通孔焊盘(layer 12)**:刀片是全贴片的,`router.py` 里一句
# `if lay not in LAY: continue` 就把它们从占用图里漏掉了 —— 底板有 DC 座 /
# 两个风扇座 / 应急排针 / 两颗直立电解,漏掉的后果是布线器直接从通孔焊盘上压过去。
# 而 `pad_nodes` 又会拿 `g.own[12]` 去查,直接 KeyError(这次是**响**的,算运气好)。
# ⇒ 拆成两条记录:顶层那条参与布线,底层那条只当障碍(脚号加后缀,`bypad` 里滤掉)。
BOT_SUFFIX = '#b'
# ⛔ **通孔焊盘要额外膨胀**:`J1`(DC 座)和 `J23`(USB-C)的孔是 **SLOT(长槽)**,
# 槽比焊盘大,而布线器只按矩形焊盘建模 ⇒ 线贴着焊盘走就压在槽上,
# DRC 报「挖槽区域 到 导线 0 mil」(实测 22 条,全是 `VIN_FUSED` 贴着 J1)。
TH_GROW = 0.6
def split_th(pads):
out = []
for p in pads:
if p[6] == 12:
a = list(p); a[6] = 1; a[4] += TH_GROW; a[5] += TH_GROW
b = list(p); b[6] = 2; b[1] = str(p[1]) + BOT_SUFFIX
b[4] += TH_GROW; b[5] += TH_GROW
out += [a, b]
else:
out.append(p)
return out
def apply_route_fix(pads):
u"""在板端尚未重抓焊盘时,把 ROUTE_FIX 平移到计划位置。
`_base_pads.json` 是 EasyEDA 当前板端的读回值;M12 的两颗让路电阻已经写进
`place_base.ROUTE_FIX`,但桥接中断时无法立即重抓。这里用同一份 live 布局快照
算平移量,使离线路由能先验证新位置。板端移动并重新 `--adopt-live --dump` 后,
live 坐标等于目标坐标,平移量自然归零,不会重复移动。
"""
import place_base as PL
pads, moved = PL.route_fixed_pads(pads) # 与 pour_base 共用同一份换算
if moved:
print(u' 路由预检计划改位:' + ' '.join(moved))
return pads
def load_board_vias():
u"""读取板端既有过孔;`--offline` 时使用本地快照供计算服务器复现。"""
if '--offline' in sys.argv:
if not os.path.exists(BOARD_VIAS):
raise RuntimeError(u'离线计算缺少 _base_board_vias.json;先在 EDA 可达机器运行一次预检')
return [tuple(v) for v in json.load(io.open(BOARD_VIAS, encoding='utf-8'))]
vias = RT.load_vias()
io.open(BOARD_VIAS, 'w', encoding='utf-8').write(
json.dumps(vias, ensure_ascii=False, indent=2))
return vias
# ── 线宽 ─────────────────────────────────────────────────────────────────
DP_NETS = []
for _name, _dp, _dm in RB.PAIRS:
DP_NETS += [_dp, _dm]
DIFF_W = 1.0 # 试:1.0/0.2 ⇒ ~100 Ω(USB 窗口 76.5~103.5 内)
RT.WIDTH = {
'VIN_RAW': 2.0, 'VIN_FUSED': 2.0, 'VIN_SW': 2.0,
'+12V': 1.5, # 单槽 1.5 A;干路 9 A 由铺铜断面保证
'FAN_12V_SW': 0.8,
'+5V': 0.6, '+3V3': 0.5, '+3V3_HUB': 0.5,
}
for _n in DP_NETS:
RT.WIDTH[_n] = DIFF_W
RT.WIDTH_AUTO = {}
# 七对 D± + 全部电源网都不按焊盘封顶(见 router.NO_CAP)。
# ⛔ 封顶对电源网是**静默降载**:`route_groups` 按「这个网最小的那只脚」封顶,
# 实测把 `+12V` 从 1.5 压到 **0.30**、`VIN_SW` 从 2.0 压到 0.30 —— 0.30 mm 1 oz
# 外层只带 0.65 A,而槽支路要 1.5 A。铺铜只盖到母线,**支路那一段没有铜面兜底**。
# 缩颈交给 `stub_w`(只缩引出脚,不缩干线)。
RT.NO_CAP = tuple(DP_NETS) + ('VIN_RAW', 'VIN_FUSED', 'VIN_SW',
'+12V', '+5V', '+3V3', '+3V3_HUB')
# ── 两层板独有的两条约束 ─────────────────────────────────────────────────
BOT_PEN = 6 # 底层每格的过路费(顶层 0)。见文件头第 1 条
BOT_PEN_W = 2 # 东西功能区的底层过路费:那里不在 D± 干线下方
if '--bot-pen-w' in sys.argv: # 服务器变体试算用:东西两区底层跳线的价格
BOT_PEN_W = int(sys.argv[sys.argv.index('--bot-pen-w') + 1])
DIFF_KEEP = 1.5 # D± 铜两侧多少 mm 内,底层不许有铜
HUG_BAND = 2.2 # DM 贴着 DP 走的自由带半宽
DIFF_GUIDE_W = 3.2 # 「导引线」宽度:够两根 1.2 并排(1.41 + 2×0.81)
GUIDE_BAND = 1.6 # 两根真线只许在导引线中心线 ±1.6 里走
HUG_PEN = 12
TOP_ONLY = [False] # 第一相:底层整层封死
_HUG = [None] # [(x0,y0,x1,y1), ...] 或 None
_LANE = [None] # (lane_y, [riser x 窗口]) 或 None
# ── M9:每对 D± 一条**专属车道**(通道里竖着一条 + 目标走廊里横着一条)─────
# 新拓扑下 D± 的路是三段:`U32` 东沿 → 逃逸扇形散到 x=29 → **D± 通道**(x 29~37)
# 里竖着走到目标槽的**北侧走廊** → 走廊里横着往东到 x≈59.5 的槽针。
# ⛔ 通道只有 8 mm,一对占 2.7 ⇒ 必须分道,否则先布的那一对会把通道占满
# (M8 实测:不分道时后布的那一对「布了 0 段」,而 DRC / 几何验证一条都不响)。
# 南北两边的对**不重叠**(`U32` 在 y=67,槽 1/2/3 在南、4/5 在北)⇒ 1&5、2&4 可以共用车道。
# ⛔ **车道要按「逃逸斜线有多长」分,不是按槽号**。扇形里外侧的脚要斜爬 ~6.5 mm
# 才到自己的 y,斜线终点 x≈32;把它分到 31.25 那条道,等于让斜线落在道外,
# 而它会从别人的起跑点上压过去 —— 实测 `USB5_DP` 的斜线正好压在 `USB5_DM`
# 的落点 (29.85, 73) 上,占用图那一格变 255(两个网碰过 = 谁也不能进),
# 现象是「一对里只进得去一根」,换五种办法都救不回来。
# ⇒ 斜线最长的(USB1/USB5)分最东那条道,最短的(USB3)分最西。
# ⛔ **车道顺序按「扇形横线不许横穿别人的竖段」定,不是按斜线长短**(上一版搞反了,
# DRC 报 USB1/USB2 之间 0 mil)。推导:南行的 USB1 扇出 y 最南、竖段只到自己以南,
# 所以它的横线必须**最先停**(最西的道);USB2 的扇出 y 在它北边,横线可以更往东。
# 北行同理反过来:USB5 扇出 y 最北 → 最西的道,USB3 → 最东。
DP_LANE_X = {1: 78.75, 2: 74.25, 3: 69.75, 4: 74.25, 5: 78.75}
LANE_HALF = 2.25 # 车道 4.5:一对要 2.63,再被一根 0.25 逃逸线横穿
# (吃掉 1.67)仍剩 2.83 ⇒ 装得下
# 逃逸扇形的落点区(所有对共用,靠扇形本身分开)。10 只脚 × 2.0 间距 = 20 mm 跨度,
# 以 U32 的 D± 脚中点(y≈66)为中心 ⇒ 56~76,两头各留 1。
U32_BOX = (73.0, 55.0, 82.0, 77.0)
RISER_U32 = None # M8 遗留,M9 不用
RISER_HALF = None
LANE_LO = LANE_HI = None
LANE_Y = {}
_orig_build = RT.build_grid
_orig_pen_pads = RT.pen_pads
_DIFF_SEGS = [] # 第一相布出来的 D± 铜,第二相用来封底层
def _extra_pen(g):
u"""底层过路费 + D± 走廊底层封死 + DM 的贴合带。**每次 reset_pen 之后都要重打。**"""
# ⛔ 底层过路费**分区收**(M9 起):D± 只在「西通道 x29~37 + 各走廊」里跑,
# 西区(x<29,Hub/ESP32/DAC 那一窝,最挤)的底层其实不在任何 D± 底下,
# 对它收满价等于白白把唯一的跳线层封掉。M8 全板降价(6→2)之所以更差
# (173→178),是因为那时 D± 横贯全板,降价换来的跳线全落在它底下。
if BOT_PEN:
# D± 从槽针 x=45 到东侧通道,这一带下方的地平面最珍贵。
g.penalize(2, B.MID[0], -1, B.DP_CHAN[2], B.BH + 1, BOT_PEN)
g.penalize(2, -1, -1, B.MID[0], B.BH + 1, BOT_PEN_W)
g.penalize(2, B.DP_CHAN[2], -1, B.BW + 1, B.BH + 1, BOT_PEN_W)
if _HUG[0] is not None:
pen = g.pen[1]
for k in range(len(pen)):
if pen[k] < HUG_PEN:
pen[k] = HUG_PEN
for (x0, y0, x1, y1) in _HUG[0]:
i0 = max(0, int(math.floor(x0 / RT.G)))
i1 = min(RT.NX - 1, int(math.ceil(x1 / RT.G)))
j0 = max(0, int(math.floor(y0 / RT.G)))
j1 = min(RT.NY - 1, int(math.ceil(y1 / RT.G)))
for j in range(j0, j1 + 1):
base = j * RT.NX
for i in range(i0, i1 + 1):
pen[base + i] = 0
def pour_blocks():
u"""顶层电源铜面 → 布线器的障碍(**按各自的网**标归属)。
⛔ 不喂进去的话,布线器会大大方方从 12V 高速路 / 铜墙中间穿过去 ——
铺铜时那根线把 9 A 的干道**整段切断**,而铺铜管理器 / DRC / 连通性
三个都只说「有铜」。
"""
import pour_base as PB
return [(net, r) for (n, net, lay, r, _p) in PB.plan()
if lay == 1 and n.startswith('PWR-')]
_POURS = [None]
_DSIDE = [None] # 当前布的是一对里的哪一根:-1 = DM(西半道),+1 = DP(东半道)
def dp_block_rects(i, side=None):
u"""布第 i 对 D± 时,顶层要封死的地方(除了它自己的车道和目标走廊)。
⛔ 通道里**不能整条封**:那一对还要在目标走廊那一段**往东拐出去**。
第一版按「通道里除本车道全封」写,结果只有 USB3 通(它的走廊正好和
`U32` 出口区重叠),其余四对全是「布了 0 段」—— 看着像通道太窄,
其实是出口被自己封死了。⇒ 通道的封锁按 y 取补集:
放行 `U32` 出口区 + 目标走廊那两段。
"""
# ⛔ **一对里的两根各占半条道**。共用整条道的下场:先布的那根自己蜿蜒占掉 3.5 mm
# (实测车道 4.5 只剩 1~2 格),第二根「没布通」。分半之后各 2.25,
# 一根 1.0 的线只要 2×0.71 = 1.42 ⇒ 都装得下,而且天然并排、耦合。
lane = DP_LANE_X[i]
if side:
lane = lane + side * (LANE_HALF / 2.0)
cor = B.usb_corridor(i) # 新针向:D± 焊盘在槽的南排
y0, y1 = B.PROJ[0], B.PROJ[1]
py = B.slot_y(B.USB_SLOT_POS[i]) - B.PAD_ROW_DY
hi = max(cor[3], py + 1.6) # 走廊上沿往上探到槽内的南排焊盘
keep = sorted([(U32_BOX[1], U32_BOX[3]), (cor[1], hi)])
# 合并重叠区间,再取补集
merged = []
for a, b in keep:
if merged and a <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], b)
else:
merged.append([a, b])
gaps, cur = [], y0
for a, b in merged + [[y1, y1]]:
if a - cur > 0.2:
gaps.append((cur, a))
cur = max(cur, b)
out = []
# ① D± 通道里不属于本对的 x 段(只在放行区间之外才封)
half = LANE_HALF / 2.0 if side else LANE_HALF
for (a, b) in ((B.DP_CHAN[0], lane - half), (lane + half, B.DP_CHAN[2])):
if b - a > 0.2:
for (ga, gb) in gaps:
out.append((a, ga, b, gb))
# ② 中区里不属于目标走廊的部分(别让它钻进别的走廊)
if cor[1] - y0 > 0.2:
out.append((B.MID[0], y0, B.MID[1], cor[1]))
if y1 - hi > 0.2:
out.append((B.MID[0], hi, B.MID[1], y1))
return out
_DPI = [None] # 当前在布第几对(1~5),None = 不限
def _block_lane(g):
if _DPI[0] is None:
return
for (a, b, c, d) in dp_block_rects(_DPI[0], _DSIDE[0]):
g.block_both(1, a, b, c, d, 0.0, net='#lane')
def ant_blocks(halfw):
u"""ESP32 天线:**缺口(没板材)+ 净空(不许有铜)**,两层都封死。
⛔ 这一条以前是漏的:`place_base`(摆位)认净空、`pour_base`(铺铜)认净空,
**布线器一个字都不知道** ⇒ A* 可以大大方方从天线底下穿线过去,
而 DRC / `board_check` / 几何校验一条都不响(设计文档 §PCB 要点写着
「天线区域下方禁止铺铜、**禁止走线**」,后半条从来没人执行)。
⚠ 净空**只往外膨胀**,朝模组那一侧不膨胀:净空自带 0.5 侧余量,
再往模组里吃 halfw 会把最靠里那几只脚的出口封死。
"""
import place_base as PL
k = PL.antenna_keepout(PL.sizes())
n = B.notch_rect()
e = RT.EDGE_CLR + halfw
ax = PL.hand()['U20'][2]
kk = {90: (k[0] - halfw, k[1] - halfw, k[2], k[3] + halfw),
270: (k[0], k[1] - halfw, k[2] + halfw, k[3] + halfw),
0: (k[0] - halfw, k[1], k[2] + halfw, k[3] + halfw),
180: (k[0] - halfw, k[1] - halfw, k[2] + halfw, k[3])}[int(round(ax)) % 360]
return [kk, (n[0] - e, n[1] - e, n[2] + e, n[3] + e)]
def build_grid(pads, halfw, vias=()):
g = _orig_build(pads, halfw, vias)
for (a, b, c, d) in ant_blocks(halfw):
for l in (1, 2):
g.block_both(l, a, b, c, d, 0.0, net='#ant')
if _POURS[0] is None:
_POURS[0] = pour_blocks()
for (net, r) in _POURS[0]:
g.block_both(1, r[0], r[1], r[2], r[3], 0.0, net=net)
_block_lane(g)
if TOP_ONLY[0]:
g.block_both(2, -1, -1, B.BW + 1, B.BH + 1, 0.0, net='#topOnly')
else:
for (l, x0, y0, x1, y1, ww, nn) in _DIFF_SEGS:
k = DIFF_KEEP + ww / 2.0 + halfw
RT.block_track(g, 2, x0, y0, x1, y1, k, '#diffKeep')
_extra_pen(g)
return g
def pen_pads(g, pads, halfw):
_orig_pen_pads(g, pads, halfw)
_extra_pen(g)
RT.build_grid = build_grid
RT.pen_pads = pen_pads
# ⛔ 0.25 不行:十根线一起走 45°,相邻两条的**垂直**间距只有 0.5×cos45 = 0.354,
# 减去线宽剩 0.104,而规则要 0.2(DRC 实测报「距离 4mil,应 ≥7.9mil」一大片)。
# 0.15 ⇒ 0.204,刚好过;JLC 两层最小线宽 0.127,做得了。
ESC_W = 0.15 # 逃逸段线宽(贴着 0.5 mm 脚距走)
ESC_STUB = 0.6 # 出脚先直着走多长再拐 45°(短一点,斜线才来得及收)
ESC_X = 81.0 # U32 西排逃逸的终点 x(= D± 通道东沿)
# ⛔ **不能取 1.5**(= 1.2 干线 + 0.3 缝)。占用图里一根已布的干线往两侧各膨胀
# `halfw + ww/2 = 0.81 + 0.6 = 1.41`,1.5 只剩 **0.09 mm** 余量 ⇒
# 每对里第一根(DP)布完之后,第二根(DM)在起跑点**一个格点都找不到**。
# 实测现象整整齐齐:七对里六对都是「DP 通、DM 出不来」,看着像板子太挤。
ESC_PITCH = 1.6 # 散开之后的线距(≥1.41 的膨胀量即可;越大斜线越长)
ESC_S_Y = 57.5 # 南排(USB6 / 上行)逃逸的终点 y
ESC_SLOT = 1.8 # 槽针 / 模组脚逃逸出去多深(够干线起步即可;
# 太深会把横跑段顶出走廊,见 dp_build 里那段)
def _pair_of(net):
u"""`USB3_DP` → 3;不是编号对(USB6 / 上行)返回 None。"""
if net.startswith('USB') and net[3].isdigit() and int(net[3]) <= B.SLOT_N:
return int(net[3])
return None
def pair_run_y(i, net):
u"""编号 D± 在目标槽走廊内的横向干线 y。"""
# USB3 的 DM 等长蛇形紧邻槽端地脚;整对南移 0.8 mm 留足 0.2 mm 净距。
extra = 0.8 if i == 3 else 0.0
return round(B.usb_corridor(i)[3] - (3.2 if net.endswith('_DP') else 1.2) - extra, 3)
def hub_escape(pads):
u"""`U32` **西沿**十只下行 D± 脚的逃逸扇形 + 虚拟起跑点。
⛔ **1.2 mm 的干线没法从 0.5 mm 间距的 QFN 脚上起步**(M8 §28 的教训,换布局照样成立):
十根线要从 0.5 的脚距散到 1.5,光斜着散开就要 4 mm。现象不是短路而是「布了 0 段」——
`pad_nodes` 在焊盘四周一个可起步的格点都找不到,看着像板子太挤。
⇒ 先用 0.25 mm 细线把十只脚扇开到 1.5 间距(0.5 脚距下净距 0.25,过 DRC 的 0.20),
扇到东侧 D± 通道的各自车道,在那儿放虚拟焊盘当干线起跑点。
"""
esc, launch = [], {}
want = set()
for nm, dp, dm in RB.PAIRS:
if nm not in ('USB6', 'USBUP'):
want.add(dp)
want.add(dm)
ds = [q for q in pads if q[0] == 'U32' and q[7] in want]
ds.sort(key=lambda q: q[3])
if len(ds) != 10:
print(u' [!] U32 西排找到 %d 只 D± 脚(应为 10)' % len(ds))
return [], {}
mid = (ds[0][3] + ds[-1][3]) / 2.0
for i, q in enumerate(ds):
px, py = q[2] - q[4] / 2.0, q[3]
ty = round(mid + (i - (len(ds) - 1) / 2.0) * ESC_PITCH, 3)
# ⛔ **每一对扇到自己的车道 x 上**,不要都停在同一条竖线(x=29)。
# 停在同一条线上的话,先布的那对横穿「出口区」时就把后面几对的
# 起跑点堵死 —— 实测五对里三对报「U32.launch 出不来」,
# 而换车道宽度 / 换先后顺序 / 加导引线三种办法都救不回来。
# 扇线本身在不同的 y 上,互相不交叉;南边的对去南边的槽、北边的去北边,
# 脚序(USB1 最南 … USB5 最北)与槽序同向,所以横穿也不会撞上别人的干线。
net = q[7]
# 一对里的两根在车道内**左右分开 1.5**(DP 在东、DM 在西):
# 共用一条竖线的话,先布的那根会压过后布那根的起跑点(实测「出不来」)。
# 1.5 > 占用图要求的 1.41,正好是差分对该有的中心距。
tx = DP_LANE_X.get(_pair_of(net), ESC_X)
# ⛔ **DP 在西、DM 在东**(不是反过来)。构造出来的路径是「竖 → 横 → 竖」:
# DM 走靠焊盘那条横线(南)、DP 走外面那条(北)。要不交叉,走南边横线的
# 那一根必须在通道里**靠东**,否则它往东跑时会横穿伙伴的竖段
# (自查实测 `USB5_DM 与 USB5_DP 净距 −0.001`)。
# 方向不同,左右要反过来:构造出来的路径是「竖 → 横 → 竖」,
# **先转弯的那一根必须在通道里靠西**,否则它向槽端横跑时会切过
# 伙伴尚未结束的竖段。方向必须按当前脚的实际起跑点和目标横线判断;
# 不能再用固定 y 阈值,U32 随手工布局移动后 USB3 会被误判并自交叉。
_i = _pair_of(net)
_yr = pair_run_y(_i, net)
_north = _yr > ty
_east = (net.endswith('_DP')) if not _north else (net.endswith('_DM'))
tx += 0.75 if _east else -0.75
x1 = px - ESC_STUB
x2 = round(x1 - abs(ty - py), 3)
esc.append((1, px, py, x1, py, ESC_W, net))
if abs(ty - py) > 1e-6:
esc.append((1, x1, py, x2, ty, ESC_W, net))
if abs(tx - x2) > 1e-6:
esc.append((1, x2, ty, tx, ty, ESC_W, net))
launch.setdefault(net, []).append(['U32', 'launch', round(tx, 3), ty,
0.3, 0.3, 1, net, 'RECT'])
return esc, launch
def hub_escape_aux(pads):
u"""`U32` **北沿**四只脚(`USB6_DP/DM` + 上行 `UP_DP/DM`)的逃逸扇形。"""
esc, launch = [], {}
want = set()
for nm, dp, dm in RB.PAIRS:
if nm in ('USB6', 'USBUP'):
want.add(dp)
want.add(dm)
cy32 = None
for q in pads:
if q[0] == 'U32' and str(q[1]) == '65':
cy32 = q[3]
ds = [q for q in pads if q[0] == 'U32' and q[7] in want
and (cy32 is None or q[3] > cy32)]
ds.sort(key=lambda q: q[2])
if len(ds) != 4:
print(u' [!] U32 北排找到 %d 只 D± 脚(应为 4)' % len(ds))
return [], {}
mid = (ds[0][2] + ds[-1][2]) / 2.0
for i, q in enumerate(ds):
px, py = q[2], q[3] + q[5] / 2.0
tx = round(mid + (i - (len(ds) - 1) / 2.0) * ESC_PITCH, 3)
y1 = py + ESC_STUB
y2 = round(y1 + abs(tx - px), 3)
net = q[7]
esc.append((1, px, py, px, y1, ESC_W, net))
if abs(tx - px) > 1e-6:
esc.append((1, px, y1, tx, y2, ESC_W, net))
# 从当前焊盘几何推导 Hub 北沿。旧版固定写成 y=79,是 M9 布局遗留;
# U32 移到 y=52.903 后会凭空多走约 20 mm,并把 USB6/上行对拉进控制区。
aux_y = round(max(p[3] + p[5] / 2.0 for p in ds) + 2.0, 3)
if abs(y2 - aux_y) > 1e-6:
esc.append((1, tx, y2, tx, aux_y, ESC_W, net))
launch.setdefault(net, []).append(['U32', 'launch', tx, aux_y,
0.3, 0.3, 1, net, 'RECT'])
return esc, launch
def aux_hub_escape(pads):
u"""U32 北沿辅助对逃逸;上行对用一小段底层完成与 USB6 的局部换序。"""
def pad(net):
hit = [q for q in pads if q[0] == 'U32' and q[7] == net]
if len(hit) != 1:
raise RuntimeError(u'U32 %s 焊盘数为 %d' % (net, len(hit)))
q = hit[0]
return (q[2], q[3] + q[5] / 2.0)
esc, launch, vias = [], {}, []
usb = {
'USB6_DP': [(90.2, 60.0)],
'USB6_DM': [(91.8, 60.0)],
}
for net, tail in usb.items():
s = pad(net)
pts = [s, (s[0], s[1] + 0.6), tail[0]]
esc += _poly(net, pts, ESC_W)
launch[net] = [['U32', 'launch', tail[0][0], tail[0][1],
0.3, 0.3, 1, net, 'RECT']]
# 顶层从 0.5 mm 脚距散开,过孔下到底层;两根底线保持 1.6 mm 中心距。
spec = {
'UP_DP': ([(87.8, 59.2)], [(93.2, 61.5)]),
'UP_DM': ([(87.8, 60.8)], [(93.2, 63.1)]),
}
for net, (a, b) in spec.items():
s = pad(net)
esc += _poly(net, [s, (s[0], s[1] + 0.6), a[0]], ESC_W)
esc += [(2, a[0][0], a[0][1], b[0][0], b[0][1], DIFF_W, net)]
vias += [(a[0][0], a[0][1], net), (b[0][0], b[0][1], net)]
launch[net] = [['U32', 'launch', b[0][0], b[0][1],
0.3, 0.3, 1, net, 'RECT']]
return esc, launch, vias
def _slot_refs():
return [B.USB_SLOT_REF[i] for i in range(1, B.SLOT_N + 1)]
SLOT_REFS = _slot_refs()
def topology_check(pads):
u"""用实测焊盘验证新拓扑。这些条件不成立时,坐标调得再好也不能下发。"""
bad = []
def one(ref, net):
return [q for q in pads if q[0] == ref and q[7] == net]
ep = [q for q in pads if q[0] == 'U32' and str(q[1]) == '65']
if not ep:
bad.append(u'U32 EP 焊盘不存在')
return bad
cx = ep[0][2]
spans = []
for i in range(1, B.SLOT_N + 1):
ref = B.USB_SLOT_REF[i]
hp = one('U32', 'USB%d_DP' % i)
sp = one(ref, 'USB%d_DP' % i)
if len(hp) != 1 or len(sp) != 1:
bad.append(u'USB%d 端点不唯一:U32=%d / %s=%d' % (i, len(hp), ref, len(sp)))
continue
if hp[0][2] >= cx:
bad.append(u'USB%d 在 U32 上没有朝西出脚' % i)
if not (B.DP_CHAN[0] < DP_LANE_X[i] < B.DP_CHAN[2]):
bad.append(u'USB%d 车道 x=%.2f 超出 D± 通道' % (i, DP_LANE_X[i]))
c = B.usb_corridor(i)
if not (c[1] <= sp[0][3] - ESC_SLOT <= c[3]):
bad.append(u'USB%d 槽端向南逃逸后没有落入目标走廊' % i)
spans.append((i, hp[0][3], sp[0][3]))
# 两端 y 顺序必须一致,否则平面内一定交叉。
for a, b in zip(spans, spans[1:]):
if (a[1] - b[1]) * (a[2] - b[2]) <= 0:
bad.append(u'USB%d/USB%d 两端 y 顺序相反,D± 必然交叉' % (a[0], b[0]))
return sorted(set(bad))
def pair_escape(pads, refs, out_dir):
u"""给**任何一端脚距太密的 D± 焊盘**接一段 0.25 mm 逃逸,末端放虚拟起跑点。
⛔ 这一条 M9 是必须的:槽针是 **1.0 mm 脚距**(盘 0.7 宽),
而 D± 干线 1.2 mm + 净距 0.21 ⇒ 半宽 0.81,`pad_nodes` 在焊盘四周
**一个可起步的格点都找不到**(实测 `J34.46` = 0 个),
现象是「差分对 USB1 布了 0 段」,看着像通道太挤,其实是起不了步。
M8 侥幸能布是因为那时槽转 90°、焊盘长边(2.0)正好顺着走线方向;
M9 转过来之后长边朝南北,这条侥幸就没了。
`out_dir`: 'N'/'S'/'E'/'W',焊盘往哪一侧逃。
"""
esc, launch = [], {}
D = {'N': (0.0, 1.0), 'S': (0.0, -1.0), 'E': (1.0, 0.0), 'W': (-1.0, 0.0)}[out_dir]
for ref, nets in refs.items():
depth = 3.2 if ref in ('U20', 'J23') else ESC_SLOT
ax = 0 if D[0] == 0 else 1 # 0 = 按 x 排,1 = 按 y 排
by = {}
for q in pads:
if q[0] == ref and q[7] in nets:
by.setdefault(q[7], []).append(q)
if len(by) != 2:
print(u' [!] %s 上只找到 %d 个 D± 网' % (ref, len(by)))
continue
# Type-C 的 A6/B6 与 A7/B7 是交错四脚。J23 必须选 A6/A7 这组:
# 它们的 DP/DM 左右顺序与 U32 一致;选“分得最开”的 B7/B6 会把
# 顺序反过来,两根线在单层平面内必然交叉。B6/B7 会在主流程里
# 保留为独立目标,不得像旧版那样静默丢掉。
na, nb = sorted(by)
if ref == 'J23':
prefer = {'UP_DP': 'A6', 'UP_DM': 'A7'}
qa = next(q for q in by[na] if str(q[1]) == prefer[na])
qb = next(q for q in by[nb] if str(q[1]) == prefer[nb])
best = (abs(qa[2 + ax] - qb[2 + ax]), qa, qb)
else:
best = None
for qa in by[na]:
for qb in by[nb]:
d0 = abs(qa[2 + ax] - qb[2 + ax])
if best is None or d0 > best[0]:
best = (d0, qa, qb)
ps = sorted([best[1], best[2]], key=lambda q: q[2 + ax])
mid = (ps[0][2 + ax] + ps[1][2 + ax]) / 2.0
for k, q in enumerate(ps):
half = (q[5] / 2.0) if D[1] else (q[4] / 2.0)
sx = q[2] + D[0] * half
sy = q[3] + D[1] * half
t = round(mid + (k - 0.5) * ESC_PITCH, 3) # 散到 1.5 间距
# 先直着走 ESC_STUB,再 45° 斜到目标位,最后直着走到 ESC_SLOT 深度
x1, y1 = sx + D[0] * ESC_STUB, sy + D[1] * ESC_STUB
d = abs((t - (sx if ax == 0 else sy)))
x2, y2 = (t, y1 + D[1] * d) if ax == 0 else (x1 + D[0] * d, t)
x3, y3 = (x2, sy + D[1] * depth) if ax == 0 else (sx + D[0] * depth, y2)
net = q[7]
esc.append((1, sx, sy, x1, y1, ESC_W, net))
if d > 1e-6:
esc.append((1, x1, y1, x2, y2, ESC_W, net))
if abs((x3 - x2) + (y3 - y2)) > 1e-6:
esc.append((1, x2, y2, x3, y3, ESC_W, net))
launch.setdefault(net, []).append([ref, 'launch', round(x3, 3), round(y3, 3),
0.3, 0.3, 1, net, 'RECT'])
return esc, launch
def hug_rects(segs, band=HUG_BAND):
out = []
for (l, x0, y0, x1, y1, _w, _n) in segs:
if l != 1:
continue
out.append((min(x0, x1) - band, min(y0, y1) - band,
max(x0, x1) + band, max(y0, y1) + band))
return out
def rail_absorb(bypad):
u"""软铺铜(`pour_base.RAILS`)盖住的脚不再交给 A*。
⛔ 不做这一步的实测后果:`+3V3` 一个网 47 只脚、A* 把它当一棵树布,
摆位一动就崩(168 条里它一个网占 45)。这些脚本来就该靠铜面连,不该有线。
⛔ **一个网有多块铜时必须一起处理**(第一版是按块逐个跑的,踩了两个坑):
① 第二块在**第一块已经加过虚拟落点的表**上再跑一遍 ⇒ 落点指数膨胀
(47 只脚变成 126 个目标),实测直接炸到 239 条;
② 两块铜之间**没有连接** —— 同网不同岛,铺完还是断的,
而铺铜管理器 / DRC 都只说「有铜」。
⇒ 现在:先按网归并所有块 → 区内的脚一次性摘掉 → 区外的脚**每只只给一个**落点
(落在离它最近的那块上)→ 再给块与块之间各加一对落点,逼布线器把它们接起来。
⚠ 判据不在这里。软铺铜可能被信号切成孤岛 ——
**唯一判据是 `board_check --base` 从板上读回铜做并查集**(方案 §27.7 ④)。
"""
import pour_base as PB
byn = {}
for name, net, r in PB.RAILS:
byn.setdefault(net, []).append((name, r))
bridges = []
for net, rects in byn.items():
ps = bypad.get(net)
if not ps:
continue
def inside(q, r):
return r[0] <= q[2] <= r[2] and r[1] <= q[3] <= r[3]
ins = [q for q in ps if any(inside(q, r) for _n, r in rects)]
out = [q for q in ps if q not in ins]
if not ins:
continue
virt = []
for q in out:
best = None
for _n, r in rects:
vx = min(max(q[2], r[0] + 1.0), r[2] - 1.0)
vy = min(max(q[3], r[1] + 1.0), r[3] - 1.0)
d = (vx - q[2]) ** 2 + (vy - q[3]) ** 2
if best is None or d < best[0]:
best = (d, vx, vy)
virt.append(['RAIL', 'rail', round(best[1], 3), round(best[2], 3),
0.6, 0.6, 1, net, 'RECT'])
# 块与块之间也要接起来(同网不同岛,铺铜自己不会连)。几何是已知的,
# 直接构造固定铜桥;把两端伪装成焊盘交给 A* 会在铜区中心判成「出不来」。
for i in range(len(rects) - 1):
ra, rb = rects[i][1], rects[i + 1][1]
ax = min(max((rb[0] + rb[2]) / 2.0, ra[0] + 1.0), ra[2] - 1.0)
ay = min(max((rb[1] + rb[3]) / 2.0, ra[1] + 1.0), ra[3] - 1.0)
bx = min(max(ax, rb[0] + 1.0), rb[2] - 1.0)
by = min(max(ay, rb[1] + 1.0), rb[3] - 1.0)
bridges.append((1, round(ax, 3), round(ay, 3), round(bx, 3), round(by, 3),
RT.WIDTH.get(net, RT.W_DEFAULT), net))
if out:
bypad[net] = out + virt
else:
bypad.pop(net, None)
print(u' 软铺铜 %-9s %d 块 吸收 %2d 只脚,区外 %2d 只(每只一个落点)+ %d 条固定铜桥'
% (net, len(rects), len(ins), len(out), len(rects) - 1))
return bridges
def wall_build():
u"""`+3V3` 槽墙之间的**底层桥**:按构造画,不走 A*。
⛔ 为什么必须是底层:每个槽的 `SDA_i`/`SCL_i` 在北排 x 75/76,要一路往西去 `U35`,
十根线全都要横穿 x 70.6~73.4 那一条。顶层连墙 = 把它们拦腰截断,
而**布线器只会报「没布通」,DRC 与铺铜管理器一个字都不说**。
⚠ 缝在地平面上:每段 9.5 mm、宽 0.8,都在 x=72。D± 全在 x<66,
不碰方案 §6.2 第 1 条「D± 走廊下方一刀不许割」。
"""
bs, vs = B.wall3v3_bridges()
segs = [(2, x, y0, x, y1, w, '+3V3') for (x, y0, y1, w) in bs]
vias = [(x, y, '+3V3') for (x, y) in vs]
return segs, vias
TAP_W = 0.30 # PWR9A 规则下限;这一脚只走栅极电容的充放电电流
TAP_Y = 0.70 # 贴板边:边距 0.4 + 半线宽 0.15 + 0.15 余量
def gate_tap(pads):
u"""`C1.1`(VIN_FUSED)→ `DZ1.1` 的固定细线:沿板边从 DZ1 正下方穿过去。
⛔ 为什么不交给 A*:
① `VIN_FUSED` 是 2.0 mm 的 9 A 网,0402 的焊盘周围找不到一个能起步的格点
(实测「VIN_FUSED: C1.1 出不来」);而这一脚本来只需要细线。
② 板边那条缝在 0.25 网格上表示不出来:板边膨胀到 0.76、DZ1 下沿膨胀到 0.87,
格点 0.75 / 1.0 两头都被判死,而真实几何下 0.30 的线放在 y=0.70 两边都有余量。
拓扑:GATE 从 Q1.4 向南接 DZ1.2,再**绕 C1 北侧**接 C1.2 / R1.2;这根线在南侧,
两者不相交,于是 C1.1 不会被 GATE 围在板边(单层平面上唯一的解)。
"""
def pad(ref, pin):
hit = [q for q in pads if q[0] == ref and str(q[1]) == pin and q[6] == 1]
if len(hit) != 1 or hit[0][7] != 'VIN_FUSED':
raise RuntimeError(u'%s.%s 应是唯一的顶层 VIN_FUSED 焊盘' % (ref, pin))
return hit[0]
c, d = pad('C1', '1'), pad('DZ1', '1')
segs = _poly('VIN_FUSED', [(c[2], c[3]), (c[2], TAP_Y), (d[2], TAP_Y), (d[2], d[3])], TAP_W)
bad = dp_obstacle_check(segs, pads, nets=('VIN_FUSED',))
for s in segs:
if min(s[2], s[4]) - s[5] / 2.0 < RT.EDGE_CLR - 1e-6:
bad.append(u'VIN_FUSED 细线离南板边不足 %.2f' % RT.EDGE_CLR)
for n, (hx, hy) in B.MOUNT_HOLES.items():
e = (1, hx, hy, hx, hy, 0, '#')
if _seg_dist(s, e) - B.HOLE_D / 2.0 - s[5] / 2.0 < 0.3:
bad.append(u'VIN_FUSED 细线压到安装孔 %s' % n)
if bad:
raise RuntimeError(u'C1.1 固定细线几何校验不过:' + u';'.join(bad))
return segs, c
def wall_absorb(bypad):
u"""`+3V3` 槽墙盖住的 15 只槽脚从 A* 摘掉,墙外的脚每只给一个落点。
⛔⛔ 落点的位号必须是 `WALL`(`router.PRE_WIRED`):那才是「这几个落点已经被铜
连在一起了」的意思,Prim 会把它们**全部当种子**,一只脚就近接到墙上就完事。
不带这个标记的话(`rail_absorb` 那种普通落点),Prim 把落点也当普通节点,
**逼 A* 在墙上再布线把落点串起来** —— 实测 `+3V3` 从最北那个落点一路蜿蜒到
板子最南(100 mm,横穿五条走廊,压在 I²C 的出脚道上),而布线器只报「布通了」。
⚠ 十个落点(每块墙的南北两个走廊帽):走廊帽整宽 2.8 mm,出口最多;
针排那三段窄,落点放上去会「挑得出来、引出脚发不出来」。
"""
ps = bypad.get('+3V3')
if not ps:
return
rects = B.wall3v3_rects()
def inside(q):
return any(r[0] <= q[2] <= r[2] and r[1] <= q[3] <= r[3] for r in rects)
ins = [q for q in ps if inside(q)]
out = [q for q in ps if not inside(q)]
if not ins:
return
caps = [r for r in rects if r[2] - r[0] > 2.0 and r[3] - r[1] < 2.0]
virt = [['WALL', 'wall', round((r[0] + r[2]) / 2.0, 3), round((r[1] + r[3]) / 2.0, 3),
0.6, 0.6, 1, '+3V3', 'RECT'] for r in caps]
bypad['+3V3'] = out + virt
print(u' 3V3 槽墙 %d 块 吸收 %2d 只槽脚,墙外 %2d 只 + %d 个 WALL 落点(互相不布线)'
% (len(rects), len(ins), len(out), len(virt)))
def bus_absorb(bypad):
u"""12V 干道已连成一块铜:覆盖的插槽脚不再参与 A*。"""
ps = bypad.get('+12V')
if not ps:
return
rects = B.bus_rects()
def inside(q):
x0, y0 = q[2] - q[4] / 2.0, q[3] - q[5] / 2.0
x1, y1 = q[2] + q[4] / 2.0, q[3] + q[5] / 2.0
return any(r[0] <= x0 and x1 <= r[2] and r[1] <= y0 and y1 <= r[3] for r in rects)
ins = [q for q in ps if inside(q)]
out = [q for q in ps if q not in ins]
r = B.BUS_W
landing = ['WALL', '12v-bus', round((r[0] + r[2]) / 2.0, 3),
round(r[1] + 2.0, 3), 0.8, 0.8, 1, '+12V', 'RECT']
bypad['+12V'] = out + [landing]
print(u' 12V 干道 %d 块 吸收 %2d 只槽脚,干道外 %2d 只 + 1 个 WALL 落点'
% (len(rects), len(ins), len(out)))
def balance_numbered_pairs(segs, esc, pairs=None, prefer_horizontal=False):
u"""在较短线的专属车道内加 45° 微蛇形,将对内长差压到 1 mm 以内。
优先使用最长的直线段;若竖向段容不下(当前 USB3 只有 3 mm),就使用
横向干线。补偿始终朝远离伴线的一侧展开,避免侵入两线之间的耦合间隙。
"""
tol = RB.DIFF['differentailPairLenTolerMax'] if hasattr(RB, 'DIFF') else 1.0
def length(net):
return sum(math.hypot(s[3] - s[1], s[4] - s[2])
for s in list(esc) + segs if s[6] == net)
targets = pairs or [('USB%d_DP' % i, 'USB%d_DM' % i)
for i in range(1, B.SLOT_N + 1)]
for dp, dm in targets:
ld, lm = length(dp), length(dm)
short, delta = (dp, lm - ld) if ld < lm else (dm, ld - lm)
if delta <= tol:
continue
# 只补到 0.25 mm 余量,避免网格/小数误差又越过 1 mm。
need = delta - 0.25
total_a = need / (2.0 * (math.sqrt(2.0) - 1.0))
amps = []
left = total_a
while left > 1e-6:
a = min(0.70, left)
amps.append(a)
left -= a
used = 2.0 * sum(amps)
# 两端各留 0.2 mm 直线,避免补偿尖点正好落在转角上。
cand = []
for k, s in enumerate(segs):
if s[6] != short:
continue
dx, dy = abs(s[3] - s[1]), abs(s[4] - s[2])
span = max(dx, dy)
if min(dx, dy) < 1e-6 and span >= used + 0.4:
axis = 0 if dy > dx else 1
preferred = 1 if prefer_horizontal else 0
cand.append((0 if axis == preferred else 1, -span, axis, k, s))
if not cand:
raise RuntimeError(u'%s 没有可容纳 %.2f mm 等长段的直线车道' % (short, used))
_pref, neg_span, _axis, k, s = min(cand)
_span = -neg_span
partner = dm if short == dp else dp
repl = []
if _axis == 0: # 竖向主线,补偿在 x 方向展开
pp = [(abs(q[4] - q[2]), q[1]) for q in segs
if q[6] == partner and abs(q[3] - q[1]) < 1e-6]
partner_coord = max(pp)[1] if pp else s[1]
away = 1.0 if s[1] > partner_coord else -1.0
sign = 1.0 if s[4] > s[2] else -1.0
x, y = s[1], s[2] + sign * ((_span - used) / 2.0)
if abs(y - s[2]) > 1e-6:
repl.append((s[0], x, s[2], x, y, s[5], s[6]))
for a in amps:
y2 = y + sign * a
repl.append((s[0], x, y, x + away * a, y2, s[5], s[6]))
y3 = y2 + sign * a
repl.append((s[0], x + away * a, y2, x, y3, s[5], s[6]))
y = y3
if abs(y - s[4]) > 1e-6:
repl.append((s[0], x, y, s[3], s[4], s[5], s[6]))
else: # 横向主线,补偿在 y 方向展开
pp = [(abs(q[3] - q[1]), q[2]) for q in segs
if q[6] == partner and abs(q[4] - q[2]) < 1e-6]
partner_coord = max(pp)[1] if pp else s[2]
away = 1.0 if s[2] > partner_coord else -1.0
sign = 1.0 if s[3] > s[1] else -1.0
x, y = s[1] + sign * ((_span - used) / 2.0), s[2]
if abs(x - s[1]) > 1e-6:
repl.append((s[0], s[1], y, x, y, s[5], s[6]))
for a in amps:
x2 = x + sign * a
repl.append((s[0], x, y, x2, y + away * a, s[5], s[6]))
x3 = x2 + sign * a
repl.append((s[0], x2, y + away * a, x3, y, s[5], s[6]))
x = x3
if abs(x - s[3]) > 1e-6:
repl.append((s[0], x, y, s[3], s[4], s[5], s[6]))
segs[k:k + 1] = repl
def dp_build(bypad, esc=()):
u"""**按构造**画五对编号 D±(不走 A*)。
⛔ 为什么不用 A*:换了七种办法(车道宽度 3.2/4.5、先后顺序、导引线、分道扇出、
半条道、降线宽、去掉 HUG)都是「一对里只进得去一根」。读占用图看到的真相是:
**先布的那一根自己蜿蜒**,4.5 mm 的车道被它占掉 3.5,第二根再也挤不进去;
而 A* 没有「贴着伙伴走」的概念。
⇒ 几何已经完全规则(通道空、走廊空、每对一条车道),路径本来就是一个 L:
`U32` 落点 →(车道里竖着走)→(走廊里横着走)→ 槽侧落点。直接画。
⚠ DM 走靠焊盘那条横线(+1.5)、DP 走外面那条(+3.0);通道里谁东谁西由
`hub_escape` 按走向定(后转弯的靠西),两者配合才不交叉。
"""
segs = []
for i in range(1, B.SLOT_N + 1):
cor = B.usb_corridor(i)
for suf in ('_DP', '_DM'):
net = 'USB%d%s' % (i, suf)
ps = bypad.get(net) or []
a = [q for q in ps if q[0] == 'U32']
b = [q for q in ps if q[0] == B.USB_SLOT_REF[i]]
if not a or not b:
print(u' [!] %s 少了落点(U32 %d / 槽 %d)' % (net, len(a), len(b)))
continue
x0, y0 = a[0][2], a[0][3]
x1, y1 = b[0][2], b[0][3]
# ⛔ 横跑段的 y 必须**从走廊南沿算**,不能从逃逸落点往上加:
# 落点一深(ESC_SLOT)就顶出过道 —— 实测 USB5 跑到 y 113.4/114.9,
# 而给它留的 `DP_N_LANE` 只有 108.74~112.74,直接从风扇块
# (C738 / R3 / L2 / L3)身上压过去,DRC 报 34 条「焊盘到导线 0 mil」。
# 两条横线间距 2.0:DM 的末段竖线顶到落点(走廊南沿 + ESC_SLOT),
# DP 的横线必须比它再高 1.2 才够净距(实测 2.7 时只有 0.9,差 0.3)。
yr = pair_run_y(i, net)
w = DIFF_W
if abs(yr - y0) > 1e-6:
segs.append((1, x0, y0, x0, yr, w, net))
if abs(x1 - x0) > 1e-6:
segs.append((1, x0, yr, x1, yr, w, net))
if abs(y1 - yr) > 1e-6:
segs.append((1, x1, yr, x1, y1, w, net))
balance_numbered_pairs(segs, esc)
bad = dp_selfcheck(segs + [e for e in esc])
if bad:
raise RuntimeError(u'构造出来的 D± 净距不过:' + u';'.join(bad[:8]))
return segs
def _poly(net, pts, width):
return [(1, a[0], a[1], b[0], b[1], width, net)
for a, b in zip(pts, pts[1:])
if abs(a[0] - b[0]) + abs(a[1] - b[1]) > 1e-9]
def aux_dp_build(bypad):
u"""按空走廊构造 USB6 与上行 USB,不再让单网 A* 抢占伙伴的通道。"""
def point(net, ref, pin=None):
hit = [q for q in bypad.get(net, [])
if q[0] == ref and (pin is None or str(q[1]) == pin)]
if len(hit) != 1:
raise RuntimeError(u'%s 的 %s.%s 端点数为 %d' % (net, ref, pin or '*', len(hit)))
return (hit[0][2], hit[0][3])
out = []
u6dp0, u6dp1 = point('USB6_DP', 'U32'), point('USB6_DP', 'U20')
u6dm0, u6dm1 = point('USB6_DM', 'U32'), point('USB6_DM', 'U20')
# U31/RT601 从中间挡住直线;两根线成对绕其东侧,再从 U20 西侧空带北上。
p = [(90.5, 60.75), (90.5, 69.5), (91.0, 70.0), (92.5, 72.0),
(92.5, 75.5), (90.0, 78.0), (82.0, 78.0), (80.75, 79.25),
(80.75, 110.1), (85.22, 110.1)]
m = [(92.1, 60.75), (92.1, 68.9), (92.6, 69.4), (94.1, 71.5),
(94.1, 75.5), (91.6, 79.6), (83.6, 79.6), (82.35, 80.85),
(82.35, 111.7), (85.80, 111.7)]
out += _poly('USB6_DP', [u6dp0, p[0]], 0.3) + _poly('USB6_DP', p, DIFF_W)
out += _poly('USB6_DP', [p[-1], u6dp1], 0.3)
out += _poly('USB6_DM', [u6dm0, m[0]], 0.3) + _poly('USB6_DM', m, DIFF_W)
out += _poly('USB6_DM', [m[-1], (86.82, 111.7), u6dm1], 0.3)
updp0, updp1 = point('UP_DP', 'J23', 'launch'), point('UP_DP', 'U32')
updm0, updm1 = point('UP_DM', 'J23', 'launch'), point('UP_DM', 'U32')
# 上行对沿东板边走,避开 Hub 去耦列;两次 90 度转弯保持 1.6 mm 中心距。
p = [(94.5, 12.5), (103.2, 12.5), (103.2, 61.5)]
m = [(96.1, 14.1), (104.8, 14.1), (104.8, 63.1)]
out += _poly('UP_DP', [updp0, p[0]], 0.3) + _poly('UP_DP', p, DIFF_W)
out += _poly('UP_DP', [p[-1], updp1], 0.3)
out += _poly('UP_DM', [updm0, m[0]], 0.3) + _poly('UP_DM', m, DIFF_W)
out += _poly('UP_DM', [m[-1], updm1], 0.3)
# Type-C 的 B6/B7 也要并回同名网络。DP 从焊盘北侧绕,DM 从南侧绕,互不交叉。
b6 = point('UP_DP', 'J23', 'B6')
b7 = point('UP_DM', 'J23', 'B7')
out += _poly('UP_DP', [b6, (95.321, 8.547), (96.5, 9.25),
(96.5, 10.25), (96.25, 12.5), (94.5, 12.5)], 0.3)
out += _poly('UP_DM', [b7, (93.823, 6.3), (94.821, 6.3),
(94.821, 7.597)], ESC_W)
return out
def _seg_dist(a, b):
u"""两段线段之间的最短距离。"""
import math as _m
def p2s(px, py, x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
L2 = dx * dx + dy * dy
t = 0.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L2))
return _m.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
return min(p2s(a[1], a[2], b[1], b[2], b[3], b[4]), p2s(a[3], a[4], b[1], b[2], b[3], b[4]),
p2s(b[1], b[2], a[1], a[2], a[3], a[4]), p2s(b[3], b[4], a[1], a[2], a[3], a[4]))
def dp_selfcheck(segs, clr=0.2):
u"""构造出来的 D± 之间**净距自查**。
⛔ 按构造画的东西**没有 A* 的占用图兜底**,几何必须自己查:第一版画完直接下发,
DRC 才发现不同对之间 0 mil(车道顺序排反、扇形横线横穿别人的竖段)。
"""
bad = []
for i in range(len(segs)):
a = segs[i]
for j in range(i + 1, len(segs)):
b = segs[j]
if a[6] == b[6] or a[0] != b[0]:
continue
need = clr + a[5] / 2.0 + b[5] / 2.0
d = _seg_dist(a, b)
if d < need - 1e-6:
bad.append(u'%s(%.2f,%.2f→%.2f,%.2f) 与 %s(%.2f,%.2f→%.2f,%.2f) 净距 %.3f < %.3f'
% (a[6], a[1], a[2], a[3], a[4],
b[6], b[1], b[2], b[3], b[4],
d - a[5] / 2.0 - b[5] / 2.0, clr))
return bad
def _seg_rect_dist(s, r):
if ((r[0] <= s[1] <= r[2] and r[1] <= s[2] <= r[3]) or
(r[0] <= s[3] <= r[2] and r[1] <= s[4] <= r[3])):
return 0.0
edges = [(1, r[0], r[1], r[2], r[1], 0, '#'),
(1, r[2], r[1], r[2], r[3], 0, '#'),
(1, r[2], r[3], r[0], r[3], 0, '#'),
(1, r[0], r[3], r[0], r[1], 0, '#')]
return min(_seg_dist(s, e) for e in edges)
def dp_obstacle_check(segs, pads, clr=0.2, nets=None):
u"""构造线没有 A* 占用图兜底,必须显式检查焊盘和硬功率铜。`nets` 缺省 = 七对 D±。"""
bad = []
for s in segs:
if s[6] not in (nets or DP_NETS):
continue
for q in pads:
if q[6] != s[0] or q[7] == s[6]:
continue
r = (q[2] - q[4] / 2.0, q[3] - q[5] / 2.0,
q[2] + q[4] / 2.0, q[3] + q[5] / 2.0)
d = _seg_rect_dist(s, r) - s[5] / 2.0
if d < clr - 1e-6:
bad.append(u'%s(%.2f,%.2f→%.2f,%.2f) vs %s.%s(%s) 净距 %.3f' %
(s[6], s[1], s[2], s[3], s[4],
q[0], q[1], q[7] or 'NC', d))
for net, r in pour_blocks():
if net == s[6]:
continue
d = _seg_rect_dist(s, r) - s[5] / 2.0
if d < clr - 1e-6:
bad.append(u'%s 切入 %s 功率铜(净距 %.3f)' % (s[6], net, d))
return sorted(set(bad))
# ── 主流程 ───────────────────────────────────────────────────────────────
def plan_all(priority=()):
pads = apply_route_fix(split_th(RT.load_pads()))
tb = topology_check(pads)
if tb:
raise RuntimeError(u'底板拓扑自检不过:' + u';'.join(tb))
board_vias = load_board_vias()
th = sum(1 for p in pads if str(p[1]).endswith(BOT_SUFFIX))
print(u'板上已有过孔 %d 个;通孔焊盘 %d 只(已拆成两层)' % (len(board_vias), th))
bypad = {}
for p in pads:
if not p[7] or p[7] in RT.SKIP or str(p[1]).endswith(BOT_SUFFIX):
continue
bypad.setdefault(p[7], []).append(p)
rail_segs = rail_absorb(bypad)
bus_absorb(bypad)
wall_absorb(bypad)
segs, vias, logs = list(rail_segs), [], []
# `+3V3` 槽墙之间的底层桥:**最先画**,好让后面所有网都把它当障碍绕开
wsegs, wvias = wall_build()
segs += wsegs
vias += wvias
# C1.1 → DZ1.1 的固定细线:同样最先画、不许拆,C1.1 从 A* 里摘掉(见 gate_tap)
tsegs, tpad = gate_tap(pads)
segs += tsegs
bypad['VIN_FUSED'] = [q for q in bypad.get('VIN_FUSED', [])
if not (q[0] == tpad[0] and str(q[1]) == str(tpad[1]))]
print(u' C1.1 → DZ1.1:板边固定细线 %d 段 %.2f mm' % (len(tsegs), TAP_W))
# 地岛 / GATE 缺口里 GND 贴片脚的专用过孔(pour_base.GND_PAD_VIAS):布线器必须先看见它们,
# 否则信号线会从孔位上压过去,缝合那一步再把孔打上就是短路
import pour_base as PB
gv = [(x, y, 'GND') for (x, y) in PB.gnd_pad_vias()[1]]
vias += gv
print(u' GND 贴片脚专用过孔 %d 个(D1.2 / C5.1 / R1.1)' % len(gv))
# `U32` 的十四只 D± 脚换成虚拟起跑点(西排 10 + 北排 4)。
esc, launch = hub_escape(pads)
aux_esc, aux_launch, aux_vias = aux_hub_escape(pads)
esc += aux_esc
vias += aux_vias
for k, v in aux_launch.items():
launch.setdefault(k, []).extend(v)
# 槽针 / U20 / USB-C 那三处脚距太密的,也各接一段逃逸(见 pair_escape)
slot_refs = {}
for i, ref in enumerate(SLOT_REFS, start=1):
slot_refs[ref] = {'USB%d_DP' % i, 'USB%d_DM' % i}
more = [pair_escape(pads, slot_refs, 'S'),
pair_escape(pads, {'U20': {'USB6_DP', 'USB6_DM'}}, 'N'),
pair_escape(pads, {'J23': {'UP_DP', 'UP_DM'}}, 'N')]
for e2, l2 in more:
esc += e2
for k, v in l2.items():
launch.setdefault(k, []).extend(v)
segs += esc
j23_escape_pin = {'UP_DP': 'A6', 'UP_DM': 'A7'}
for net in DP_NETS:
drop = {'U32'} | set(SLOT_REFS) | {'U20', 'J23'}
keep = [q for q in bypad.get(net, []) if q[0] not in drop]
# Type-C 同名的另一只 D± 脚也必须接上;只摘掉已经由
# pair_escape 连到 launch 的 A6/A7,保留 B6/B7 给 A* 做局部交叉。
if net in j23_escape_pin:
keep += [q for q in bypad.get(net, [])
if q[0] == 'J23' and str(q[1]) != j23_escape_pin[net]]
bypad[net] = keep + launch.get(net, [])
print(u' U32 逃逸扇形:%d 段 %.2f mm 细线(西排 10 只进东侧车道,北排 4 只向北)'
% (len(esc), ESC_W))
# ⛔ **第一相把「焊盘过路费」降到 2**(默认 10):D± 是第一批布的,没有「后面的网」
# 要保护,收下来只会让它们绕开满板的焊盘光环而蜿蜒(M8 实测一对布出 40 段)。
_pad_pen = RT.PAD_PEN
RT.PAD_PEN = 2
# ── 第一相:七对 D± 全部按构造画,避免逐网 A* 抢占伙伴通道 ─────────────
dpb = dp_build(bypad, esc)
segs += dpb
print(u' 五对编号 D±:按构造画了 %d 段(不走 A*,见 dp_build)' % len(dpb))
aux = aux_dp_build(bypad)
balance_numbered_pairs(aux, esc,
[('USB6_DP', 'USB6_DM'), ('UP_DP', 'UP_DM')],
prefer_horizontal=True)
segs += aux
print(u' USB6/上行 D±:按构造画了 %d 段(不走单网 A*)' % len(aux))
dbad = dp_selfcheck([s for s in segs if s[6] in DP_NETS])
dbad += dp_obstacle_check([s for s in segs if s[6] in DP_NETS], pads)
if dbad:
raise RuntimeError(u'构造 D± 几何校验不过(%d 条):%s' %
(len(dbad), u';'.join(dbad[:80])))
RT.PAD_PEN = _pad_pen
_DIFF_SEGS[:] = [s for s in segs if s[0] == 1]
print(u'第一相:D± %d 段,底层 D± 走廊封死 %.1f mm' % (len(segs), DIFF_KEEP))
if '--dp-only' in sys.argv:
return segs, vias, logs
# ── 第二相:其余全部 ───────────────────────────────────────────────
rest = set(bypad) - set(DP_NETS)
RT.AUTO_CACHE.clear()
blocked = list(pour_blocks())
blocked += [('#ant', r) for r in ant_blocks(0.0)]
# 有优先集时只处理上一版失败网;给已成功网络提前铺短铜只会占掉共享通道。
direct_nets = rest & set(priority) if priority else rest
local_map = {net: bypad[net] for net in direct_nets}
dsegs, merged = RT.direct_links(pads, local_map, segs, vias, blocked_rects=blocked)
segs += dsegs
for net, groups in merged.items():
drop = set()
for group in groups:
drop |= set(list(group)[1:])
bypad[net] = [q for q in bypad[net] if (q[0], q[1]) not in drop]
print(u' 普通信号短直连:%d 段 / %d 个网络(硬功率铜与天线禁区已参与判定)'
% (len(dsegs), len(merged)))
fixed_s, fixed_v = len(segs), len(vias)
logs += RT.route_groups(bypad, pads, segs, vias, board_vias, {},
priority=priority, only=rest)
if logs and '--repair' in sys.argv:
print(u' 首轮 %d 条没搞定,进入拆线重布' % len(logs))
segs, vias, logs = rip_repair(bypad, pads, segs, vias, logs, board_vias,
rest, fixed_s, fixed_v)
return segs, vias, logs
def pour_nets():
u"""靠铜面 / 铜墙成网的网络:它们的焊盘本来就没有线,几何连通性判据不适用。"""
import pour_base as PB
out = {net for (_n, net, _r) in PB.RAILS}
out |= {net for (net, _r) in pour_blocks()}
return out | {'+12V', '+3V3'}
def rip_repair(bypad, pads, segs, vias, logs, board_vias, rest, fixed_s, fixed_v):
u"""把 `router.repair`(拆线重布)接到底板上。
⛔ 不能直接把整张 `bypad` 交给它,有两处会静默出错:
① D± 七对也在 `bypad` 里(落点形式)。它们的铜是第一相按构造画的、在固定段里,
一旦被选为「挡路的网」,repair 会用 A* 在落点之间**再布一遍** —— 板上多出
一根同名线,净距自查不会报(同网)。⇒ 只交 `rest`。
② repair 用 `check_connected` 验收,它按「每只焊盘都要有线端」判。
铜面 / 铜墙网(+3V3、+12V、各 RAIL)的焊盘本来就没线,会被判断线 ——
只会让重布更难被接受,不会放进坏结果,但等于这些网永远拆不动。
⇒ 验收时去掉铜面网、通孔焊盘的底层副本和单脚网。
前 `fixed_s` / `fixed_v` 条(槽墙桥、逃逸扇形、D±、直连)一根都不拆。
"""
import time
sub = {n: bypad[n] for n in rest if n in bypad}
skip = pour_nets() | set(DP_NETS)
cnt = {}
for q in pads:
cnt[q[7]] = cnt.get(q[7], 0) + 1
chk_pads = [q for q in pads if q[7] in sub and q[7] not in skip and cnt[q[7]] >= 2
and not str(q[1]).endswith(BOT_SUFFIX)]
orig_chk, orig_rg = RT.check_connected, RT.route_groups
t0, n = time.time(), [0]
def chk(_pads, s, v):
return orig_chk(chk_pads, s, v)
def rg(*a, **k):
n[0] += 1
out = orig_rg(*a, **k)
print(u' 拆线重布 #%d:重布 %d 个网,失败 %d 条(累计 %.0f s)'
% (n[0], len(k.get('only') or ()), len(out), time.time() - t0))
sys.stdout.flush()
return out
RT.check_connected, RT.route_groups = chk, rg
try:
return RT.repair(sub, pads, segs, vias, logs, board_vias, {}, fixed_s, fixed_v)
finally:
RT.check_connected, RT.route_groups = orig_chk, orig_rg
def report(segs, vias, logs):
tot = sum(math.hypot(s[3] - s[1], s[4] - s[2]) for s in segs)
bot = sum(math.hypot(s[3] - s[1], s[4] - s[2]) for s in segs if s[0] == 2)
print(u'导线 %d 段 / 总长 %.1f mm(底层 %.1f mm = %.0f%%)/ 过孔 %d'
% (len(segs), tot, bot, 100.0 * bot / max(tot, 1e-9), len(vias)))
nets = set(s[6] for s in segs)
print(u'涉及 %d 个网络' % len(nets))
# D± 长度:每个网自己的铜有多长
print(u'差分对实测铜长:')
for name, dp, dm in RB.PAIRS:
for n in (dp, dm):
L = sum(math.hypot(s[3] - s[1], s[4] - s[2]) for s in segs if s[6] == n)
nb = sum(1 for s in segs if s[6] == n and s[0] == 2)
print(u' %-9s %6.1f mm%s' % (n, L, u' ⚠ 有 %d 段在底层' % nb if nb else u''))
if logs:
print(u'[!] %d 条没搞定:' % len(logs))
for m in logs[:40]:
print(u' ' + m)
if __name__ == '__main__':
if '--clear' in sys.argv:
RT.clear_mine()
sys.exit(0)
if '--snapshot-vias' in sys.argv:
print(u'底板既有过孔快照:%d 个 -> %s' % (len(load_board_vias()), BOARD_VIAS))
sys.exit(0)
c = json.load(io.open(CACHE, encoding='utf-8')) if os.path.exists(CACHE) else {}
cache_ok = (c.get('schema') == CACHE_SCHEMA and c.get('slot_rot') == B.SLOT_ROT and
c.get('usb_slot_ref') == {str(k): v for k, v in B.USB_SLOT_REF.items()})
if '--replan' not in sys.argv and cache_ok:
segs, vias, logs = [tuple(s) for s in c['segs']], [tuple(v) for v in c['vias']], c['logs']
print(u'(用缓存 _base_routes.json)')
else:
if os.path.exists(CACHE) and not cache_ok:
print(u'(忽略 rot=180 / 西侧 D± 旧缓存,按当前拓扑重算)')
best, prio = None, set()
# 用上一轮的失败网络给新一轮排序,避免为了得到第二轮
# 优先级而重算一遍完全相同的首轮。
if '--priority-cache' in sys.argv and os.path.exists(CACHE):
old = json.load(io.open(CACHE, encoding='utf-8'))
prio = set(RT._msg_net(m) for m in old.get('logs', []))
print(u'(用上轮 %d 条失败记录优先排网)' % len(old.get('logs', [])))
# `--passes N`:改动布线模型时先跑一轮拿信号,别一上来就等四轮(一轮 ~20 min)
if '--passes' in sys.argv:
RT.PASSES = int(sys.argv[sys.argv.index('--passes') + 1])
for it in range(RT.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(RT._msg_net(m) for m in logs)
segs, vias, logs = best
if '--dp-only' not in sys.argv:
io.open(CACHE, 'w', encoding='utf-8').write(json.dumps({
'schema': CACHE_SCHEMA, 'slot_rot': B.SLOT_ROT,
'usb_slot_ref': B.USB_SLOT_REF,
'segs': segs, 'vias': vias, 'logs': logs}, ensure_ascii=False))
report(segs, vias, logs)
if '--push' in sys.argv:
if logs:
sys.exit(u'!! 仍有 %d 条未布通,拒绝下发不完整路由' % len(logs))
RT.push(segs, vias)