R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""给 R900~R909 就近找合法位置 —— 用布线器自己的障碍表搜,不靠肉眼看空地。
|
||||
|
||||
/c/Python314/python .claude/pcb2_strap_place.py # 搜 + 打印建议坐标
|
||||
|
||||
## 为什么要重摆
|
||||
|
||||
第一版把十颗全摆在 U32 北边 y=79 那一排(那里确实是唯一的整块空地),
|
||||
结果 `pcb2_route_sig.py` **10 条只布通 3 条** —— 南边(pin 49/50/51)、西边(15/17)、
|
||||
东南(47/48)那七个脚要绕过整颗 QFN 和一圈去耦,绕不出去。
|
||||
|
||||
⇒ 改成**每颗贴着自己的引脚放**。空地是按障碍表搜出来的:
|
||||
以引脚为心一圈圈往外找,第一个「两个焊盘都离异网铜 >= 0.20 mm」的位置就用。
|
||||
判据和 `pcb2_clear.py` / 扇出用的是同一套(`pcb2_route_dp.seg_rect` + `CLR`)。
|
||||
"""
|
||||
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
|
||||
|
||||
CLR = D.CLR
|
||||
PW, PH = 0.5664, 0.5410 # R0402 单个焊盘 mm(22.3 x 21.3 mil)
|
||||
POFF = 0.433 # 焊盘中心离元件中心
|
||||
BODY = 0.55 # 本体半长,用来判两颗新件之间
|
||||
|
||||
# 位号 -> U32 脚号
|
||||
MAP = [('R906', '15'), ('R905', '17'), ('R904', '31'), ('R903', '33'), ('R909', '34'),
|
||||
('R902', '47'), ('R908', '48'), ('R901', '49'), ('R907', '50'), ('R900', '51')]
|
||||
|
||||
|
||||
def call(js, timeout=240):
|
||||
r = run("await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');"
|
||||
"await new Promise(r=>setTimeout(r,2000));" + js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600])
|
||||
return r['result']
|
||||
|
||||
|
||||
def u32_pins():
|
||||
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 = 39.3700787401575, 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];
|
||||
}
|
||||
return out;""")
|
||||
|
||||
|
||||
def pad_rects(cx, cy, rot):
|
||||
u"""返回 [pad1(GND), pad2(strap)]。rot 0/90/180/270 四个方向都要给 ——
|
||||
⚠️ 第一版只搜 0 和 90,等于默认「GND 端朝西/朝南」,结果 strap 线要跨过自己的
|
||||
GND 焊盘 + GND 短线 + GND 过孔才够得着 U32 的脚,十条里九条过不了净距校核。"""
|
||||
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"""⚠️ 障碍表只有焊盘/过孔/导线,**不知道元件本体**。第一版没这一条,
|
||||
搜索结果里 R906/R909 直接落进了 U32 那颗 QFN 的本体底下(焊盘确实没撞,物理上装不了)。
|
||||
这里用「同一个 ref 的所有焊盘的包围盒」当本体,QFN/SOIC 都够准。"""
|
||||
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 [(r, b) for r, b in bb.items()]
|
||||
|
||||
|
||||
def clear(cx, cy, rot, pads, vias, lines, mine, taken, bods):
|
||||
u"""两个焊盘离**所有**异网铜 >= CLR(GND 也算,0402 本体不能骑在 GND 盘上),
|
||||
而且不许落进任何已有元件的本体投影。"""
|
||||
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, u'%s 本体' % ref
|
||||
for px, py, hw, hh in pad_rects(cx, cy, rot):
|
||||
for q in pads:
|
||||
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, u'焊盘 %s.%s(%s)' % (q['ref'], q['num'], q['net'])
|
||||
for v in vias:
|
||||
if math.hypot(v['x'] - px, v['y'] - py) < max(hw, hh) + v['r'] + CLR:
|
||||
return False, u'过孔(%s)' % v['net']
|
||||
for l in lines:
|
||||
if l['lay'] not in (2, 12):
|
||||
continue
|
||||
d = D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py)
|
||||
if d < max(hw, hh) + l['w'] / 2 + CLR:
|
||||
return False, u'导线(%s)' % l['net']
|
||||
# ⚠️ 新件之间也要按**真实焊盘矩形**判,不能用「中心距 > 本体」这种糙判据 ——
|
||||
# 第一版用 ±1.3 mm 的方框,push 之后 pcb2_clear 立刻抓出三对 0.03~0.15 mm。
|
||||
for tx, ty, trot in taken:
|
||||
for ax, ay, ahw, ahh in pad_rects(cx, cy, rot):
|
||||
for bx, by, bhw, bhh in pad_rects(tx, ty, trot):
|
||||
dx = abs(ax - bx) - (ahw + bhw)
|
||||
dy = abs(ay - by) - (ahh + bhh)
|
||||
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, u'挨着另一颗新件 (%.3f)' % d
|
||||
return True, ''
|
||||
|
||||
|
||||
def gnd_via_ok(cx, cy, rot, pads, vias, lines, mine, placed):
|
||||
u"""⚠️ 摆得下 ≠ 接得出去:地端还得能落一个合法过孔到 Inner2。
|
||||
第一版没这一条,R903/R906 摆完之后地过孔被 NONREM0 / USB5_DM 围死,
|
||||
`pcb2_strap_gnd.py` 直接报「找不到扇出位置」。现在把它并进摆位判据。
|
||||
返回 (可行, 过孔坐标)。"""
|
||||
gx, gy = pad_rects(cx, cy, rot)[0][:2] # pad1 = GND 端
|
||||
for dist in [d / 10.0 for d in range(9, 31)]:
|
||||
for a in range(0, 360, 15):
|
||||
th = math.radians(a)
|
||||
vx, vy = round(gx + math.cos(th) * dist, 3), round(gy + math.sin(th) * dist, 3)
|
||||
if any(math.hypot(vx - ax, vy - ay) < 0.33 + K.VIA_H for ax, ay in placed):
|
||||
continue
|
||||
ok, _ = K._clear(vx, vy, K.VIA_D / 2, 12, pads, vias, lines, None)
|
||||
if not ok:
|
||||
continue
|
||||
ok, _ = K._seg_clear((gx, gy), (vx, vy), K.W_STUB, pads, vias, lines, None)
|
||||
if ok:
|
||||
return True, (vx, vy)
|
||||
return False, None
|
||||
|
||||
|
||||
def main():
|
||||
if '--nodump' not in sys.argv:
|
||||
D.dump()
|
||||
pads, vias, lines = D.geom()
|
||||
pins = u32_pins()
|
||||
mine = {d for d, _ in MAP}
|
||||
bods = bodies(pads, mine)
|
||||
taken, out, gvias = [], {}, []
|
||||
for des, pin in MAP:
|
||||
px, py = pins[pin]
|
||||
best, why = None, []
|
||||
for r in [x / 10.0 for x in range(12, 81, 2)]:
|
||||
for a in range(0, 360, 10):
|
||||
th = math.radians(a)
|
||||
cx, cy = round(px + r * math.cos(th), 3), round(py + r * math.sin(th), 3)
|
||||
for rot in (0, 90, 180, 270):
|
||||
# ⭐ strap 端(pad2)必须比 GND 端(pad1)更靠近 U32 的脚,
|
||||
# 否则那条 strap 线只能绕过自己的 GND 焊盘/短线/过孔 —— 走不通。
|
||||
g, t = pad_rects(cx, cy, rot)
|
||||
if math.hypot(t[0] - px, t[1] - py) >= math.hypot(g[0] - px, g[1] - py):
|
||||
continue
|
||||
ok, msg = clear(cx, cy, rot, pads, vias, lines, mine, taken, bods)
|
||||
if ok:
|
||||
vok, vxy = gnd_via_ok(cx, cy, rot, pads, vias, lines, mine, gvias)
|
||||
if not vok:
|
||||
if len(why) < 6:
|
||||
why.append(u'(%.2f,%.2f)r%d 地过孔出不去' % (cx, cy, rot))
|
||||
continue
|
||||
best = (cx, cy, rot, r, vxy)
|
||||
gvias.append(vxy)
|
||||
break
|
||||
if len(why) < 6:
|
||||
why.append(u'(%.2f,%.2f)r%d 撞 %s' % (cx, cy, rot, msg))
|
||||
if best:
|
||||
break
|
||||
if best:
|
||||
break
|
||||
if not best:
|
||||
print(u'!! %s (U32.%s) 找不到位置' % (des, pin))
|
||||
for w in why:
|
||||
print(u' ' + w)
|
||||
sys.exit(1)
|
||||
out[des] = best
|
||||
taken.append((best[0], best[1], best[2]))
|
||||
print(u' %-5s U32.%-3s 脚(%7.3f,%6.3f) -> (%7.3f,%6.3f) rot%-3d 距 %.1f mm 地过孔(%7.3f,%6.3f)'
|
||||
% (des, pin, px, py, best[0], best[1], best[2], best[3], best[4][0], best[4][1]))
|
||||
io.open(os.path.join(HERE, '_pcb2_strap_place.json'), 'w', encoding='utf-8').write(
|
||||
json.dumps(out, ensure_ascii=False, indent=1))
|
||||
print(u'\n粘到 pcb2_place.GLOBAL 里:')
|
||||
for des, pin in MAP:
|
||||
cx, cy, rot, r = out[des][:4]
|
||||
print(u" '%s': (%.3f, %.3f, %d), # U32.%s" % (des, cx, cy, rot, pin))
|
||||
|
||||
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户