R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 cd01640c40
当前提交 cf7cc9cf90
共修改 759 个文件,包含 76962 行新增和 5299 行删除
+365
查看文件
@@ -0,0 +1,365 @@
# -*- coding: utf-8 -*-
u"""R900~R904 一次解完:**位置 + 朝向 + 地过孔 + strap 走线,四件同时成立才算数**。
/c/Python314/python .claude/pcb2_strap_solve.py # 求解 + 权威校核
/c/Python314/python .claude/pcb2_strap_solve.py --push # 摆件 + 下发铜 + 存盘
## 为什么要合到一起解
分三步走(摆位 → 地扇出 → 布线)连着踩了三次,每次都是「前一步只顾自己那一项」:
1. 只按空地摆(U32 北边 y=79 那条空带)→ 迷宫布线器 **10 条只布通 3 条**;
2. 改成贴着引脚 → 两颗的**地过孔**被围死出不去;
3. 加了「地过孔必须有位置」→ **电阻方向反了**:0402 的 GND 端朝着 U32,
strap 线得跨过自己的 GND 焊盘 + 短线 + 过孔,十条里九条过不了净距校核。
⇒ 分步搜索每一步都「局部最优」,合起来无解。这里对每颗电阻枚举(位置 × 四个朝向),
**当场把地过孔和 strap 路径也一并试出来**,四件全过才落定。
## 两条硬约束
- **QFN 焊盘只能垂直出边**。U32 是 QFN-64 9×9、0.5 mm 间距,焊盘之间只有约 0.2 mm 缝,
**穿不过 0.20 mm 的线**。所以路径按「垂直法线出边,出来之后才有自由度」生成,不做迷宫搜索。
- **0402 的 GND 端必须背对 U32**(`pad2` 比 `pad1` 更靠近引脚),否则见上面第 3 条。
## 性能:搜索用网格索引,权威校核只跑一次
⚠️ 搜索循环里**不能**调 `D.check()` —— 它是 O(全部障碍) 的(1000 焊盘 / 900 过孔 / 2000 导线),
一个候选位置要试几十条路径,几万次全板扫描,实测单颗电阻 40 分钟出不来。
所以搜索用按 2 mm 分桶的网格索引做**同判据的快速版**,
**最后再对整体解跑一次 `D.check()`**(和差分对、整板布线器同一套)作为权威校核。
"""
import io
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
import pcb2_route_dp as D
import pcb2_keypads as K
PCB = '942ed1560d81c1e7'
MIL = 39.3700787401575
LAY = D.L_BOT
WID = 0.20
CLR = D.CLR
PW, PH, POFF = 0.5664, 0.5410, 0.433
CELL = 2.0
OUT = os.path.join(HERE, '_pcb2_strap_solved.json')
# 位号 -> (U32 脚号, QFN 边)。2026-09-07 用户拍板:只做 PRT_SWP1~5,
# BOOST/GANG_EN/已禁用口的 SWP6-7 不装(采到 1 也无功能后果,见设计文档 §21.6a)。
MAP = [('R903', '33', 'E'), ('R902', '47', 'E'), ('R904', '31', 'N'),
('R901', '49', 'S'), ('R900', '51', 'S')]
NORM = {'N': (0.0, 1.0), 'S': (0.0, -1.0), 'E': (1.0, 0.0), 'W': (-1.0, 0.0)}
def call(js, timeout=300):
r = run("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2000));" % PCB + js, timeout=timeout)
if not r.get('success'):
sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600])
return r['result']
def u32_pads():
return call("""
let pid = null;
for (const c of await eda.pcb_PrimitiveComponent.getAll())
if (c.getState_Designator() === 'U32') pid = c.getState_PrimitiveId();
const M = %f, out = {};
for (const p of await eda.pcb_PrimitivePad.getAll()) {
if ((p.primitiveId || '').indexOf(pid) !== 0) continue;
out[String(p.getState_PadNumber ? p.getState_PadNumber() : p.number)] =
[Math.round(p.getState_X()/M*1000)/1000, Math.round(p.getState_Y()/M*1000)/1000, p.getState_Net()];
}
return out;""" % MIL)
class Grid(object):
u"""按 2 mm 方格给焊盘/过孔/导线分桶,供搜索期的快速净距判据用。"""
def __init__(self, pads, vias, lines):
self.p, self.v, self.l = {}, {}, {}
for q in pads:
self._add(self.p, q['x'] - q['hw'], q['y'] - q['hh'], q['x'] + q['hw'], q['y'] + q['hh'], q)
for q in vias:
self._add(self.v, q['x'] - q['r'], q['y'] - q['r'], q['x'] + q['r'], q['y'] + q['r'], q)
for q in lines:
self._add(self.l, min(q['x0'], q['x1']), min(q['y0'], q['y1']),
max(q['x0'], q['x1']), max(q['y0'], q['y1']), q)
def _add(self, ix, x0, y0, x1, y1, obj):
for i in range(int(x0 // CELL) - 1, int(x1 // CELL) + 2):
for j in range(int(y0 // CELL) - 1, int(y1 // CELL) + 2):
ix.setdefault((i, j), []).append(obj)
def _cells(self, ix, x0, y0, x1, y1):
out, seen = [], set()
for i in range(int(x0 // CELL) - 1, int(x1 // CELL) + 2):
for j in range(int(y0 // CELL) - 1, int(y1 // CELL) + 2):
for o in ix.get((i, j), ()):
k = id(o)
if k not in seen:
seen.add(k)
out.append(o)
return out
def near(self, ix, x, y, r=1.5):
return self._cells(ix, x - r, y - r, x + r, y + r)
def near_seg(self, ix, a, b, r=1.5):
return self._cells(ix, min(a[0], b[0]) - r, min(a[1], b[1]) - r,
max(a[0], b[0]) + r, max(a[1], b[1]) + r)
def pad_rects(cx, cy, rot):
u"""[pad1(GND), pad2(strap)],每个是 (x, y, hw, hh)。"""
r = rot % 360
if r == 0:
return [(cx - POFF, cy, PW / 2, PH / 2), (cx + POFF, cy, PW / 2, PH / 2)]
if r == 180:
return [(cx + POFF, cy, PW / 2, PH / 2), (cx - POFF, cy, PW / 2, PH / 2)]
if r == 90:
return [(cx, cy - POFF, PH / 2, PW / 2), (cx, cy + POFF, PH / 2, PW / 2)]
return [(cx, cy + POFF, PH / 2, PW / 2), (cx, cy - POFF, PH / 2, PW / 2)]
def bodies(pads, mine):
u"""障碍表里没有元件本体,只有焊盘。用「同 ref 所有焊盘的包围盒」当本体 ——
不加这一条,搜索会把 0402 摆进 U32 那颗 QFN 的本体底下(焊盘确实没撞,物理上装不了)。"""
bb = {}
for q in pads:
if q['ref'] in mine or not q['ref']:
continue
b = bb.setdefault(q['ref'], [1e9, 1e9, -1e9, -1e9])
b[0] = min(b[0], q['x'] - q['hw'])
b[1] = min(b[1], q['y'] - q['hh'])
b[2] = max(b[2], q['x'] + q['hw'])
b[3] = max(b[3], q['y'] + q['hh'])
return list(bb.items())
def pads_clear(cx, cy, rot, G, mine, bods, taken, ex_l, ex_v):
for px, py, hw, hh in pad_rects(cx, cy, rot):
for ref, b in bods:
if b[0] - CLR < px + hw and px - hw < b[2] + CLR and \
b[1] - CLR < py + hh and py - hh < b[3] + CLR:
return False
for q in G.near(G.p, px, py):
if q['ref'] in mine:
continue
if D.seg_rect((px, py), (px, py), q['x'], q['y'], q['hw'], q['hh']) < hw + hh + CLR:
return False
for v in list(G.near(G.v, px, py)) + ex_v:
if math.hypot(v['x'] - px, v['y'] - py) < max(hw, hh) + v['r'] + CLR:
return False
for l in list(G.near(G.l, px, py)) + ex_l:
if l['lay'] not in (LAY, 12):
continue
if D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) < max(hw, hh) + l['w'] / 2 + CLR:
return False
for tx, ty, trot in taken:
for a in pad_rects(cx, cy, rot):
for b in pad_rects(tx, ty, trot):
dx = abs(a[0] - b[0]) - (a[2] + b[2])
dy = abs(a[1] - b[1]) - (a[3] + b[3])
d = math.hypot(max(dx, 0.0), max(dy, 0.0)) if (dx > 0 or dy > 0) else max(dx, dy)
if d < CLR:
return False
return True
def via_ok(vx, vy, G, ex_v, ex_l):
for q in G.near(G.p, vx, vy, 2.0):
need = K.VIA_D / 2 + (0.0 if q['net'] == 'GND' else CLR)
if q['lay'] == 12:
need = max(need, K.VIA_D / 2 + K.HOLE2HOLE)
if D.seg_rect((vx, vy), (vx, vy), q['x'], q['y'], q['hw'], q['hh']) < need:
return False
for v in list(G.near(G.v, vx, vy, 2.0)) + ex_v:
d = math.hypot(v['x'] - vx, v['y'] - vy)
if d < K.VIA_D / 2 + v['r'] + K.HOLE2HOLE:
return False
for l in list(G.near(G.l, vx, vy, 2.0)) + ex_l:
if l['net'] == 'GND':
continue
if D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], vx, vy) < K.VIA_D / 2 + l['w'] / 2 + CLR:
return False
return True
def stub_ok(a, b, G, ex_l, ex_v):
h = K.W_STUB / 2.0
for q in G.near_seg(G.p, a, b):
if q['net'] == 'GND':
continue
if D.seg_rect(a, b, q['x'], q['y'], q['hw'], q['hh']) < h + CLR:
return False
for v in list(G.near_seg(G.v, a, b)) + ex_v:
if v['net'] == 'GND':
continue
if D.seg_pt(a[0], a[1], b[0], b[1], v['x'], v['y']) < h + v['r'] + CLR:
return False
for l in list(G.near_seg(G.l, a, b)) + ex_l:
if l['net'] == 'GND' or l['lay'] not in (LAY, 12):
continue
if D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) < h + l['w'] / 2 + CLR:
return False
return True
def path_ok(path, net, G, mine, ex_l, ex_v):
h = WID / 2.0
for i in range(len(path) - 1):
a = (path[i][0], path[i][1])
b = (path[i + 1][0], path[i + 1][1])
for q in G.near_seg(G.p, a, b):
if q['net'] == net or q['ref'] in mine or q['lay'] not in (LAY, 12):
continue
if D.seg_rect(a, b, q['x'], q['y'], q['hw'], q['hh']) < h + CLR:
return False
for v in list(G.near_seg(G.v, a, b)) + ex_v:
if v['net'] == net:
continue
if D.seg_pt(a[0], a[1], b[0], b[1], v['x'], v['y']) < h + v['r'] + CLR:
return False
for l in list(G.near_seg(G.l, a, b)) + ex_l:
if l['net'] == net or l['lay'] not in (LAY, 12):
continue
if D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) < h + l['w'] / 2 + CLR:
return False
return True
def strap_paths(a, b, nd):
u"""出边 -> L(两种拐序)/ 斜接 / 侧移再折。QFN 只能垂直出边,出来之后才有自由度。"""
ax, ay = a
bx, by = b
nx, ny = nd
tx, ty = -ny, nx
for d in [v / 100.0 for v in range(30, 201, 10)]:
mx, my = round(ax + nx * d, 3), round(ay + ny * d, 3)
opts = [[(ax, ay), (mx, my), (round(bx, 3), round(my, 3)), (bx, by)],
[(ax, ay), (mx, my), (round(mx, 3), round(by, 3)), (bx, by)],
[(ax, ay), (mx, my), (bx, by)]]
for k in (0.5, -0.5, 1.0, -1.0, 1.5, -1.5, 2.0, -2.0, 3.0, -3.0):
sx, sy = round(mx + tx * k, 3), round(my + ty * k, 3)
opts.append([(ax, ay), (mx, my), (sx, sy), (round(bx, 3), round(sy, 3)), (bx, by)])
opts.append([(ax, ay), (mx, my), (sx, sy), (round(sx, 3), round(by, 3)), (bx, by)])
for pts in opts:
clean = [pts[0]]
for p in pts[1:]:
if abs(p[0] - clean[-1][0]) > 1e-6 or abs(p[1] - clean[-1][1]) > 1e-6:
clean.append(p)
if len(clean) >= 2:
yield [(x, y, LAY, WID) for x, y in clean]
def solve(pads, vias, lines, up):
mine = {d for d, _, _ in MAP}
bods = bodies(pads, mine)
G = Grid(pads, vias, lines)
taken, gvias, routes, out = [], [], {}, {}
ex_l, ex_v = [], []
for des, pin, side in MAP:
px, py, net = up[pin]
nd = NORM[side]
base = math.degrees(math.atan2(nd[1], nd[0]))
angs = sorted(range(-90, 91, 10), key=abs) # 只在引脚的外侧半平面里找
best = None
for r in [v / 10.0 for v in range(10, 71, 2)]:
for da in angs:
th = math.radians(base + da)
cx, cy = round(px + r * math.cos(th), 3), round(py + r * math.sin(th), 3)
for rot in (0, 90, 180, 270):
g, t = pad_rects(cx, cy, rot)
# strap 端必须比 GND 端更靠近引脚,否则线要跨过自己的地
if math.hypot(t[0] - px, t[1] - py) >= math.hypot(g[0] - px, g[1] - py):
continue
if not pads_clear(cx, cy, rot, G, mine, bods, taken, ex_l, ex_v):
continue
vsel = None
for vd in [v / 10.0 for v in range(9, 26)]:
for va in range(0, 360, 15):
vth = math.radians(va)
vx = round(g[0] + math.cos(vth) * vd, 3)
vy = round(g[1] + math.sin(vth) * vd, 3)
if math.hypot(vx - px, vy - py) <= math.hypot(g[0] - px, g[1] - py):
continue # 地过孔往远离引脚的一侧走
if any(math.hypot(vx - ax, vy - ay) < 0.33 + K.VIA_H for ax, ay in gvias):
continue
if not via_ok(vx, vy, G, ex_v, ex_l):
continue
if stub_ok((g[0], g[1]), (vx, vy), G, ex_l, ex_v):
vsel = (vx, vy)
break
if vsel:
break
if not vsel:
continue
l2 = ex_l + [{'net': 'GND', 'lay': LAY, 'x0': g[0], 'y0': g[1],
'x1': vsel[0], 'y1': vsel[1], 'w': K.W_STUB}]
v2 = ex_v + [{'net': 'GND', 'x': vsel[0], 'y': vsel[1], 'r': K.VIA_D / 2}]
for path in strap_paths((px, py), (t[0], t[1]), nd):
if path_ok(path, net, G, mine, l2, v2):
best = (cx, cy, rot, r, vsel, path, (g[0], g[1]))
break
if best:
break
if best:
break
if best:
break
if best is None:
print(u'!! %s (U32.%s) 无解' % (des, pin))
return None
cx, cy, rot, r, vsel, path, gp = best
routes[net] = path
taken.append((cx, cy, rot))
gvias.append(vsel)
ex_l.append({'net': 'GND', 'lay': LAY, 'x0': gp[0], 'y0': gp[1],
'x1': vsel[0], 'y1': vsel[1], 'w': K.W_STUB})
ex_v.append({'net': 'GND', 'x': vsel[0], 'y': vsel[1], 'r': K.VIA_D / 2})
for i in range(len(path) - 1):
ex_l.append({'net': net, 'lay': LAY, 'x0': path[i][0], 'y0': path[i][1],
'x1': path[i + 1][0], 'y1': path[i + 1][1], 'w': WID})
out[des] = {'pos': [cx, cy, rot], 'pin': pin, 'net': net, 'gnd_pad': list(gp),
'via': list(vsel), 'path': [[p[0], p[1]] for p in path]}
L = sum(math.hypot(path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1])
for i in range(len(path) - 1))
print(u' %-5s U32.%-3s %s边 电阻(%7.3f,%6.3f) rot%-4d 离脚 %.1f strap %.2f mm %d 拐点 地孔(%7.3f,%6.3f)'
% (des, pin, side, cx, cy, rot, r, L, len(path) - 2, vsel[0], vsel[1]))
sys.stdout.flush()
return out, routes
def main():
if '--nodump' not in sys.argv:
D.dump()
pads, vias, lines = D.geom()
up = u32_pads()
got = solve(pads, vias, lines, up)
if not got:
sys.exit(1)
sol, routes = got
# 权威校核:把地短线/地过孔也加进障碍,再跑一次和差分对同一套的 check()
lines2 = lines + [{'net': 'GND', 'lay': LAY, 'x0': v['gnd_pad'][0], 'y0': v['gnd_pad'][1],
'x1': v['via'][0], 'y1': v['via'][1], 'w': K.W_STUB} for v in sol.values()]
vias2 = vias + [{'net': 'GND', 'x': v['via'][0], 'y': v['via'][1], 'r': K.VIA_D / 2}
for v in sol.values()]
bad = D.check(routes, pads, vias2, lines2, verbose=True)
if bad:
sys.exit(u'权威校核未过 %d 条' % len(bad))
io.open(OUT, 'w', encoding='utf-8').write(json.dumps(sol, ensure_ascii=False, indent=1))
print(u'\n五颗全部解出,D.check() 权威校核通过;已存 %s' % OUT)
print(u'粘到 pcb2_place.GLOBAL:')
for des, pin, side in MAP:
cx, cy, rot = sol[des]['pos']
print(u" '%s': (%.3f, %.3f, %d), # U32.%s" % (des, cx, cy, rot, pin))
main()