- 规则:铜层 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
1341 行
66 KiB
Python
1341 行
66 KiB
Python
# -*- 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)
|