# -*- coding: utf-8 -*- u"""R2 底板 PCB 布线(M5,**4 层**,D15)。 # ⛔ 计算只在服务器上跑:.claude/r2/remote_route.ps1(它会带 DCDC_ROUTE_SERVER=1 --offline --replan) /c/Python314/python .claude/r2/route_base.py # 读缓存 + 报告(本机可跑) /c/Python314/python .claude/r2/route_base.py --push # 下发导线 + 过孔(本机) /c/Python314/python .claude/r2/route_base.py --clear # 清掉全部布线(本机) /c/Python314/python .claude/r2/route_base.py --snapshot-vias 2 层版(底层过路费 / D± 构造 / 12V 干道 / 3V3 槽墙 / 车道)整份在 `archive_base2L/route_base.py`。 **引擎仍是刀片那份 `router.py`**(0.25 网格 + 8 邻接 A*,L1/L4 两层可布、内层做平面),这里只换板级常量、加底板的障碍、写主流程。 ## 叠层与分工(D15) | 层 | 内容 | 布线器怎么看 | |---|---|---| | L1 | 5 个插槽 + 西侧 `+12V` 大铜(`pcb_base.l1_12v_rects`)| 可布;12V 大铜**只挡导线**(过孔可以落进去,铺铜自己挖反焊盘)| | L2/L3 | GND 平面 | 不布线;GND 焊盘各打一个过孔下来 | | L4 | 其余 133 件 | 主力布线层 | ## 主流程(`plan_all`) 1. **Kelvin**:`U13.10→Rs6.1`、`U13.9→Rs6.2` 各一条 0.3 专线,最先布。就近扎进平面的话, INA226 量到的是「分流电阻 + 一段铜箔」的压降,读数随铜温漂,而 DRC / 连通性全过。 2. **9 A 上 L1**:`Rs6.2` 西侧一块 L4 `+12V` 铜(`V12_PATCH`)+ 15 个过孔,落进 L1 大铜。 3. **D± 七对**:DP 先 A*;DM 再 A*,离 DP 远的格点全收过路费(`HUG_PEN`),于是贴着 DP 走。 ⚠ 北去三对(USB1~3)**极性是反的**:U32 北沿 DM 在西、槽上 D+ 在西(x 47.5)。同层两根线 不交叉换不过来 —— 交给 A*,它会让 DM 打一个过孔绕过去。四层薄介质上 0.28 线不耦合时 Zdiff ≈ 100 Ω,仍在 USB 2.0 的 90 Ω ±15% 里,所以局部松耦合可以接受。 4. **平面网扇出**:每只 GND 贴片焊盘一个过孔(通孔焊盘本来就穿到内层,跳过); 两边都 ≥ `EP_MIN` 的 GND 焊盘(U32 散热盘 / 插槽外壳盘 / DAC 底盘 / 模组底盘)在焊盘里打孔阵; 每只 L4 的 `+12V` 焊盘一个过孔,**必须落在 L1 12V 主块里**。 5. 其余:直连 + `router.route_groups` 按线宽分档 A*;`--repair` 拆线重布。 """ import io import json import math import os import sys # `--hash-seed N`:固定字符串哈希(只对 CPython 有意义;PyPy 按插入序遍历,结果与种子无关)。 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 的板级常量换成底板的 ──────────────────────────────────────── # `--grid G`:布线网格(默认 0.25)。规则 0.15 / 0.15 的最小线距是 0.3,0.25 网格只能按 0.5 一道排, # 通道容量浪费约四成(0.5 / 0.65 间距的芯片周围尤其吃亏)。0.125 每轮慢 4~8 倍,只配顺序布线。 if '--grid' in sys.argv: RT.G = float(sys.argv[sys.argv.index('--grid') + 1]) 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 = [] # Kelvin 在这里自己布(router 那份只在它自己的 plan_all 里用) RT.KELVIN_PADS = set() RT.CLR = 0.16 # 规则 0.15(四层,同刀片),留一点给网格吸附 RT.W_DEFAULT = 0.15 RT.EDGE_CLR = 0.4 RT.LAYER_DIR = {} # 八方向同价(D± 全是南北向,L1 走廊全是东西向,偏好反而添乱) # 靠平面成网:焊盘各自一个过孔,铜在几何上本来就是分开的(+12V → L1 大铜,+3V3 → L2 整层) RT.ISLAND_NETS = ('+12V', '+3V3') CACHE = os.path.join(HERE, '_base_routes.json') CACHE_SCHEMA = 8 # v8:4 层(D15)。旧的 2 层缓存(v7)必须被拒 BOARD_VIAS = os.path.join(HERE, '_base_board_vias.json') PLACE_JSON = os.path.join(HERE, '_base_place.json') # ⛔ router 里有刀片专用的 `J1` 特判(那边 J1 是金手指:不封顶、不排序、**连通性检查直接跳过**)。 # 底板的 J1 是 DC 座,照原样 VIN_RAW 的入口脚就永远不被检查 —— 断了也报「0 条没搞定」。 # ⇒ 载入焊盘时改名,布线结果里不含位号,下发不受影响。 J1_ALIAS = 'J1dc' class _NoNotch(object): u"""`build_grid` 要 `pcb_pour.notch_cut()`(刀片金手指防呆缺口)—— 底板没有。""" @staticmethod def notch_cut(): return (-10.0, -10.0, -9.0, -9.0) RT.P = _NoNotch def _rule_min(net, auto): u"""线宽下限查底板自己的 `rules_base`(router 那份会去 import 刀片的 pcb_rules)。""" cls = RB.NET_TRACK.get(net) if net in DP_NETS: return RB.DIFF['strokeWidthMin'] if cls is None: return 0.15 return RB.TRACK_RULES[cls]['strokeWidthMin'] RT.rule_min = _rule_min # ── 通孔焊盘(layer 12)────────────────────────────────────────────────── # 刀片全贴片,router 一句 `if lay not in LAY: continue` 就把它们漏出占用图。拆成两条: # L4 那条可布(器件在背面),L1 那条带后缀、只当障碍(同网的线仍可从上面过)。 TH_SUFFIX = '#t' TH_GROW = 0.6 # J1/J23 的孔是长槽,槽比焊盘大 TH_KEYS = set() def split_th(pads): out = [] for p in pads: if p[6] == 12: # ⛔ 用**改名之后**的位号(J1 → J1dc)。第一版记的是 J1,load_pads 随后改名, # 于是 J1 的两只通孔地脚没被认成通孔,被当成贴片散热盘在孔里打了孔阵 # (DRC「钻孔到钻孔」4 条,J1_2 / J1_3)。 TH_KEYS.add((J1_ALIAS if p[0] == 'J1' else p[0], str(p[1]))) a = list(p); a[6] = 2; a[4] += TH_GROW; a[5] += TH_GROW b = list(p); b[6] = 1; b[1] = str(p[1]) + TH_SUFFIX b[4] += TH_GROW; b[5] += TH_GROW out += [a, b] else: out.append(list(p)) return out def load_pads(): # ⚠ 丢掉零尺寸的「幽灵焊盘」:多边形焊盘拆条时的残留(J23 的 A1B12 / A4B9 各有两块 0.30×0.00), # 真焊盘另有一块。留着的话平面网扇出要给它们找落孔点、信号网要去接它们,全报失败。 pads = [p for p in split_th(RT.load_pads()) if p[4] >= 0.05 and p[5] >= 0.05] for p in pads: if p[0] == 'J1': p[0] = J1_ALIAS 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 # ── 线宽 ───────────────────────────────────────────────────────────────── PAIRS = {name: (dp, dm) for name, dp, dm in RB.PAIRS} DP_NETS = [] for _name, _dp, _dm in RB.PAIRS: DP_NETS += [_dp, _dm] DIFF_W = RB.DIFF['strokeWidthDefault'] # 0.28(L4 参考 L3 / L1 参考 L2,~90 Ω) RT.WIDTH = { 'VIN_RAW': 2.0, 'VIN_FUSED': 2.0, 'VIN_SW': 2.0, # 9 A 由之后的铺铜 + 断面校核保证 'FAN_12V_SW': 0.8, # +5V 只给每槽刀片上的负载开关(规则 PWR5V 下限 0.2)。0.6 的干线从槽针排里出不来 —— # +3V3 改平面网那一轮,5 个槽的 10 只 5V 针全部没接上。 '+5V': 0.4, '+3V3': 0.4, '+3V3_HUB': 0.3, # 要从 U32 北沿夹在 D± 中间的 5/10/13 脚出来,粗了出不来 } for _n in DP_NETS: RT.WIDTH[_n] = DIFF_W RT.WIDTH_AUTO = {} RT.NO_CAP = tuple(DP_NETS) + ('VIN_RAW', 'VIN_FUSED', 'VIN_SW', '+5V', '+3V3', '+3V3_HUB') # ⭐ +3V3 是平面网(2026-09-10 用户拍板:L2 整层 +3V3、L3 整层地,D15)。十轮布线信号网卡在 37~38, # 大头是这棵 47 只脚、跨全板的电源树;改成每脚一孔扎进 L2,不再布线。 PLANE_NETS = ('GND', '+12V', '+3V3') LATE_PLANE_NETS = ('GND', '+3V3') # 布完信号之后才打孔的平面网(早打会封死 1 mm 针排里的信号脚) # ── Kelvin 取样 ────────────────────────────────────────────────────────── KELVIN = [(('U13', '10'), ('Rs6', '1'), 0.3), # IN+ ← 分流电阻上游 (('U13', '9'), ('Rs6', '2'), 0.3)] # IN− ← 分流电阻下游 KELVIN_W = 0.3 # ── 9 A 上 L1:Rs6.2 西侧那块 L4 铜 + 过孔阵 ──────────────────────────────── # 北边是泄放链(R9A/R9B 焊盘下沿 48.29),东边是 C4 / U13,只有西侧 x 5.3~10.8 空着。 # 铜块东端盖住 Rs6.2 焊盘(同网),过孔 5 × 3 = 15 个(0.3 孔 1 oz 每个 ~1 A 保守)。 V12_PATCH = (5.3, 44.4, 14.3, 48.05) V12_VIA_X = (5.9, 7.0, 8.1, 9.2, 10.25) V12_VIA_Y = (45.0, 46.1, 47.2) def v12_vias(): return [(x, y) for x in V12_VIA_X for y in V12_VIA_Y] # ── 入口链那一条(L4):别的网收过路费,之后 9 A 的铜面要从这里铺 ───────────── CHAIN_STRIP = (7.5, 0.5, 18.5, 48.0) CHAIN_PEN = 4 CHAIN_NETS = ('VIN_RAW', 'VIN_FUSED', 'VIN_SW') # ── D± 贴合 ────────────────────────────────────────────────────────────── HUG_BAND = 0.9 # DP 铜外 0.9 mm 以内不收费(DM 中心距 0.5 就落在里面) HUG_PAD = 1.5 # DM 自己的脚周围也不收费,好起步 / 收尾 HUG_PEN = 12 DP_PAD_PEN = 3 # 布 D± 时的焊盘过路费(默认 10 会让它们绕着满板焊盘蜿蜒) # ⛔ 北去三对(USB1~3)必须**先**布。第一版先布 USB5/USB4(最长),A* 从 U32 北沿横着往西抄近路, # 把 USB1~3 的出口整排封死(服务器首轮:三对全「没布通」)。先布北去的,它们的竖线就是一道墙, # 南去的两对只能往东绕 —— 那正是摆位时给它们留的 DP_H 车道。 # ⚠ USB1 必须在 USB2 前面:两对都要往西绕开 USB3 的 U 形,USB2 先布就横着压过 USB1 的出口 # (第三轮:USB1 两只脚都「出不来」)。最西的先走、走得最远。 PAIR_ORDER = ['USB6', 'USBUP'] # USB1~5 全部按构造画(usb3_uturn / dp_construct);这两对从 L1 的孔起步 # 最后一段才布的电源网(它们的脚在前两段一直收过路费)。 # ⛔ 第二轮是按线宽分档、宽的先布:+3V3 / +5V 的干线沿走廊边一铺,正好横在 U35 两排脚、 # U20 西排、风扇那一窝的出口上,于是几十条信号「没布通」。 # +3V3 已是平面网(L2);+5V 挪进信号那一段一起布(最后布时通道早被占完,10 只槽针全落空) POWER_LATE = ('+3V3_HUB', 'FAN_12V_SW') # ── 平面网扇出 ──────────────────────────────────────────────────────────── EP_MIN = 1.2 # GND 焊盘两边都 ≥ 这个 ⇒ 焊盘里打孔(散热盘 / 外壳盘) EP_PITCH = 1.2 EP_EDGE = 0.45 # 孔心离焊盘边 V12_LAND = 0.6 # +12V 过孔离 L1 主块边 def place_pos(): d = json.load(io.open(PLACE_JSON, encoding='utf-8')) return {k: (v[0], v[1], v[2]) for k, v in d.items()} def keep_rects(): u"""两层都不许有铜:天线净空 + 板框缺口(外扩板边净距)。""" import place_base as PL ant = PL.antenna_keepout(PL.sizes(), place_pos()) n = B.notch_rect() e = RT.EDGE_CLR return [ant, (n[0] - e, n[1] - e, n[2] + 1.0, n[3] + e)] _KEEP = [None] _orig_build = RT.build_grid _orig_pen = RT.pen_pads def _build(pads, halfw, vias=()): g = _orig_build(pads, halfw, vias) _no_via_in_pad(g, pads) extra = max(0.0, RT.VIA_D / 2.0 + RT.CLR - halfw) for r in B.l1_12v_rects(): # 只打导线图:过孔可以落进大铜 g.block_rect(1, r[0] - halfw, r[1] - halfw, r[2] + halfw, r[3] + halfw, net='+12V') x0, y0, x1, y1 = V12_PATCH g.block_both(2, x0 - halfw, y0 - halfw, x1 + halfw, y1 + halfw, extra, net='+12V') if _KEEP[0] is None: _KEEP[0] = keep_rects() for r in _KEEP[0]: for l in RT.LAY: g.block_both(l, r[0] - halfw, r[1] - halfw, r[2] + halfw, r[3] + halfw, extra, net='#edge') return g # 还没打孔的 GND 贴片焊盘 + 后面阶段才布的网的焊盘:一律收过路费,给它们自己留出口。 GND_PEN_PADS = [] LATER_PEN_PADS = [] def _pen(g, pads, halfw): _orig_pen(g, list(pads) + GND_PEN_PADS + LATER_PEN_PADS, halfw) g.penalize(2, CHAIN_STRIP[0], CHAIN_STRIP[1], CHAIN_STRIP[2], CHAIN_STRIP[3], CHAIN_PEN) # ⛔ **贴片焊盘里不许落过孔**(散热 / 外壳盘除外,它们的孔阵是 plane_vias 专门打的)。 # router 按「网」标归属,同网的过孔可以落进自己的焊盘 —— 插槽是手焊的,孔在焊盘里会吸锡; # 0402 的盘上打孔也一样。第二轮已见 A* 把 D± 的孔打在槽针焊盘边上。 def _no_via_in_pad(g, pads): k = g.nid('#pad') r = RT.VIA_D / 2.0 for p in pads: if p[6] not in RT.LAY or is_ep(p) or str(p[1]).endswith(TH_SUFFIX) or (p[0], str(p[1])) in TH_KEYS: continue # 只打焊盘所在那一层就够:过孔是通孔,free_via 两层都查 g._mark(g.ownv, p[6], p[2] - p[4] / 2 - r, p[3] - p[5] / 2 - r, p[2] + p[4] / 2 + r, p[3] + p[5] / 2 + r, k, True) RT.build_grid = _build RT.pen_pads = _pen # ── 12V 大铜底下三个区(L1 被 12V 占着,只能走 L4):没有焊盘在这里的网,路过要加价 ───────── # 第九轮:Buck 反馈分压($2N72 / $2N70)、U13 的 I²C、风扇几个本地网「没布通」—— 它们一两毫米的路 # 被跨半块板的 +3V3 / 风扇信号 / I²C 总线从这里穿过去占了。 UNDER_12V_ZONES = ('A1', 'A2', 'A6') UNDER_12V_PEN = 6 _orig_route_net = RT.route_net def _route_net(g, net, pads_of_net, *a, **k): for z in UNDER_12V_ZONES: r = B.ZONES[z] if not any(r[0] <= q[2] <= r[2] and r[1] <= q[3] <= r[3] for q in pads_of_net): g.penalize(2, r[0], r[1], r[2], r[3], UNDER_12V_PEN) return _orig_route_net(g, net, pads_of_net, *a, **k) if '--zone-pen' in sys.argv: RT.route_net = _route_net # ── 小工具 ─────────────────────────────────────────────────────────────── def grid_now(pads, halfw, segs, vias, board_vias): u"""按 halfw 建占用图并打进已布的铜;再建一张「引出脚口径」的图 gt。和 route_groups 同一套判据。""" g = RT.build_grid(pads, halfw, board_vias) for (l, a, b, c, d, ww, nn) in segs: RT.block_track(g, l, a, b, c, d, halfw + ww / 2.0, nn, extra=max(0.0, RT.VIA_D / 2.0 + RT.CLR - (halfw + ww / 2.0))) for (vx, vy, nn) in vias: rv = RT.VIA_D / 2.0 + RT.CLR + halfw for l in RT.LAY: g.block_rect(l, vx - rv, vy - rv, vx + rv, vy + rv, net=nn) gt = RT.Grid() sh = RT.W_DEFAULT / 2.0 for (l, a, b, c, d, ww, nn) in segs: RT.block_track(gt, l, a, b, c, d, RT.CLR + sh + ww / 2.0, nn) for (vx, vy, nn) in vias: r = RT.VIA_D / 2.0 + RT.CLR + sh for l in RT.LAY: gt.block_rect(l, vx - r, vy - r, vx + r, vy + r, net=nn) others = {1: [RT.pad_rect(p) for p in pads if p[6] == 1], 2: [RT.pad_rect(p) for p in pads if p[6] == 2]} return g, gt, others def route_one(net, ps, w, pads, segs, vias, board_vias, logs, pen_pads=(), hug=None, fence=()): halfw = w / 2.0 + RT.CLR g, gt, others = grid_now(pads, halfw, segs, vias, board_vias) for (l, r) in fence: # 只对这一个网的禁区(导线图,按层) g.block_rect(l, r[0], r[1], r[2], r[3], net='#fence') RT.pen_pads(g, list(pen_pads), halfw) if hug: apply_hug(g, hug) s, v, st = RT.route_net(g, net, ps, halfw, w, others, logs, gt) out = list(s) for (q, node, ww) in st: out += RT.stub_emit(q, RT.wx(node[1]), RT.wy(node[2]), [] if RT._is_launch(q) else others.get(q[6], []), ww / 2.0, ww, net, gt) return out, list(v) def apply_hug(g, band): u"""band = {层: [矩形]}:矩形外的格点一律至少收 HUG_PEN。**在 pen_pads 之后调**(它会先清零)。""" n = RT.NX * RT.NY for l in RT.LAY: field = bytearray([HUG_PEN]) * n for (x0, y0, x1, y1) in band.get(l, []): i0, i1 = max(0, int(x0 / RT.G)), min(RT.NX - 1, int(math.ceil(x1 / RT.G))) j0, j1 = max(0, int(y0 / RT.G)), min(RT.NY - 1, int(math.ceil(y1 / RT.G))) z = bytes(i1 - i0 + 1) for j in range(j0, j1 + 1): field[j * RT.NX + i0: j * RT.NX + i1 + 1] = z g.pen[l] = bytearray(map(max, g.pen[l], field)) def _seg_box(s, m): return (min(s[1], s[3]) - m, min(s[2], s[4]) - m, max(s[1], s[3]) + m, max(s[2], s[4]) + m) # ── 各阶段 ─────────────────────────────────────────────────────────────── def kelvin(pads, segs, vias, board_vias, logs): padmap = {(q[0], str(q[1])): q for q in pads} out_s, out_v = [], [] for aa, bb, w in KELVIN: pa, pb = padmap.get(aa), padmap.get(bb) if not pa or not pb: logs.append(u'Kelvin: 找不到 %s.%s / %s.%s' % (aa + bb)) continue halfk = w / 2.0 + RT.CLR g, gt, others = grid_now(pads, halfk, segs + out_s, vias + out_v, board_vias) na = RT.pad_nodes(g, pa, halfk, others.get(pa[6], []), w / 2.0, gt) nb = RT.pad_nodes(g, pb, halfk, others.get(pb[6], []), w / 2.0, gt) path = RT.astar(g, na[:16], nb[:16], pa[7], halfk) if not path: logs.append(u'Kelvin: %s.%s → %s.%s 没布通' % (aa + bb)) continue sg, vv = RT.to_segments(path) out_s += [(l, x0, y0, x1, y1, w, pa[7]) for (l, x0, y0, x1, y1) in sg] out_v += [(x, y, pa[7]) for (x, y) in vv] out_s += RT.stub_emit(pa, RT.wx(path[0][1]), RT.wy(path[0][2]), others.get(pa[6], []), w / 2.0, w, pa[7], gt) out_s += RT.stub_emit(pb, RT.wx(path[-1][1]), RT.wy(path[-1][2]), others.get(pb[6], []), w / 2.0, w, pb[7], gt) return out_s, out_v def v12_handoff(pads): u"""Rs6.2 西侧 L4 铜块 + 15 个过孔。铜块本身画成若干条同网宽线(铺铜阶段再用正式铜面覆盖)。""" bad = [] x0, y0, x1, y1 = V12_PATCH for p in pads: if p[6] not in (2, 12) or p[7] == '+12V': continue r = RT.pad_rect(p) d = max(r[0] - x1, x0 - r[2], r[1] - y1, y0 - r[3]) if d < RB.MIN_CLEARANCE + 0.05: bad.append(u'%s.%s(%s) 离 V12_PATCH 只有 %.2f' % (p[0], p[1], p[7], d)) if bad: raise RuntimeError(u'V12_PATCH 与异网焊盘冲突(摆位改过?):' + u';'.join(bad)) segs = [] h = y1 - y0 segs.append((2, x0 + h / 2.0, (y0 + y1) / 2.0, x1 - h / 2.0, (y0 + y1) / 2.0, h, '+12V')) vias = [(x, y, '+12V') for (x, y) in v12_vias()] return segs, vias # ── J23(Type-C)同名 D± 脚的桥 ───────────────────────────────────────────── # 一排里是 B6(DP) A7(DM) A6(DP) B7(DM) 交错,0.5 mm 间距。先布的那根把自己两只脚一桥, # 正好横在另一根内侧那只脚的出口上(首轮:UP_DM「A7 出不来」)。 # ⇒ 两座桥按构造画:DP 从焊盘**南侧**(连接器本体一侧)绕、DM 从**北侧**绕,互不交叉; # 之后 A* 只接外侧的 B6(DP,西北方向是空的)和 B7(DM,东北方向是空的)。 J23_BRIDGE_W = 0.15 J23_BRIDGE_OUT = 0.6 # 桥的横段离焊盘端多远 def j23_bridges(pads): pm = {(q[0], str(q[1])): q for q in pads if q[0] == 'J23'} need = [('B6', 'UP_DP'), ('A6', 'UP_DP'), ('A7', 'UP_DM'), ('B7', 'UP_DM')] for pin, net in need: if ('J23', pin) not in pm or pm[('J23', pin)][7] != net: raise RuntimeError(u'J23.%s 不是 %s(封装 / 朝向改过?)' % (pin, net)) b6, a6, a7, b7 = (pm[('J23', k)] for k in ('B6', 'A6', 'A7', 'B7')) if not (b6[2] < a7[2] < a6[2] < b7[2]): raise RuntimeError(u'J23 的 D± 脚顺序不是 B6 A7 A6 B7(西 → 东)') ys = b6[3] - b6[5] / 2.0 - J23_BRIDGE_OUT # 南 yn = a7[3] + a7[5] / 2.0 + J23_BRIDGE_OUT # 北 w = J23_BRIDGE_W segs = [(2, b6[2], b6[3], b6[2], ys, w, 'UP_DP'), (2, b6[2], ys, a6[2], ys, w, 'UP_DP'), (2, a6[2], ys, a6[2], a6[3], w, 'UP_DP'), (2, a7[2], a7[3], a7[2], yn, w, 'UP_DM'), (2, a7[2], yn, b7[2], yn, w, 'UP_DM'), (2, b7[2], yn, b7[2], b7[3], w, 'UP_DM')] bad = [] for s in segs: for q in pads: if q[6] != 2 or q[7] == s[6]: continue d = _seg_rect_gap(s, RT.pad_rect(q)) - w / 2.0 if d < RB.MIN_CLEARANCE - 1e-6: bad.append(u'%s 桥离 %s.%s(%s) 只有 %.3f' % (s[6], q[0], q[1], q[7] or 'NC', d)) if bad: raise RuntimeError(u'J23 桥净距不够:' + u';'.join(sorted(set(bad)))) return segs, {('J23', 'A6'), ('J23', 'A7')} # ── USB3:U 形掉头(按构造画)─────────────────────────────────────────────── # 北去三对的极性都是反的:U32 北沿西 → 东是 DM DP,槽上南排是 D+(x 47.5) D−(x 48.5)。 # 同层两根线不交叉换不过来,但**掉头一次左右自然对调**。USB3 的目标 J36 就在 U32 正北 1 mm, # 所以 U32 东移 2.5 mm(place_base.HUB_CX),让它的两只脚落在 J36 D± 的东边: # L4:两根一起北上,从 J36 南排底下穿过(焊盘在 L1,L4 空着)→ 到两排中间向西 → 南下到过孔 # L1:过孔 → 往南进焊盘的内端(从北边进,不占南排外侧的出口) # DM 在内圈、DP 在外圈,DP 的过孔在 DM 过孔的西北,两根 L1 引出脚互不跨越。 U3_DMV = 1.2 # DM 过孔离焊盘内端 U3_DMH = 0.4 # DM 横段在自己过孔北边多少 U3_DPH = 0.8 # DP 横段在 DM 横段北边多少(外圈) U3_DPV = 0.5 # DP 过孔在自己横段南边多少 def usb3_uturn(pads): pm = {(q[0], str(q[1])): q for q in pads} dm0, dp0 = pm[('U32', '6')], pm[('U32', '7')] sdp, sdm = pm[('J36', '46')], pm[('J36', '47')] for q, net in ((dm0, 'USB3_DM'), (dp0, 'USB3_DP'), (sdp, 'USB3_DP'), (sdm, 'USB3_DM')): if q[7] != net: raise RuntimeError(u'%s.%s 是 %s 不是 %s' % (q[0], q[1], q[7], net)) if not (sdp[2] < sdm[2] < dm0[2] < dp0[2]): raise RuntimeError(u'USB3 掉头的前提不成立:要 J36 D+ < D− < U32.6 < U32.7(西 → 东),' u'实际 %.2f %.2f %.2f %.2f —— U32 的 x 改过?' % (sdp[2], sdm[2], dm0[2], dp0[2])) w = DIFF_W y_in = sdp[3] + sdp[5] / 2.0 y_dmv = y_in + U3_DMV y_dmh = y_dmv + U3_DMH y_dph = y_dmh + U3_DPH y_dpv = y_dph - U3_DPV dm = [(dm0[2], dm0[3]), (dm0[2], y_dmh), (sdm[2], y_dmh), (sdm[2], y_dmv)] dp = [(dp0[2], dp0[3]), (dp0[2], y_dph), (sdp[2], y_dph), (sdp[2], y_dpv)] segs = [] for net, pts in (('USB3_DM', dm), ('USB3_DP', dp)): segs += [(2, a[0], a[1], b[0], b[1], w, net) for a, b in zip(pts, pts[1:])] segs += [(1, sdm[2], y_dmv, sdm[2], sdm[3], w, 'USB3_DM'), (1, sdp[2], y_dpv, sdp[2], sdp[3], w, 'USB3_DP')] vias = [(sdm[2], y_dmv, 'USB3_DM'), (sdp[2], y_dpv, 'USB3_DP')] bad = [] c = RB.MIN_CLEARANCE for s in segs: for q in pads: if q[6] not in (s[0], 12) or q[7] == s[6]: continue d = _seg_rect_gap(s, RT.pad_rect(q)) - w / 2.0 if d < c - 1e-6: bad.append(u'%s 离 %s.%s(%s) %.3f' % (s[6], q[0], q[1], q[7] or 'NC', d)) for (vx, vy, net) in vias: for q in pads: if q[7] == net: continue d = _seg_rect_gap((0, vx, vy, vx, vy), RT.pad_rect(q)) - RT.VIA_D / 2.0 if d < c - 1e-6: bad.append(u'%s 的过孔离 %s.%s(%s) %.3f' % (net, q[0], q[1], q[7] or 'NC', d)) a = [s for s in segs if s[6] == 'USB3_DP'] b = [s for s in segs if s[6] == 'USB3_DM'] for s in a: for t in b: if s[0] == t[0] and _seg_seg(s, t) - w < c - 1e-6: bad.append(u'DP/DM 对内净距 %.3f' % (_seg_seg(s, t) - w)) for (vx, vy, net) in vias: for t in segs: if t[6] != net and _seg_seg((0, vx, vy, vx, vy), t) - RT.VIA_D / 2.0 - w / 2.0 < c - 1e-6: bad.append(u'%s 的过孔离 %s 的线太近' % (net, t[6])) if bad: raise RuntimeError(u'USB3 掉头几何不过:' + u';'.join(sorted(set(bad)))) return segs, vias, {('U32', '6'), ('U32', '7'), ('J36', '46'), ('J36', '47')} def geom_check(tag, segs, vias, pads, prior_s=(), prior_v=()): u"""按构造画的铜没有 A* 占用图兜底,净距必须自己查:焊盘(同层 / 通孔)、此前构造的铜、彼此之间。""" c = RB.MIN_CLEARANCE bad = [] r = RT.VIA_D / 2.0 for s in segs: for q in pads: if q[6] not in (s[0], 12) or q[7] == s[6]: continue d = _seg_rect_gap(s, RT.pad_rect(q)) - s[5] / 2.0 if d < c - 1e-6: bad.append(u'%s 线离 %s.%s(%s) %.3f' % (s[6], q[0], q[1], q[7] or 'NC', d)) for (vx, vy, net) in vias: for q in pads: if q[7] == net: continue d = _seg_rect_gap((0, vx, vy, vx, vy), RT.pad_rect(q)) - r if d < c - 1e-6: bad.append(u'%s 孔离 %s.%s(%s) %.3f' % (net, q[0], q[1], q[7] or 'NC', d)) alls = list(segs) + list(prior_s) allv = list(vias) + list(prior_v) for s in segs: for t in alls: if t is s or t[6] == s[6] or t[0] != s[0]: continue d = _seg_seg(s, t) - s[5] / 2.0 - t[5] / 2.0 if d < c - 1e-6: bad.append(u'%s 线离 %s 线 %.3f' % (s[6], t[6], d)) for (vx, vy, net) in allv: if net == s[6]: continue d = _seg_seg(s, (0, vx, vy, vx, vy)) - s[5] / 2.0 - r if d < c - 1e-6: bad.append(u'%s 线离 %s 孔 %.3f' % (s[6], net, d)) for (vx, vy, net) in vias: for (wx_, wy_, n2) in allv: if n2 != net and (wx_, wy_) != (vx, vy) and math.hypot(vx - wx_, vy - wy_) - 2 * r < c - 1e-6: bad.append(u'%s 孔离 %s 孔 %.3f' % (net, n2, math.hypot(vx - wx_, vy - wy_) - 2 * r)) for t in prior_s: if t[6] != net and _seg_seg(t, (0, vx, vy, vx, vy)) - t[5] / 2.0 - r < c - 1e-6: bad.append(u'%s 孔离 %s 线' % (net, t[6])) if bad: raise RuntimeError(u'%s 几何不过:%s' % (tag, u';'.join(sorted(set(bad))))) # ── U32 北沿夹在 D± 中间的三只 3V3_HUB(5 / 10 / 13):按构造画 ──────────────────── # 口排西 → 东:DM1 DP1 DM2 DP2 **3V3(5)** DM3 DP3 DM4 DP4 **3V3(10)** DM5 DP5 **3V3(13)** NC NC NC。 # 五对一出来,这三只脚就被包进口袋(QFN 内圈出不去:角上缝只有 0.2 mm)。 # 13 脚:沿北沿贴着往东(压过三只 NC 脚上方)→ 下东侧 → 接 24 脚(东侧 3V3),东侧打孔上 L1。 # USB4/5 往东绕时从它上面过,它和 24 / VDD18 去耦一起留在绕行环的里面,靠这个孔接出去。 # 5 脚: 北上进 USB3 掉头的口袋,在 J36 两排之间打孔上 L1。 # 10 脚:北上夹在 USB4/USB5 之间,同样在 J36 两排之间打孔上 L1。 # L1:两孔沿 J36 南排内端外 0.5 mm 往东连,到东头外壳盘前打孔回 L4(USB4/5 绕行车道的**外面**)。 U32N_W = 0.2 U32N_TOP = 0.45 # 13 脚那条横线离北排焊盘外沿 U32N_EAST = 0.5 # 东侧竖线离东排焊盘外沿 U32N_V = 0.5 # 5 / 10 脚的孔离 J36 南排内端 def u32_north(pads, prior_s, prior_v): pm = {(q[0], str(q[1])): q for q in pads} p5, p10, p13, p24 = (pm[('U32', k)] for k in ('5', '10', '13', '24')) for q in (p5, p10, p13, p24): if q[7] != '+3V3_HUB': raise RuntimeError(u'U32.%s 是 %s 不是 +3V3_HUB' % (q[1], q[7])) sdp = pm[('J36', '46')] u32 = [q for q in pads if q[0] == 'U32' and not is_ep(q)] x_east = max(q[2] + q[4] / 2.0 for q in u32) y_top = p13[3] + p13[5] / 2.0 shell_e = min(q[2] - q[4] / 2.0 for q in pads if q[0] == 'J36' and q[4] >= 2.5 and q[2] > B.SLOT_CX) n, w = '+3V3_HUB', U32N_W t13 = round(y_top + U32N_TOP, 3) xe = round(x_east + U32N_EAST, 3) yv = round(sdp[3] + sdp[5] / 2.0 + U32N_V, 3) v5 = (round(p5[2] - 0.35, 3), yv) v10 = (round(p10[2] + 0.75, 3), yv) v13 = (xe, round(p24[3] + 1.5, 3)) vd = (round(shell_e - 1.0, 3), yv) ya = round(yv - 0.6, 3) segs = [(2, p13[2], p13[3], p13[2], t13, w, n), (2, p13[2], t13, xe, t13, w, n), (2, xe, t13, xe, p24[3], w, n), (2, xe, p24[3], p24[2], p24[3], w, n), (2, p5[2], p5[3], p5[2], ya, w, n), (2, p5[2], ya, v5[0], v5[1], w, n), (2, p10[2], p10[3], p10[2], ya, w, n), (2, p10[2], ya, v10[0], v10[1], w, n), (1, v5[0], v5[1], vd[0], vd[1], w, n)] vias = [(v5[0], v5[1], n), (v10[0], v10[1], n), (v13[0], v13[1], n), (vd[0], vd[1], n)] geom_check(u'U32 北沿 3V3_HUB', segs, vias, pads, prior_s, prior_v) virt = [['U32', 'fan:e13', v13[0], v13[1], 0.3, 0.3, 1, n, 'RECT'], ['U32', 'fan:vd', vd[0], vd[1], 0.3, 0.3, 2, n, 'RECT']] return segs, vias, {('U32', '5'), ('U32', '10'), ('U32', '13'), ('U32', '24')}, virt # ── 其余 D± 按构造画(2026-09-10 用户选定:构造 D± + 划横穿车道)──────────────────── # A* 逐网布 D± 不认拓扑:DP 抄最短路把自家 DM 的出口封死(第四轮 USB2/4/5/6 的 DM 全「出不来」)。 # 规则只有一条:**槽上 D+ 在西、D− 在东 ⇒ 这一对必须「向南」进焊盘**(掉头 / 绕行都保持 DM 在行进方向左侧)。 # 北去(USB1 → J34、USB2 → J35):出脚 45° 往西让开 USB3 的 U 形,北上到目标槽,在两排之间**向东掉头**, # 打孔上 L1、从北边进焊盘内端。 # 南去(USB4 → J37、USB5 → J38):出脚向东(USB5 在 13 脚那条线上面、USB4 在 10 脚过孔上面), # 到 DP_H 车道南下。USB4 在内圈外侧 ⇒ 转回西时必然横过 USB5 —— 用它本来就要的 L4→L1 那一跳: # 在 J37 两排之间打孔上 L1、在 L1 上向西进焊盘;USB5 在 L4 从它底下继续南下到 J38 再向西打孔。 # 上行 / USB6:在 U32 西侧过孔带(place_base.LANES['U32_WVIA'])各打两颗交错孔上 L1,之后交给 A*(从 L1 起步)。 DPC_JOG_W = 0.25 # 北去四根出脚后的转弯高度步进(45° 斜段互相平行、中心距 ≥ 0.5) DPC_SHIFT = {'USB1': 3.5, 'USB2': 2.5} DPC_VIA = 1.0 # 过孔离焊盘内端 DPC_UT = 0.6 # 内圈横段在过孔外侧多少 DPC_UT_OUT = 0.5 # 外圈横段再往外多少 # 离 DP_H 车道西沿(DP, DM)。USB4 两根拉开到 0.7:DM 的 L4 竖线要一直下到自己的孔(在 DP 孔南边), # 0.5 的话它擦过 DP 的孔只剩 0.06(静态校验实测)。 DPC_LANE = {'USB5': (0.0, 0.5), 'USB4': (1.8, 2.5)} DPC_Y5 = 0.5 # USB5 DP 横段在 13 脚那条线上方多少 DPC_Y4 = 0.85 # USB4 DP 横段在 10 脚过孔中心上方多少 WVIA_DX = (0.85, 1.45) # USB6/UP 两颗孔离西排焊盘外沿(DM, DP) def _poly2(layer, pts, w, net): return [(layer, a[0], a[1], b[0], b[1], w, net) for a, b in zip(pts, pts[1:]) if abs(a[0] - b[0]) + abs(a[1] - b[1]) > 1e-9] def dp_construct(pads, prior_s, prior_v): import place_base as PL pm = {(q[0], str(q[1])): q for q in pads} u = lambda k: pm[('U32', k)] # noqa: E731 w = DIFF_W segs, vias, done, virt = [], [], set(), [] top = u('1')[3] + u('1')[5] / 2.0 def slot(i): ref = B.USB_SLOT_REF[i] sp, sm = pm[(ref, '46')], pm[(ref, '47')] done.update({(ref, '46'), (ref, '47')}) return sp, sm # 北去:USB1 / USB2 order = [('USB1', '1', '2'), ('USB2', '3', '4')] k = 0 for name, pdm, pdp in order: dp_net, dm_net = PAIRS[name] i = int(name[3]) sp, sm = slot(i) s = DPC_SHIFT[name] y_in = sp[3] + sp[5] / 2.0 yv = round(y_in + DPC_VIA, 3) yt = round(yv + DPC_UT, 3) for net, pk, xt, ytt in ((dm_net, pdm, sm[2], yt + DPC_UT_OUT), (dp_net, pdp, sp[2], yt)): q = u(pk) ya = round(top + 0.3 + k * DPC_JOG_W, 3) k += 1 x1 = round(q[2] - s, 3) pts = [(q[2], q[3]), (q[2], ya), (x1, round(ya + s, 3)), (x1, ytt), (xt, ytt), (xt, yv)] segs += _poly2(2, pts, w, net) segs += _poly2(1, [(xt, yv), (xt, sp[3])], w, net) vias.append((xt, yv, net)) done.add(('U32', pk)) # 南去:USB5(内圈)/ USB4(外圈) lane_x = PL.LANES['DP_H'][0] t13 = [s2 for s2 in prior_s if s2[6] == '+3V3_HUB' and s2[0] == 2 and abs(s2[2] - s2[4]) < 1e-9 and abs(s2[1] - s2[3]) > 2.0] y13 = max(s2[2] for s2 in t13) # 13 脚那条横线的 y v10 = [v for v in prior_v if v[2] == '+3V3_HUB'] y10 = max(v[1] for v in v10 if v[1] < 50) # 5 / 10 脚的孔(J36 两排之间) sp5, sm5 = slot(5) y_in5 = sp5[3] + sp5[5] / 2.0 yv5 = round(y_in5 + DPC_VIA, 3) y5dp = round(y13 + DPC_Y5, 3) xdp5, xdm5 = round(lane_x + DPC_LANE['USB5'][0], 3), round(lane_x + DPC_LANE['USB5'][1], 3) wdm5, wdp5 = round(yv5 + 0.64, 3), round(yv5 + 1.14, 3) for net, pk, yh, xl, yw, xt in ((PAIRS['USB5'][0], '12', y5dp, xdp5, wdp5, sp5[2]), (PAIRS['USB5'][1], '11', y5dp + 0.5, xdm5, wdm5, sm5[2])): q = u(pk) pts = [(q[2], q[3]), (q[2], yh), (xl, yh), (xl, yw), (xt, yw), (xt, yv5)] segs += _poly2(2, pts, w, net) segs += _poly2(1, [(xt, yv5), (xt, sp5[3])], w, net) vias.append((xt, yv5, net)) done.add(('U32', pk)) sp4, sm4 = slot(4) y_in4 = sp4[3] + sp4[5] / 2.0 y4dp = round(y10 + DPC_Y4, 3) xdp4, xdm4 = round(lane_x + DPC_LANE['USB4'][0], 3), round(lane_x + DPC_LANE['USB4'][1], 3) vdp4, vdm4 = round(y_in4 + 4.32, 3), round(y_in4 + 3.52, 3) # 交错两颗孔(J37 两排之间) for net, pk, yh, xl, yvv, xt in ((PAIRS['USB4'][0], '9', y4dp, xdp4, vdp4, sp4[2]), (PAIRS['USB4'][1], '8', y4dp + 0.5, xdm4, vdm4, sm4[2])): q = u(pk) segs += _poly2(2, [(q[2], q[3]), (q[2], yh), (xl, yh), (xl, yvv)], w, net) segs += _poly2(1, [(xl, yvv), (xt, yvv), (xt, sp4[3])], w, net) vias.append((xl, yvv, net)) done.add(('U32', pk)) # 上行 / USB6:西侧过孔带里各两颗交错孔,之后从 L1 起步 west = [q for q in pads if q[0] == 'U32' and not is_ep(q) and q[4] > q[5]] x_left = min(q[2] - q[4] / 2.0 for q in west) for name, pdm, pdp in (('USB6', '53', '54'), ('USBUP', '58', '59')): dp_net, dm_net = PAIRS[name] for net, pk, dx, dy in ((dm_net, pdm, WVIA_DX[0], -0.25), (dp_net, pdp, WVIA_DX[1], 0.0)): q = u(pk) vx, vy = round(x_left - dx, 3), round(q[3] + dy, 3) pts = [(q[2], q[3]), (round(x_left - 0.35, 3), q[3]), (vx, vy)] segs += _poly2(2, pts, 0.2, net) vias.append((vx, vy, net)) virt.append(['U32', 'fan:%s' % pk, vx, vy, 0.3, 0.3, 1, net, 'RECT']) done.add(('U32', pk)) geom_check(u'D± 构造(USB1/2/4/5 + 上行/USB6 西侧孔)', segs, vias, pads, prior_s, prior_v) return segs, vias, done, virt # ── 通用扇出:这几颗芯片的每只非地、非 D± 脚先打一个孔上 L1,布线从 L1 的落点起步 ───────── FANOUT_REFS = ('U35', 'U6', 'U7', 'U20') FAN_W = 0.15 # 这几个分区里**所有件**的非地信号脚也扇出(第五轮:D± 全通之后剩 37 个网,21 条在 U20 北沿那排上拉 / # NTC 阻容上 —— 0402 一字排开、两头都在 L4,头顶的 L1 是空的却上不去)。 # ⚠ A4H(U32 那条走廊)**不扇**:那里的晶振 / RBIAS / strap 要和 U32 的脚在 L4 就近连,打孔反而绕远。 # ⚠ U31(LDO,宽电源脚)与 J23(Type-C,0.5 间距,出口带另有安排)不扇。 # ⛔ 第七轮实测**反效果**(信号网 37 → 43、GND 打孔失败 97 → 106):小件挨着排(0.6 mm 装配间距), # 每只焊盘就近打孔 = 把 L4 上的一簇件变成两层都占的一簇孔,A3 里两头都已在 L1 的脚照样「没布通」。 # 根子在摆位间距,不在扇出。先关掉,留着代码。 FAN_ZONES = () FAN_ZONE_SKIP = ('U31', 'J23', 'J33') def fan_zone_refs(): import place_base as PL pg = PL.page_of() return set(r for r in pg if r not in FAN_ZONE_SKIP and PL.zone_of_ref(r, pg) in FAN_ZONES) # U20 的脚距 1.27:最近的落点排成一列、孔距 1.27 ⇒ 在 L1 上连成一道墙(第四轮 29 条失败在 U20 周围)。 # ⇒ 奇数脚往外多让 FAN_STAGGER,孔错成两排。 FAN_STAGGER = {'U20': 1.0} def fanout(pads, bypad, segs, vias, board_vias, logs): u"""原地改 bypad:扇出成功的脚换成 L1 上的虚拟落点 `fan:<脚号>`。返回 (segs, vias, 成功数, 失败数)。""" halfv = RT.VIA_D / 2.0 + RT.CLR g, gt, others = grid_now(pads, halfv, segs, vias, board_vias) sh = FAN_W / 2.0 todo = [] zref = fan_zone_refs() for net, ps in bypad.items(): if net in DP_NETS or net in PLANE_NETS: continue for q in ps: if (q[0] in FANOUT_REFS or q[0] in zref) and not RT._is_launch(q) and q[6] in RT.LAY \ and (q[0], str(q[1])) not in TH_KEYS: todo.append(q) def cands(q): return [nd for nd in RT.pad_nodes(g, q, halfv, others.get(q[6], []), sh, gt) if g.free_via(nd[1], nd[2], q[7])] todo.sort(key=lambda q: (len(cands(q)), q[0], str(q[1]))) out_s, out_v, ok, fail = [], [], 0, 0 for q in todo: cs = cands(q) if not cs: fail += 1 continue nd = cs[0] stg = FAN_STAGGER.get(q[0], 0.0) if stg and str(q[1]).isdigit() and int(q[1]) % 2: d0 = math.hypot(RT.wx(cs[0][1]) - q[2], RT.wy(cs[0][2]) - q[3]) far = [c2 for c2 in cs if math.hypot(RT.wx(c2[1]) - q[2], RT.wy(c2[2]) - q[3]) >= d0 + stg] if far: nd = far[0] vx, vy = RT.wx(nd[1]), RT.wy(nd[2]) ss = RT.stub_emit(q, vx, vy, others.get(q[6], []), sh, FAN_W, q[7], gt) out_s += ss out_v.append((vx, vy, q[7])) keep = halfv + FAN_W / 2.0 for (l, a, b, c, d, ww, nn) in ss: RT.block_track(g, l, a, b, c, d, keep, nn) RT.block_track(gt, l, a, b, c, d, RT.CLR + sh + ww / 2.0, nn) rr = RT.VIA_D / 2.0 + halfv rt = RT.VIA_D / 2.0 + RT.CLR + sh for l in RT.LAY: g.block_both(l, vx - rr, vy - rr, vx + rr, vy + rr, 0.0, net=q[7]) gt.block_rect(l, vx - rt, vy - rt, vx + rt, vy + rt, net=q[7]) bypad[q[7]] = [o for o in bypad[q[7]] if o is not q] bypad[q[7]].append([q[0], 'fan:%s' % q[1], vx, vy, 0.3, 0.3, 1, q[7], 'RECT']) ok += 1 return out_s, out_v, ok, fail 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 math.hypot(px - (x0 + t * dx), py - (y0 + t * dy)) def _seg_seg(a, b): u"""两段中心线之间的最短距离(不相交时)。""" 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 _seg_rect_gap(s, r): u"""线段(中心线)到矩形的最短距离(相交为 0)。""" 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 math.hypot(px - (x0 + t * dx), py - (y0 + t * dy)) x0, y0, x1, y1 = s[1], s[2], s[3], s[4] n = max(2, int(math.hypot(x1 - x0, y1 - y0) / 0.02)) best = 1e9 for k in range(n + 1): t = k / float(n) px, py = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t dx = max(r[0] - px, px - r[2], 0.0) dy = max(r[1] - py, py - r[3], 0.0) best = min(best, math.hypot(dx, dy)) return best # ⛔ 上行 / USB6 两对的脚在 U32 **西**侧,去处却在东边(J23 / U20)。首轮 A* 从 L4 绕 U32 南边, # 贴着南沿把 strap 脚(CFGSEL0/1、NONREM0/1、HUB_RST、VBUS_DET)全封死。 # ⇒ 布这两对时在 U32 的南 / 北 / 东三面围一道**只对它们**的 L4 栅栏(离焊盘 3 mm), # 逼它们在西侧打孔上 L1、从芯片顶上过去(L1 在走廊 2 是空的)。 FENCED_PAIRS = ('USB6', 'USBUP') FENCE_OUT = 3.0 FENCE_IN = 0.9 # 栅栏西端离西排焊盘外沿多远(给西排脚自己的引出脚留地方) def u32_fence(pads): ps = [q for q in pads if q[0] == 'U32' and q[7] != 'GND'] x0 = min(q[2] - q[4] / 2 for q in ps) y0 = min(q[3] - q[5] / 2 for q in ps) x1 = max(q[2] + q[4] / 2 for q in ps) y1 = max(q[3] + q[5] / 2 for q in ps) xw = x0 + FENCE_IN return [(2, (xw, y0 - FENCE_OUT, x1 + FENCE_OUT, y0 + FENCE_IN)), # 南 (2, (xw, y1 - FENCE_IN, x1 + FENCE_OUT, y1 + FENCE_OUT)), # 北 (2, (x1 - FENCE_IN, y0 - FENCE_OUT, x1 + FENCE_OUT, y1 + FENCE_OUT))] # 东 def route_pairs(bypad, pads, segs, vias, board_vias, logs): u"""七对 D±:DP 先布,DM 贴着它布。返回新布的 (segs, vias)。""" out_s, out_v = [], [] pad_pen = RT.PAD_PEN RT.PAD_PEN = DP_PAD_PEN fence = u32_fence(pads) try: for k, name in enumerate(PAIR_ORDER): dp, dm = PAIRS[name] later = [q for nm in PAIR_ORDER[k + 1:] for n in PAIRS[nm] for q in bypad.get(n, [])] fc = fence if name in FENCED_PAIRS else () # ⛔ 上行 / USB6 的 DP:L4 上除了两端脚附近 3 mm 全收过路费,逼它上 L1 走空走廊。 # 第二轮只有栅栏时,它从栅栏西南角绕出去、贴着 J37 底下往东,照样把 strap 脚南路堵了。 # DM 不用单独处理:它贴着 DP 的带子走。 lp = None if name in FENCED_PAIRS: lp = {1: [(0.0, 0.0, B.BW, B.BH)], 2: [RT.pad_rect(q, 3.0) for q in bypad.get(dp, []) + bypad.get(dm, [])]} s, v = route_one(dp, bypad.get(dp, []), DIFF_W, pads, segs + out_s, vias + out_v, board_vias, logs, pen_pads=later + bypad.get(dm, []), fence=fc, hug=lp) out_s += s out_v += v band = None if s: band = {1: [], 2: []} for q in s: band[q[0]].append(_seg_box(q, HUG_BAND)) for (vx, vy, _n) in v: for l in RT.LAY: band[l].append((vx - 1.2, vy - 1.2, vx + 1.2, vy + 1.2)) for q in bypad.get(dm, []): r = RT.pad_rect(q, HUG_PAD) for l in RT.LAY: band[l].append(r) s2, v2 = route_one(dm, bypad.get(dm, []), DIFF_W, pads, segs + out_s, vias + out_v, board_vias, logs, pen_pads=later, hug=band, fence=fc) out_s += s2 out_v += v2 finally: RT.PAD_PEN = pad_pen return out_s, out_v def is_ep(p): return p[7] == 'GND' and min(p[4], p[5]) >= EP_MIN def plane_vias(pads, segs, vias, board_vias, logs, skip, which, done): u"""GND / +12V 平面网:每只焊盘一个过孔(大 GND 焊盘在盘里打孔阵)。 `which='early'`:+12V 焊盘 + GND 散热 / 外壳盘(盘内孔阵,不占任何人的出口),布信号之前做。 `which='late'`:其余 GND 贴片焊盘,**布完信号之后**做。 ⛔ 首轮是全部放在信号之前:插槽 1 mm 间距的针排里,GND 脚的孔一落就把隔壁信号脚的出口 夹成 0.4 mm(0.15 的线都过不去)—— I²C / VDAC / EFUSE_EN / PRSNT 几十条全报「没布通」。 `done` 记已处理的焊盘(早段孔阵没打上的,晚段按普通焊盘再试)。 """ halfv = RT.VIA_D / 2.0 + RT.CLR g, gt, others = grid_now(pads, halfv, segs, vias, board_vias) m = B.l1_12v_rects()[0] land = (m[0] + V12_LAND, m[1] + V12_LAND, m[2] - V12_LAND, m[3] - V12_LAND) out_s, out_v = [], [] cnt = {'GND': 0, '+12V': 0, '+3V3': 0, 'EP': 0} sh = RT.W_DEFAULT / 2.0 def occupy(ss, vx, vy, net, w): keep = halfv + w / 2.0 for (l, a, b, c, d, ww, _nn) in ss: RT.block_track(g, l, a, b, c, d, keep, net) RT.block_track(gt, l, a, b, c, d, RT.CLR + sh + ww / 2.0, net) r = RT.VIA_D / 2.0 + halfv rt = RT.VIA_D / 2.0 + RT.CLR + sh for l in RT.LAY: g.block_both(l, vx - r, vy - r, vx + r, vy + r, 0.0, net=net) gt.block_rect(l, vx - rt, vy - rt, vx + rt, vy + rt, net=net) for p in pads: net = p[7] if net not in PLANE_NETS or p[6] not in RT.LAY: continue key = (p[0], str(p[1])) if str(p[1]).endswith(TH_SUFFIX) or key in TH_KEYS or key in skip: continue if net == '+12V' and p[6] == 1: continue # 槽上的 12V 针本来就在 L1 大铜里 if key in done: continue # 早段:+12V、散热盘孔阵、以及几颗扇出芯片自己的 +3V3 脚(它们最容易被信号封住) early_3v3 = net == '+3V3' and p[0] in FANOUT_REFS if which == 'early' and not (net == '+12V' or is_ep(p) or early_3v3): continue if which == 'late' and net not in LATE_PLANE_NETS: continue if which == 'early' and is_ep(p): x0, y0, x1, y1 = RT.pad_rect(p) nx = max(1, int((x1 - x0 - 2 * EP_EDGE) / EP_PITCH) + 1) ny = max(1, int((y1 - y0 - 2 * EP_EDGE) / EP_PITCH) + 1) nx, ny = min(nx, 3), min(ny, 3) got = 0 for a in range(nx): for b in range(ny): vx = p[2] if nx == 1 else x0 + EP_EDGE + a * (x1 - x0 - 2 * EP_EDGE) / (nx - 1) vy = p[3] if ny == 1 else y0 + EP_EDGE + b * (y1 - y0 - 2 * EP_EDGE) / (ny - 1) i, j = RT.gx(vx), RT.gy(vy) if g.free_via(i, j, 'GND'): out_v.append((round(vx, 3), round(vy, 3), 'GND')) occupy([], vx, vy, 'GND', 0.0) got += 1 if got: cnt['EP'] += 1 done.add(key) continue # 没打上的留给晚段按普通焊盘处理 w = max(RT.rule_min(net, {}), min(0.5 if net == '+12V' else 0.3, p[4], p[5])) # ⛔ 落孔只看**过孔图**(free_via,两层都按过孔口径让开)。第一版还要求两层**导线图**都空, # 而 L1 的 12V 大铜在导线图里是占着的 —— A1/A2/A6 里 40 多只 GND 脚因此全报「找不到落孔点」。 # GND 孔落进 12V 大铜是合法的:铺铜自己挖反焊盘。 cand = [nd for nd in RT.pad_nodes(g, p, halfv, others.get(p[6], []), w / 2.0, gt) if g.free_via(nd[1], nd[2], net) and (net != '+12V' or (land[0] <= RT.wx(nd[1]) <= land[2] and land[1] <= RT.wy(nd[2]) <= land[3]))] if not cand: logs.append(u'%s: %s.%s 找不到落过孔的地方' % (net, p[0], p[1])) continue nd = cand[0] vx, vy = RT.wx(nd[1]), RT.wy(nd[2]) ss = RT.stub_emit(p, vx, vy, others.get(p[6], []), w / 2.0, w, net, gt) out_s += ss out_v.append((vx, vy, net)) occupy(ss, vx, vy, net, w) cnt[net] += 1 done.add(key) print(u' 平面网扇出(%s):GND %d / +3V3 %d 只脚各一孔、散热/外壳盘 %d 块打孔阵、+12V %d 只脚;共 %d 孔' % (which, cnt['GND'], cnt['+3V3'], cnt['EP'], cnt['+12V'], len(out_v))) return out_s, out_v # ── 主流程 ─────────────────────────────────────────────────────────────── def plan_all(priority=()): pads = load_pads() board_vias = load_board_vias() print(u'板上已有过孔 %d 个;通孔焊盘 %d 只(L4 可布 + L1 障碍)' % (len(board_vias), len(TH_KEYS))) segs, vias, logs = [], [], [] ks, kv = kelvin(pads, segs, vias, board_vias, logs) segs += ks vias += kv print(u' Kelvin:%d 段 / %d 孔' % (len(ks), len(kv))) hs, hv = v12_handoff(pads) segs += hs vias += hv print(u' 9 A 上 L1:Rs6.2 西侧铜块 + %d 个过孔' % len(hv)) js, jdone = j23_bridges(pads) segs += js print(u' J23 同名 D± 脚两座桥:%d 段(DP 南绕 / DM 北绕)' % len(js)) us, uv, udone = usb3_uturn(pads) segs += us vias += uv jdone |= udone print(u' USB3 U 形掉头:%d 段 / %d 孔(按构造画,换位不交叉)' % (len(us), len(uv))) ns, nv, ndone, nvirt = u32_north(pads, segs, vias) segs += ns vias += nv jdone |= ndone print(u' U32 北沿 3V3_HUB(5/10/13 + 24):%d 段 / %d 孔(按构造画)' % (len(ns), len(nv))) cs_, cv, cdone, cvirt = dp_construct(pads, segs, vias) segs += cs_ vias += cv jdone |= cdone nvirt = nvirt + cvirt print(u' D± 构造(USB1/2 北去掉头、USB4/5 东绕、上行/USB6 西侧上 L1):%d 段 / %d 孔' % (len(cs_), len(cv))) kel = {a for a, _b, _w in KELVIN} # U13.9/.10 已由专线接好 bypad = {} for p in pads: if not p[7] or p[7] in PLANE_NETS or str(p[1]).endswith(TH_SUFFIX): continue if (p[0], str(p[1])) in kel or (p[0], str(p[1])) in jdone: continue bypad.setdefault(p[7], []).append(p) for v in nvirt: bypad.setdefault(v[7], []).append(v) ds, dv = route_pairs(bypad, pads, segs, vias, board_vias, logs) segs += ds vias += dv print(u' D± 七对:%d 段 / %d 孔' % (len(ds), len(dv))) if '--dp-only' in sys.argv: return segs, vias, logs skip = set(kel) | {('Rs6', '2')} done = set() ps, pv = plane_vias(pads, segs, vias, board_vias, logs, skip, 'early', done) segs += ps vias += pv GND_PEN_PADS[:] = [q for q in pads if q[7] in LATE_PLANE_NETS and q[6] in RT.LAY and (q[0], str(q[1])) not in done and (q[0], str(q[1])) not in TH_KEYS and not str(q[1]).endswith(TH_SUFFIX)] fs, fv, fok, ffail = fanout(pads, bypad, segs, vias, board_vias, logs) segs += fs vias += fv print(u' 扇出上 L1(%s):%d 只脚打孔,%d 只没落点(留给 A* 直接接)' % (' '.join(FANOUT_REFS), fok, ffail)) rest = set(bypad) - set(DP_NETS) RT.AUTO_CACHE.clear() blocked = [('#ant', r) for r in (_KEEP[0] or keep_rects())] + [('+12V', V12_PATCH)] direct_nets = rest & set(priority) if priority else rest dsegs, merged = RT.direct_links(pads, {n: bypad[n] for n in direct_nets}, 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))) # U32 的本地网(晶振 / RBIAS / VDD18PLL / VDD18 / strap / HUB_RST / VBUS_DET):第六轮它们的脚全被 # 先布的长信号在 U32 西侧落的孔、南边拉的线堵住,而要连的件就在一两毫米外 ⇒ 在长信号之前先布。 u32_local = set(n for n in rest if n not in POWER_LATE and n not in CHAIN_NETS and any(q[0] == 'U32' for q in bypad.get(n, []))) fixed_s, fixed_v = len(segs), len(vias) # 四段:输入链(短、宽、有专属条带)→ U32 本地网 → 普通信号 → 电源(最后布,脚一直收过路费) phases = [set(CHAIN_NETS) & rest, u32_local, set(), set(POWER_LATE) & rest] phases[2] = rest - phases[0] - phases[1] - phases[3] if '--pf' in sys.argv: LATER_PEN_PADS[:] = [q for n in rest - u32_local for q in bypad.get(n, [])] logs += RT.route_groups(bypad, pads, segs, vias, board_vias, {}, priority=priority, only=u32_local) print(u' U32 本地网(%d 个)先布:失败 %d 条' % (len(u32_local), len(logs))) fixed_s, fixed_v = len(segs), len(vias) rest = rest - u32_local # 协商式拥塞布线(router.pf_solve,PathFinder):每轮所有网重布、被多网压着的格点逐轮涨价, # 直到有人让路。第四轮的失败已经不是「脚出不来」而是贪心抢通道(长网先布、走最短路, # 走廊 3 和槽体西段 x 36~43 很快被占光)—— 正是 PathFinder 要解决的那一类。 if '--pf-iters' in sys.argv: RT.PF_ITERS = int(sys.argv[sys.argv.index('--pf-iters') + 1]) LATER_PEN_PADS[:] = [] logs += RT.pf_solve({n: bypad[n] for n in rest if n in bypad}, pads, segs, vias, board_vias, {}, fixed_s, fixed_v) print(u' PathFinder:失败 %d 条' % len(logs)) phases = [] for k, ph in enumerate(phases): later = set().union(*phases[k + 1:]) if k + 1 < len(phases) else set() LATER_PEN_PADS[:] = [q for n in later for q in bypad.get(n, [])] n0 = len(logs) logs += RT.route_groups(bypad, pads, segs, vias, board_vias, {}, priority=priority, only=ph) print(u' 第 %d 段(%d 个网):失败 %d 条' % (k + 1, len(ph), len(logs) - n0)) LATER_PEN_PADS[:] = [] 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) GND_PEN_PADS[:] = [] ps, pv = plane_vias(pads, segs, vias, board_vias, logs, skip, 'late', done) segs += ps vias += pv gs, nres = gnd_rescue(pads, segs, vias, logs, done, blocked) segs += gs print(u' GND 打不上孔的脚:直连到邻近已接地焊盘救回 %d 只(%d 段)' % (nres, len(gs))) segs, vias = RT.dedupe_vias(segs, vias) for m in RT.check_connected(check_pads(pads), segs, vias): if m not in logs: logs.append(u'[连通性] ' + m) return segs, vias, logs GND_RESCUE_R = 2.6 # 找「邻近已接地焊盘」的半径(mm,同层) def gnd_rescue(pads, segs, vias, logs, done, blocked): u"""晚段打不上孔的 GND 脚:用 router.direct_links 直连到邻近**已接地**的同层焊盘(原地删掉救回的日志)。 插槽针排里两只相邻的 GND 脚(1 mm 间距、缝 0.3)最常见:隔壁那只有孔,直连一段就接上了。 「已接地」= 已有平面孔(done)或通孔焊盘(本来就穿到内层地)。 """ tag = u'找不到落过孔的地方' anchor = set(done) | TH_KEYS out_s, nok = [], 0 for net in LATE_PLANE_NETS: # GND 和 +3V3 各救各的 head = net + ': ' fl = [m for m in logs if m.startswith(head) and tag in m] if not fl: continue fkeys = set() for m in fl: ref, pin = m[len(head):].split(' ')[0].rsplit('.', 1) fkeys.add((ref, pin)) fpads = [q for q in pads if (q[0], str(q[1])) in fkeys and q[6] in RT.LAY] near = [q for q in pads if q[7] == net and q[6] in RT.LAY and (q[0], str(q[1])) in anchor and not str(q[1]).endswith(TH_SUFFIX) and any(q[6] == f[6] and abs(q[2] - f[2]) < GND_RESCUE_R and abs(q[3] - f[3]) < GND_RESCUE_R for f in fpads)] dsegs, merged = RT.direct_links(pads, {net: fpads + near}, segs + out_s, vias, blocked_rects=blocked) ok = set() for grp in merged.get(net, []): keys = set((k[0], str(k[1])) for k in grp) if keys & anchor: ok |= keys & fkeys logs[:] = [m for m in logs if not (m in fl and tuple(m[len(head):].split(' ')[0].rsplit('.', 1)) in ok)] out_s += dsegs nok += len(ok) return out_s, nok def check_pads(pads): u"""连通性判据用的焊盘:去掉 L1 的通孔副本、L1 大铜里的 12V 针、单脚网。""" cnt = {} for q in pads: cnt[q[7]] = cnt.get(q[7], 0) + 1 return [q for q in pads if not str(q[1]).endswith(TH_SUFFIX) and not (q[7] == '+12V' and q[6] == 1) and cnt.get(q[7], 0) >= 2] def rip_repair(bypad, pads, segs, vias, logs, board_vias, rest, fixed_s, fixed_v): u"""`router.repair` 接到底板上:只交普通信号(D± / 平面网不拆),验收用 check_pads。""" import time sub = {n: bypad[n] for n in rest if n in bypad} chk_pads = [q for q in check_pads(pads) if q[7] in sub] 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): L = lambda s: math.hypot(s[3] - s[1], s[4] - s[2]) # noqa: E731 tot = sum(L(s) for s in segs) top = sum(L(s) for s in segs if s[0] == 1) print(u'导线 %d 段 / 总长 %.1f mm(L1 %.1f mm = %.0f%%)/ 过孔 %d(GND %d / +12V %d / 信号 %d)' % (len(segs), tot, top, 100.0 * top / max(tot, 1e-9), len(vias), sum(1 for v in vias if v[2] == 'GND'), sum(1 for v in vias if v[2] == '+12V'), sum(1 for v in vias if v[2] not in PLANE_NETS))) print(u'差分对实测铜长(含引出脚)/ 过孔数:') for name, dp, dm in RB.PAIRS: a = sum(L(s) for s in segs if s[6] == dp) b = sum(L(s) for s in segs if s[6] == dm) va = sum(1 for v in vias if v[2] == dp) vb = sum(1 for v in vias if v[2] == dm) print(u' %-6s DP %5.1f mm / %d 孔 DM %5.1f mm / %d 孔 差 %.1f mm' % (name, a, va, b, vb, abs(a - b))) 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)') elif '--replan' not in sys.argv: sys.exit(u'!! 没有 4 层版缓存(schema %d);布线要在服务器上算:.claude/r2/remote_route.ps1' % CACHE_SCHEMA) else: best, prio = None, set() if '--priority-cache' in sys.argv and cache_ok: prio = set(RT._msg_net(m) for m in c.get('logs', [])) print(u'(用上轮 %d 条失败记录优先排网)' % len(c.get('logs', []))) 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, 'layers': B.LAYERS, 'slot_rot': B.SLOT_ROT, 'usb_slot_ref': B.USB_SLOT_REF, 'v12_patch': V12_PATCH, 'segs': segs, 'vias': vias, 'logs': logs}, ensure_ascii=False)) report(segs, vias, logs) if '--push' in sys.argv: # 默认仍拒绝下发不完整路由;`--force` 是 2026-09-10 用户选定的收尾方式: # 先把最好的一版下发,剩下的在客户端手工补 / 逐条构造。 if logs and '--force' not in sys.argv: sys.exit(u'!! 仍有 %d 条未布通,拒绝下发不完整路由(确认要先下发再手补,加 --force)' % len(logs)) if logs: print(u'⚠ --force:带着 %d 条未完成下发' % len(logs)) RT.push(segs, vias)