文件
phonerail/.claude/r2/archive_base2L/pcb_base.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 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
2026-09-10 21:02:10 +08:00

674 行
34 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 PCB(M8):板框 + 分区 + 插槽阵列 + 安装孔。
/c/Python314/python .claude/r2/pcb_base.py # 只算 + 自检
/c/Python314/python .claude/r2/pcb_base.py --outline # 画/重画板框(幂等)
/c/Python314/python .claude/r2/pcb_base.py --holes # 打安装孔
/c/Python314/python .claude/r2/pcb_base.py --verify # 读回校验
与 `pcb_blade.py` 是同一套接口(`PCB` / `BW` / `BH` / `ZONES` / `mill_cuts()` …),
下游的 `pcb_place` / `router` / `pcb_pour` 用 `--base` 切到这一份。
## 坐标系
**板左下角 = (0, 0),x 向右、y 向上**(EasyEDA PCB 里 y 向上为正,方案 §22.5)。
x = 槽距方向(5 槽并排),y = 槽长方向 / 板深。方案 §6.5 那张分区图可以直接抄。
## ⛔ 板框必须是**折线**不是导线
`pcb_PrimitiveLine` 在嘉立创里是**导线(铜)**。M4b~M6 的刀片板框就是这么画的:
渲染成品红、DRC 干净、四道几何校验全过,而客户端根本不认它是板框
(自动布线一路弹「请先绘制板框!」)。见方案 §27.3。
⇒ 板框只用 `pcb_PrimitivePolyline`。底板文档里现成那 4 条 `Line`(建工程时的默认框)
属于同一个坑,`push_outline()` 会把它们删掉。
## 刀片投影是硬碰撞,不是净距问题
插槽把刀片底边抬到底板上方 ~11 mm ⇒ **投影带内元件 ≤ 8 mm**(方案 §6.6)。
两颗 470 µF 直立电解、DC-005 座、TH 排针 / 风扇座都只能放到投影带**外面**
(`y < PROJ[0]` 或 `y > PROJ[1]`)。`zone_of()` / `height_ok()` 就是干这个的。
"""
import io
import json
import math
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
# ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本,先插 .claude 会 import 到 R1 那一份且不报错。
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import fp_edge # noqa: E402
PCB = '7b9524b6aaf6f235' # BASE-PCB(r2-uuids.json)
MIL = fp_edge.MIL
# ── 板框(mm,出处 方案 §3.1)─────────────────────────────────────────
BW, BH = 110.0, 125.0
CORNER_R = 5.0 # 底板四角 R5(刀片是 R2)
CORNER_SEG = 10
OUTLINE_W = 0.15
# ══ M9 新坐标模型(2026-09-09 用户拍板)══════════════════════════════════
# ⛔ **M8 的分带式布局(槽阵横排 + ctrl 带在北)在两层板上是死的**,
# 根因见方案 §29.1:单颗 Hub 扭出 5 路 D±,在扇出带里形成一道**横贯全板的墙**
# (`USB5`/`USB1` 共用 y=41.4 那条道,两段拼起来 x 24.5~105.1),
# 而墙以北没有横向通道(5 个槽体 + 每槽两端的屏蔽脚把槽阵横向堵死)。
# ⇒ 12V / 5V / 3V3 和 25 条槽上信号全都过不去,实测 173/314 条布不出来,
# 四轮打平(173/174/175/173),换 `BOT_PEN` 无效(178,更差)。
#
# ⇒ **槽阵转 90°**:槽体长轴沿 x,五片刀片沿 y 堆叠。于是
# **D± 走西缘、电源走东缘,走廊里全是平行车道,一个交叉都没有。**
#
# 为什么这条能成立(针位实测,见 §29.2):把槽转成 A1 端**朝东**之后,
# 每只脚落在哪一列 x 是定死的 ——
# x 71.5 +12V(5) ← 紧贴东缘,12V 铜条只进走廊 4.5 mm
# x 66.5 SCL/SDA x 64.0 +3V3(3) x 61.5 EFUSE_EN / INT#
# x 56.0 D± x 54.5 VDAC x 51.5 / 38.5 +5V
# x 39.5 PRSNT#
# **x > 69 只有 `+12V` 一个网** ⇒ 12V 的铜条可以在走廊东端做成「顶天立地」的一堵墙
# (同时够到上下两排焊盘),而不挡任何别的东西:其余全部从西边进来,最远只到 x=67。
SLOT_PITCH = 20.32 # 刀片堆叠间距(沿 y),= 手机+刀片+间隙,机械定死
SLOT_N = 5
SLOT_ROT = 0 # 2026-09-10 用户要求:五个插槽从 rot 180 再转 180°
# ⚠ 槽体长轴中心**不在板中心**:西区要塞下 ESP32 的「天线 7.5 + 焊盘 19 = 26.5」,
# 所以整条阵列往东挪 3 mm。刀片 98 沿 x ⇒ 盖住 x 9~107,两边各留 3 mm。
# ⛔ 再东移 2 mm(58→60):D± 通道要塞下**三条**并行车道。一对 1.2 mm 的线在占用图里
# 要 `1.41 + 2×0.81 = 3.03 mm`,三条 = 9.6,8 mm 的通道装不下 —— 现象是每对永远
# 只进得去一根(「DP 通、DM 出不来」,七对里六对)。刀片随之盖住 x 11~109。
# ⛔ **2026-09-09 用户拍板 A 方案**:通道从 10 加宽到 13.5,槽阵再东移 3.5(60→63.5)。
# 账:一对 1.0 mm 的 D± 在占用图里要 `1.21 + 2×0.71 = 2.63`;车道还要能被一根
# 0.25 的逃逸线横穿(吃掉 1.67)而仍装得下一对 ⇒ 车道 4.5,三条 = 13.5。
# 代价:刀片(98 宽)盖住 x 14.5~112.5,**伸出东板边 2.5 mm**(用户已确认)。
SLOT_CX = 46.5 # 插槽翻转后整体西移,东侧留 13.5 mm D± 通道
# ⚠ 阵列中心比板中心(62.5)**往北挪 1 mm**:南带要塞下 DC-005 的 14.5 mm 桶身,外加 0.6 净距,
# 17.26 差 1 mm。北带少 1 mm 不影响(风扇块全是小件)。
# M13:整组北移 3 mm,为最南槽 USB5 与 9 A 输入铜之间留出独立顶层走廊。
# 槽距不变,并保留北侧辅助 Buck / 风扇服务带。
ARRAY_CY = 66.5
SLOT_BODY_L = 42.0 # 槽体沿 **x**(实测 `PCIE-64P11L` 焊盘外框)
SLOT_BODY_W = 9.2 # 槽体沿 **y**
# 两排焊盘:y = 槽心 ± 3.6;单盘 0.7(x) × 2.0(y),脚距 1.0
PAD_ROW_DY = 3.6
PAD_L, PAD_W = 0.7, 2.0
# 长轴上的针位(u = 距槽体中心,正 = 西)。A1 端朝东 ⇒ 板上 x = SLOT_CX − u。
PIN_U = {'+12V': (-16.5, -14.5), 'I2C': (-12.5, -11.5), '+3V3': (-9.5, -7.5),
'EN': (-6.5, -6.5), 'DIFF': (-1.5, -0.5), 'VDAC': (0.5, 0.5),
'+5V': (3.5, 16.5), 'PRSNT': (15.5, 15.5)}
def pin_x(u):
u"""长轴坐标 u(封装)→ 板上 x。rot 0 与 rot 180 互为镜像。"""
return round(SLOT_CX + u if SLOT_ROT == 0 else SLOT_CX - u, 3)
def slot_y(i):
u"""槽 i(1 = 最南)的中心 y。"""
return round(ARRAY_CY + (i - (SLOT_N + 1) / 2.0) * SLOT_PITCH, 3)
def slot_ys():
return [slot_y(i) for i in range(1, SLOT_N + 1)]
def slot_body(i):
y = slot_y(i)
return (SLOT_CX - SLOT_BODY_L / 2, y - SLOT_BODY_W / 2,
SLOT_CX + SLOT_BODY_L / 2, y + SLOT_BODY_W / 2)
# ── 走廊:槽体之间那 11.12 mm,外加阵列两头 ────────────────────────────────
# 走廊 k(0 = 阵列最南那条,5 = 最北)。内部四条各服务上下两个槽的一排焊盘。
def corridor(k):
y0 = 0.0 if k == 0 else slot_y(k) + SLOT_BODY_W / 2
y1 = BH if k == SLOT_N else slot_y(k + 1) - SLOT_BODY_W / 2
return (0.0, round(y0, 3), BW, round(y1, 3))
def corridors():
return [corridor(k) for k in range(SLOT_N + 1)]
# ── 三条纵向分区(沿 x)───────────────────────────────────────────────────
# 西区:12V 干道;中区:5 个槽体;东区:D± 通道 + Hub / 控制器。
WEST = (0.0, 25.5) # 西侧 12V 服务区
MID = (SLOT_CX - SLOT_BODY_L / 2, SLOT_CX + SLOT_BODY_L / 2) # 34 ~ 76
EAST = (81.0, BW) # 东侧 Hub / ESP32 / mux / DAC 功能区
# 逻辑 USB 号 -> 插槽位号 / 物理槽位(南起 1..5)。
# 插槽翻转后物理顺序与 USB 号反向;混用会让五对 D± 全交叉。
USB_SLOT_REF = {1: 'J34', 2: 'J35', 3: 'J36', 4: 'J37', 5: 'J38'}
USB_SLOT_POS = {i: SLOT_N + 1 - i for i in range(1, SLOT_N + 1)}
def usb_corridor(i):
u"""USB i 的 D± 所在插槽南侧走廊。"""
return corridor(USB_SLOT_POS[i] - 1)
# ── 刀片投影:每片刀片盖住 x 6~104,沿 y 占 ±7.35 ──────────────────────────
# 保守起见整条阵列带按投影处理(限高 8 mm,方案 §6.6)。
BLADE_W = 98.0 # 刀片沿 x
BLADE_X0 = round(SLOT_CX - BLADE_W / 2, 2) # 6.0
PROJ = (round(slot_y(1) - SLOT_BODY_W / 2, 2),
round(slot_y(SLOT_N) + SLOT_BODY_W / 2, 2)) # 17.26 ~ 107.74
H_PROJ = 8.0
def blade_x_to_base_x(bx):
u"""刀片自身坐标 x → 底板 x(刀片 98 沿 x 铺开,居中对齐槽体)。"""
return round(BLADE_X0 + bx, 3)
# ── 分区(y 方向只剩三条:南带 / 阵列带 / 北带)────────────────────────────
ZONES = {
'input': (0.0, 0.0, BW, PROJ[0]), # 投影外,可放高件:DC 座 / F1 / Q1 / Φ8 电解
'array': (0.0, PROJ[0], BW, PROJ[1]), # 槽体 + 走廊 + 西区器件 + 东区高速路
'fan': (0.0, PROJ[1], BW, BH), # 投影外:风扇座 ×2 / 驱动 / J33 应急口
}
# ── 12V(9 A):南带进料 → 西侧高速路 → 每条走廊分段进槽 ────────────────
# M12 当前布局把 U35/U6/U7 放在 x=10~15;主干只能使用它们与槽体之间
# x=15.5~24.5 的连续通道。实测无异网焊盘侵入,9 mm 净宽仍高于 8 mm 要求。
# ⛔ 北端只到 y=110:再往北是风扇驱动 `Q6`/`Q7`(它们本来就该贴着风扇座),
# 高速路铺过去会被它们的非 12V 脚掐到 **5.70 mm**(断面校核实测)。
# 那一段只带风扇 + 两颗 Buck(~3 A,5.7 mm 够),但校核按 9 A 一刀切 ⇒
# 与其放宽判据,不如让干道在 110 收口,北边的 12V 由走廊 5 那堵铜墙供。
# ⛔ 铜墙必须**伸进上下两个槽体 2.0 mm**:焊盘中心距槽心 3.6、盘高 2.0,也就是占了槽体边缘往里 0~2.0 那一段。
# 只画走廊本身的话**一只 12V 脚都碰不到**,而铺铜管理器 / DRC 都不会响
# (只有 `board_check` 从板上读回铜做并查集才看得见)。
RIB_OVER = 1.4
# 旧 BUS_S 与定稿后的 J1/F1/Q1 输入链重叠,会把 VIN_RAW 直接短到 +12V。
# 干道改从投影带南沿开始,Rs6 -> 干道由路由器单独连接。
# 南端避开 y=16.25 的 2 mm VIN_SW 引线;向东加宽后,扣除其净距仍超过 8 mm。
# 东界距 x=27、宽 3 mm 的槽外壳 GND 焊盘保留 0.25 mm 净距。
# RIB_X 保持从 x=24.5 起,与主干重叠 0.75 mm,并继续避开 x=33 的 GND 针。
BUS_W = (15.5, 18.75, 25.25, BH - 0.5)
RIB_X = (24.5, 32.4)
def rib_rects():
u"""五个插槽的 12V 分段铜墙。
下排是 x=30/31/32,上排是 x=31/32;上排 x=30 紧邻 GND,槽心 x=27
还有巨大的外壳 GND 盘。所以不能再用「整条走廊顶天立地」的矩形。
每排用窄指覆盖电源脚,再在排外用宽帽接回西侧干道。
"""
out = []
c = 0.25
row_o = PAD_ROW_DY + PAD_W / 2
row_i = PAD_ROW_DY - PAD_W / 2
x_lower = round(pin_x(-16.5) - PAD_L / 2 - 0.05, 3) # x=30 焊盘左侧
x_upper = round(pin_x(-15.5) - PAD_L / 2 - 0.05, 3) # x=31 焊盘左侧
for i in range(1, SLOT_N + 1):
cy = slot_y(i)
cap_lo = round(cy - row_o - RIB_OVER, 3)
cap_hi = round(cy - row_o - c, 3)
# 最南宽帽直接接到主干,并停在 J38 外壳 GND 的净距边界以内。
if i == 1:
cap_lo = BUS_W[1]
top_hi = round(cy + row_o + RIB_OVER, 3)
# 北移后的 J34 靠近 C734;北帽只需覆盖针排并接到主干,不再向北多伸。
if i == SLOT_N:
top_hi = round(cy + row_o + 0.9, 3)
out += [
(RIB_X[0], cap_lo, RIB_X[1], cap_hi),
(x_lower, round(cy - row_o - c - 0.05, 3), RIB_X[1],
round(cy - row_i + c + 0.05, 3)),
(x_upper, round(cy + row_i - c - 0.05, 3), RIB_X[1],
round(cy + row_o + c + 0.05, 3)),
(RIB_X[0], round(cy + row_o + c, 3), RIB_X[1], top_hi),
]
return out
def bus_rects():
return [BUS_W] + rib_rects()
# ── +3V3 铜墙(M9b,2026-09-10 用户拍板)───────────────────────────────────
# `+3V3` 是全板最大的网(47 只脚),A* 当一棵树布 ⇒ **整网崩**(M9 实测 46/47 没布通)。
# 办法与 12V 同:把脚位那一段直接做成铜,脚从 A* 里摘掉。
#
# ⛔ 但 3V3 的墙**不能像 12V 那样顶天立地**,因为它不在东缘尽头,而在信号中间:
# 南排 … EN(70) 3V3(71) 3V3(72) NC(73) NC(74) NC(75) NC(76) GND(77) 12V(78,79) …
# 北排 … INT#(70) NC(71) NC(72) 3V3(73) GND(74) SDA(75) SCL(76) GND(77) 12V(78~80)
# 每个槽的 `SDA_i`/`SCL_i` 在**北排 x 75/76**,要一路往西去 `U35`(x≈10~16) ——
# 任何一条从 x>73 一直连到 x<70 的**顶层**南北向铜,都会把这十根线拦腰截断。
# ⇒ 墙只在**槽体那一段**(顶层,够着三只脚),走廊那一段**走底层桥**(0.8 mm + 两个过孔)。
# 代价是地平面上多五道 9.5 mm 长的窄缝(都在 x≈72,D± 全在 x<66,不碰 §6.2 第 1 条)。
#
# ⛔ 墙的 x 是**逐排不同的**(同一个矩形盖不住两排):
# 南排的 3V3 在 71/72、北排的在 73,而它们各自旁边就是**没有网的 NC 针**
# (南排 73、北排 71/72)。一个 70.6~73.4 的方块会同时压上 NC 针 ——
# 铺铜会绕开它们、把墙在排上切碎,而铺铜管理器和 DRC 都只说「有铜」。
# ⇒ 拆成「南窄 / 中宽 / 北窄」三段同网矩形(同网重叠是允许的,12V 那几块也是这么接的)。
W3V3_CLR = 0.25 # 到异网针的净距(规则底线 0.20,留 0.05)
W3V3_OVER = 1.4 # 墙伸进走廊多少:给底层桥的过孔留地方
W3V3_BRIDGE_W = 0.8 # 走廊底层桥的线宽(3V3 全板 ~0.3 A,0.8 够 2 A)
W3V3_VIA_DY = 5.4 # 过孔离槽心多远(针排外沿 4.6 + 0.55 环 + 余量)
def wall3v3_rects():
u"""五块 `+3V3` 槽墙,每块三段(南窄 / 中宽 / 北窄)+ 两块走廊帽。
x 的来历(全部从 `PIN_U` 算,不手抄):
西边界 = `EN` 针右沿 + 净距;东边界 = `GND(74)` 针左沿 − 净距;
南段东界 = 南排 NC(73) 针左沿 − 净距;北段西界 = 北排 NC(72) 针右沿 − 净距。
"""
c = W3V3_CLR
# 新方向是旧铜墙相对槽心的水平镜像。
x_w = round(SLOT_CX - 9.9, 3)
x_e = round(SLOT_CX - 7.1, 3)
x_s_e = round(SLOT_CX - 9.1, 3)
x_n_w = round(SLOT_CX - 8.9, 3)
row_o = PAD_ROW_DY + PAD_W / 2 # 针排外沿 4.6
row_i = PAD_ROW_DY - PAD_W / 2 # 针排内沿 2.6
out = []
for i in range(1, SLOT_N + 1):
cy = slot_y(i)
out += [
# 走廊帽(排外,没有任何针):整宽,给桥的过孔一个圆整的落点
(x_w, round(cy - row_o - W3V3_OVER, 3), x_e, round(cy - row_o - c, 3)),
# 南段:从走廊穿过南排(盖住 71/72)进到两排之间
(x_w, round(cy - row_o - c - 0.05, 3), x_s_e, round(cy - row_i + c + 0.05, 3)),
# 中段:两排之间那 5.2 mm,整宽
(x_w, round(cy - row_i + c, 3), x_e, round(cy + row_i - c, 3)),
# 北段:穿过北排(盖住 73)
(x_n_w, round(cy + row_i - c - 0.05, 3), x_e, round(cy + row_o + c + 0.05, 3)),
(x_w, round(cy + row_o + c, 3), x_e, round(cy + row_o + W3V3_OVER, 3)),
]
return out
def wall3v3_bridges():
u"""走廊里的**底层**桥:[(x, y0, y1, 宽)],以及两端的过孔 [(x, y)]。
只连相邻两块槽墙(4 段)。北端到 `U19`/`L2` 那一段交给 A*:
它是一条普通的短网,不像槽脚那样被十根 I²C 挡着。
"""
x = round(SLOT_CX - 8.5, 3) # 38.0,新方向的 3V3 墙中心
segs, vias = [], []
for i in range(1, SLOT_N):
y0 = round(slot_y(i) + W3V3_VIA_DY, 3)
y1 = round(slot_y(i + 1) - W3V3_VIA_DY, 3)
segs.append((x, y0, y1, W3V3_BRIDGE_W))
vias += [(x, y0), (x, y1)]
return segs, vias
# ── D± 竖向扇出通道(西区):`U32` 在西区中部,五对沿 y 散开再往东进走廊 ────
# ⛔ 这条带里**一件都不许放**(同 M8 的 FANOUT,理由一样:排布器会把去耦塞满,
# 然后布线器报「某一对布了 0 段」,而摆位自检 / DRC / 几何验证一条都不响)。
DP_CHAN = (67.5, PROJ[0], 81.0, PROJ[1])
# Hub USB6 -> ESP32 原生 USB 的北向短通道。U20/U32 是端点,其他元件不得进入。
USB6_CHAN = (80.5, 78.0, 84.8, 107.2)
# ⛔ 阵列**北边**还要留一条 4 mm 的过道:最北那个槽(槽 5)的 D± 焊盘在它的北排,
# 北侧走廊就是风扇带本身。不留的话逃逸落点会掉进风扇块里 ——
# 实测 `J38.launch` 起步格点 **0**,现象是「差分对 USB5 布了 0 段」。
DP_N_LANE = (MID[0], PROJ[1], BW, PROJ[1] + 4.0)
# ── 走廊「喉口」:西区器件与 D± 通道之间那一段,**一件都不许放** ────────────
# ⛔ 2026-09-09 用户看出来的:`R605~R608` / `C9` / `C731` 那一窝落在走廊 2 的
# y 段、x 20~28 上 —— 那正是西区**所有**去往槽 2 的信号(含它那一对 D±)
# 往东挤进通道的唯一喉口。堵在这儿的后果是布线器报「出不来 / 没布通」,
# 而摆位自检、DRC、几何校验一条都不会响。
# ⇒ 每条内走廊在 x 17~29 这一段设成摆位禁区(器件退到槽体那几段 y 上去)。
# ⚠ 宽度是权衡出来的:17~29(12 mm)会把西区挤爆(去耦平均从 22.9 涨到 33.3 mm),
# 而喉口只要够信号穿过去就行 —— 6 mm 在 0.5 间距下可过十根线,够一条走廊的量。
THROAT_X = (DP_CHAN[2], 87.0)
# `U32` 东沿到 D± 通道之间那条**逃逸扇形带**:十根 0.15 的细线从 QFN 脚扇到车道,
# 一件都不许放(放了就是 DRC「焊盘到导线」,实测 22 条压在 C602/C607 上)。
HUB_ESC = (DP_CHAN[2], 55.0, 86.0, 78.0)
def throat_rects():
out = []
for k in range(1, SLOT_N):
c = corridor(k)
out.append((THROAT_X[0], c[1], THROAT_X[1], c[3]))
return out
def keepout_rects():
u"""摆位禁区(不是铺铜禁区):12V 高速路 + 铜墙 + 3V3 槽墙 + D± 竖向通道 + 天线缺口。"""
return (bus_rects() + wall3v3_rects()
+ [DP_CHAN, USB6_CHAN, DP_N_LANE, ANT_NOTCH, HUB_ESC] + throat_rects())
# ── ESP32 天线缺口(西板边)────────────────────────────────────────────────
# 见 §28.14:`U20` 转 rot=90 天线朝西、天线那 7.5 mm 下方板材整块挖掉。
# 新布局里 `U20` 挪到西北(靠近风扇带的 FAN_*/NTC 和 J33),缺口跟着走。
ANT_NOTCH = (BW - 7.5, 86.0, BW, 108.0)
def notch_rect():
return ANT_NOTCH
# ── 安装孔(M3 尼龙立柱,NPTH)────────────────────────────────────────
HOLE_D = 3.2
HOLE_KEEP = 3.2
HOLE_INSET = 4.0
MOUNT_HOLES = {
'H1': (HOLE_INSET, HOLE_INSET),
'H2': (BW - HOLE_INSET, HOLE_INSET),
'H3': (HOLE_INSET, BH - HOLE_INSET),
'H4': (BW - HOLE_INSET, BH - HOLE_INSET),
}
# ── 下游脚本要的空壳 ─────────────────────────────────────────────────────
FLEX_W = {}
SLOT_Y = None
PHONE_ORG = None
PHONE_WH = None
def phone_holes():
u"""两层都不许压的孔:四个 M3 安装孔(它们是 `pcb_PrimitivePad`,不在 pads.json 里)。"""
return dict(MOUNT_HOLES)
def mill_cuts():
return []
def notch_break():
return None
def corner(cx, cy, a0, a1):
pts = []
for k in range(CORNER_SEG + 1):
a = math.radians(a0 + (a1 - a0) * k / CORNER_SEG)
pts.append((cx + CORNER_R * math.cos(a), cy + CORNER_R * math.sin(a)))
return pts
def outline_pts():
u"""板框点列(逆时针,从底边左端 (r,0) 起,闭合回 (r,0))。东边带天线缺口。"""
r = CORNER_R
pts = [(r, 0.0), (BW - r, 0.0)]
pts += corner(BW - r, r, 270, 360)
pts += [(BW, ANT_NOTCH[1]), (ANT_NOTCH[0], ANT_NOTCH[1]),
(ANT_NOTCH[0], ANT_NOTCH[3]), (BW, ANT_NOTCH[3]),
(BW, BH - r)]
pts += corner(BW - r, BH - r, 0, 90)
pts += [(r, BH)]
pts += corner(r, BH - r, 90, 180)
pts += [(0.0, r)]
pts += corner(r, r, 180, 270)
out = [pts[0]]
for q in pts[1:]:
if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9:
out.append(q)
assert abs(out[0][0] - out[-1][0]) < 1e-9 and abs(out[0][1] - out[-1][1]) < 1e-9, u'板框点列没有闭合'
return out
def outline_polygon():
return outline_pts()
def zone_of(y):
for k, z in ZONES.items():
if z[1] <= y < z[3]:
return k
return None
def height_ok(y, h, x=None):
u"""该位置允许 h mm 高的件吗(刀片投影是二维硬碰撞)。"""
in_y = PROJ[0] <= y <= PROJ[1]
in_x = x is None or BLADE_X0 <= x <= BLADE_X0 + BLADE_W
return not (in_x and in_y and h > H_PROJ)
def selfcheck():
bad = []
ys = slot_ys()
# 阵列必须居中、间距是机械定死的 20.32
for a, b in zip(ys, ys[1:]):
if abs((b - a) - SLOT_PITCH) > 1e-6:
bad.append(u'槽间距 %.3f != %.2f' % (b - a, SLOT_PITCH))
if abs((ys[0] + ys[-1]) / 2.0 - ARRAY_CY) > 1e-6:
bad.append(u'阵列没居中: %s' % ys)
# 槽体不许出板边(南北两头)
if ys[0] - SLOT_BODY_W / 2 < 2.0 or ys[-1] + SLOT_BODY_W / 2 > BH - 2.0:
bad.append(u'槽体贴板边太近: %.2f / %.2f'
% (ys[0] - SLOT_BODY_W / 2, ys[-1] + SLOT_BODY_W / 2))
# 槽体长轴不许出板边(东西两头),且要给东区高速路让出地方
if SLOT_CX - SLOT_BODY_L / 2 < WEST[1] - 1e-6 or SLOT_CX + SLOT_BODY_L / 2 > EAST[0] + 1e-6:
bad.append(u'槽体 x %.1f~%.1f 越过了西/东区边界 %.1f / %.1f'
% (SLOT_CX - SLOT_BODY_L / 2, SLOT_CX + SLOT_BODY_L / 2, WEST[1], EAST[0]))
# 走廊必须首尾相接、铺满板
cs = corridors()
for a, b in zip(cs, cs[1:]):
if a[3] > b[1] + 1e-6:
bad.append(u'走廊重叠: %s / %s' % (a, b))
if abs(cs[0][1]) > 1e-6 or abs(cs[-1][3] - BH) > 1e-6:
bad.append(u'走廊没铺满 0~%.1f' % BH)
for k in range(1, SLOT_N):
w = cs[k][3] - cs[k][1]
if abs(w - (SLOT_PITCH - SLOT_BODY_W)) > 1e-6:
bad.append(u'走廊 %d 净宽 %.2f != %.2f' % (k, w, SLOT_PITCH - SLOT_BODY_W))
# 分区必须首尾相接
ks = sorted(ZONES, key=lambda k: ZONES[k][1])
for a, b in zip(ks, ks[1:]):
if abs(ZONES[a][3] - ZONES[b][1]) > 1e-6:
bad.append(u'分区 %s 顶 %.1f != %s 底 %.1f' % (a, ZONES[a][3], b, ZONES[b][1]))
if abs(ZONES[ks[0]][1]) > 1e-6 or abs(ZONES[ks[-1]][3] - BH) > 1e-6:
bad.append(u'分区没铺满 0~%.1f' % BH)
if abs(PROJ[0] - ZONES['input'][3]) > 1e-6 or abs(PROJ[1] - ZONES['fan'][1]) > 1e-6:
bad.append(u'刀片投影 %s 与 input/fan 带边界对不上' % (PROJ,))
# ⛔ 这条是新布局能成立的**判据本身**:x > RIB_X[0] 只许有 +12V。
# 别的网只要有一只脚落到铜墙的 x 区间里,那堵墙就不能顶天立地了。
for tag, (u0, u1) in PIN_U.items():
if tag == '+12V':
continue
for u in (u0, u1):
if pin_x(u) < RIB_X[1] + 0.5:
bad.append(u'%s 的针落在 x=%.1f,侵入 12V 铜墙区(x<%.1f 只许有 +12V)'
% (tag, pin_x(u), RIB_X[1]))
# 12V 铜墙要够得着 12V 针
if not (RIB_X[0] < pin_x(PIN_U['+12V'][0]) < RIB_X[1]):
bad.append(u'12V 铜墙 x %.1f~%.1f 够不着 12V 针 x=%.1f'
% (RIB_X[0], RIB_X[1], pin_x(PIN_U['+12V'][0])))
# 东缘高速路净宽(1 oz 外层 9 A ΔT20K 要 ≥8 mm)
if BUS_W[2] - BUS_W[0] < 8.0:
bad.append(u'西缘 12V 高速路只有 %.1f mm,9 A 要 ≥8' % (BUS_W[2] - BUS_W[0]))
if [USB_SLOT_REF[i] for i in range(1, SLOT_N + 1)] != ['J34', 'J35', 'J36', 'J37', 'J38']:
bad.append(u'USB -> 插槽位号映射被改坏')
for i in range(1, SLOT_N + 1):
c = usb_corridor(i)
if c[0] >= c[2] or c[1] >= c[3]:
bad.append(u'USB%d 的南侧走廊无效: %s' % (i, c))
# 安装孔:不许出板 / 压槽体 / 被圆角切 / 落进天线缺口
for n, (hx, hy) in MOUNT_HOLES.items():
if not (HOLE_KEEP < hx < BW - HOLE_KEEP and HOLE_KEEP < hy < BH - HOLE_KEEP):
bad.append(u'安装孔 %s 出板或贴边: (%.1f, %.1f)' % (n, hx, hy))
for i in range(1, SLOT_N + 1):
b0, b1, b2, b3 = slot_body(i)
if b0 - HOLE_KEEP < hx < b2 + HOLE_KEEP and b1 - HOLE_KEEP < hy < b3 + HOLE_KEEP:
bad.append(u'安装孔 %s 压到槽 %d' % (n, i))
for cx, cy in ((CORNER_R, CORNER_R), (BW - CORNER_R, CORNER_R),
(CORNER_R, BH - CORNER_R), (BW - CORNER_R, BH - CORNER_R)):
out_x = hx < cx if cx < BW / 2 else hx > cx
out_y = hy < cy if cy < BH / 2 else hy > cy
if out_x and out_y and math.hypot(hx - cx, hy - cy) > CORNER_R - HOLE_KEEP:
bad.append(u'安装孔 %s 被 R%.0f 圆角切到' % (n, CORNER_R))
if (ANT_NOTCH[0] - HOLE_KEEP < hx < ANT_NOTCH[2] + HOLE_KEEP and
ANT_NOTCH[1] - HOLE_KEEP < hy < ANT_NOTCH[3] + HOLE_KEEP):
bad.append(u'安装孔 %s 落进 ESP32 天线缺口 (%.1f, %.1f)' % (n, hx, hy))
sep = chr(10) + ' '
assert not bad, u'底板自检不过:' + sep + sep.join(bad)
return True
# ── 下发 ──────────────────────────────────────────────────────────────
def call(js, timeout=180):
r = run(js, timeout=timeout)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800])
return r['result']
def mil(v):
return round(v * MIL, 4)
def polygon_src(pts):
a = [mil(pts[0][0]), mil(pts[0][1])]
for q in pts[1:]:
a += ['L', mil(q[0]), mil(q[1])]
if pts[0] != pts[-1]:
a += ['L', mil(pts[0][0]), mil(pts[0][1])]
return a
def push_outline():
u"""板框 = 一条闭合折线;顺带删掉建工程时留下的 4 条层 11 **导线**(§27.3 的坑)。"""
src = polygon_src(outline_polygon())
js = ("await eda.dmt_EditorControl.openDocument('%s');" % PCB +
"await new Promise(r=>setTimeout(r,2500));" +
"const oldL = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11)"
".map(l=>l.primitiveId);" +
"if (oldL.length) await eda.pcb_PrimitiveLine.delete(oldL);" +
"const oldP = (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11).map(p=>p.getState_PrimitiveId());" +
"if (oldP.length) await eda.pcb_PrimitivePolyline.delete(oldP);" +
"const poly = await eda.pcb_MathPolygon.createPolygon(%s);" % json.dumps(src) +
"const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, %s);" % mil(OUTLINE_W) +
"await eda.pcb_Document.save();" +
"return {delLine: oldL.length, delPoly: oldP.length, made: !!r,"
" now: (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11).length,"
" line11: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11).length};")
r = call(js, timeout=300)
print(u'板框折线: %s(删旧导线 %d / 旧折线 %d);层 11 现有折线 %d / 导线 %d'
% (u'建好' if r['made'] else u'失败', r['delLine'], r['delPoly'], r['now'], r['line11']))
if not r['made'] or r['now'] != 1:
raise SystemExit(u'!! 板框没画成')
return r
PUSH_HOLES_JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,2500));
const H = %s, D = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const num = p.getState_PadNumber();
if (typeof num === 'string' && /^H[1-9]$/.test(num))
await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (const [name, x, y] of H) {
// layer 12 = 多层;末位 metallization=false ⇒ NPTH。参数位序照抄 pcb_blade.push_holes
await eda.pcb_PrimitivePad.create(12, name, x, y, 0, ['ELLIPSE', D, D], '',
['ROUND', D], 0, 0, 0, false);
k++;
}
await eda.pcb_Document.save();
return {made: k, now: (await eda.pcb_PrimitivePad.getAll())
.filter(p => /^H[1-9]$/.test(String(p.getState_PadNumber()))).length};
"""
def push_holes():
arr = [[k, mil(v[0]), mil(v[1])] for k, v in sorted(MOUNT_HOLES.items())]
r = call(PUSH_HOLES_JS % (PCB, json.dumps(arr), mil(HOLE_D)), timeout=300)
print(u'安装孔: 下发 %d / 读回 %d,NPTH Φ%.1f(M3 尼龙立柱)' % (r['made'], r['now'], HOLE_D))
for k, v in sorted(MOUNT_HOLES.items()):
print(u' %-3s (%6.2f, %6.2f)' % (k, v[0], v[1]))
return r
def verify():
r = call("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2000));"
"const ps = (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11);"
"const line11 = (await eda.pcb_PrimitiveLine.getAll()||[])"
".filter(l=>l.layer===11).length;"
"const holes = (await eda.pcb_PrimitivePad.getAll())"
".filter(p=>/^H[1-9]$/.test(String(p.getState_PadNumber())))"
".map(p=>[String(p.getState_PadNumber()), p.getState_X(), p.getState_Y()]);"
"return {n: ps.length, poly: ps.length ? ps[0].getState_Polygon() : null,"
" line11, holes, comps: (await eda.pcb_PrimitiveComponent.getAll()).length};" % PCB,
timeout=180)
bad = []
if r['n'] != 1:
bad.append(u'层 11 上有 %d 条折线(应为 1)' % r['n'])
if r['line11']:
bad.append(u'层 11 上还有 %d 条**导线**(板框画成铜的老坑,§27.3)' % r['line11'])
pts = []
src = r['poly']
if isinstance(src, dict):
src = src.get('pointArr') or src.get('points') or src.get('polygon') or src
if isinstance(src, list):
i = 0
while i < len(src):
if src[i] in ('L', 'l'):
i += 1
continue
if isinstance(src[i], (int, float)) and i + 1 < len(src) and isinstance(src[i + 1], (int, float)):
pts.append((src[i] / MIL, src[i + 1] / MIL))
i += 2
else:
i += 1
if pts:
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
if abs(min(xs)) > 0.01 or abs(max(xs) - BW) > 0.01 or \
abs(min(ys)) > 0.01 or abs(max(ys) - BH) > 0.01:
bad.append(u'外接矩形 %.2f~%.2f x %.2f~%.2f != 0~%.0f x 0~%.0f'
% (min(xs), max(xs), min(ys), max(ys), BW, BH))
if abs(pts[0][0] - pts[-1][0]) > 0.01 or abs(pts[0][1] - pts[-1][1]) > 0.01:
bad.append(u'折线首尾不重合: %s vs %s' % (pts[0], pts[-1]))
if abs(len(pts) - len(outline_polygon())) > 1:
bad.append(u'点数 %d != %d' % (len(pts), len(outline_polygon())))
print(u'读回板框折线 1 条 / %d 点,外接 %.2f~%.2f x %.2f~%.2f;层 11 遗留导线 %d'
% (len(pts), min(xs), max(xs), min(ys), max(ys), r['line11']))
else:
bad.append(u'读不回折线点列(原始值 %s)' % str(r['poly'])[:120])
got = {h[0]: (round(h[1] / MIL, 3), round(h[2] / MIL, 3)) for h in r['holes']}
for n, (hx, hy) in sorted(MOUNT_HOLES.items()):
if n not in got:
bad.append(u'安装孔 %s 没打' % n)
elif abs(got[n][0] - hx) > 0.02 or abs(got[n][1] - hy) > 0.02:
bad.append(u'安装孔 %s 位置 %s != (%.1f, %.1f)' % (n, got[n], hx, hy))
print(u'安装孔读回 %d 个;板上元件 %d 件' % (len(got), r['comps']))
if bad:
print(u'[X] 校验不过:')
for b in bad:
print(u' ' + b)
else:
print(u'[OK] 板框是一条闭合折线、外接矩形对上、安装孔齐')
return bad
if __name__ == '__main__':
selfcheck()
print(u'底板 %.0f x %.0f,四角 R%.0f;PCB=%s' % (BW, BH, CORNER_R, PCB))
print(u'槽阵 5 槽 @%.2f(沿 y 堆叠,长轴沿 x,rot %d): %s'
% (SLOT_PITCH, SLOT_ROT, u' / '.join('%.2f' % y for y in slot_ys())))
print(u'槽体 x %.1f~%.1f;刀片投影 y %.1f~%.1f(带内元件 ≤ %.0f mm)'
% (MID[0], MID[1], PROJ[0], PROJ[1], H_PROJ))
print(u'西区 x %.0f~%.0f | D± 通道 %.0f~%.0f | 中区 %.0f~%.0f | 东区 %.0f~%.0f'
% (WEST[0], WEST[1], DP_CHAN[0], DP_CHAN[2], MID[0], MID[1], EAST[0], EAST[1]))
print(u'12V:Rs6 单独接入 → 西缘高速路 %s → %d 堵铜墙'
% (BUS_W, len(rib_rects())))
if '--outline' in sys.argv:
push_outline()
verify()
elif '--holes' in sys.argv:
push_holes()
verify()
elif '--verify' in sys.argv:
verify()