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
这个提交包含在:
zhou yang
2026-09-10 21:02:10 +08:00
共同撰写人 Claude Opus 5
父节点 18d3d69856
当前提交 efcba30a0c
共修改 280 个文件,包含 92007 行新增和 15053 行删除
+263
查看文件
@@ -0,0 +1,263 @@
# -*- coding: utf-8 -*-
u"""摆位体检:外围件有没有摆在**自己那只脚的那一侧**(M7)。
/c/Python314/python .claude/r2/place_audit.py # 全板扫一遍
/c/Python314/python .claude/r2/place_audit.py U2 U1 # 只看这几颗
/c/Python314/python .claude/r2/place_audit.py --base # R2 底板
## 这个脚本在抓什么
`U2` 那一窝暴露出来的毛病:它是 WQFN-20,**四条边都有脚**,而外围 12 件全被摆在了
西边 —— 接东边 `p17~p20` 的四件要**绕整颗芯片**才够得着。这类错:
- **DRC 不报**(没有间距问题),
- **几何自检不报**(没有重叠、没出板框),
- **布线器只会报「XXX 出不来 / 没布通」**,而那是症状不是病因,
- 人眼看截图能看出来,但要一颗一颗放大。
⇒ 判据写成一条能算的:**从芯片的这只脚,到同网对端焊盘的直线,
如果从芯片自己的本体里穿过去,这件就是摆错边了。**
同一条判据顺手也能抓「被第三个件挡住」(直线穿过别人的本体),
以及「去耦电容离电源脚太远」。
## 判据:绕行代价,不是「穿没穿过」
第一版只判「直线穿不穿过本体」,结果 64 处里绝大多数是噪声 —— `U1.14` 到 `U4.14`
隔着 81 mm,线从 `U4` 自己本体上方过一下,绕行代价才 2 mm,那不是摆错边。
⇒ 判据改成**绕行代价**:把本体当障碍,算「绕过去的最短路 − 直线」,
再看它占直线长度的比例。只有
· 对端是**小外围件**(≤ `SAT_PINS` 只脚,即 0402/SOT23 这类),
· 两者相距 < `NEAR`(默认 20 mm),
· 绕行多走 > `EXTRA_MM` 且占比 > `EXTRA_RATIO`
三条同时成立,才算「摆错边」—— 那才是「本可以放对面、白白绕一圈」的情形。
平面网(`GND` / `VO_P` / `+12V`)不走线,直接排除;成员多于 `BIG_NET` 的大网
(`+3V3` 这种)也排除,它们本来就不该按直线看。
"""
import io
import json
import math
import os
import sys
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)
if '--base' in sys.argv:
import place_base as PL # noqa: E402
PADS = os.path.join(HERE, '_base_pads.json')
PLANE = ('GND', '+12V', '+5V', '+3V3', '+3V3_HUB',
'VIN_RAW', 'VIN_FUSED', 'VIN_SW')
else:
import pcb_place as PL # noqa: E402
PADS = os.path.join(HERE, '_blade_pads.json')
PLANE = ('GND', 'VO_P', '+12V', '+12V_F')
BIG_NET = 6 # 成员多于这个数的网不判(+3V3 这种大网不该按直线看)
IC_PINS = 4 # 引脚数 >= 这个数才算「芯片」(两脚件不算)
MAX_IC_PINS = 24 # 脚多于这个数的当**连接器**,不参与「摆错边」
SAT_PINS = 3 # 引脚数 <= 这个数才算「小外围件」
INSET = 0.25 # 本体矩形往里缩多少再判相交(焊盘本来就贴着边)
NEAR = 20.0 # 只看这个距离内的(远距离网绕一下无所谓)
EXTRA_MM = 1.5 # 绕行多走多少 mm 才算数
EXTRA_RATIO = 0.10 # 且要占直线长度的这个比例
FAR_DECAP = 3.0 # 去耦电容离电源脚超过这个距离就提示(mm)
def load():
rows = json.load(io.open(PADS, encoding='utf-8'))
pads, bynet, byref = [], {}, {}
for r in rows:
ref, pin, x, y, net = r[0], r[1], r[2], r[3], r[7]
p = {'ref': ref, 'pin': pin, 'x': x, 'y': y, 'net': net}
pads.append(p)
byref.setdefault(ref, []).append(p)
if net and net not in PLANE:
bynet.setdefault(net, []).append(p)
body = {}
for ref, ps in byref.items():
xs = [p['x'] for p in ps]
ys = [p['y'] for p in ps]
w, h = PL.BODY.get(ref, (0, 0))
if w: # 焊盘框量不到本体的几件(C60/L1/J4/U4),用真实本体
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
body[ref] = (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2)
else:
body[ref] = (min(xs), min(ys), max(xs), max(ys))
return pads, bynet, byref, body
def seg_rect(x1, y1, x2, y2, r, inset=INSET):
u"""线段和矩形(往里缩 inset)有没有真的相交。Liang–Barsky。"""
a, b, c, d = r[0] + inset, r[1] + inset, r[2] - inset, r[3] - inset
if a >= c or b >= d:
return False
dx, dy = x2 - x1, y2 - y1
t0, t1 = 0.0, 1.0
for p, q in ((-dx, x1 - a), (dx, c - x1), (-dy, y1 - b), (dy, d - y1)):
if abs(p) < 1e-12:
if q < 0:
return False
else:
t = q / p
if p < 0:
if t > t1:
return False
t0 = max(t0, t)
else:
if t < t0:
return False
t1 = min(t1, t)
return t0 < t1
def detour(ax, ay, bx, by, r):
u"""把矩形 r(缩 INSET)当障碍,绕过去的最短路 − 直线 = 多走多少 mm。
可见图只有 6 个点(两端 + 四角),Dijkstra 直接跑。不相交就返回 0。
"""
if not seg_rect(ax, ay, bx, by, r):
return 0.0
a, b, c, d = r[0] + INSET, r[1] + INSET, r[2] - INSET, r[3] - INSET
pts = [(ax, ay), (bx, by), (a, b), (a, d), (c, b), (c, d)]
n = len(pts)
INF = float('inf')
dist = [INF] * n
dist[0] = 0.0
done = [False] * n
for _ in range(n):
u = min((k for k in range(n) if not done[k]), key=lambda k: dist[k], default=None)
if u is None or dist[u] == INF:
break
done[u] = True
for v in range(n):
if done[v]:
continue
if seg_rect(pts[u][0], pts[u][1], pts[v][0], pts[v][1], r):
continue # 这一段还是穿本体,不可走
w = math.hypot(pts[u][0] - pts[v][0], pts[u][1] - pts[v][1])
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
straight = math.hypot(ax - bx, ay - by)
return 0.0 if dist[1] == INF else max(0.0, dist[1] - straight)
def wrong_side(icpad, sat, r):
u"""外围件在不在这只脚**朝外的那一侧**;不在的话,绕过去大约要多走多少 mm。
⛔ 不用「直线穿不穿本体」判:芯片的**焊盘本身就在本体框里**(甚至框边上),
线段起点落在障碍内部,绕行最短路直接算成「无解」,返回 0 —— 于是
`U1.1`(EN,在西边) 配 `R3`(在东边) 这种教科书式的摆错边**一条都报不出来**。
这是第二版判据交的学费。
改判「侧」:脚离哪条边近,那条边的**外法线**就是它的出线方向;
外围件相对这只脚的方向和外法线反着来(点积 < -0.3),就是摆到对面去了,
代价约等于「芯片在这个方向上的尺寸 + 两倍净距」(线得绕过去)。
"""
x, y = icpad['x'], icpad['y']
dists = ((x - r[0], (-1.0, 0.0)), (r[2] - x, (1.0, 0.0)),
(y - r[1], (0.0, -1.0)), (r[3] - y, (0.0, 1.0)))
_d, nrm = min(dists, key=lambda t: t[0])
vx, vy = sat['x'] - x, sat['y'] - y
vlen = math.hypot(vx, vy)
if vlen < 1e-9:
return 0.0
dot = (vx * nrm[0] + vy * nrm[1]) / vlen
if dot > -0.3:
return 0.0
span = (r[2] - r[0]) if nrm[1] == 0 else (r[3] - r[1])
return span + 2 * INSET
def main():
only = [a for a in sys.argv[1:] if not a.startswith('--')]
_pads, bynet, byref, body = load()
npin = {r: len(ps) for r, ps in byref.items()}
ics = sorted(r for r in byref if npin[r] >= IC_PINS)
own, blocked, decap = [], [], []
for net, ps in sorted(bynet.items()):
if len(ps) > BIG_NET:
continue
for i in range(len(ps)):
for j in range(i + 1, len(ps)):
a, b = ps[i], ps[j]
if a['ref'] == b['ref']:
continue
d = math.hypot(a['x'] - b['x'], a['y'] - b['y'])
if d > NEAR:
continue
# 「摆错边」:一端是芯片、另一端是小外围件,而绕自家本体要多走不少
for (icpad, sat) in ((a, b), (b, a)):
ic = icpad['ref']
# ⚠ `J1`(64 脚) / `J4`(50 脚) 这类连接器不判:它们的脚排成一长条,
# 「离哪条边近」对端头的脚毫无意义(`J4.51` 会被判成朝西出线),
# 而连接器的出线方向是设计定死的,不是摆位能改的。
if (ic not in ics or npin[ic] > MAX_IC_PINS
or npin[sat['ref']] > SAT_PINS):
continue
ex = wrong_side(icpad, sat, body[ic])
if ex > EXTRA_MM and ex / d > EXTRA_RATIO:
own.append((ex, ex / d, ic, net, a, b))
for ref, r in body.items():
if ref in (a['ref'], b['ref']):
continue
ex = detour(a['x'], a['y'], b['x'], b['y'], r)
if ex > EXTRA_MM and ex / d > EXTRA_RATIO:
blocked.append((ex, ex / d, ref, net, a, b))
for ref, ps in sorted(byref.items()):
if len(ps) != 2 or not ref.startswith('C'):
continue
if 'GND' not in [p['net'] for p in ps]:
continue
pw = [p for p in ps if p['net'] != 'GND']
if not pw or pw[0]['net'] in PLANE:
continue
best = None
for q in bynet.get(pw[0]['net'], []):
if q['ref'] == ref or q['ref'] not in ics:
continue
dd = math.hypot(pw[0]['x'] - q['x'], pw[0]['y'] - q['y'])
if best is None or dd < best[0]:
best = (dd, q)
if best and best[0] > FAR_DECAP:
decap.append((best[0], ref, pw[0]['net'], best[1]))
def dedup(rows):
seen, out = set(), []
for t in sorted(rows, reverse=True):
k = (t[2], t[4]['ref'], t[4]['pin'], t[5]['ref'], t[5]['pin'])
if k not in seen:
seen.add(k)
out.append(t)
return out
def fmt(t):
ex, ratio, key, net, a, b = t
return (u' 绕 %5.1f mm (%3.0f%%) %-5s %-10s %s.%s(%.1f,%.1f) — %s.%s(%.1f,%.1f)'
% (ex, ratio * 100, key, net, a['ref'], a['pin'], a['x'], a['y'],
b['ref'], b['pin'], b['x'], b['y']))
if only:
keep = lambda t: t[2] in only or t[4]['ref'] in only or t[5]['ref'] in only
own, blocked = [t for t in own if keep(t)], [t for t in blocked if keep(t)]
decap = [t for t in decap if t[1] in only or t[3]['ref'] in only]
own, blocked = dedup(own), dedup(blocked)
print(u'⛔ 外围件摆错边(要绕自家芯片本体,多走 > %.1f mm 且 > %.0f%%):%d 处'
% (EXTRA_MM, EXTRA_RATIO * 100, len(own)))
for t in own:
print(fmt(t))
print(u'\n⚠ 被第三个件挡住(绕得过去,只是提示):%d 处' % len(blocked))
for t in blocked[:15]:
print(fmt(t))
if len(blocked) > 15:
print(u' ...(还有 %d 处)' % (len(blocked) - 15))
print(u'\n⚠ 去耦电容离电源脚 > %.1f mm:%d 处' % (FAR_DECAP, len(decap)))
for d, ref, net, q in sorted(decap, reverse=True):
print(u' %-5s %-8s 离 %s.%s %.1f mm' % (ref, net, q['ref'], q['pin'], d))
if __name__ == '__main__':
main()