R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
这个提交包含在:
+435
-434
@@ -1,434 +1,435 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""五路 `VOn_P` 的**贴片电容接到 Inner1 电源岛** —— 每个焊盘旁边打一个过孔。
|
||||
|
||||
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
|
||||
/c/Python314/python .claude/pcb2_vo_vias.py # 2. 只算 + 本地净距校核
|
||||
/c/Python314/python .claude/pcb2_vo_vias.py --push # 3. 下发
|
||||
/c/Python314/python .claude/pcb2_pour.py --push # 4. **必须重铺**(新过孔要反焊盘)
|
||||
|
||||
## 为什么要有这一步
|
||||
|
||||
`VO1_P`~`VO5_P` 每个网络 10 个焊盘:**5 个通孔**(`C8n1.1` Ø10 电解正极、`TP8n1`
|
||||
测试点、排母 3 针)本身就落在 Inner1 的岛上,不用管;剩下 **5 个贴片焊盘**
|
||||
(`C8n2`/`C8n3` 100µF 1206、`C8n4`/`C8n5` 22µF 1206、`C8n6` 100nF 0402)
|
||||
只有顶层铜,**和岛之间没有任何通路** —— 这就是 §25.14 里连接性 51 条中的 31 条。
|
||||
|
||||
所以这一步和布线不是一回事:不需要找路径,只需要**在焊盘边上打一个过孔**,
|
||||
用一小段线接过去,垂直下到 Inner1 的岛上。25 个焊盘 = 25 个过孔。
|
||||
|
||||
## 选点判据(不是"看着差不多")
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 方向 | 只取 8 个方向(0/45/90…),**不许出现斜的怪角度**(全板 45° 化,§25.14)|
|
||||
| 起步距离 | 过孔铜边到本焊盘矩形 **≥ 0.15 mm**(同网,不是 DRC 约束,是不想做 via-in-pad)|
|
||||
| 净距 | 过孔和那一小段线对**异网**焊盘/过孔/导线 ≥ 0.20 —— 直接借 `pcb2_route_dp.check()` |
|
||||
| 岛内余量 | 过孔中心离岛多边形边界 ≥ 0.61/2 + 0.30 = **0.605 mm**,免得铺出来只搭上一点点 |
|
||||
| 线宽 | `PWR4A` 最小 0.30;从 1.00 起往下退让,取第一个过的 |
|
||||
|
||||
⚠️ 过孔打在**路径末点**上 —— `expand()` 会静默丢掉末端过孔(§25.14 那条最阴的 bug)。
|
||||
这里靠把末点写两遍绕开(`_path()` 里 `(vx,vy,L_BOT)` 后面再来一份)。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import pcb2_route_dp as R
|
||||
from pcb2_pour import ISLANDS, island_poly
|
||||
|
||||
W_STEPS = [1.00, 0.80, 0.60, 0.50, 0.40, 0.30] # PWR4A 的 strokeWidthMin = 0.30
|
||||
PAD_GAP = 0.15 # 过孔铜边到本焊盘的余量(同网,防 via-in-pad)
|
||||
EDGE_IN = R.VIA_D / 2.0 + 0.30 # 过孔中心到岛边界的余量
|
||||
D_MAX = 3.5
|
||||
D_STEP = 0.05
|
||||
DRILL = 0.33 # 钻孔间距(规则 otherClearance / copperThickness1oz 都要求 >= 11.8mil
|
||||
# = 0.2997 mm),留 30 µm 余量。再大就过不了 J23 的屏蔽脚,见 place() 的注释
|
||||
DIRS = [(math.cos(math.radians(a)), math.sin(math.radians(a))) for a in range(0, 360, 45)]
|
||||
|
||||
# ── 哪些位号要打孔、各打几个 ─────────────────────────────────────────
|
||||
# 2026-09-06 补两类(上一版只有 25 个贴片电容):
|
||||
#
|
||||
# `C8n1.1` —— 换成 Rubycon 6.3TPV2200M10X10.5 之后**是贴片**(V-chip),
|
||||
# 原来当它是直插电解、以为正极自己就落在岛上,其实一点铜都不通。
|
||||
# `J8n.1` —— 电池排线改成 4 焊盘直焊(§13.5 末尾)之后,P+ 也是贴片。
|
||||
# `TP8n1` 那个通孔测试点已经取消 ⇒ **每路 VOn_P 现在只剩排母那 3 针是通孔**。
|
||||
#
|
||||
# 个数按设计文档 §10.3「0.3 mm 孔单孔 1~1.5 A,功率网络每个焊盘打 3 个」:
|
||||
# · `J8n.1` 是这一路 **4 A 的唯一去路**(岛 → 排线 → 手机),必须 3 个;
|
||||
# · `C8n1.1` 是 2200 µF 的主储能,纹波电流也走它,给 3 个;
|
||||
# · `C8n2`~`C8n6` 只承担高频纹波,各 1 个 —— 全按 3 个来会往五块岛里
|
||||
# 再钻 50 个反焊盘,最窄断面(下限 7.0 mm)扛不住。这是本项目对 §10.3 的
|
||||
# 一处**有意偏离**,见 PROGRESS。
|
||||
N_VIA = {}
|
||||
REFS = []
|
||||
for _m in range(5):
|
||||
REFS.append('C8%d1' % _m)
|
||||
N_VIA['C8%d1' % _m] = 3
|
||||
REFS += ['C8%d%d' % (_m, _k) for _k in range(2, 7)]
|
||||
REFS.append('J8%d' % (_m + 1))
|
||||
N_VIA['J8%d' % (_m + 1)] = 3
|
||||
CAPS = REFS
|
||||
|
||||
|
||||
def poly_of(net):
|
||||
for _name, n, main, neck in ISLANDS:
|
||||
if n == net:
|
||||
return island_poly(main, neck)
|
||||
raise KeyError(net)
|
||||
|
||||
|
||||
def inside(poly, x, y, margin):
|
||||
u"""点在多边形内,且离每条边 >= margin。岛都是轴对齐的凸/凹矩形拼接,用射线法 + 边距。"""
|
||||
c = False
|
||||
n = len(poly)
|
||||
for i in range(n):
|
||||
ax, ay = poly[i]
|
||||
bx, by = poly[(i + 1) % n]
|
||||
if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay):
|
||||
c = not c
|
||||
if not c:
|
||||
return False
|
||||
for i in range(n):
|
||||
ax, ay = poly[i]
|
||||
bx, by = poly[(i + 1) % n]
|
||||
if R.seg_pt(ax, ay, bx, by, x, y) < margin:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def pad_dist(px, py, hw, hh, x, y):
|
||||
u"""点到轴对齐焊盘矩形的距离(0 = 在里面)。"""
|
||||
return math.hypot(max(abs(x - px) - hw, 0.0), max(abs(y - py) - hh, 0.0))
|
||||
|
||||
|
||||
def _path(px, py, vx, vy, w):
|
||||
u"""末点上的过孔要把点写两遍,否则 expand() 会把它丢掉(§25.14)。"""
|
||||
return [(px, py, R.L_TOP, w), (vx, vy, R.L_TOP, w),
|
||||
(vx, vy, R.L_BOT, w), (vx, vy, R.L_BOT, w)]
|
||||
|
||||
|
||||
NEAR = 8.0 # 只拿焊盘 8 mm 以内的东西当障碍(全表 816+393+1373,全查太慢)
|
||||
|
||||
|
||||
def holes(pads):
|
||||
u"""每个**带钻孔的焊盘**的孔 -> 一段胶囊 (a, b, r)(圆孔就是 a == b)。
|
||||
|
||||
**两类孔的待遇不一样**(2026-09-06 用 DRC 一条条试出来的):
|
||||
|
||||
| | 线能不能压上去 | 过孔能不能靠近 |
|
||||
|---|---|---|
|
||||
| 焊盘自己的孔(`SLOT` / `ROUND`,金属化)| **能** —— 它就是焊盘的一部分 | 不能,钻孔间距 |
|
||||
| 封装自带的非金属化孔(`npth=True`)| **不能**,DRC 报「挖槽区域 到 导线」| 不能 |
|
||||
|
||||
一开始把两类混在一起判,结果是「谁都连不上 J23 的屏蔽脚」——
|
||||
槽孔焊盘的**任何一点**离自己的槽都不足 0.80 mm,按那个判据连焊盘都碰不得。
|
||||
|
||||
槽的长轴按焊盘的长轴定(`_pcb2_route_geom.json` 里的 hw/hh 已经是转过的),
|
||||
`_pcb2_pads_full.json` 只存了孔的两个尺寸、没存角度,所以只能这么反推。
|
||||
"""
|
||||
out = []
|
||||
# ⚠️ 封装自带的非金属化孔(J23 那两个 Ø0.60)**API 一个都读不出来**,
|
||||
# 只能从内层铺铜的反焊盘反推 —— `pcb2_npth.py --write` 生成这份清单。
|
||||
# 没有它的话,「从 A1B12 往东拉线打孔」这条最自然的走法会被 DRC
|
||||
# 以「挖槽区域 到 导线 距离 0mil」打回来,而净距校核一条都不报。
|
||||
try:
|
||||
for cx, cy, r in json.load(open(os.path.join(HERE, '_pcb2_npth.json'), encoding='utf-8')):
|
||||
out.append({'net': None, 'npth': True, 'a': (cx, cy), 'b': (cx, cy), 'r': r})
|
||||
except Exception:
|
||||
print(u' [!] 没有 _pcb2_npth.json —— 先跑 pcb2_npth.py --write,否则封装自带的孔看不见')
|
||||
full = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
|
||||
bykey = {}
|
||||
for ref, num, _net, _lay, _x, _y, _shape, hole in full:
|
||||
if hole:
|
||||
bykey[(ref, num)] = hole
|
||||
for p in pads:
|
||||
h = bykey.get((p['ref'], p['num']))
|
||||
if not h:
|
||||
continue
|
||||
if h[0] == 'ROUND':
|
||||
r = float(h[1]) / R.MIL / 2.0
|
||||
out.append({'net': p['net'], 'npth': False,
|
||||
'a': (p['x'], p['y']), 'b': (p['x'], p['y']), 'r': r})
|
||||
continue
|
||||
w, L = sorted((float(h[1]) / R.MIL, float(h[2]) / R.MIL)) # 槽宽 / 槽长
|
||||
r, ext = w / 2.0, (L - w) / 2.0
|
||||
if p['hw'] >= p['hh']: # 焊盘长轴在 x 上
|
||||
out.append({'net': p['net'], 'npth': False,
|
||||
'a': (p['x'] - ext, p['y']), 'b': (p['x'] + ext, p['y']), 'r': r})
|
||||
else:
|
||||
out.append({'net': p['net'], 'npth': False,
|
||||
'a': (p['x'], p['y'] - ext), 'b': (p['x'], p['y'] + ext), 'r': r})
|
||||
return out
|
||||
|
||||
|
||||
def local(pads, vias, lines, px, py, hl=()):
|
||||
return ([p for p in pads if abs(p['x'] - px) < NEAR and abs(p['y'] - py) < NEAR],
|
||||
[v for v in vias if abs(v['x'] - px) < NEAR and abs(v['y'] - py) < NEAR],
|
||||
[l for l in lines
|
||||
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) < NEAR],
|
||||
[h for h in hl if abs(h['a'][0] - px) < NEAR and abs(h['a'][1] - py) < NEAR])
|
||||
|
||||
|
||||
def fits(net, segs, via, lp, lv, ll, lh, mine, mvias):
|
||||
u"""几段线 + 一个过孔对**异网**的净距都 >= CLR?
|
||||
|
||||
`segs` = [(lay, x0, y0, x1, y1, w), ...](本次要画的)
|
||||
`via` = (x, y) 或 None
|
||||
`mine` = 本轮已定的线 [(net, lay, x0, y0, x1, y1, w), ...]
|
||||
`mvias` = 本轮已定的孔 [(net, x, y), ...]
|
||||
`lh` = 附近的钻孔胶囊(`holes()` 的输出);**不分网络**,钻孔和铜是两条规则
|
||||
|
||||
⚠️ 线的层必须逐段拿,**不能写死顶层** —— 底层的 0402
|
||||
第一版就是这么错的,本地判完全过、到 `check()` 才炸。
|
||||
"""
|
||||
Rv = R.VIA_D / 2.0
|
||||
|
||||
def seg_bad(lay, x0, y0, x1, y1, w):
|
||||
h, a, b = w / 2.0, (x0, y0), (x1, y1)
|
||||
for p in lp:
|
||||
if p['lay'] != 12 and p['lay'] != lay:
|
||||
continue
|
||||
if p['net'] != net:
|
||||
if R.seg_rect(a, b, p['x'], p['y'], p['hw'], p['hh']) - h < R.CLR - 1e-4:
|
||||
return True
|
||||
|
||||
for v in lv:
|
||||
if v['net'] == net:
|
||||
continue
|
||||
if R.seg_pt(x0, y0, x1, y1, v['x'], v['y']) - v['r'] - h < R.CLR - 1e-4:
|
||||
return True
|
||||
for l in ll:
|
||||
if l['net'] == net or l['lay'] != lay:
|
||||
continue
|
||||
if R.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) - h - l['w'] / 2.0 < R.CLR - 1e-4:
|
||||
return True
|
||||
for n2, l2, ax, ay, bx, by, w2 in mine:
|
||||
if n2 == net or l2 != lay:
|
||||
continue
|
||||
if R.seg_seg(a, b, (ax, ay), (bx, by)) - h - w2 / 2.0 < R.CLR - 1e-4:
|
||||
return True
|
||||
for n2, vx, vy in mvias:
|
||||
if n2 == net:
|
||||
continue
|
||||
if R.seg_pt(x0, y0, x1, y1, vx, vy) - Rv - h < R.CLR - 1e-4:
|
||||
return True
|
||||
for q in lh: # 线只躲**非金属化**孔;焊盘自己的孔是焊盘的一部分
|
||||
if q['npth'] and R.seg_seg(a, b, q['a'], q['b']) - q['r'] - h < DRILL - 1e-4:
|
||||
return True
|
||||
return False
|
||||
|
||||
for s in segs:
|
||||
if seg_bad(*s):
|
||||
return False
|
||||
if via is None:
|
||||
return True
|
||||
vx, vy = via
|
||||
for p in lp: # 过孔穿所有层,两个信号层的焊盘都要让
|
||||
if p['net'] != net:
|
||||
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < R.CLR - 1e-4:
|
||||
return False
|
||||
elif p['lay'] == 12:
|
||||
# ⚠️ **同网也要躲开通孔焊盘** —— 铜可以重叠,钻孔不行。
|
||||
# 第一版跳过了同网,于是给 J23 的两个 GND 屏蔽脚打的孔正落在 J23_1/J23_4
|
||||
# 的**焊盘里**:净距校核全过,DRC 却报「钻孔 到 钻孔 距离 0mil」。
|
||||
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < DRILL - 1e-4:
|
||||
return False
|
||||
for v in lv:
|
||||
if v['net'] != net:
|
||||
if math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < R.CLR - 1e-4:
|
||||
return False
|
||||
elif math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < DRILL - 1e-4:
|
||||
return False # 同网过孔之间也是钻孔间距说了算
|
||||
for l in ll:
|
||||
if l['net'] == net:
|
||||
continue
|
||||
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], vx, vy) - Rv - l['w'] / 2.0 < R.CLR - 1e-4:
|
||||
return False
|
||||
for n2, l2, ax, ay, bx, by, w2 in mine:
|
||||
if n2 == net:
|
||||
continue
|
||||
if R.seg_pt(ax, ay, bx, by, vx, vy) - Rv - w2 / 2.0 < R.CLR - 1e-4:
|
||||
return False
|
||||
for n2, ox, oy in mvias:
|
||||
# ⚠️ 本轮新打的同网过孔也要按钻孔间距判 —— 漏了这一条,U34 相邻三个 GND 脚
|
||||
# 的过孔排成一列、中心只隔 0.23 mm(三个钻孔互相打穿)。
|
||||
need = R.CLR if n2 != net else DRILL
|
||||
if math.hypot(vx - ox, vy - oy) - 2 * Rv < need - 1e-4:
|
||||
return False
|
||||
for q in lh:
|
||||
if R.seg_pt(q['a'][0], q['a'][1], q['b'][0], q['b'][1], vx, vy) - q['r'] - Rv < DRILL - 1e-4:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def place(pad, lay, w_steps, lp, lv, ll, lh, mine, mvias, inside_ok=None):
|
||||
u"""给一个焊盘找一个过孔位。
|
||||
|
||||
先试**一段直线**(只取 8 个方向,不会出现斜的怪角度);
|
||||
不行再试**两段 45°**(第二段相对第一段只允许 ±45°,不允许直角)。
|
||||
返回 (w, segs, (vx, vy)) 或 None。`inside_ok(x, y)` 可选,用来卡在铜岛内。
|
||||
|
||||
⚠️ 线**默认从焊盘中心起步**(最稳的连接),但焊盘中间可能有钻孔
|
||||
(J23 的 A1B12/B1A12 就是 `ROUND 0.36`),压上去 DRC 报「挖槽区域 到 导线」。
|
||||
所以起点会沿出线方向**往外退**,一直退到焊盘边缘内 `INSET` 为止,取第一个过的。
|
||||
"""
|
||||
px, py, hw, hh = pad['x'], pad['y'], pad['hw'], pad['hh']
|
||||
RMIN = R.VIA_D / 2.0 + PAD_GAP
|
||||
# 退到离焊盘边 0.02 为止:J23 的 A1B12 是 1.30×0.60 的焊盘、正中一个 Ø0.36 的
|
||||
# **非金属化**定位孔,孔边到焊盘边只有 0.47 mm,要塞下 0.2997 的净距 + 半线宽,
|
||||
# 起点必须一路退到边上(0.20 的线,实际余量 0.63−0.18−0.10−0.2997 = 50 µm)。
|
||||
INSET = 0.02
|
||||
|
||||
def starts(dx, dy):
|
||||
u"""沿 (dx, dy) 从焊盘中心往外的候选起点:中心 -> 焊盘边缘内 0.05。"""
|
||||
t_edge = 1e9
|
||||
if abs(dx) > 1e-9:
|
||||
t_edge = min(t_edge, hw / abs(dx))
|
||||
if abs(dy) > 1e-9:
|
||||
t_edge = min(t_edge, hh / abs(dy))
|
||||
t_max = max(0.0, t_edge - INSET)
|
||||
out, t = [], 0.0
|
||||
while t <= t_max + 1e-9:
|
||||
out.append((round(px + t * dx, 4), round(py + t * dy, 4)))
|
||||
t += 0.05
|
||||
if out[-1] != (round(px + t_max * dx, 4), round(py + t_max * dy, 4)):
|
||||
out.append((round(px + t_max * dx, 4), round(py + t_max * dy, 4)))
|
||||
return out
|
||||
|
||||
def try_via(vx, vy, w, segs):
|
||||
if pad_dist(px, py, hw, hh, vx, vy) < RMIN:
|
||||
return None
|
||||
if inside_ok and not inside_ok(vx, vy):
|
||||
return None
|
||||
if not fits(pad['net'], segs, (vx, vy), lp, lv, ll, lh, mine, mvias):
|
||||
return None
|
||||
return (w, segs, (vx, vy))
|
||||
|
||||
for w in w_steps:
|
||||
d = 0.0
|
||||
while d <= D_MAX + 1e-9:
|
||||
d += D_STEP
|
||||
for dx, dy in DIRS:
|
||||
vx, vy = round(px + d * dx, 4), round(py + d * dy, 4)
|
||||
for sx, sy in starts(dx, dy):
|
||||
if math.hypot(vx - sx, vy - sy) < 1e-6:
|
||||
continue
|
||||
got = try_via(vx, vy, w, [(lay, sx, sy, vx, vy, w)])
|
||||
if got:
|
||||
return got
|
||||
# 一段直线不行:拐一个 45°
|
||||
for w in w_steps:
|
||||
for i, (dx, dy) in enumerate(DIRS):
|
||||
d1 = 0.0
|
||||
while d1 <= 2.5 + 1e-9:
|
||||
d1 += 0.10
|
||||
mx, my = round(px + d1 * dx, 4), round(py + d1 * dy, 4)
|
||||
for j in (i - 1, i + 1):
|
||||
ex, ey = DIRS[j % len(DIRS)]
|
||||
d2 = 0.0
|
||||
while d2 <= D_MAX + 1e-9:
|
||||
d2 += D_STEP
|
||||
vx, vy = round(mx + d2 * ex, 4), round(my + d2 * ey, 4)
|
||||
for sx, sy in starts(dx, dy):
|
||||
if math.hypot(mx - sx, my - sy) < 1e-6:
|
||||
continue
|
||||
got = try_via(vx, vy, w, [(lay, sx, sy, mx, my, w),
|
||||
(lay, mx, my, vx, vy, w)])
|
||||
if got:
|
||||
return got
|
||||
return None
|
||||
|
||||
|
||||
def solve(pads, vias, lines):
|
||||
u"""逐个贴片焊盘挑过孔位置。返回 {key: path}。"""
|
||||
byref = {}
|
||||
for p in pads:
|
||||
byref.setdefault(p['ref'], []).append(p)
|
||||
|
||||
hl = holes(pads)
|
||||
routes, report, mine, mvias = {}, [], [], []
|
||||
for ref in CAPS:
|
||||
got = [p for p in byref.get(ref, []) if p['net'].startswith('VO') and p['net'].endswith('_P')]
|
||||
if len(got) != 1:
|
||||
sys.exit(u'%s 上找不到唯一的 VOn_P 焊盘(找到 %d 个)' % (ref, len(got)))
|
||||
pad = got[0]
|
||||
net, px, py = pad['net'], pad['x'], pad['y']
|
||||
poly = poly_of(net)
|
||||
lp, lv, ll, lh = local(pads, vias, lines, px, py, hl)
|
||||
# 一个焊盘上要几个孔(`N_VIA`)。**逐个放、每放一个就进 mine/mvias** ——
|
||||
# 后一个孔会自动躲开前一个(同网也按钻孔间距 0.33 判,见 fits())。
|
||||
want = N_VIA.get(ref, 1)
|
||||
for k in range(want):
|
||||
# push()/check() 靠 netof(key) 取网络名,所以 '#' 前面必须是网络名
|
||||
key = u'%s#%s' % (net, ref) if want == 1 else u'%s#%s-%d' % (net, ref, k + 1)
|
||||
# ⚠️ 出线画在**焊盘自己那一层**,不是写死顶层。
|
||||
# v1 时代全板器件在顶层,写死 L_TOP 没暴露问题;v2 之后
|
||||
# VOn_P 的 35 个焊盘**全在底层**,照旧写 L_TOP 会画出 35 条
|
||||
# 悬在顶层、一头不搭焊盘的线 —— 而且 fits() 拿 lay 去筛障碍,
|
||||
# 连净距校核都是照着空荡荡的顶层算的,一条都不会报。
|
||||
lay = pad['lay']
|
||||
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
|
||||
got2 = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias,
|
||||
inside_ok=lambda x, y: inside(poly, x, y, EDGE_IN))
|
||||
if not got2:
|
||||
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net, None))
|
||||
continue
|
||||
w, segs, (vx, vy) = got2
|
||||
pts = ([(segs[0][1], segs[0][2], lay, w)]
|
||||
+ [(sx1, sy1, lay, w) for _l, _x0, _y0, sx1, sy1, _w in segs])
|
||||
routes[key] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
|
||||
for l_, x0, y0, x1, y1, w_ in segs:
|
||||
mine.append((net, l_, x0, y0, x1, y1, w_))
|
||||
mvias.append((net, vx, vy))
|
||||
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net,
|
||||
(w, math.hypot(vx - px, vy - py), vx, vy, len(segs))))
|
||||
return routes, report
|
||||
|
||||
|
||||
def main():
|
||||
pads, vias, lines = R.geom()
|
||||
ours = set('VO%d_P' % n for n in range(1, 6))
|
||||
vias = [v for v in vias if v['net'] not in ours]
|
||||
lines = [l for l in lines if l['net'] not in ours]
|
||||
|
||||
routes, report = solve(pads, vias, lines)
|
||||
fail = 0
|
||||
for ref, net, r in report:
|
||||
if not r:
|
||||
print(u' [!!] %-6s %-7s 找不到能落孔的位置' % (ref, net))
|
||||
fail += 1
|
||||
else:
|
||||
w, d, vx, vy, ns = r
|
||||
print(u' %-6s %-7s 过孔 (%8.3f,%8.3f) 离焊盘中心 %.2f 线宽 %.2f %d 段'
|
||||
% (ref, net, vx, vy, d, w, ns))
|
||||
segs = sum(len(R.expand(p)[0]) for p in routes.values())
|
||||
nv = sum(len(R.expand(p)[1]) for p in routes.values())
|
||||
print(u'')
|
||||
want = sum(N_VIA.get(r, 1) for r in CAPS)
|
||||
print(u'%d/%d 个过孔搞定(%d 个焊盘):%d 段导线 / %d 个过孔'
|
||||
% (len(routes), want, len(CAPS), segs, nv))
|
||||
if nv != len(routes):
|
||||
sys.exit(u'!! 过孔数和焊盘数对不上 —— expand() 又把末端过孔吞了')
|
||||
|
||||
n_sharp = len(R.sharp_corners(routes))
|
||||
print(u'直角拐点 %d(每条只有一段直线,应当是 0)' % n_sharp)
|
||||
|
||||
print(u'')
|
||||
print(u'净距校核(一般 %.2f mm):' % R.CLR)
|
||||
bad = R.check(routes, pads, vias, lines)
|
||||
if not bad:
|
||||
print(u' 全部通过 ✔')
|
||||
|
||||
if '--push' in sys.argv:
|
||||
if bad or fail or n_sharp:
|
||||
sys.exit(u'\n!! 有未解决的问题,不下发。')
|
||||
print(u'')
|
||||
R.push(routes)
|
||||
print(u'⚠️ 下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""五路 `VOn_P` 的**贴片电容接到 Inner1 电源岛** —— 每个焊盘旁边打一个过孔。
|
||||
|
||||
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
|
||||
/c/Python314/python .claude/pcb2_vo_vias.py # 2. 只算 + 本地净距校核
|
||||
/c/Python314/python .claude/pcb2_vo_vias.py --push # 3. 下发
|
||||
/c/Python314/python .claude/pcb2_pour.py --push # 4. **必须重铺**(新过孔要反焊盘)
|
||||
|
||||
## 为什么要有这一步
|
||||
|
||||
`VO1_P`~`VO5_P` 每个网络 10 个焊盘:**5 个通孔**(`C8n1.1` Ø10 电解正极、`TP8n1`
|
||||
测试点、排母 3 针)本身就落在 Inner1 的岛上,不用管;剩下 **5 个贴片焊盘**
|
||||
(`C8n2`/`C8n3` 100µF 1206、`C8n4`/`C8n5` 22µF 1206、`C8n6` 100nF 0402)
|
||||
只有顶层铜,**和岛之间没有任何通路** —— 这就是 §25.14 里连接性 51 条中的 31 条。
|
||||
|
||||
所以这一步和布线不是一回事:不需要找路径,只需要**在焊盘边上打一个过孔**,
|
||||
用一小段线接过去,垂直下到 Inner1 的岛上。25 个焊盘 = 25 个过孔。
|
||||
|
||||
## 选点判据(不是"看着差不多")
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 方向 | 只取 8 个方向(0/45/90…),**不许出现斜的怪角度**(全板 45° 化,§25.14)|
|
||||
| 起步距离 | 过孔铜边到本焊盘矩形 **≥ 0.15 mm**(同网,不是 DRC 约束,是不想做 via-in-pad)|
|
||||
| 净距 | 过孔和那一小段线对**异网**焊盘/过孔/导线 ≥ 0.20 —— 直接借 `pcb2_route_dp.check()` |
|
||||
| 岛内余量 | 过孔中心离岛多边形边界 ≥ 0.61/2 + 0.30 = **0.605 mm**,免得铺出来只搭上一点点 |
|
||||
| 线宽 | `PWR4A` 最小 0.30;从 1.00 起往下退让,取第一个过的 |
|
||||
|
||||
⚠️ 过孔打在**路径末点**上 —— `expand()` 会静默丢掉末端过孔(§25.14 那条最阴的 bug)。
|
||||
这里靠把末点写两遍绕开(`_path()` 里 `(vx,vy,L_BOT)` 后面再来一份)。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import pcb2_route_dp as R
|
||||
from pcb2_pour import ISLANDS, island_poly
|
||||
|
||||
W_STEPS = [1.00, 0.80, 0.60, 0.50, 0.40, 0.30] # PWR4A 的 strokeWidthMin = 0.30
|
||||
PAD_GAP = 0.15 # 过孔铜边到本焊盘的余量(同网,防 via-in-pad)
|
||||
EDGE_IN = R.VIA_D / 2.0 + 0.30 # 过孔中心到岛边界的余量
|
||||
D_MAX = 3.5
|
||||
D_STEP = 0.05
|
||||
DRILL = 0.33 # 钻孔间距(规则 otherClearance / copperThickness1oz 都要求 >= 11.8mil
|
||||
# = 0.2997 mm),留 30 µm 余量。再大就过不了 J23 的屏蔽脚,见 place() 的注释
|
||||
DIRS = [(math.cos(math.radians(a)), math.sin(math.radians(a))) for a in range(0, 360, 45)]
|
||||
|
||||
# ── 哪些位号要打孔、各打几个 ─────────────────────────────────────────
|
||||
# 2026-09-06 补两类(上一版只有 25 个贴片电容):
|
||||
#
|
||||
# `C8n1.1` —— 换成 Rubycon 6.3TPV2200M10X10.5 之后**是贴片**(V-chip),
|
||||
# 原来当它是直插电解、以为正极自己就落在岛上,其实一点铜都不通。
|
||||
# `J8n.1` —— 电池排线改成 4 焊盘直焊(§13.5 末尾)之后,P+ 也是贴片。
|
||||
# `TP8n1` 那个通孔测试点已经取消 ⇒ **每路 VOn_P 现在只剩排母那 3 针是通孔**。
|
||||
#
|
||||
# 个数按设计文档 §10.3「0.3 mm 孔单孔 1~1.5 A,功率网络每个焊盘打 3 个」:
|
||||
# · `J8n.1` 是这一路 **4 A 的唯一去路**(岛 → 排线 → 手机),必须 3 个;
|
||||
# · `C8n1.1` 是 2200 µF 的主储能,纹波电流也走它,给 3 个;
|
||||
# · `C8n2`~`C8n6` 只承担高频纹波,各 1 个 —— 全按 3 个来会往五块岛里
|
||||
# 再钻 50 个反焊盘,最窄断面(下限 7.0 mm)扛不住。这是本项目对 §10.3 的
|
||||
# 一处**有意偏离**,见 PROGRESS。
|
||||
N_VIA = {}
|
||||
REFS = []
|
||||
for _m in range(5):
|
||||
REFS.append('C8%d1' % _m)
|
||||
N_VIA['C8%d1' % _m] = 3
|
||||
REFS += ['C8%d%d' % (_m, _k) for _k in range(2, 7)]
|
||||
REFS.append('J8%d' % (_m + 1))
|
||||
N_VIA['J8%d' % (_m + 1)] = 3
|
||||
CAPS = REFS
|
||||
|
||||
|
||||
def poly_of(net):
|
||||
for _name, n, main, neck in ISLANDS:
|
||||
if n == net:
|
||||
return island_poly(main, neck)
|
||||
raise KeyError(net)
|
||||
|
||||
|
||||
def inside(poly, x, y, margin):
|
||||
u"""点在多边形内,且离每条边 >= margin。岛都是轴对齐的凸/凹矩形拼接,用射线法 + 边距。"""
|
||||
c = False
|
||||
n = len(poly)
|
||||
for i in range(n):
|
||||
ax, ay = poly[i]
|
||||
bx, by = poly[(i + 1) % n]
|
||||
if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay):
|
||||
c = not c
|
||||
if not c:
|
||||
return False
|
||||
for i in range(n):
|
||||
ax, ay = poly[i]
|
||||
bx, by = poly[(i + 1) % n]
|
||||
if R.seg_pt(ax, ay, bx, by, x, y) < margin:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def pad_dist(px, py, hw, hh, x, y):
|
||||
u"""点到轴对齐焊盘矩形的距离(0 = 在里面)。"""
|
||||
return math.hypot(max(abs(x - px) - hw, 0.0), max(abs(y - py) - hh, 0.0))
|
||||
|
||||
|
||||
def _path(px, py, vx, vy, w):
|
||||
u"""末点上的过孔要把点写两遍,否则 expand() 会把它丢掉(§25.14)。"""
|
||||
return [(px, py, R.L_TOP, w), (vx, vy, R.L_TOP, w),
|
||||
(vx, vy, R.L_BOT, w), (vx, vy, R.L_BOT, w)]
|
||||
|
||||
|
||||
NEAR = 8.0 # 只拿焊盘 8 mm 以内的东西当障碍(全表 816+393+1373,全查太慢)
|
||||
|
||||
|
||||
def holes(pads):
|
||||
u"""每个**带钻孔的焊盘**的孔 -> 一段胶囊 (a, b, r)(圆孔就是 a == b)。
|
||||
|
||||
**两类孔的待遇不一样**(2026-09-06 用 DRC 一条条试出来的):
|
||||
|
||||
| | 线能不能压上去 | 过孔能不能靠近 |
|
||||
|---|---|---|
|
||||
| 焊盘自己的孔(`SLOT` / `ROUND`,金属化)| **能** —— 它就是焊盘的一部分 | 不能,钻孔间距 |
|
||||
| 封装自带的非金属化孔(`npth=True`)| **不能**,DRC 报「挖槽区域 到 导线」| 不能 |
|
||||
|
||||
一开始把两类混在一起判,结果是「谁都连不上 J23 的屏蔽脚」——
|
||||
槽孔焊盘的**任何一点**离自己的槽都不足 0.80 mm,按那个判据连焊盘都碰不得。
|
||||
|
||||
槽的长轴按焊盘的长轴定(`_pcb2_route_geom.json` 里的 hw/hh 已经是转过的),
|
||||
`_pcb2_pads_full.json` 只存了孔的两个尺寸、没存角度,所以只能这么反推。
|
||||
"""
|
||||
out = []
|
||||
# ⚠️ 封装自带的非金属化孔(J23 那两个 Ø0.60)**API 一个都读不出来**,
|
||||
# 只能从内层铺铜的反焊盘反推 —— `pcb2_npth.py --write` 生成这份清单。
|
||||
# 没有它的话,「从 A1B12 往东拉线打孔」这条最自然的走法会被 DRC
|
||||
# 以「挖槽区域 到 导线 距离 0mil」打回来,而净距校核一条都不报。
|
||||
try:
|
||||
for cx, cy, r in json.load(open(os.path.join(HERE, '_pcb2_npth.json'), encoding='utf-8')):
|
||||
out.append({'net': None, 'npth': True, 'a': (cx, cy), 'b': (cx, cy), 'r': r})
|
||||
except Exception:
|
||||
print(u' [!] 没有 _pcb2_npth.json —— 先跑 pcb2_npth.py --write,否则封装自带的孔看不见')
|
||||
full = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
|
||||
bykey = {}
|
||||
for ref, num, _net, _lay, _x, _y, _shape, hole in full:
|
||||
if hole:
|
||||
bykey[(ref, num)] = hole
|
||||
for p in pads:
|
||||
h = bykey.get((p['ref'], p['num']))
|
||||
if not h:
|
||||
continue
|
||||
if h[0] == 'ROUND':
|
||||
r = float(h[1]) / R.MIL / 2.0
|
||||
out.append({'net': p['net'], 'npth': False,
|
||||
'a': (p['x'], p['y']), 'b': (p['x'], p['y']), 'r': r})
|
||||
continue
|
||||
w, L = sorted((float(h[1]) / R.MIL, float(h[2]) / R.MIL)) # 槽宽 / 槽长
|
||||
r, ext = w / 2.0, (L - w) / 2.0
|
||||
if p['hw'] >= p['hh']: # 焊盘长轴在 x 上
|
||||
out.append({'net': p['net'], 'npth': False,
|
||||
'a': (p['x'] - ext, p['y']), 'b': (p['x'] + ext, p['y']), 'r': r})
|
||||
else:
|
||||
out.append({'net': p['net'], 'npth': False,
|
||||
'a': (p['x'], p['y'] - ext), 'b': (p['x'], p['y'] + ext), 'r': r})
|
||||
return out
|
||||
|
||||
|
||||
def local(pads, vias, lines, px, py, hl=()):
|
||||
return ([p for p in pads if abs(p['x'] - px) < NEAR and abs(p['y'] - py) < NEAR],
|
||||
[v for v in vias if abs(v['x'] - px) < NEAR and abs(v['y'] - py) < NEAR],
|
||||
[l for l in lines
|
||||
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) < NEAR],
|
||||
[h for h in hl if abs(h['a'][0] - px) < NEAR and abs(h['a'][1] - py) < NEAR])
|
||||
|
||||
|
||||
def fits(net, segs, via, lp, lv, ll, lh, mine, mvias):
|
||||
u"""几段线 + 一个过孔对**异网**的净距都 >= CLR?
|
||||
|
||||
`segs` = [(lay, x0, y0, x1, y1, w), ...](本次要画的)
|
||||
`via` = (x, y) 或 None
|
||||
`mine` = 本轮已定的线 [(net, lay, x0, y0, x1, y1, w), ...]
|
||||
`mvias` = 本轮已定的孔 [(net, x, y), ...]
|
||||
`lh` = 附近的钻孔胶囊(`holes()` 的输出);**不分网络**,钻孔和铜是两条规则
|
||||
|
||||
⚠️ 线的层必须逐段拿,**不能写死顶层** —— 底层的 0402
|
||||
第一版就是这么错的,本地判完全过、到 `check()` 才炸。
|
||||
"""
|
||||
Rv = R.VIA_D / 2.0
|
||||
|
||||
def seg_bad(lay, x0, y0, x1, y1, w):
|
||||
h, a, b = w / 2.0, (x0, y0), (x1, y1)
|
||||
for p in lp:
|
||||
if p['lay'] != 12 and p['lay'] != lay:
|
||||
continue
|
||||
if p['net'] != net:
|
||||
if R.seg_rect(a, b, p['x'], p['y'], p['hw'], p['hh']) - h < R.CLR - 1e-4:
|
||||
return True
|
||||
|
||||
for v in lv:
|
||||
if v['net'] == net:
|
||||
continue
|
||||
if R.seg_pt(x0, y0, x1, y1, v['x'], v['y']) - v['r'] - h < R.CLR - 1e-4:
|
||||
return True
|
||||
for l in ll:
|
||||
if l['net'] == net or l['lay'] != lay:
|
||||
continue
|
||||
if R.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) - h - l['w'] / 2.0 < R.CLR - 1e-4:
|
||||
return True
|
||||
for n2, l2, ax, ay, bx, by, w2 in mine:
|
||||
if n2 == net or l2 != lay:
|
||||
continue
|
||||
if R.seg_seg(a, b, (ax, ay), (bx, by)) - h - w2 / 2.0 < R.CLR - 1e-4:
|
||||
return True
|
||||
for n2, vx, vy in mvias:
|
||||
if n2 == net:
|
||||
continue
|
||||
if R.seg_pt(x0, y0, x1, y1, vx, vy) - Rv - h < R.CLR - 1e-4:
|
||||
return True
|
||||
for q in lh: # 线只躲**非金属化**孔;焊盘自己的孔是焊盘的一部分
|
||||
if q['npth'] and R.seg_seg(a, b, q['a'], q['b']) - q['r'] - h < DRILL - 1e-4:
|
||||
return True
|
||||
return False
|
||||
|
||||
for s in segs:
|
||||
if seg_bad(*s):
|
||||
return False
|
||||
if via is None:
|
||||
return True
|
||||
vx, vy = via
|
||||
for p in lp: # 过孔穿所有层,两个信号层的焊盘都要让
|
||||
if p['net'] != net:
|
||||
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < R.CLR - 1e-4:
|
||||
return False
|
||||
elif p['lay'] == 12:
|
||||
# ⚠️ **同网也要躲开通孔焊盘** —— 铜可以重叠,钻孔不行。
|
||||
# 第一版跳过了同网,于是给 J23 的两个 GND 屏蔽脚打的孔正落在 J23_1/J23_4
|
||||
# 的**焊盘里**:净距校核全过,DRC 却报「钻孔 到 钻孔 距离 0mil」。
|
||||
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < DRILL - 1e-4:
|
||||
return False
|
||||
for v in lv:
|
||||
if v['net'] != net:
|
||||
if math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < R.CLR - 1e-4:
|
||||
return False
|
||||
elif math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < DRILL - 1e-4:
|
||||
return False # 同网过孔之间也是钻孔间距说了算
|
||||
for l in ll:
|
||||
if l['net'] == net:
|
||||
continue
|
||||
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], vx, vy) - Rv - l['w'] / 2.0 < R.CLR - 1e-4:
|
||||
return False
|
||||
for n2, l2, ax, ay, bx, by, w2 in mine:
|
||||
if n2 == net:
|
||||
continue
|
||||
if R.seg_pt(ax, ay, bx, by, vx, vy) - Rv - w2 / 2.0 < R.CLR - 1e-4:
|
||||
return False
|
||||
for n2, ox, oy in mvias:
|
||||
# ⚠️ 本轮新打的同网过孔也要按钻孔间距判 —— 漏了这一条,U34 相邻三个 GND 脚
|
||||
# 的过孔排成一列、中心只隔 0.23 mm(三个钻孔互相打穿)。
|
||||
need = R.CLR if n2 != net else DRILL
|
||||
if math.hypot(vx - ox, vy - oy) - 2 * Rv < need - 1e-4:
|
||||
return False
|
||||
for q in lh:
|
||||
if R.seg_pt(q['a'][0], q['a'][1], q['b'][0], q['b'][1], vx, vy) - q['r'] - Rv < DRILL - 1e-4:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def place(pad, lay, w_steps, lp, lv, ll, lh, mine, mvias, inside_ok=None):
|
||||
u"""给一个焊盘找一个过孔位。
|
||||
|
||||
先试**一段直线**(只取 8 个方向,不会出现斜的怪角度);
|
||||
不行再试**两段 45°**(第二段相对第一段只允许 ±45°,不允许直角)。
|
||||
返回 (w, segs, (vx, vy)) 或 None。`inside_ok(x, y)` 可选,用来卡在铜岛内。
|
||||
|
||||
⚠️ 线**默认从焊盘中心起步**(最稳的连接),但焊盘中间可能有钻孔
|
||||
(J23 的 A1B12/B1A12 就是 `ROUND 0.36`),压上去 DRC 报「挖槽区域 到 导线」。
|
||||
所以起点会沿出线方向**往外退**,一直退到焊盘边缘内 `INSET` 为止,取第一个过的。
|
||||
"""
|
||||
px, py, hw, hh = pad['x'], pad['y'], pad['hw'], pad['hh']
|
||||
RMIN = R.VIA_D / 2.0 + PAD_GAP
|
||||
# 退到离焊盘边 0.02 为止:J23 的 A1B12 是 1.30×0.60 的焊盘、正中一个 Ø0.36 的
|
||||
# **非金属化**定位孔,孔边到焊盘边只有 0.47 mm,要塞下 0.2997 的净距 + 半线宽,
|
||||
# 起点必须一路退到边上(0.20 的线,实际余量 0.63−0.18−0.10−0.2997 = 50 µm)。
|
||||
INSET = 0.02
|
||||
|
||||
def starts(dx, dy):
|
||||
u"""沿 (dx, dy) 从焊盘中心往外的候选起点:中心 -> 焊盘边缘内 0.05。"""
|
||||
t_edge = 1e9
|
||||
if abs(dx) > 1e-9:
|
||||
t_edge = min(t_edge, hw / abs(dx))
|
||||
if abs(dy) > 1e-9:
|
||||
t_edge = min(t_edge, hh / abs(dy))
|
||||
t_max = max(0.0, t_edge - INSET)
|
||||
out, t = [], 0.0
|
||||
while t <= t_max + 1e-9:
|
||||
out.append((round(px + t * dx, 4), round(py + t * dy, 4)))
|
||||
t += 0.05
|
||||
if out[-1] != (round(px + t_max * dx, 4), round(py + t_max * dy, 4)):
|
||||
out.append((round(px + t_max * dx, 4), round(py + t_max * dy, 4)))
|
||||
return out
|
||||
|
||||
def try_via(vx, vy, w, segs):
|
||||
if pad_dist(px, py, hw, hh, vx, vy) < RMIN:
|
||||
return None
|
||||
if inside_ok and not inside_ok(vx, vy):
|
||||
return None
|
||||
if not fits(pad['net'], segs, (vx, vy), lp, lv, ll, lh, mine, mvias):
|
||||
return None
|
||||
return (w, segs, (vx, vy))
|
||||
|
||||
for w in w_steps:
|
||||
d = 0.0
|
||||
while d <= D_MAX + 1e-9:
|
||||
d += D_STEP
|
||||
for dx, dy in DIRS:
|
||||
vx, vy = round(px + d * dx, 4), round(py + d * dy, 4)
|
||||
for sx, sy in starts(dx, dy):
|
||||
if math.hypot(vx - sx, vy - sy) < 1e-6:
|
||||
continue
|
||||
got = try_via(vx, vy, w, [(lay, sx, sy, vx, vy, w)])
|
||||
if got:
|
||||
return got
|
||||
# 一段直线不行:拐一个 45°
|
||||
for w in w_steps:
|
||||
for i, (dx, dy) in enumerate(DIRS):
|
||||
d1 = 0.0
|
||||
while d1 <= 2.5 + 1e-9:
|
||||
d1 += 0.10
|
||||
mx, my = round(px + d1 * dx, 4), round(py + d1 * dy, 4)
|
||||
for j in (i - 1, i + 1):
|
||||
ex, ey = DIRS[j % len(DIRS)]
|
||||
d2 = 0.0
|
||||
while d2 <= D_MAX + 1e-9:
|
||||
d2 += D_STEP
|
||||
vx, vy = round(mx + d2 * ex, 4), round(my + d2 * ey, 4)
|
||||
for sx, sy in starts(dx, dy):
|
||||
if math.hypot(mx - sx, my - sy) < 1e-6:
|
||||
continue
|
||||
got = try_via(vx, vy, w, [(lay, sx, sy, mx, my, w),
|
||||
(lay, mx, my, vx, vy, w)])
|
||||
if got:
|
||||
return got
|
||||
return None
|
||||
|
||||
|
||||
def solve(pads, vias, lines):
|
||||
u"""逐个贴片焊盘挑过孔位置。返回 {key: path}。"""
|
||||
byref = {}
|
||||
for p in pads:
|
||||
byref.setdefault(p['ref'], []).append(p)
|
||||
|
||||
hl = holes(pads)
|
||||
routes, report, mine, mvias = {}, [], [], []
|
||||
for ref in CAPS:
|
||||
got = [p for p in byref.get(ref, []) if p['net'].startswith('VO') and p['net'].endswith('_P')]
|
||||
if len(got) != 1:
|
||||
sys.exit(u'%s 上找不到唯一的 VOn_P 焊盘(找到 %d 个)' % (ref, len(got)))
|
||||
pad = got[0]
|
||||
net, px, py = pad['net'], pad['x'], pad['y']
|
||||
poly = poly_of(net)
|
||||
lp, lv, ll, lh = local(pads, vias, lines, px, py, hl)
|
||||
# 一个焊盘上要几个孔(`N_VIA`)。**逐个放、每放一个就进 mine/mvias** ——
|
||||
# 后一个孔会自动躲开前一个(同网也按钻孔间距 0.33 判,见 fits())。
|
||||
want = N_VIA.get(ref, 1)
|
||||
for k in range(want):
|
||||
# push()/check() 靠 netof(key) 取网络名,所以 '#' 前面必须是网络名
|
||||
# 2026-09-07:分隔符改用 R.KSEP(chr(0)),'#' 会和 KEY_OE# 撞、也让 netof() 拆不开(第三十轮实测 13 条假「过孔-焊盘」)
|
||||
key = u'%s%s%s' % (net, R.KSEP, ref) if want == 1 else u'%s%s%s-%d' % (net, R.KSEP, ref, k + 1)
|
||||
# ⚠️ 出线画在**焊盘自己那一层**,不是写死顶层。
|
||||
# v1 时代全板器件在顶层,写死 L_TOP 没暴露问题;v2 之后
|
||||
# VOn_P 的 35 个焊盘**全在底层**,照旧写 L_TOP 会画出 35 条
|
||||
# 悬在顶层、一头不搭焊盘的线 —— 而且 fits() 拿 lay 去筛障碍,
|
||||
# 连净距校核都是照着空荡荡的顶层算的,一条都不会报。
|
||||
lay = pad['lay']
|
||||
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
|
||||
got2 = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias,
|
||||
inside_ok=lambda x, y: inside(poly, x, y, EDGE_IN))
|
||||
if not got2:
|
||||
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net, None))
|
||||
continue
|
||||
w, segs, (vx, vy) = got2
|
||||
pts = ([(segs[0][1], segs[0][2], lay, w)]
|
||||
+ [(sx1, sy1, lay, w) for _l, _x0, _y0, sx1, sy1, _w in segs])
|
||||
routes[key] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
|
||||
for l_, x0, y0, x1, y1, w_ in segs:
|
||||
mine.append((net, l_, x0, y0, x1, y1, w_))
|
||||
mvias.append((net, vx, vy))
|
||||
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net,
|
||||
(w, math.hypot(vx - px, vy - py), vx, vy, len(segs))))
|
||||
return routes, report
|
||||
|
||||
|
||||
def main():
|
||||
pads, vias, lines = R.geom()
|
||||
ours = set('VO%d_P' % n for n in range(1, 6))
|
||||
vias = [v for v in vias if v['net'] not in ours]
|
||||
lines = [l for l in lines if l['net'] not in ours]
|
||||
|
||||
routes, report = solve(pads, vias, lines)
|
||||
fail = 0
|
||||
for ref, net, r in report:
|
||||
if not r:
|
||||
print(u' [!!] %-6s %-7s 找不到能落孔的位置' % (ref, net))
|
||||
fail += 1
|
||||
else:
|
||||
w, d, vx, vy, ns = r
|
||||
print(u' %-6s %-7s 过孔 (%8.3f,%8.3f) 离焊盘中心 %.2f 线宽 %.2f %d 段'
|
||||
% (ref, net, vx, vy, d, w, ns))
|
||||
segs = sum(len(R.expand(p)[0]) for p in routes.values())
|
||||
nv = sum(len(R.expand(p)[1]) for p in routes.values())
|
||||
print(u'')
|
||||
want = sum(N_VIA.get(r, 1) for r in CAPS)
|
||||
print(u'%d/%d 个过孔搞定(%d 个焊盘):%d 段导线 / %d 个过孔'
|
||||
% (len(routes), want, len(CAPS), segs, nv))
|
||||
if nv != len(routes):
|
||||
sys.exit(u'!! 过孔数和焊盘数对不上 —— expand() 又把末端过孔吞了')
|
||||
|
||||
n_sharp = len(R.sharp_corners(routes))
|
||||
print(u'直角拐点 %d(每条只有一段直线,应当是 0)' % n_sharp)
|
||||
|
||||
print(u'')
|
||||
print(u'净距校核(一般 %.2f mm):' % R.CLR)
|
||||
bad = R.check(routes, pads, vias, lines)
|
||||
if not bad:
|
||||
print(u' 全部通过 ✔')
|
||||
|
||||
if '--push' in sys.argv:
|
||||
if bad or fail or n_sharp:
|
||||
sys.exit(u'\n!! 有未解决的问题,不下发。')
|
||||
print(u'')
|
||||
R.push(routes)
|
||||
print(u'⚠️ 下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
在新工单中引用
屏蔽一个用户