- 规则:铜层 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
186 行
9.7 KiB
Python
186 行
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""载板 PCB2-CARRIER 的扇出 —— 复用下板 `pcb_fanout.py` 的规划器,只换掉板子相关的常量。
|
||
|
||
/c/Python314/python .claude/dump_pads.py 942ed1560d81c1e7 pcb2-copper.json # 1. 抓障碍表
|
||
/c/Python314/python .claude/pcb2_fanout.py # 2. 只规划
|
||
/c/Python314/python .claude/pcb2_fanout.py --push # 3. 下发
|
||
|
||
⚠️ 顺序同 `pcb_fanout.py` 文件头那条:**布局一动就要 `--clean` → 重新 dump → `--push`**,
|
||
三步不能省也不能并(并了会拿旧障碍表规划,或者板上剩两套过孔)。
|
||
|
||
## 为什么现在只扇 GND
|
||
|
||
Inner2 是整板地平面,已经铺好了;但载板上 242 个 GND 焊盘里只有 **20 个是直插**
|
||
(五路的 C8n1 负极 + TP8n2、J23 的四个屏蔽脚、6 个 M3),
|
||
其余两百多个贴片焊盘**根本没连到平面上**。
|
||
Inner1 的电源岛还没划(卡在排母选型),所以 `+5V` / `+3V3_HUB` / `VOn_P`
|
||
现在扇过去就是一堆悬空过孔 —— 等 Inner1 铺完再一起扇。
|
||
|
||
## 和下板的四处不同
|
||
|
||
| | 下板 PCB1 | 载板 PCB2 |
|
||
|---|---|---|
|
||
| 板框 | 160 × 125 | **220 × 135** |
|
||
| 元件面 | 全在顶层 | **两面都是**(J31/J32 + 整个 Hub 在底层)→ `FANOUT_LAYERS = (1, 2)` |
|
||
| 天线禁铺区 | ESP32 在下板 | **没有** → 挪到板外,等于不生效 |
|
||
| 目标平面层 | GND→Inner1(15) | **GND→Inner2(16)**(层角色是镜像的,§25.6 ①)|
|
||
|
||
~~⛔ J31/J32 的 33 个 GND 脚故意不扇~~ —— **2026-09-05 这条整个作废**(`SKIP_HDR = False`)。
|
||
排母换成直插的 `C5307344`(§22.3f)之后,那 80 个脚**本身就是通孔**,
|
||
直接落到 Inner1/Inner2 上,根本不需要扇出 —— 而且 `pcb_fanout.one_net()` 的
|
||
`todo` 本来就带 `not p['hole']`,通孔焊盘一律不进扇出清单。
|
||
所以这里只要把 `SKIP_HDR` 关掉,`_pour_filter` 返回 None,规划器自己会跳过它们。
|
||
`HDR_BOXES` 留着当历史记录(那是**旧的贴片**焊盘区,坐标已经作废)。
|
||
"""
|
||
import importlib.util
|
||
import io
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
|
||
_spec = importlib.util.spec_from_file_location(
|
||
'pcb_fanout', os.path.join(HERE, 'pcb_fanout.py'))
|
||
F = importlib.util.module_from_spec(_spec)
|
||
_spec.loader.exec_module(F)
|
||
|
||
# ── 换掉板子相关的常量 ────────────────────────────────────────────────
|
||
F.PCB = '942ed1560d81c1e7' # PCB2-CARRIER
|
||
F.SRC = os.path.join(HERE, 'pcb2-copper.json')
|
||
F.BOARD = (0.0, 0.0, 220.0, 135.0)
|
||
F.R_CORNER = 5.0
|
||
F.ANT = (-100.0, -100.0, -99.0, -99.0) # 载板没有天线禁铺区,挪到板外
|
||
F.FANOUT_LAYERS = (F.L_TOP, F.L_BOT) # 两面都是元件面
|
||
|
||
# ⚠️ 载板的层角色和下板是**镜像的**:Inner1(15)=电源面 / Inner2(16)=完整地平面。
|
||
# 照抄下板的 `TARGET['GND'] = L_GND(15)` 会把地过孔打到电源面上。
|
||
# 下板 L_GND=15 是它的地平面,载板的地平面是 **Inner2 = 16**(= 下板的 L_PWR 常量)。
|
||
F.TARGET = {'GND': 16}
|
||
F.N_VIA = {'GND': 1}
|
||
F.TRK_W_MAX = {'GND': F.TRK_W}
|
||
F.TRK_W_NET = F.TRK_W_MAX
|
||
|
||
# ── J31/J32 的焊盘区:现在不扇(排母未定,换件后整体移位)──────────────
|
||
SKIP_HDR = False # 2026-09-05 换成直插排母,80 个脚自己就是过孔
|
||
HDR_BOXES = [(36.5, 63.5, 87.5, 71.5), # J31,实测焊盘 x 37.87~86.13 / y 65.1~69.9
|
||
(132.5, 63.5, 183.5, 71.5)] # J32,实测 x 133.87~182.13
|
||
|
||
|
||
def _pour_filter(net):
|
||
u"""借 `pcb_fanout.one_net()` 里现成的钩子来**排除**焊盘(原本是用来筛铜面覆盖的)。
|
||
|
||
返回的谓词为 True 才扇出。这里对 GND 返回「不在 J31/J32 焊盘区里」。
|
||
"""
|
||
if not SKIP_HDR or net != 'GND':
|
||
return None
|
||
|
||
def keep(x, y):
|
||
return not any(b[0] <= x <= b[2] and b[1] <= y <= b[3] for b in HDR_BOXES)
|
||
return keep
|
||
|
||
|
||
F._pour_filter = _pour_filter
|
||
|
||
|
||
# ── v5 差分布完之后加的三条硬约束(PROGRESS「接下来按顺序 · 2」)────────
|
||
# ⚠️ 前提:**这一轮不许 `--clean`**。板上那 178 条线(11 对差分 + 3 条 +3V3 车道)
|
||
# 是上一轮的成果,`clean2()` 会把信号层的线连同过孔一起删光。
|
||
# 规划器现在会把它们当障碍(`pcb_fanout.EXIST_LAYERS`,同层比线 / 过孔不分层)。
|
||
|
||
# U32(Hub)西缘 pin13 GND —— **不往西挤了,改往 QFN 肚子里打**。
|
||
#
|
||
# 上一轮留的那条「孔位钉在 (94.6, 71.4)」实测**画不出来**:
|
||
# 西边的走廊确实只剩 x ∈ [94.59, 94.625](DP5 车道东沿 94.09 + 0.5,U32 西排焊盘西沿 95.125 − 0.5),
|
||
# 但 pin13 的**引线出不去**:pin13(y 70.10~70.40) 与 pin14(70.60~70.90) 之间只有 0.20 mm,
|
||
# 0.3 的线要斜着往西北走,一定切进 pin14 的净距圈;而留在 y=70.25 平着往西,
|
||
# 过孔又躲不开 USB5_DP 的横段(y 69.75,只差 0.5 < 0.64)。
|
||
#
|
||
# 正解是往**东**:QFN 的引脚环与散热盘之间有一整圈空地
|
||
# (西排东沿 95.975 ←→ 散热盘西沿 97.600,净宽 1.625),
|
||
# 自动搜索已经把 pin13 放在 (96.50, 70.25)、pin19 放在 (97.25, 71.50),都在这一圈里。
|
||
# 那里到差分车道有 2 mm 以上,比西边的走廊宽裕得多。⇒ 不再钉死孔位。
|
||
F.FORCE_VIA = {}
|
||
|
||
# C747 / C748 的 GND 脚(都是 pin2,在各自 +3V3 脚的正北):
|
||
# 南边是留给 +3V3_HUB 的进线,过孔一律往北打。
|
||
F.ANG_RANGE = {('C747', '2'): (45, 135),
|
||
('C748', '2'): (45, 135)}
|
||
|
||
|
||
# ── J7n(尾插 BM24,五个站各 13 个 GND 脚)整批不扇 ────────────────────
|
||
# 这 65 个脚是 `pcb2_route_sig.py` 的 `bm24_pre()` **手写死的两条梳子 + pin52 单独出线**
|
||
# (§25.8:0.7 mm 间距里根本打不下 Ø0.61 的孔,只能逃逸出来再并)。
|
||
# 让自动扇出去碰它们有两个后果:
|
||
# ① 十个脚必然失败(实测 50 个),失败清单被这些噪声塞满,真正的问题看不见;
|
||
# ② pin 1 / 2 / 52 又「成功」了 —— 打出来的孔正好落在梳子和车道上,
|
||
# route_sig 还得写一个删除框把它们收回去(那个删除框就是这么来的)。
|
||
def _skip(p):
|
||
return p['net'] == 'GND' and (p['des'] or '').startswith('J7')
|
||
|
||
|
||
F.SKIP_PAD = _skip
|
||
|
||
|
||
# ── 载板专用的 clean ──────────────────────────────────────────────────
|
||
# ⚠️⚠️ **不能用 `F.clean()`**:下板那个 CLEAN_JS 删的是「layer !== 11 的所有导线」。
|
||
# 下板上除了板框没有别的线,所以没出事;载板上却有
|
||
# layer 3 丝印 50 条(5 个手机位外框 + 接口边加粗)
|
||
# layer 9 顶层装配 100 条
|
||
# layer 13 文档层 20 条
|
||
# 照抄会把这 170 条一起删掉,而且 DRC / 几何检查一条都不会报。
|
||
# 这里只删**过孔**和**信号层**(1/2/15/16)的导线。
|
||
CLEAN_JS2 = u"""
|
||
await eda.dmt_EditorControl.openDocument('__PCB__');
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
const SIG = [1, 2, 15, 16];
|
||
const vs = await eda.pcb_PrimitiveVia.getAllPrimitiveId();
|
||
if (vs.length) await eda.pcb_PrimitiveVia.delete(vs);
|
||
const ls = (await eda.pcb_PrimitiveLine.getAll()).filter(l => SIG.includes(l.layer));
|
||
if (ls.length) await eda.pcb_PrimitiveLine.delete(ls.map(l => l.primitiveId));
|
||
await eda.pcb_Document.save();
|
||
const left = {};
|
||
for (const l of await eda.pcb_PrimitiveLine.getAll()) left[l.layer] = (left[l.layer] || 0) + 1;
|
||
// ⚠ 板框是 layer 11 上的一条**多段线**(pcb_PrimitivePolyline),不是 Line ——
|
||
// 所以「剩余导线」里永远没有 11,别拿它当板框还在的证据。
|
||
const outline = (await eda.pcb_PrimitivePolyline.getAll()).filter(p => p.layer === 11).length;
|
||
return {vias: vs.length, lines: ls.length, outline,
|
||
leftVia: (await eda.pcb_PrimitiveVia.getAll()).length, leftByLayer: left};
|
||
"""
|
||
|
||
|
||
def clean2():
|
||
r = F.run(CLEAN_JS2.replace('__PCB__', F.PCB), timeout=600)
|
||
if not r.get('success'):
|
||
sys.exit(u'清理失败: ' + json.dumps(r, ensure_ascii=False)[:400])
|
||
x = r['result']
|
||
print(u'清掉:过孔 %d / 信号层导线 %d;剩余过孔 %d' % (x['vias'], x['lines'], x['leftVia']))
|
||
print(u' 剩余导线按层: %s(3=丝印 / 9=装配 / 13=文档,都必须还在);板框多段线 %d 条'
|
||
% (x['leftByLayer'], x['outline']))
|
||
for lay in ('3', '9', '13'):
|
||
if not x['leftByLayer'].get(lay):
|
||
sys.exit(u'!! layer %s 的导线被清光了,说明用错了 clean' % lay)
|
||
if x['outline'] != 1:
|
||
sys.exit(u'!! layer 11 的板框多段线有 %d 条,应为 1' % x['outline'])
|
||
|
||
|
||
def main():
|
||
if not os.path.exists(F.SRC):
|
||
sys.exit(u'先抓障碍表:/c/Python314/python .claude/dump_pads.py '
|
||
u'942ed1560d81c1e7 pcb2-copper.json')
|
||
print(u'载板扇出:板框 %.0f × %.0f,元件面 %s,GND -> layer %d'
|
||
% (F.BOARD[2], F.BOARD[3], F.FANOUT_LAYERS, F.TARGET['GND']))
|
||
if SKIP_HDR:
|
||
print(u'⛔ J31/J32 的焊盘区不扇(排母未定,换件后 80 个焊盘会整体移位)')
|
||
if '--clean' in sys.argv:
|
||
clean2()
|
||
if '--push' not in sys.argv:
|
||
return
|
||
d = json.load(io.open(F.SRC, encoding='utf-8'))
|
||
F.one_net('GND', d)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|