199 行
9.2 KiB
Python
199 行
9.2 KiB
Python
# -*- 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()
|