# -*- 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()