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 行删除
+143
查看文件
@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
u"""把每个器件微调 ≤ ±0.125 mm,让它的焊盘尽量落在 **0.25 mm 布线网格**上(M7)。
/c/Python314/python .claude/r2/grid_snap.py # 从板上抓焊盘,写 _blade_pad_offsets.json
/c/Python314/python .claude/r2/grid_snap.py --report # 只看现状(板上有多少焊盘在格点上)
## 为什么值得做
`router.py` 是 0.25 mm 网格 A*,焊盘中心几乎都不在格点上,所以每个焊盘要先长一段
**引出脚**(pad 中心 → 最近的可用格点)再交给 A*。M6 欠的 5 只脚全是
**「出口被自家邻脚的引出脚封死」**(方案 §26.6)—— 引出脚是斜的、占的格点比线本身多,
0402 挨着 0402 时两根引出脚就能把中间那条通道堵死。
焊盘落在格点上 ⇒ 引出脚长度为 0 ⇒ 这一类堵塞直接消失。
## 能对齐到什么程度(不是所有封装都行)
单个器件只有一个 (dx, dy) 可调,所以**只有焊盘间距是 0.25 的整数倍时才能全部对齐**:
| 封装 | 焊盘间距 | 能否全对齐 |
|---|---|---|
| 0402 / 0603 / 0805 两脚件 | 两脚,间距任意 | **能**(两脚共线,一个 dx 就够)|
| QFN 0.5 / SOT-23 0.95×2 | 0.5 的倍数 | **能** |
| TSSOP-24(`U4`)| 0.65 | **不能**,只能让一排里每隔 5 只脚对齐一次(0.65×5 = 3.25)|
| BM24-50DS(`J4`)| 0.4 | 不能(0.4 不是 0.25 的倍数),取残差最小 |
所以判据不是「全部对齐」,而是**「对齐的焊盘数显著变多、残差总和显著变小」**。
## ⛔ 不许动的件
`K1~K4`(位置由小米8 主板弹片决定,Ø1.5 的盘只容 0.5 mm 误差)、
`J5` / `J4`(排线穿槽的落点)、`J1`(金手指,1.0 mm 间距本来就在格点上)。
这几件的焊盘对不齐就对不齐 —— 机械优先于布线。
## 用法上的约定
存的是**焊盘相对本体中心的局部偏移**(`_blade_pad_offsets.json`),不是绝对微调量 ——
件在 `pcb_place` 里还会被「按连线就近落位」挪到别的货架位置去,绝对量当场过期,
局部偏移只跟封装和旋转角有关。`pcb_place.plan()` 最后一步拿它现算 (dx, dy)。
**改了旋转角要重跑这个脚本**(换了位置不用)。
"""
import io
import json
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)
from eda import run # noqa: E402
import pcb_blade as B # noqa: E402
import pcb_place as PL # noqa: E402
G = 0.25 # 和 router.py 的 G 必须一致
MAX_D = G / 2.0 # 单件最多挪半格
OUT = os.path.join(HERE, '_blade_pad_offsets.json')
PADS = os.path.join(HERE, '_blade_pads.json')
# 机械锚定,不许为了布线挪
FROZEN = set(PL.TOP_ONLY) | {'J5', 'J4'}
def board_pos():
u"""从板上读回每个器件的中心(mm)—— 焊盘的局部偏移要拿它来减。"""
r = run("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2000));"
"return (await eda.pcb_PrimitiveComponent.getAll()).map("
" c=>[c.designator, c.x, c.y, c.rotation, c.layer]);" % B.PCB, timeout=300)
if not r.get('success'):
sys.exit(json.dumps(r, ensure_ascii=False)[:600])
return {a[0]: (a[1] / B.MIL, a[2] / B.MIL) for a in r['result']}
def pads_by_ref():
d = json.load(io.open(PADS, encoding='utf-8'))
out = {}
for p in d:
out.setdefault(p[0], []).append((p[2], p[3]))
return out
def resid(v, d):
u"""v 挪 d 之后离最近格点的距离。"""
t = (v + d) / G
return abs(t - round(t)) * G
def best_delta(vals):
u"""在 [-MAX_D, MAX_D] 里扫一个偏移,使这一列坐标的残差和最小。
⚠ 用扫描不用解析解:焊盘间距不是 0.25 倍数时(TSSOP 0.65 / BM24 0.4)
没有解析最优,扫描 1 µm 一步在这里足够快(83 件 × 251 步)。
"""
best, bd = None, 0.0
n = int(round(MAX_D * 1000))
for k in range(-n, n + 1):
d = k / 1000.0
s = sum(resid(v, d) for v in vals)
if best is None or s < best - 1e-9:
best, bd = s, d
return round(bd, 3), best
def on_grid(v, tol=0.02):
t = v / G
return abs(t - round(t)) * G <= tol
def main():
u"""把每个器件的焊盘**局部偏移**(相对本体中心)算出来存盘。
⚠ 存的是**局部偏移**不是绝对微调量:`pcb_place` 里件还会被
「按连线就近落位」挪到别的货架位置去,绝对微调量当场就过期了,
而局部偏移只跟封装和旋转角有关,挪到哪儿都还成立。
"""
pads = pads_by_ref()
pos = board_pos()
n_before = sum(1 for ps in pads.values() for p in ps if on_grid(p[0]) and on_grid(p[1]))
n_tot = sum(len(ps) for ps in pads.values())
print(u'现状:%d / %d 个焊盘在 0.25 网格上' % (n_before, n_tot))
if '--report' in sys.argv:
return
out = {}
for ref, ps in sorted(pads.items()):
if ref in FROZEN or ref not in pos:
continue
cx, cy = pos[ref]
out[ref] = [[round(x - cx, 4), round(y - cy, 4)] for x, y in ps]
io.open(OUT, 'w', encoding='utf-8').write(json.dumps(out, ensure_ascii=False))
gain = 0
for ref, offs in out.items():
cx, cy = pos[ref]
dx = best_delta([cx + o[0] for o in offs])[0]
dy = best_delta([cy + o[1] for o in offs])[0]
gain += (sum(1 for o in offs if on_grid(cx + o[0] + dx) and on_grid(cy + o[1] + dy))
- sum(1 for o in offs if on_grid(cx + o[0]) and on_grid(cy + o[1])))
print(u'已写 %s(%d 件的局部焊盘偏移);按当前摆位估算能把对齐焊盘从 %d 提到 %d'
% (os.path.basename(OUT), len(out), n_before, n_before + gain))
print(u'⇒ `pcb_place.plan()` 会用它现算微调量(只调货架排布器摆的件),重跑 --push 生效')
if __name__ == '__main__':
main()