- 规则:铜层 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
178 行
7.7 KiB
Python
178 行
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""下板 PCB1 的**环境约束图**:把载板 PCB2 底面的东西折算到下板坐标系。
|
||
|
||
下板 160×125 居中在载板 220×135 下方 ⇒ **下板坐标 = 载板坐标 − (30, 5)**
|
||
(事实来源 `pcb2-header-pads.json` 的 frame 字段 / 设计文档 §25.1)。
|
||
两板净空 **13 mm**(§22.3f:公头塑高 2.5 + 母座塑高 10.5)。
|
||
|
||
产出 `pcb1-env.json`:
|
||
|
||
| 字段 | 含义 |
|
||
|---|---|
|
||
| `anchors` | J24 / J25 的 40 个焊盘在下板坐标下的目标位置(**对位的唯一依据**,别按引脚号推)|
|
||
| `zones` | 一堆矩形,每个带 `hmax` = 下板在这块地方允许的最大元件高度 = 13 − h载板 − 1 |
|
||
| `lanes` | 排线在载板背面占的车道(下板坐标)。排线要平贴着走,这里 `hmax` 取 11.0 |
|
||
| `slots` | 载板的 10 条穿线槽投影(下板边缘 y<1.3 / y>123.9 两条带)|
|
||
|
||
用法: /c/Python314/python .claude/pcb1_env.py
|
||
"""
|
||
import io, json, os, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
|
||
import pcb2_place as P2
|
||
from eda import run
|
||
|
||
DX, DY = 30.0, 5.0 # 下板原点在载板坐标里的位置
|
||
GAP = 13.0 # 两板净空
|
||
MARGIN = 1.0 # 装配余量
|
||
# 2026-09-07:跟 pcb1_place 一起改成真板框(220x135,左下角 (-30,-5))。
|
||
BX0, BY0, BX1, BY1 = -30.0, -5.0, 190.0, 130.0
|
||
BW, BH = BX1 - BX0, BY1 - BY0
|
||
|
||
LANE_H = 1.0 # 排线(0.2 厚)+ 走线余量,按 1 mm 算
|
||
|
||
|
||
def shift(x0, y0, x1, y1):
|
||
return [round(x0 - DX, 2), round(y0 - DY, 2), round(x1 - DX, 2), round(y1 - DY, 2)]
|
||
|
||
|
||
def clip(r):
|
||
u"""裁到下板板框内;完全在框外返回 None。"""
|
||
x0 = max(r[0], 0.0); y0 = max(r[1], 0.0)
|
||
x1 = min(r[2], BX1); y1 = min(r[3], BY1)
|
||
if x1 <= x0 or y1 <= y0:
|
||
return None
|
||
return [round(x0, 2), round(y0, 2), round(x1, 2), round(y1, 2)]
|
||
|
||
|
||
def header_anchors():
|
||
u"""载板 J31/J32 的 80 个焊盘 -> 下板坐标 + **网络名**。
|
||
|
||
⚠️ 对插的判据是**网络**,不是针号。两颗封装的焊盘编号同手性、母座又在载板底层,
|
||
所以下板 pin(2r−1) 正对载板 pin(2r)(设计文档 §26.5)。针号一定对不上,网络必须对上。
|
||
|
||
坐标来自 `pcb2-header-pads.json`(`pcb2_place --push` 导出),网络当场从载板读。
|
||
"""
|
||
pads = json.load(io.open(os.path.join(HERE, 'pcb2-header-pads.json'), encoding='utf-8'))['pads']
|
||
nets = {}
|
||
r = run("""
|
||
await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');
|
||
await new Promise(r=>setTimeout(r,2000));
|
||
const out = {};
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
|
||
const d = c.getState_Designator();
|
||
if (d !== 'J31' && d !== 'J32') continue;
|
||
const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
|
||
out[d] = ps.map(p => [p.padNumber, p.net]);
|
||
}
|
||
return out;
|
||
""", timeout=300)
|
||
if not r.get('success'):
|
||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:400])
|
||
for ref, lst in r['result'].items():
|
||
for pad, net in lst:
|
||
nets[(ref, pad)] = net
|
||
out = {}
|
||
for q in pads:
|
||
out.setdefault(q['ref'], {})[q['pad']] = [round(q['x'] - DX, 3), round(q['y'] - DY, 3),
|
||
nets.get((q['ref'], q['pad']))]
|
||
# J31 -> J24,J32 -> J25
|
||
return {'J24': out['J31'], 'J25': out['J32']}
|
||
|
||
|
||
def build():
|
||
pos = P2.plan()
|
||
bb = P2.load_bb()
|
||
P2.CAN = {P2.desig(n)['c1'] for n in P2.SITES}
|
||
|
||
zones = []
|
||
for d, (x, y, r, lay) in sorted(pos.items()):
|
||
if lay != 2 and d not in P2.th_parts():
|
||
continue
|
||
h = P2.part_height(d)
|
||
b = P2.box(d, r, bb, 2)
|
||
rect = clip(shift(x + b[0], y + b[1], x + b[2], y + b[3]))
|
||
if not rect:
|
||
continue
|
||
zones.append({'ref': d, 'rect': rect, 'h': h,
|
||
'hmax': round(GAP - h - MARGIN, 2)})
|
||
|
||
lanes = []
|
||
for n in P2.SITES:
|
||
for lname, ((u0, u1, v0, v1), owner) in P2.LANES.items():
|
||
ax, ay, _ = P2.to_board(n, u0, v0)
|
||
bx, by, _ = P2.to_board(n, u1, v1)
|
||
rect = clip(shift(min(ax, bx), min(ay, by), max(ax, bx), max(ay, by)))
|
||
if rect:
|
||
lanes.append({'ref': 'CH%d-%s-lane' % (n, lname), 'rect': rect,
|
||
'h': LANE_H, 'hmax': round(GAP - LANE_H - MARGIN, 2)})
|
||
|
||
slots = []
|
||
for name, x0, y0, x1, y1 in P2.slot_rects():
|
||
rect = clip(shift(x0, y0, x1, y1))
|
||
if rect:
|
||
slots.append({'ref': name, 'rect': rect})
|
||
|
||
anchors = header_anchors()
|
||
|
||
data = {
|
||
'frame': u'下板 PCB1 坐标系 mm,原点左下角,Y 向上。下板坐标 = 载板坐标 − (30,5)。'
|
||
u'hmax = 该矩形内下板元件允许的最大高度 = 13(净空) − h(载板底面元件) − 1(余量)',
|
||
'gap': GAP, 'board': [BW, BH], 'board_rect': [BX0, BY0, BX1, BY1],
|
||
'anchors': anchors, 'zones': zones, 'lanes': lanes, 'slots': slots,
|
||
}
|
||
out = os.path.join(HERE, 'pcb1-env.json')
|
||
json.dump(data, io.open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
||
return data, out
|
||
|
||
|
||
def report(data, out):
|
||
print(u'已写 %s' % out)
|
||
print(u' 锚点 J24/J25 各 %d / %d 个焊盘(含载板侧网络名)'
|
||
% (len(data['anchors']['J24']), len(data['anchors']['J25'])))
|
||
print(u' 受限区 %d 块 / 排线车道 %d 条 / 槽投影 %d 条'
|
||
% (len(data['zones']), len(data['lanes']), len(data['slots'])))
|
||
tight = sorted([z for z in data['zones'] if z['hmax'] < 10.0], key=lambda z: z['hmax'])
|
||
print(u'\n 下板高度受限的地方(hmax < 10 mm):')
|
||
for z in tight:
|
||
r = z['rect']
|
||
print(u' %-6s hmax=%5.2f x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], z['hmax'], r[0], r[2], r[1], r[3]))
|
||
print(u'\n 排线车道(下板这些地方元件 ≤ %.1f mm):' % data['lanes'][0]['hmax'] if data['lanes'] else '')
|
||
for z in data['lanes']:
|
||
r = z['rect']
|
||
print(u' %-14s x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], r[0], r[2], r[1], r[3]))
|
||
print(u'\n 槽在下板上的投影(排线从这里下穿,别放高件):')
|
||
for z in data['slots']:
|
||
r = z['rect']
|
||
print(u' %-12s x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], r[0], r[2], r[1], r[3]))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
d, o = build()
|
||
report(d, o)
|
||
|
||
|
||
# ═════════════════ 高度地图(给人看的)═════════════════
|
||
def hmap(cell=2.0):
|
||
u"""把 zones/lanes 栅格化成一张 hmax 地图,字符越大越矮。"""
|
||
d = json.load(io.open(os.path.join(HERE, 'pcb1-env.json'), encoding='utf-8'))
|
||
nx, ny = int(BW / cell), int(BH / cell)
|
||
g = [[GAP - MARGIN] * nx for _ in range(ny)] # 12.0 = 载板那边是光板
|
||
for z in d['zones'] + d['lanes']:
|
||
x0, y0, x1, y1 = z['rect']
|
||
for j in range(max(0, int(y0 / cell)), min(ny, int((y1 - 1e-6) / cell) + 1)):
|
||
for i in range(max(0, int(x0 / cell)), min(nx, int((x1 - 1e-6) / cell) + 1)):
|
||
g[j][i] = min(g[j][i], z['hmax'])
|
||
def ch(h):
|
||
if h >= 12.0: return '.'
|
||
if h >= 11.0: return ','
|
||
if h >= 10.0: return ':'
|
||
if h >= 8.0: return '='
|
||
if h >= 3.5: return '#'
|
||
return '@'
|
||
print(u'\n高度地图(cell=%.0f mm,左下角是原点;. ≥12 , ≥11 : ≥10 = ≥8 # ≥3.5 @ <3.5)' % cell)
|
||
print(u' ' + ''.join(str(int(i * cell / 10) % 10) for i in range(nx)))
|
||
for j in range(ny - 1, -1, -1):
|
||
print(u'%4.0f ' % (j * cell) + ''.join(ch(v) for v in g[j]))
|
||
return g
|