- 规则:铜层 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
1527 行
82 KiB
Python
1527 行
82 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""下板 PCB1 的铺铜 —— **事实来源**,改铜面改这里。
|
||
|
||
/c/Python314/python .claude/pcb1_pour.py # 打印计划 + 全部校核 + ASCII 图
|
||
/c/Python314/python .claude/pcb1_pour.py --map # 只打 ASCII 图
|
||
/c/Python314/python .claude/pcb1_pour.py --push # 校核 -> 清旧 -> 建边框 -> 重建 -> 存盘 -> 回读复核
|
||
/c/Python314/python .claude/pcb1_pour.py --verify # 只回读实测铺出来的铜(不改板)
|
||
/c/Python314/python .claude/pcb1_pour.py --verify --cache # 同上,但用上次回读的缓存
|
||
|
||
⚠️ **先扇出再铺铜,顺序不能反**(PROGRESS 第十五轮那一条):`+3V3` / `VOn` 在板上
|
||
一个通孔焊盘都没有,没扇出之前那几块区域里没有本网焊盘 —— 铺铜的孤岛判定
|
||
是**按铺铜对象**算的,会把整块删光(边框还在、nPoured 是 0)。
|
||
|
||
## 叠层(§10.1,层角色和载板是镜像的)
|
||
|
||
| 设计文档 | EasyEDA 层 | id | 下板上干什么 |
|
||
|---|---|---|---|
|
||
| L1 | Top Layer | 1 | 功率器件 + 全部 338 个元件 + **8 A 输入链的宽铜** |
|
||
| L2 | Inner1 | 15 | **完整地平面,不许割断**(只挖 ESP32 天线区)|
|
||
| L3 | Inner2 | 16 | **分割电源面**:+12V(主)+ +3V3(南)+ GND 底填 |
|
||
| L4 | Bottom | 2 | 信号(板上元件全在顶层,这一层基本是空的)+ `+5V` 干线 + `VOn_P` 小岛 |
|
||
|
||
## ⭐ Inner2 到底分几块 —— 算出来的结果,不是上一轮草案里的「八块」
|
||
|
||
上一轮 PROGRESS 写的是「+12V / +5V / +3V3 / VO1~VO5 八块」。**逐条算下来只剩两块半。**
|
||
判据是 IPC-2221 **内层 0.5 oz**:`A=(I/(0.024·ΔT^0.44))^(1/0.725)` mil²,W = A / 0.685 mil,
|
||
ΔT = 20 °C:
|
||
|
||
8 A → 18.1 mm 6.4 A → 13.4 mm 4 A → 6.99 mm
|
||
2.8 A → 4.27 mm 2.0 A → 2.69 mm 1.6 A → 1.97 mm 0.5 A → 0.40 mm
|
||
|
||
**外层 1 oz 的同一条式子常数是 0.048(不是 0.024)、铜厚是 1.37 mil(不是 0.685)**,
|
||
所以外层每毫米能带的电流大约是内层的 **4 倍**:2.8 A 在外层只要 **0.45 mm**。
|
||
⇒ 「这一路要不要占内层」的门槛,比直觉高得多。逐个网络的结论:
|
||
|
||
| 网络 | 实际要跑多远 / 多大电流 | 结论 |
|
||
|---|---|---|
|
||
| `+12V` | 源在北左 Rs6.2 (9.6, 91),负载散布南北五路 + 五个风扇;南边要 5.6 A | ✅ **Inner2 主平面** |
|
||
| `+3V3` | 源 L2 (25.5, 104),64 个焊盘遍布全板,但**只有 0.5 A** | ✅ **Inner2 南部两块**(要的不是载流,是「64 个焊盘各打一个孔就完事」)|
|
||
| `+5V` | 源 L3 (73.3, 78),去处**全是排针上的 10 个通孔针**,最坏 2.8 A | ❌ 走**底层 1.2 mm 干线**(`PWR5V`)。外层 0.45 mm 就够,占内层是浪费 |
|
||
| `VO1`~`VO5` | Buck 输出 → eFuse,**通道体内约 12 mm**,4 A | ❌ 走**顶层 1.5~2.0 mm**(`PWR4A`)。§10.2 本来就要求这一段回路面积最小 |
|
||
| `VO1_P`~`VO5_P` | 分流电阻 → 排针,**只有约 9 mm**,4 A | ❌ 走**底层小岛**(排针针脚是通孔,自动连上)|
|
||
| 8 A 输入链三段 | J1→F1→Q1→C2/C3→Rs6,全长约 25 mm | ❌ 走**顶层宽铜 ≥3.5 mm**(见下)|
|
||
|
||
### 为什么 8 A 输入链**不**上 Inner2(和 §10.4 不一样,是有意的)
|
||
|
||
§10.4 是给 v5(单板时代)写的,那时这条链被摊开到 85 mm。v6 已经按 §10.2b 收进
|
||
北左角 (x 3~28, y 69~91),**全长只剩 25 mm**,于是两个新的事实起了作用:
|
||
|
||
1. **内层扛不动。** 8 A 在 0.5 oz 内层要 **18.1 mm 宽**,而那个角落总共才 22 mm 高,
|
||
三段各要 18 mm 根本排不下。
|
||
2. **过孔才是真瓶颈。** F1 / Q1 / D1 / C4~C6 全是贴片,要下到 Inner2 只能靠过孔;
|
||
Ø0.3 过孔单孔 1~1.5 A,扇出规划器每个焊盘给 3 个 ⇒ 一个转换点最多 3~4.5 A ≪ 8 A。
|
||
|
||
顶层反而是**零过孔的最短路径**(这条链上每一个元件都在顶层):
|
||
1 oz 外层 8 A 只要 **3.49 mm**,而那个角落横向有 25 mm 可用。
|
||
⇒ `$1N223` / `$1N227` / `$1N236` 三段一律**顶层灌铜 ≥3.5 mm**,Inner2 上那一片留给 GND 底填
|
||
(顺带给这条大电流回路铺了一整块贴身地平面,比原方案还好)。
|
||
§10.1 原文给的三个选项里,这就是「顶层灌铜 ≥3.5 mm 宽」那一条。
|
||
|
||
## 排针把 Inner2 劈成南北两半 —— 只有三条通道
|
||
|
||
J24 焊盘 x 6.97~57.03、J25 x 102.97~153.03,两排 y 61.2 / 63.8,Ø1.8 焊盘 + 0.2 净距
|
||
⇒ 同排相邻反焊盘之间只剩 **0.34 mm**、两排之间 **0.4 mm** 的铜网。数值上没断,电气上当断路看。
|
||
|
||
| 通道 | x | 净宽 | 内层 0.5 oz 能带 |
|
||
|---|---|---|---|
|
||
| 左 | 0.5 ~ 6.7 | 6.2 mm | 3.7 A(但北端被 J1 / C2 的通孔掐到 2.8~3.6 mm)|
|
||
| **中(主干道)** | 57.3 ~ 102.7 | **45.4 mm** | — |
|
||
| 右 | 153.3 ~ 159.5 | 6.2 mm | 3.7 A |
|
||
|
||
**南边要 5.6 A**(CH1 + CH4 + CH5 各 1.6 A,加左右四个风扇约 0.4+0.4 A)。
|
||
分法:**中桥喂 CH1 + CH4 + 左风扇 = 3.6 A,右通道喂 CH5 + 右风扇 = 2.0 A。**
|
||
|
||
> ⚠️ 2026-09-07 FAN-R2:五个 3 针风扇座删成两个 4 针,而且挪到了**北边两个上角**,
|
||
> 供电也从 `+12V` 改成 Q7 开关之后的 `FAN_12V_SW`。所以**南边现在只有 4.8 A**
|
||
> (中桥 3.2 / 右通道 1.6),风扇那 1.34 A 全部走北带经 Q7。上面两个 3.6 / 2.0 的
|
||
> 判据没改,现在只是更保守,不用重算。
|
||
左通道不用 —— 它的北端要从 J1 (6.0,74) 和 C2.1 (4.8,82) 两个通孔旁边挤过去,
|
||
只剩 2.8 mm、带 2.06 A,够不上一路通道的量,而且那一片正好在 8 A 输入链的顶层大铜下面,
|
||
留成 GND 底填**比塞一条 12V 细带有用得多**。
|
||
|
||
## ⚠️ 一条绕不过去的结论:`+3V3` 的南部必然被 `+12V` 中桥切成两半
|
||
|
||
中桥必须从北(y≥88)穿到南(y≤34),也就是**纵向穿过 y 41~58.5 这条带**;
|
||
而 `+3V3` 的南部焊盘(CH1/CH4 的 INA226 与上拉、控制块、CH5 的那一组)
|
||
正好横向铺满同一条带。**同一层上,两个都要连通的 2-D 区域,交叉是拓扑上避不开的。**
|
||
⇒ 让电流小的那个换层:`+3V3` 只有 0.5 A,
|
||
**南西块与南东块各自从北边的源单独引一条底层干线**(0.6 mm 就够,底层是空的),
|
||
不在 Inner2 上强行连通。这和载板 §25.10 取消两块岛是同一类判断。
|
||
|
||
## ⚠️ 顶层那三块 8 A 宽铜:和内层不是一套算法(§26.11)
|
||
|
||
`VINRAW-TOP` / `VINFUSED-TOP` / `VINSW-TOP`。网名是**自动命名网**,
|
||
文件开头 `resolve_auto()` 按「位号.引脚」当场反查,**没有写死**。三条要记住:
|
||
|
||
1. **外层挡路的东西比内层多。** 内层只怕通孔和过孔(贴片焊盘对内层完全透明),
|
||
外层连**贴片焊盘和本层走线**一起挡。`Grid` 里 `outer` 那个分支就是干这个的,
|
||
障碍来自 `obstacles()`(过孔全层、走线只挡本层)。
|
||
`snapshot()` 发现缓存里没有 `via`/`line` 就强制重抓 —— 拿旧缓存等于按
|
||
「板上没有任何过孔和走线」去算顶层铜。
|
||
2. **栅格校核不够,还要 `check_top_clearance()` 量真实几何。**
|
||
0.25 mm 的格子漏得掉 0.1 mm 的侵入(实测栽过:Rs6.2 那条 1 mm 宽的 +12V 出线,
|
||
**圆头端帽比端点还往外探 0.5 mm**,真实净距只有 0.101 mm,栅格全绿)。
|
||
侵入了 EDA 会自己缩铜,DRC 不报 —— 但板上那块铜就不是这里写的那块了。
|
||
3. **`VIN_SW` 是一个铺铜对象,不是几块拼的。** 同层同网重叠时低优先级那块会被裁,
|
||
裁的时候留不留净距 API 没说;留了就是一条看不见的缝,而且连接性 DRC 报不出来。
|
||
代价是要手写 24 个点 ⇒ 区域里带 `rects=[...]`,`poly_vs_rects()` 逐格对照。
|
||
|
||
## 已知要靠布线(不进平面)的焊盘,都是有意的
|
||
|
||
| 焊盘 | 为什么 |
|
||
|---|---|
|
||
| `U13.8` / `U13.9`(+12V)| **Kelvin 脚**。§10.4:U8~U13 的取样脚一律不进铜面,5 mΩ 分流器满量程才 20 mV |
|
||
| `C11.1`(+3V3)| 陪着 U13 一起被挖出去 |
|
||
| `C739.1`(+3V3)| 一颗去耦电容,孤零零落在风扇块的 +12V 指里;走 11 mm 顶层线到 U33.10 |
|
||
| CH2 / CH3 的 10 个 `+3V3` | 在北边,各 50 mA;沿 y≈84 的空带走顶层线到中间那块 |
|
||
| 北左辅助 Buck 那一窝 | U19 / L2 / C7~C10 / R2~R6 / Q3,12V 与 3V3 在 16×24 mm 里交错,整窝走顶层 |
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
|
||
PARTS = os.path.join(HERE, 'pcb1-parts.json')
|
||
PLACE = os.path.join(HERE, '_pcb1_place_now.json') # 元件位姿快照(--refresh 抓)
|
||
|
||
# 8 A 输入链三段是**自动命名网**(`$1N223` 一类),网名会随「从原理图导入变更」变。
|
||
# ⚠️ 别把网名写死在这里 —— `pcb1_rules.resolve_auto()` 按「位号.引脚」当场反查,
|
||
# 并逐条断言两端同网、互不重复;对不上就直接退出,不下发(那份文件头第 4 条)。
|
||
from pcb1_rules import resolve_auto
|
||
|
||
AUTO = resolve_auto()
|
||
VIN_RAW, VIN_FUSED, VIN_SW = AUTO['VIN_RAW'], AUTO['VIN_FUSED'], AUTO['VIN_SW']
|
||
CHAIN = ((VIN_RAW, 'VIN_RAW'), (VIN_FUSED, 'VIN_FUSED'), (VIN_SW, 'VIN_SW'))
|
||
|
||
MIL = 39.3700787401575
|
||
# ⚠️⚠️ 2026-09-08:这里原来写的是 `BW, BH = 160.0, 125.0`,栅格从 (0,0) 起索引 ——
|
||
# 那是**过期的假板框**,和 `pcb1_place.py` 当年那条是同一个病(它已在第三十轮改掉)。
|
||
# 真板框是 **220 × 135、左下角 (−30, −5)**(`pcb1_outline.py` 就是这么画的)。
|
||
# 后果:**本地校核看不见 x<0 / y<0 那两条边**,落在那里的铜一律算成 0 ——
|
||
# 板上的铜不受影响(多边形是原样下发给 EasyEDA 的、由它去填),
|
||
# 但**任何摆到负坐标的东西,断面/连通性校核都会静悄悄地给出 0**。
|
||
# 第三十四轮把输入首级西移到 x −17~2 之后当场撞上:三块顶层铜全报「碎成 0 块」。
|
||
BX0, BY0 = -30.0, -5.0 # 板框左下角
|
||
BW, BH = 220.0, 135.0 # 板框尺寸
|
||
INSET = 0.5 # 铜面离板边
|
||
CLEAR = 0.2 # 安全间距(pcb1_rules.MIN_CLEARANCE)
|
||
|
||
L_TOP, L_GND, L_PWR, L_BOT = 1, 15, 16, 2
|
||
|
||
# ESP32 天线禁铺(全层)。和 pcb_fanout.ANT 是同一个框,改一处要改两处 —— 这里直接引用。
|
||
try:
|
||
from pcb_fanout import ANT
|
||
except Exception:
|
||
ANT = (78.0, 0.8, 98.0, 8.9)
|
||
|
||
# ── 载流:IPC-2221 ─────────────────────────────────────────────────────
|
||
# 外层 1 oz:k=0.048、t=1.37 mil;内层 0.5 oz:k=0.024、t=0.685 mil
|
||
def amps(width_mm, inner=True, dT=20.0):
|
||
u"""这么宽的铜能带多少安(ΔT=20 °C)。"""
|
||
k, t = (0.024, 0.685) if inner else (0.048, 1.37)
|
||
a = width_mm / 0.0254 * t # mil²
|
||
return k * (dT ** 0.44) * (a ** 0.725)
|
||
|
||
|
||
def width_for(i_amp, inner=True, dT=20.0):
|
||
u"""带这么多安需要多宽(mm)。"""
|
||
k, t = (0.024, 0.685) if inner else (0.048, 1.37)
|
||
a = (i_amp / (k * dT ** 0.44)) ** (1 / 0.725) # mil²
|
||
return a / t * 0.0254
|
||
|
||
|
||
def rect(x0, y0, x1, y1):
|
||
return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||
|
||
|
||
# ── 毫米 ↔ 栅格:**一律走这四个函数,不要再写 int(x / G)** ──────────────
|
||
# 板框左下角不是 (0,0),直接除 G 会把负坐标截成 0(见上面 BX0/BY0 那段)。
|
||
def gi(x):
|
||
return int((x - BX0) / G)
|
||
|
||
|
||
def gj(y):
|
||
return int((y - BY0) / G)
|
||
|
||
|
||
def gx(i):
|
||
return BX0 + (i + 0.5) * G
|
||
|
||
|
||
def gy(j):
|
||
return BY0 + (j + 0.5) * G
|
||
|
||
|
||
CORNER_R = 5.0
|
||
|
||
|
||
def board_poly(inset=INSET, seg=6):
|
||
u"""板框内缩 inset 之后的多边形,**四角是 R5 圆角**。
|
||
|
||
⚠️ 直接用 rect(0.5, 0.5, 159.5, 124.5) 会让底填在四个角上探出板外
|
||
(板框是 R5 圆角,不是直角)。铺铜自己会被板框裁掉,但那样一来
|
||
「铺出来的和这里写的不一样」,后面的断面校核就失去意义了。
|
||
"""
|
||
r = CORNER_R - inset
|
||
pts = []
|
||
for cx, cy, a0 in ((BX0 + BW - CORNER_R, BY0 + CORNER_R, -90.0),
|
||
(BX0 + BW - CORNER_R, BY0 + BH - CORNER_R, 0.0),
|
||
(BX0 + CORNER_R, BY0 + BH - CORNER_R, 90.0),
|
||
(BX0 + CORNER_R, BY0 + CORNER_R, 180.0)):
|
||
for k in range(seg + 1):
|
||
a = math.radians(a0 + 90.0 * k / seg)
|
||
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||
return pts
|
||
|
||
|
||
# ══════════════════ Inner2(L3)分割电源面 ══════════════════
|
||
# 顺序 = 铺铜优先级(数字小的先铺、占地盘)。GND 底填必须排最后。
|
||
# ⚠️ 这个顺序是 `create()` 的调用顺序,`pourPriority` 参数不生效(载板 §9j 实测)。
|
||
REGIONS = [
|
||
# ── +12V:三块(不是六块)。**每一块都必须自己含一个本网通孔焊盘** ──
|
||
# ⚠️ 第一版按几何写成六个矩形(北带 / 中桥 / 风扇指 / 南西 / 南东 / 右通道),
|
||
# 校核当场抓出来:`12V-RC`(右通道)里一个 +12V 焊盘都没有。
|
||
# 铺铜的孤岛判定是**按铺铜对象**算的,不是按网络 —— 哪怕它和北带紧挨着,
|
||
# 自己那一块里没有本网焊盘/过孔就会被整块删光(边框还在、nPoured = 0,
|
||
# 载板 `pcb2_pour.py` 文件头第 4 条)。
|
||
# ⇒ 把中桥并进南西、右通道并进南东、风扇指并进北带,凑成三个 L / T 形多边形,
|
||
# 每块各自含 J28.1 / J26.1+J27.1 / J29.1+J30.1,**不依赖扇出就活得下来**。
|
||
# ⚠️⚠️ **2026-09-07 FAN-R2 之后上面这句不再成立**:J28/J29/J30 删了,J26/J27 挪到两个上角
|
||
# 而且 pin2 已经从 `+12V` 改成开关后的 `FAN_12V_SW`(经 Q7)——
|
||
# 三块 +12V 里**一个风扇座的通孔焊盘都不剩了**,现在靠 `pcb_fanout` 打的过孔活着
|
||
# (实测三块各填出 1 段,没被当孤岛删)。⇒ **重铺之前必须先扇出**,
|
||
# 否则会退回「边框还在、nPoured=0」那个状态。整板重布时把这三块各自含一个本网
|
||
# 通孔焊盘的条件重新落实一次,别再依赖扇出的运气。
|
||
dict(net='+12V', layer=L_PWR, name='12V-N', poly=[
|
||
# 北大块 y 88~124.5 通板 + 风扇指下伸到 y 66.5,**挖掉 U13 那一角**(Kelvin 脚,§10.4)
|
||
(0.5, 88.0), (96.5, 88.0), (96.5, 66.5), (112.0, 66.5), (112.0, 88.0),
|
||
(159.5, 88.0), (159.5, 124.5), (0.5, 124.5),
|
||
(0.5, 102.4), (12.5, 102.4), (12.5, 95.6), (0.5, 95.6)],
|
||
note=u'北带(CH2/CH3 的 Cin、U19、U29;FAN-R2 之后还含 Q7 源极 = 风扇 1.34 A 的入口)+ 风扇指(现已空载);'
|
||
u'**源 Rs6.2 的焊盘是 (8.93~10.21, 91.77~95.23)** —— U13 那个缺口原来写成 y 93.0~101.5,'
|
||
u'正好把 Rs6.2 的中心咬掉,扇出于是把这颗 8 A 源判成「不在铜面里」一个过孔都没打(2026-09-06 抓到)。'
|
||
u'缺口收成 y 95.6~102.4,只罩住 U13 十个脚(96.08~101.92)与 C11'),
|
||
dict(net='+12V', layer=L_PWR, name='12V-SW+BR', poly=[
|
||
# 南西块 + 中桥:⌐ 形。中桥西边界 61.5 是为了躲开 R706/R707 的 0402 焊盘 (x 59.35~60.65)
|
||
# —— 那两个是**贴片**焊盘,对内层是透明的,所以下面这一段可以放心往西铺过去。
|
||
# ⚠️⚠️ **南出口 y 34~40.7 加宽到 x 54**(2026-09-06 第二十一轮)。
|
||
# 信号布线之后,中桥在 y=35 那一行被三个异网过孔的反焊盘切成 1.50 / 0.75 / 5.00 三段,
|
||
# 最长 5.00 mm → 3.1 A < 需要的 3.6 A(要 6.04 mm)。**这是布线把电源面啃窄了**,
|
||
# 不是铺铜算错。往西加宽 7.5 mm 之后那一行连续 8.75 mm。
|
||
# 只能加宽到 y 40.7 —— 再往上 y 41 起是 `3V3-SW`(x 0.8~58.5),要留 0.2 净距。
|
||
# 实测 x 52~62.5 / y 33~42 里只有 4 个过孔,且没有一个够得到 y 35 那一行。
|
||
# ⚠️ 东边界 72.0 -> 72.6:72.0 与 `3V3-SE` 的西边界 73.0 之间那 1.0 mm,
|
||
# 两边各让 0.2 之后剩 0.6,GND 底填就在里面挤出一条 **0.3 x 14.5 mm 的浮铜**
|
||
# (y 65.5~80,7.3 mm²,里面一个 GND 落脚点都没有)。
|
||
# 把 +12V 推到 72.6,缝就没了;离 3V3-SE 还有 0.4,够。
|
||
(0.5, 9.0), (72.6, 9.0), (72.6, 88.0), (61.5, 88.0), (61.5, 40.7),
|
||
(54.0, 40.7), (54.0, 34.0), (0.5, 34.0)],
|
||
note=u'CH1 + CH4 的 Cin(FAN-R2 之后南边没有风扇了);中桥穿中间通道下来,喂 3.2 A,校核仍按 3.6 A 卡'),
|
||
dict(net='+12V', layer=L_PWR, name='12V-SE+RC', poly=[
|
||
# 南东块 + 右通道:L 形。J22 只有两个无网络的壳体通孔在 x=151.9,不挡 x≥153.5
|
||
(116.0, 9.0), (159.5, 9.0), (159.5, 88.0), (153.5, 88.0), (153.5, 34.0), (116.0, 34.0)],
|
||
note=u'CH5 的 Cin(FAN-R2 之后南边没有风扇了);右通道接回北带,喂 1.6 A,校核仍按 2.0 A 卡'),
|
||
|
||
# ── +3V3:南部两块,各自从北边的源单独引底层干线(见文件头「绕不过去的结论」)──
|
||
dict(net='+3V3', layer=L_PWR, name='3V3-SW', poly=rect(0.8, 41.0, 58.5, 58.5),
|
||
note=u'CH1 / CH4 的 INA226 与上拉;西边一直铺到 x 0.8,给底层干线留落孔点'),
|
||
dict(net='+3V3', layer=L_PWR, name='3V3-SE', poly=[
|
||
# L 形:控制块(x 73~115,一直到 y 9 接 U20 的 3V3 脚)+ 南东臂(到 CH5)+ 北臂(到 U33)
|
||
(73.0, 9.0), (115.0, 9.0), (115.0, 41.0), (148.0, 41.0),
|
||
(148.0, 58.5), (96.0, 58.5), (96.0, 86.5), (73.0, 86.5)],
|
||
note=u'控制块 P3/P3B + U20 + CH5 那一组 + 北臂上的 U33;北臂顺带占住中间通道东半边'),
|
||
|
||
# ── GND 底填:**必须排最后**(优先级数字最大)──
|
||
dict(net='GND', layer=L_PWR, name='GND-FILL-L3',
|
||
poly=board_poly(),
|
||
note=u'Inner2 剩余全部。北左角那一片正好垫在 8 A 输入链的顶层大铜下面'),
|
||
|
||
# ══════════════════ Bottom(L4):五路 VOn_P 小岛 ══════════════════
|
||
# 分流电阻 → 排针只有约 9 mm,本来不必上平面;做成小岛纯粹是因为
|
||
# **排针那 3 根针是通孔**,小岛一铺就自动连上,省掉三条 4 A 的走线。
|
||
# 每块都框住「Rs*01.1 + U* 的 VIN−/VBUS 两脚 + R*19 泄放 + 排针 3 针」。
|
||
dict(net='VO1_P', layer=L_BOT, name='VO1P-ISL', poly=rect(6.5, 50.5, 27.5, 65.5),
|
||
note=u'Rs101.1 / U8.8,9 / R119.1 / J24 三针 (7.9~12.9, 63.8)'),
|
||
dict(net='VO4_P', layer=L_BOT, name='VO4P-ISL', poly=rect(41.5, 50.5, 59.5, 63.0),
|
||
note=u'Rs401.1 / U11 / R419.1 / J24 三针 (43.4~48.5, 61.2)'),
|
||
dict(net='VO2_P', layer=L_BOT, name='VO2P-ISL', poly=rect(40.5, 62.0, 58.5, 74.5),
|
||
note=u'Rs201.1 / U9.8,9 / R219.1 / J24 三针 (51.0~56.1, 63.8)'),
|
||
dict(net='VO5_P', layer=L_BOT, name='VO5P-ISL', poly=rect(121.0, 50.5, 139.0, 63.0),
|
||
note=u'Rs501.1 / U12 / R519.1 / J25 三针 (124.2~129.3, 61.2)'),
|
||
dict(net='VO3_P', layer=L_BOT, name='VO3P-ISL', poly=rect(128.5, 62.0, 146.5, 74.5),
|
||
note=u'Rs301.1 / U10.8,9 / R319.1 / J25 三针 (139.4~144.5, 63.8)'),
|
||
|
||
# ══════════════════ Inner1(L2)完整地平面 ══════════════════
|
||
dict(net='GND', layer=L_GND, name='GND-L2',
|
||
poly=board_poly(),
|
||
note=u'§10.1「不许被割断」。只挖 ESP32 天线区(下面 check_ant 单独核)'),
|
||
|
||
# ══════════════════ Top(L1):8 A 输入链的宽铜(§26.8)══════════════════
|
||
# 这三段**不上 Inner2**,是有意的:8 A 在 0.5 oz 内层要 18.2 mm,北左角只有 22 mm 高;
|
||
# 而链上每个元件都在顶层 ⇒ 顶层是**零过孔的最短路径**,外层 1 oz 只要 3.49 mm。
|
||
# 网名是自动命名网,上面 resolve_auto() 当场反查出来的,**没有写死**。
|
||
#
|
||
# ⚠️ 顶层和内层挡路的东西不一样(Grid 的表):这一片里 J1/C2/C3 的通孔、
|
||
# D1/C4/C5/C6/R1 的 GND 贴片焊盘、以及扇出给它们打的 9 个 GND 过孔 + 短线,
|
||
# **每一样都要躲**。下面每一条边界后面括号里写的就是它躲的那个东西。
|
||
# ⚠️⚠️ 2026-09-08 第三十四轮**整段重画**:输入首级(J1/F1/Q1/D1 + 栅极卫星)西移了 30 mm
|
||
# (J1 换圆形 DC 座并挪到真西板边)。旧的那一版坐标写在 x 11.9~22.5,
|
||
# 那一片现在**一个本网焊盘都没有** —— 铺出来是孤岛、会被整块删掉,
|
||
# 而断面校核量到的是空气(报 0.00 mm)。这是本文件第二次踩同一条,
|
||
# 第一次是第二十三轮(注释还在下面 CUTS 里)。⇒ **摆位一改就要回来重画这三块。**
|
||
#
|
||
# 新链路是**单向**的,比旧版顺:J1(−18.8) → F1(−12.5) → Q1(−6) → D1(1) → 东北去 C2/C3/Rs6。
|
||
# 旧版是 J1 一路向东到 Q1、再折回西边找 Rs6,多绕了一圈。
|
||
dict(net=VIN_RAW, layer=L_TOP, name='VINRAW-TOP',
|
||
poly=[(-17.0, 70.7), (-11.5, 70.7), (-11.5, 73.80),
|
||
(-13.65, 73.80), (-13.65, 76.5), (-17.0, 76.5)],
|
||
note=u'J1.1(−15.80,74.35,中心正极,通孔盘 1.6×3.8) → F1.1(−12.50,73.06)。'
|
||
u'南边 70.7 躲 J1.3 的 GND 通孔盘(顶 70.450);'
|
||
u'东边 −11.5 刚好盖住 F1.1(东 −11.637),离 Q1 源极三脚(西 −9.786) 还有 1.7;'
|
||
u'x −13.65 那级台阶是躲 F1.2(西 −13.364, 南 75.350) —— 台阶以东只铺到 73.80,'
|
||
u'盖住 F1.1(顶 73.651) 并给 VIN_FUSED 让 0.30。'
|
||
u'**载流断面在 J1.1 与 F1.1 之间**(x≈−14.2),那里是满高 5.8 mm;'
|
||
u'台阶以东那 3.10 mm 不走穿过电流,F1.1 就是终点。'
|
||
u'⚠️ 两块的**竖直边**也要拉开:VINRAW 东界 −11.5 与 VINFUSED 的台阶 −11.15 '
|
||
u'差 0.35 —— 第一版这两条边只隔 0.20(正好压在下限上),是校核报出来的'),
|
||
dict(net=VIN_FUSED, layer=L_TOP, name='VINFUSED-TOP',
|
||
poly=[(-13.4, 74.10), (-11.15, 74.10), (-11.15, 73.3),
|
||
(-7.5, 73.3), (-7.5, 77.8), (-13.4, 77.8)],
|
||
note=u'F1.2(−12.50,75.95) → Q1.1~3(源极三脚, x −9.786~−7.751)。'
|
||
u'南边两段:西半 74.10 同时躲 F1.1(顶 73.651) 和 VINRAW 的台阶(顶 73.80,让 0.30),'
|
||
u'东半 73.3 躲 Q1.4 栅极脚(顶 72.882) 并盖住 Q1.3(底 73.578);'
|
||
u'东边 −7.5 离 Q1 漏极四脚(西 −4.250) 还有 3.25。'
|
||
u'断面在 F1.2 与源极之间(x≈−10.7)= 4.5 mm。'
|
||
u'⚠️ DZ1.1/C1.1 这两个栅极钳位件同属本网但在 y≈68,**不进这块铜** ——'
|
||
u'µA 级,交给 pcb1_route_sig 拉一条细线'),
|
||
|
||
# ⚠ `VIN_SW` 必须是**一个铺铜对象**,不能拆成几块靠互相重叠连起来。
|
||
# 同层同网两块铜重叠时,低优先级那块会被高优先级那块裁掉 —— 裁的时候
|
||
# 会不会留 0.2 mm 净距,API 文档没说。留了的话干道与漏极块之间就是一条
|
||
# 看不见的缝,而**连通性 DRC 也报不出来**(两边都还连在各自的焊盘上)。
|
||
# ⇒ 宁可手写 18 个点。下面的 `rects` 是这 18 个点该等于的矩形并集,
|
||
# `report()` 里逐格对照,写错一个点当场就报(check_poly_rects)。
|
||
dict(net=VIN_SW, layer=L_TOP, name='VINSW-TOP',
|
||
poly=[(-4.45, 72.10), (-0.30, 72.10), (-0.30, 73.40), (2.40, 73.40),
|
||
(2.40, 78.85), (30.46, 78.85), (30.46, 86.47), (28.6, 86.47),
|
||
(28.6, 83.49), (16.72, 83.49), (16.72, 85.5), (12.5, 85.5),
|
||
(12.5, 83.49), (6.65, 83.49), (6.65, 87.0), (5.3, 87.0),
|
||
(5.3, 95.25), (1.5, 95.25), (1.5, 83.49), (-4.45, 83.49)],
|
||
rects=[(-4.45, 72.10, -0.30, 78.85), # Q1 漏极四脚(x −4.250~−2.215)。南边 72.10 躲 R1 两脚(顶 71.772),留 0.328
|
||
(-0.30, 73.40, 2.40, 78.85), # D1.1 TVS(−0.096~2.096, 75.838~77.885)。
|
||
# 南边抬到 73.40 是躲 D1.2 的 GND 盘(顶 73.163) 和它正下方 6 个扇出孔
|
||
(-4.45, 78.85, 28.6, 83.49), # ★ 8 A 干道,y 净宽 4.64 mm。**西端由 1.5 延到 −4.45**:
|
||
# 旧版西端卡在 1.5 是因为老 J1 的机械脚 J1.4(顶 78.401) 顶着,那颗件已经挪走
|
||
(28.6, 78.85, 30.46, 86.47), # C4.2/C5.2/C6.2 去耦(东边 30.46 躲 C6.1 的 GND 盘 30.670)
|
||
(12.5, 83.49, 16.72, 85.5), # C3.1 口袋(东边 16.72 躲 C3.2 GND 通孔 16.950)
|
||
(1.5, 83.49, 6.65, 87.0), # C2.1 口袋(净宽 5.15 mm)
|
||
(1.5, 87.0, 5.3, 95.25)], # 北柱到 Rs6.1(净宽 **3.8 mm** = 8.5 A)
|
||
# ⚠️⚠️ **北柱在 y=87 以上收窄到 x 1.5~5.3(3.8 mm)是给 U13 让路的**(第二十三轮):
|
||
# U13 南排五个脚 (x 5.36~7.64, y 96.08) 只能往南出线,而原来的柱子铺到 x 6.65、
|
||
# 顶到 y 95.4 —— 离 U13.2 只剩 0.379 mm,`pcb1_escape_audit` 报「被围死」。
|
||
# 收窄之后 U13 南边是一整片 x 5.5~14 / y 91.6~96 的空地。
|
||
# (Rs6.2 那两根往西甩的 1 mm 扇出短线也一起没了 —— 见 pcb_fanout.FORCE_ARRAY)
|
||
note=u'**8 A 主干**:Q1 漏极(−3.2) → 东经 D1 → 北上 → **东行**干道 → C2.1/C3.1 → 北柱 → Rs6.1,'
|
||
u'一路无过孔;东端挂 C4/C5/C6 去耦。'
|
||
u'⚠️ 第三十四轮之后干道是**东行**不是西行 —— 源头 Q1 从 x 24 挪到了 x −6,'
|
||
u'而 C2/C3/Rs6/C4~C6 一颗没动,于是电流方向整个翻过来了。'
|
||
u'旧注释「干道比 v10 北提 2.8 mm 躲 XT30 机械脚」已作废:那颗 XT30 已经不在这块板上'),
|
||
]
|
||
|
||
# ── 断面校核:每一条都是「这里必须能过多少安」──────────────────────────
|
||
# (名字, 网络, 层, 轴, 坐标, 跨度, 需要的电流)
|
||
# axis='y' → 沿 y=坐标 这条横线量,跨度是 x 区间
|
||
# axis='x' → 沿 x=坐标 这条竖线量,跨度是 y 区间
|
||
# ⚠️ **层必须写对**:同一个宽度,外层 1 oz 能带的电流约是内层 0.5 oz 的 4 倍。
|
||
# 把顶层那三段按内层算,8 A 会要 18.2 mm,那个角落根本排不下、结论全错。
|
||
CUTS = [
|
||
(u'中桥 @排针带', '+12V', L_PWR, 'y', 62.0, (57.3, 102.7), 3.6),
|
||
(u'中桥 @R706/R707', '+12V', L_PWR, 'y', 72.5, (57.3, 102.7), 3.6),
|
||
# 量测窗口跟着加宽的南出口往西开到 53.0(铜真的铺到了 x 54)
|
||
(u'中桥 @南出口', '+12V', L_PWR, 'y', 35.0, (53.0, 102.7), 3.6),
|
||
(u'右通道 @排针带', '+12V', L_PWR, 'y', 62.0, (153.0, 159.5), 2.0),
|
||
(u'右通道 @J22', '+12V', L_PWR, 'y', 52.0, (150.0, 159.5), 2.0),
|
||
(u'南西块 → CH1', '+12V', L_PWR, 'x', 33.0, (9.0, 34.0), 2.0),
|
||
(u'南西块 → CH4+CH1', '+12V', L_PWR, 'x', 61.0, (9.0, 34.0), 3.6),
|
||
(u'南东块 → CH5', '+12V', L_PWR, 'x', 140.0, (9.0, 34.0), 1.6),
|
||
# FAN-R2 之后风扇指下面已经没有 +12V 负载(风扇改吃开关后的 FAN_12V_SW)。
|
||
# 这一条留着当**几何回归**:北带往南这一指还在,断面别被后来的布线啃掉。
|
||
(u'风扇指 @接北带(现空载)', '+12V', L_PWR, 'y', 87.0, (94.0, 114.0), 1.0),
|
||
(u'北大块 @Rs6 东出', '+12V', L_PWR, 'x', 20.0, (86.0, 124.5), 8.0),
|
||
(u'3V3 南西块', '+3V3', L_PWR, 'x', 33.0, (41.0, 58.5), 0.5),
|
||
(u'3V3 南东块 @控制块', '+3V3', L_PWR, 'x', 95.0, (9.0, 58.5), 0.5),
|
||
(u'3V3 南东块 @北臂', '+3V3', L_PWR, 'y', 70.0, (70.0, 100.0), 0.1),
|
||
|
||
# 8 A 输入链(顶层 1 oz)。J1 → F1 → Q1 → D1/C4~C6 → C3/C2 → Rs6,分叉之前全程 8 A。
|
||
# ⚠ 这一排坐标跟着摆位走,**摆位一改就要重定**:2026-09-06 第二十三轮把
|
||
# F1 竖过来、D1 与 C4/C5/C6 挪到东列之后,旧的 `x=10` 那一刀量到的是空气(报 0.00 mm)。
|
||
# 2026-09-08:输入首级西移 30 mm,下面前四条**全部重定**。
|
||
# ⚠️ 断面要卡在「电流真正流过的那一段」,不是随便找个地方量:
|
||
# VIN_RAW 的载流段是 J1.1 与 F1.1 **之间**(x≈−14.2);台阶以东那 3.15 mm
|
||
# 是 F1.1 的落脚区、不走穿过电流,在那里量会得到一个偏严且没意义的数。
|
||
(u'RAW J1.1→F1.1', VIN_RAW, L_TOP, 'x', -14.2, (68.0, 79.0), 8.0),
|
||
(u'FUSED F1.2→Q1 源极', VIN_FUSED, L_TOP, 'x', -10.7, (70.0, 80.0), 8.0),
|
||
(u'SW Q1 漏极东出', VIN_SW, L_TOP, 'x', -1.5, (69.0, 81.0), 8.0),
|
||
(u'SW 过 D1 北上', VIN_SW, L_TOP, 'x', 2.0, (70.0, 86.0), 8.0),
|
||
(u'SW 干道 @x24 东端', VIN_SW, L_TOP, 'x', 24.0, (74.0, 86.0), 8.0),
|
||
(u'SW 干道 @x12', VIN_SW, L_TOP, 'x', 12.0, (74.0, 86.0), 8.0),
|
||
(u'SW 干道拐北 @y84', VIN_SW, L_TOP, 'y', 84.0, (0.5, 12.0), 8.0),
|
||
(u'SW 北柱 @C2 上方', VIN_SW, L_TOP, 'y', 86.0, (1.0, 12.0), 8.0),
|
||
(u'SW 北柱 @Rs6.1', VIN_SW, L_TOP, 'y', 92.0, (1.0, 12.0), 8.0),
|
||
# 支路:体电容与去耦只走纹波,不走 8 A 直流(§10.2)
|
||
(u'SW C3 口袋', VIN_SW, L_TOP, 'y', 84.5, (11.0, 18.0), 2.0),
|
||
(u'SW C4/C5/C6 那一条', VIN_SW, L_TOP, 'y', 82.0, (27.0, 34.0), 2.0),
|
||
]
|
||
|
||
# 明知故犯:这些焊盘不进自己网络的平面,理由见文件头那张表。
|
||
# 校核时它们不算「漏掉」,但会单独列出来提醒去布线。
|
||
ROUTED_PADS = {
|
||
'U13.8': u'Kelvin(VIN−),§10.4 取样脚不进铜面',
|
||
'U13.9': u'Kelvin(VIN−),§10.4 取样脚不进铜面',
|
||
# ⭐ 五路 INA226 的 IN−(pin 9):**必须单独引线到 Rs*01 的焊盘,不许进 VOn_P 小岛。**
|
||
# 2026-09-06 第十九轮实测:让它走岛的话,取样点在岛上而不是在分流电阻的焊盘上,
|
||
# 中间隔着 Rs*01 那三个扇出孔 + 引线(并联约 0.5 mΩ)。Rs 只有 5 mΩ、满载 20 mV,
|
||
# 0.5 mΩ x 4 A = 2 mV = **10% 的读数误差**,而 DRC 和连通性检查都看不出来
|
||
# (IN− 和 VO*_P 本来就是同一个网,连上了就算过)。
|
||
# ⚠️ pin 8 是 VBUS(测输出电压),照旧进岛 —— 它要的就是负载端的电位。
|
||
'U8.9': u'Kelvin(IN−):单独引线到 Rs101.1 的焊盘,不进 VO1_P 小岛',
|
||
'U9.9': u'Kelvin(IN−):单独引线到 Rs201.1 的焊盘,不进 VO2_P 小岛',
|
||
'U10.9': u'Kelvin(IN−):单独引线到 Rs301.1 的焊盘,不进 VO3_P 小岛',
|
||
'U11.9': u'Kelvin(IN−):单独引线到 Rs401.1 的焊盘,不进 VO4_P 小岛',
|
||
'U12.9': u'Kelvin(IN−):单独引线到 Rs501.1 的焊盘,不进 VO5_P 小岛',
|
||
'C11.1': u'陪 U13 一起挖出去,走顶层到 L2 输出',
|
||
'U13.6': u'INA226 的 VS+,陪 U13 一起挖出去(C11 就在旁边)',
|
||
# U23 西侧那两颗(v11 控制块退火重排之后落到 x 71.9~72.4):
|
||
# 那一条 x 是 `12V-SW+BR`(东界 72.6) 与 `3V3-SE`(西界 73.0) 中间的 0.4 mm 缝,
|
||
# 两块内层平面谁都够不着。**贴片焊盘对内层透明,所以不是错误**,走顶层短线即可:
|
||
# C606.1 挨着 U23.24(0.3 mm,去耦回路本来就该这么短),R608.1 顺着 U23 西缘上去搭 C606.1。
|
||
'C606.1': u'U23 的去耦,紧贴 U23.24(本身在 3V3-SE 上),顶层 0.3 mm 短线',
|
||
'R608.1': u'IOEXP_INT# 上拉,10 kΩ;沿 U23 西缘走 6 mm 顶层线搭到 C606.1',
|
||
# 8 A 输入链上不进宽铜的三个(都不是漏掉)
|
||
'U13.10': u'Kelvin(VIN+)。§10.4:Rs6 满量程才 40 mV,取样脚单独引细线到 Rs6.1,'
|
||
u'不能从大铜面上随便找个点接',
|
||
'DZ1.1': u'Q1 栅极钳位齐纳的上端,µA 级;夹在 F1.1 与 Q1.4 之间只有 3.1 mm,走细线',
|
||
'C1.1': u'Q1 栅极 RC 的上端,µA 级;同 DZ1.1',
|
||
# 2026-09-08 第三十四轮:两路 TACH 接收跟着风扇座搬到了东西**板边**
|
||
# (R753 在 x −25.6、R757 在 x 181,都在 y≈100),离 `3V3-SW`/`3V3-SE` 两块内层
|
||
# 平面各有 80~100 mm。**不要为这两颗把平面拉过去** —— 10 kΩ 上拉到 3.3 V
|
||
# 只有 **0.33 mA**,外层 0.2 mm 线绰绰有余,而拉一条 100 mm 的内层电源面
|
||
# 会把 Inner2 的分割搅乱。它们是「接收电路在信号源头整形」那条设计决定的代价。
|
||
'R753.2': u'进风 TACH 的 3.3V 上拉(10k, 0.33 mA),随风扇座在西板边;走顶层细线',
|
||
'R757.2': u'排风 TACH 的 3.3V 上拉(10k, 0.33 mA),随风扇座在东板边;走顶层细线',
|
||
}
|
||
# 整块「有意不进平面」的区域(位号前缀 → 说明)
|
||
ROUTED_BLOCKS = [
|
||
((14.0, 100.0, 30.0, 124.0), u'北左辅助 Buck 那一窝:12V 与 3V3 在 16×24 mm 里交错,整窝走顶层'),
|
||
((36.0, 70.0, 58.0, 84.0), u'CH2 的 +3V3(5 个,50 mA):沿 y≈84 走顶层到中间那块'),
|
||
((126.0, 70.0, 146.0, 84.0), u'CH3 的 +3V3(5 个,50 mA):同上'),
|
||
# `R518.1`(CH5 的 +3V3):2026-09-06 第二十六轮起不再落在 Inner2 的 +3V3 上 ——
|
||
# 它本来就是「短线 + 扇出孔」那一类,只是以前**碰巧**还压着平面所以没被点名。
|
||
# 实测通路:R518.1 → 顶层 0.66 mm → +3V3 扇出孔 (134.575, 51.000) → 平面,
|
||
# `pcb1_netlist` 里 +3V3 整网连通(不在「还差」名单里)。框只罩这一颗。
|
||
((135.0, 50.4, 136.0, 51.6), u'CH5 的 R518.1:顶层 0.66 mm 短线到 (134.575,51.000) 的 +3V3 扇出孔'),
|
||
]
|
||
|
||
|
||
# ── 板上元件位姿 + 焊盘 ────────────────────────────────────────────────
|
||
SNAP_JS = u"""
|
||
await eda.dmt_EditorControl.openDocument('7e9415a16a909c51');
|
||
await new Promise(r=>setTimeout(r,1500));
|
||
const MIL = 39.3700787401575;
|
||
const comp = [], th = [];
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll())
|
||
comp.push([c.designator, +(c.x/MIL).toFixed(4), +(c.y/MIL).toFixed(4),
|
||
((c.rotation%360)+360)%360, c.layer]);
|
||
for (const p of await eda.pcb_PrimitivePad.getAll()) {
|
||
const hole = Array.isArray(p.hole) ? p.hole : null;
|
||
if (!((p.layer === 12) || (hole && hole.length > 1 && +hole[1] > 0))) continue;
|
||
let w = 0, h = 0;
|
||
try { w = +p.pad[1]||0; h = +p.pad[2]||0; } catch(e) {}
|
||
th.push([p.net||'', +(p.x/MIL).toFixed(3), +(p.y/MIL).toFixed(3),
|
||
+(w/MIL).toFixed(3), +(h/MIL).toFixed(3)]);
|
||
}
|
||
// 扇出留下的过孔与短线。**顶层铺铜要躲它们**(内层只躲过孔,短线在别的层上)。
|
||
const via = [], line = [];
|
||
for (const v of await eda.pcb_PrimitiveVia.getAll())
|
||
via.push([v.net||'', +(v.x/MIL).toFixed(3), +(v.y/MIL).toFixed(3),
|
||
+((v.outerDiameter||v.diameter||0)/MIL).toFixed(3)]);
|
||
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
|
||
if (l.layer === 11) continue; // 11 = 板框
|
||
line.push([l.net||'', l.layer,
|
||
+(l.startX/MIL).toFixed(3), +(l.startY/MIL).toFixed(3),
|
||
+(l.endX/MIL).toFixed(3), +(l.endY/MIL).toFixed(3),
|
||
+(l.lineWidth/MIL).toFixed(3)]);
|
||
}
|
||
return {comp, th, via, line};
|
||
"""
|
||
|
||
|
||
def snapshot(refresh=False):
|
||
# 缓存里没有 via/line 那一段 = 是加顶层铜面之前抓的,必须重抓,
|
||
# 否则顶层铺铜会按「板上没有任何过孔和走线」去算 —— 那正是要防的静默失效。
|
||
if not (refresh or not os.path.exists(PLACE)):
|
||
try:
|
||
if 'via' not in json.load(io.open(PLACE, encoding='utf-8')):
|
||
refresh = True
|
||
except Exception:
|
||
refresh = True
|
||
if refresh or not os.path.exists(PLACE):
|
||
from eda import run
|
||
r = run(SNAP_JS, timeout=300)
|
||
if not r.get('success'):
|
||
sys.exit(json.dumps(r, ensure_ascii=False)[:400])
|
||
io.open(PLACE, 'w', encoding='utf-8').write(
|
||
json.dumps(r['result'], ensure_ascii=False))
|
||
return json.load(io.open(PLACE, encoding='utf-8'))
|
||
|
||
|
||
def load_pads(refresh=False):
|
||
u"""返回 [net, x, y, hx, hy, th, tag],坐标 mm、hx/hy 是焊盘半宽半高。
|
||
|
||
⚠️ `th` 这一位是整件事的关键:**只有通孔焊盘才在内层挖反焊盘**,
|
||
贴片焊盘对内层完全透明。按「所有焊盘都挡内层」去算,
|
||
这块板 1200 多个贴片焊盘会把 Inner2 算成一张网,结论全错。
|
||
"""
|
||
snap = snapshot(refresh)
|
||
comp = {c[0]: c for c in snap['comp']}
|
||
parts = json.load(io.open(PARTS, encoding='utf-8'))
|
||
out = []
|
||
for p in parts:
|
||
c = comp.get(p['d'])
|
||
if not c:
|
||
continue
|
||
_, cx, cy, rot, lay = c
|
||
rad = math.radians(rot)
|
||
ct, st = math.cos(rad), math.sin(rad)
|
||
for num, lx, ly, w, h, net in p['pads']:
|
||
lx, ly, w, h = lx / MIL, ly / MIL, w / MIL, h / MIL
|
||
if lay == 2:
|
||
ly = -ly
|
||
x = cx + lx * ct - ly * st
|
||
y = cy + lx * st + ly * ct
|
||
if rot in (90, 270):
|
||
w, h = h, w
|
||
out.append([net or '', x, y, w / 2.0, h / 2.0, False,
|
||
'%s.%s' % (p['d'], num), lay])
|
||
# 通孔标记:拿板上实测的通孔表最近邻配(坐标各自舍入过,容差 0.05)
|
||
n = 0
|
||
for net, x, y, w, h in snap['th']:
|
||
best, bd = None, 0.05
|
||
for q in out:
|
||
if q[0] != net or q[5]:
|
||
continue
|
||
d = abs(q[1] - x) + abs(q[2] - y)
|
||
if d < bd:
|
||
best, bd = q, d
|
||
if best is not None:
|
||
best[5] = True
|
||
best[3] = max(best[3], w / 2.0)
|
||
best[4] = max(best[4], h / 2.0)
|
||
n += 1
|
||
return out, n, len(snap['th'])
|
||
|
||
|
||
def obstacles(refresh=False):
|
||
u"""板上已有的铜:扇出的过孔(全层)+ 每一层的走线。
|
||
|
||
返回 [(net, layer_or_None, x0, y0, x1, y1)],`layer_or_None is None` = 过孔,全层都挡。
|
||
⚠️ 斜线按**包围盒**处理(偏保守,会多挖一点)。扇出的短线全是正交的,
|
||
真正的斜线要等信号布线,那时这块顶层铜早就在了。
|
||
"""
|
||
snap = snapshot(refresh)
|
||
obs = []
|
||
for net, x, y, od in snap.get('via', ()):
|
||
r = (od or 0.61) / 2.0
|
||
obs.append((net or '', None, x - r, y - r, x + r, y + r))
|
||
for net, lay, x1, y1, x2, y2, w in snap.get('line', ()):
|
||
h = (w or 0.0) / 2.0
|
||
obs.append((net or '', lay, min(x1, x2) - h, min(y1, y2) - h,
|
||
max(x1, x2) + h, max(y1, y2) + h))
|
||
return obs
|
||
|
||
|
||
def line_segs(refresh=False):
|
||
u"""板上走线的**真实线段**(不折成包围盒):[(net, layer, x0, y0, x1, y1, w)]。"""
|
||
snap = snapshot(refresh)
|
||
return [(net or '', lay, x1, y1, x2, y2, w or 0.0)
|
||
for net, lay, x1, y1, x2, y2, w in snap.get('line', ())]
|
||
|
||
|
||
def inside(poly, x, y):
|
||
n, c = len(poly), False
|
||
for i in range(n):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % n]
|
||
if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1:
|
||
c = not c
|
||
return c
|
||
|
||
|
||
# ══════════════════ 栅格化 + 校核 ══════════════════
|
||
G = 0.25 # 栅格(mm)。0.25 下 45 mm 的通道是 180 格,够细
|
||
|
||
#: 浮铜检查的**最小面积**(mm²)。小于它的当栅格碎屑放过,只打印不判红。
|
||
#:
|
||
#: ⚠️ 这不是"把检查关小声点",是给它一个说得出理由的下限:布线一多,
|
||
#: 过孔的反焊盘之间必然夹出 0.25×0.25(**正好一个栅格**)这种碎屑 ——
|
||
#: 2026-09-06 第二十五轮实测两粒:Inner1 (68.9,74.6) 0.06 mm²、Inner2 (138.5,75.2) 0.19 mm²,
|
||
#: 都比一个过孔盘还小,电气上没有意义,而且嘉立创铺铜自己也会按死铜阈值清掉。
|
||
#: 门槛定在 1.0 mm²(1×1 mm):这一轮真正抓到的两条 —— 南沿那条 **1312.8 mm²** 的
|
||
#: GND 底填、`3V3-SW` 里那块 **7.2 mm²** 的孤岛 —— 都远在门槛之上,一条都不会漏。
|
||
MIN_FLOAT_AREA = 1.0
|
||
|
||
|
||
class Grid(object):
|
||
u"""某一层上某个网络的铜:先按区域填,再挖异网的反焊盘 / 过孔 / 走线。
|
||
|
||
⚠️ **内层和外层挡路的东西不是同一批**,这是整件事最容易算错的地方:
|
||
|
||
| | 通孔焊盘 | 贴片焊盘 | 过孔 | 走线 |
|
||
|---|---|---|---|---|
|
||
| 内层 Inner1/2 | 挡 | **不挡**(对内层完全透明)| 挡 | 挡本层的(内层现在没有走线)|
|
||
| 外层 Top/Bottom | 挡 | **挡** | 挡 | 挡本层的 |
|
||
|
||
按「所有焊盘都挡内层」算,这块板 1200 多个贴片焊盘会把 Inner2 算成一张网;
|
||
反过来按「只有通孔挡」去算顶层,8 A 那块宽铜会算得比实际宽得多。
|
||
"""
|
||
|
||
def __init__(self, net, layer, regions, pads, obs=()):
|
||
self.net, self.layer = net, layer
|
||
outer = layer in (L_TOP, L_BOT)
|
||
self.nx, self.ny = int(BW / G), int(BH / G)
|
||
self.c = [[False] * self.nx for _ in range(self.ny)]
|
||
for r in regions:
|
||
xs = [p[0] for p in r['poly']]
|
||
ys = [p[1] for p in r['poly']]
|
||
i0, i1 = gi(min(xs)), gi(max(xs)) + 1
|
||
j0, j1 = gj(min(ys)), gj(max(ys)) + 1
|
||
for j in range(max(0, j0), min(self.ny, j1)):
|
||
for i in range(max(0, i0), min(self.nx, i1)):
|
||
if not self.c[j][i] and inside(r['poly'], gx(i), gy(j)):
|
||
self.c[j][i] = True
|
||
# 反焊盘:异网焊盘(内层只算通孔,外层连贴片一起算)
|
||
self.carved = 0
|
||
for pnet, px, py, hx, hy, th, tag, plane in pads:
|
||
if pnet == net:
|
||
continue
|
||
if not (th or (outer and plane == layer)):
|
||
continue
|
||
if self._carve(px - hx - CLEAR, py - hy - CLEAR,
|
||
px + hx + CLEAR, py + hy + CLEAR):
|
||
self.carved += 1
|
||
# 板上已有的铜:过孔(全层)+ 本层走线
|
||
self.carved_obs = 0
|
||
for onet, olay, x0, y0, x1, y1 in obs:
|
||
if onet == net or (olay is not None and olay != layer):
|
||
continue
|
||
if self._carve(x0 - CLEAR, y0 - CLEAR, x1 + CLEAR, y1 + CLEAR):
|
||
self.carved_obs += 1
|
||
# 天线禁铺(全层)
|
||
for j in range(self.ny):
|
||
for i in range(self.nx):
|
||
if self.c[j][i] and ANT[0] <= gx(i) <= ANT[2] \
|
||
and ANT[1] <= gy(j) <= ANT[3]:
|
||
self.c[j][i] = False
|
||
self.label()
|
||
|
||
def _carve(self, x0, y0, x1, y1):
|
||
u"""挖掉一个矩形(已经把净距算进去了)。返回有没有真挖到东西。"""
|
||
hit = False
|
||
for j in range(max(0, gj(y0)), min(self.ny, gj(y1) + 1)):
|
||
yc = gy(j)
|
||
if not (y0 <= yc <= y1):
|
||
continue
|
||
row = self.c[j]
|
||
for i in range(max(0, gi(x0)), min(self.nx, gi(x1) + 1)):
|
||
if row[i] and x0 <= gx(i) <= x1:
|
||
row[i] = False
|
||
hit = True
|
||
return hit
|
||
|
||
def label(self):
|
||
self.lab = [[0] * self.nx for _ in range(self.ny)]
|
||
self.sizes = {}
|
||
k = 0
|
||
for j in range(self.ny):
|
||
for i in range(self.nx):
|
||
if not self.c[j][i] or self.lab[j][i]:
|
||
continue
|
||
k += 1
|
||
st = [(j, i)]
|
||
self.lab[j][i] = k
|
||
n = 0
|
||
while st:
|
||
a, b = st.pop()
|
||
n += 1
|
||
for da, db in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
||
p, q = a + da, b + db
|
||
if 0 <= p < self.ny and 0 <= q < self.nx \
|
||
and self.c[p][q] and not self.lab[p][q]:
|
||
self.lab[p][q] = k
|
||
st.append((p, q))
|
||
self.sizes[k] = n
|
||
self.ncomp = k
|
||
self.main = max(self.sizes, key=self.sizes.get) if self.sizes else 0
|
||
|
||
def cut(self, axis, coord, span):
|
||
u"""沿一条线量本网铜的总宽与最长连续段(mm)。"""
|
||
tot = run_max = run = 0
|
||
if axis == 'y':
|
||
j = gj(coord)
|
||
for i in range(gi(span[0]), gi(span[1]) + 1):
|
||
v = 0 <= i < self.nx and 0 <= j < self.ny and self.c[j][i]
|
||
tot += 1 if v else 0
|
||
run = run + 1 if v else 0
|
||
run_max = max(run_max, run)
|
||
else:
|
||
i = gi(coord)
|
||
for j in range(gj(span[0]), gj(span[1]) + 1):
|
||
v = 0 <= i < self.nx and 0 <= j < self.ny and self.c[j][i]
|
||
tot += 1 if v else 0
|
||
run = run + 1 if v else 0
|
||
run_max = max(run_max, run)
|
||
return tot * G, run_max * G
|
||
|
||
def hit(self, x, y, hx, hy):
|
||
u"""这个焊盘有没有落在本网主连通域上(焊盘框内任一格)。"""
|
||
i0, i1 = gi(x - hx), gi(x + hx) + 1
|
||
j0, j1 = gj(y - hy), gj(y + hy) + 1
|
||
for j in range(max(0, j0), min(self.ny, j1 + 1)):
|
||
for i in range(max(0, i0), min(self.nx, i1 + 1)):
|
||
if self.lab[j][i]:
|
||
return self.lab[j][i]
|
||
return 0
|
||
|
||
|
||
# ══════════════════ 报告 ══════════════════
|
||
def in_blocks(x, y):
|
||
for (bx0, by0, bx1, by1), why in ROUTED_BLOCKS:
|
||
if bx0 <= x <= bx1 and by0 <= y <= by1:
|
||
return why
|
||
return None
|
||
|
||
|
||
def grid_checks(G_, pads, chk):
|
||
u"""连通域 / 断面 / 焊盘覆盖 —— **下发前按 REGIONS 算、下发后按实测铜算,走的是同一段代码**。
|
||
|
||
⚠️ 这三条必须共用一份实现。分成两套写,「预测过了、实测也过了」就只证明
|
||
两套代码各自自洽,证明不了板上那块铜真的是计划里那块铜。
|
||
"""
|
||
# 1) +12V 必须是一整块
|
||
g12 = G_[('+12V', L_PWR)]
|
||
chk(g12.ncomp == 1, u'+12V 在 Inner2 上是**一个**连通域(六块矩形首尾相接)',
|
||
u'+12V 在 Inner2 上碎成 %d 块 —— 六块矩形没接上' % g12.ncomp)
|
||
|
||
# 2) +3V3 预期 2 块(南西 / 南东),文件头解释过为什么不强行连通
|
||
g33 = G_[('+3V3', L_PWR)]
|
||
chk(g33.ncomp == 2, u'+3V3 在 Inner2 上是 2 个连通域(南西 / 南东,各自从北边引底层干线)',
|
||
u'+3V3 在 Inner2 上是 %d 个连通域,预期 2' % g33.ncomp)
|
||
|
||
# 2b) 8 A 输入链三段:每一段顶层都必须是**一整块**。
|
||
# `VIN_SW` 是四个铺铜对象拼出来的(干道 / 漏极块 / 电容条 / C3 口袋),
|
||
# 它们靠互相重叠连成一片 —— 碎成两块就说明某处重叠被反焊盘啃穿了。
|
||
for net, nm in CHAIN:
|
||
g = G_.get((net, L_TOP))
|
||
if g is None:
|
||
continue
|
||
chk(g.ncomp == 1, u'%s(%s) 顶层是一整块铜' % (nm, net),
|
||
u'%s(%s) 顶层碎成 %d 块(碎片 %s mm²)'
|
||
% (nm, net, g.ncomp,
|
||
' '.join('%.1f' % (c * G * G)
|
||
for c in sorted(g.sizes.values(), reverse=True)[1:5])))
|
||
|
||
# 3) 断面
|
||
print()
|
||
print(u'── 断面校核(内层 0.5 oz / 顶层 1 oz)──')
|
||
for name, net, layer, axis, coord, span, need in CUTS:
|
||
g = G_.get((net, layer))
|
||
if g is None:
|
||
continue
|
||
inner = layer not in (L_TOP, L_BOT)
|
||
tot, run = g.cut(axis, coord, span)
|
||
cap = amps(run, inner=inner) if run > 0 else 0.0
|
||
need_w = width_for(need, inner=inner)
|
||
ok = run >= need_w
|
||
lay = {1: 'Top', 2: 'Bot', 15: 'L2', 16: 'L3'}[layer]
|
||
chk(ok, u'%-4s %-18s %s=%-6.1f 连续 %5.2f mm(总 %5.2f)→ %4.1f A ≥ 需要 %.1f A'
|
||
% (lay, name, axis, coord, run, tot, cap, need),
|
||
u'%-4s %-18s %s=%-6.1f 连续只有 %5.2f mm(总 %5.2f)→ %4.1f A < 需要 %.1f A(要 %.2f mm)'
|
||
% (lay, name, axis, coord, run, tot, cap, need, need_w))
|
||
|
||
# 4) 本网焊盘覆盖
|
||
print()
|
||
print(u'── 焊盘覆盖(每个电源焊盘是不是落在自己的铜上)──')
|
||
for net, layer in ([('+12V', L_PWR), ('+3V3', L_PWR)]
|
||
+ [(n, L_TOP) for n, _nm in CHAIN]):
|
||
g = G_.get((net, layer))
|
||
if g is None:
|
||
continue
|
||
own = [p for p in pads if p[0] == net]
|
||
miss, excused = [], []
|
||
for pnet, x, y, hx, hy, th, tag, plane in own:
|
||
if g.hit(x, y, hx, hy):
|
||
continue
|
||
if tag in ROUTED_PADS:
|
||
excused.append((tag, ROUTED_PADS[tag]))
|
||
else:
|
||
why = in_blocks(x, y)
|
||
(excused if why else miss).append((tag, why or ''))
|
||
print(u' %-4s %-8s %3d 个焊盘:落在铜上 %3d / 有意走线 %2d / **没着落 %d**'
|
||
% ({1: 'Top', 2: 'Bot', 15: 'L2', 16: 'L3'}[layer], net, len(own),
|
||
len(own) - len(miss) - len(excused), len(excused), len(miss)))
|
||
for tag, why in sorted(excused):
|
||
print(u' · %-9s 走线:%s' % (tag, why))
|
||
if miss:
|
||
for tag, _ in sorted(miss)[:20]:
|
||
print(u' [!!] %s 既不在平面上,也不在「有意走线」名单里' % tag)
|
||
chk(False, '', u'%s 有 %d 个焊盘没着落' % (net, len(miss)))
|
||
|
||
|
||
def poly_vs_rects(poly, rects, g=0.05):
|
||
u"""手写的多边形是不是恰好等于那一堆矩形的并集。
|
||
|
||
手写 24 个点的复杂多边形,写错一个坐标**几何检查全都发现不了**:
|
||
断面照样够宽、连通域照样是 1、净距照样过 —— 错的地方在别处,
|
||
比如某条边界往里缩了 2 mm,正好没被任何一条断面线量到。
|
||
⇒ 用一张 0.05 mm 的细格逐格对照,返回 (只在多边形里, 只在矩形并集里) 的格数。
|
||
"""
|
||
xs = [p[0] for p in poly] + [r[0] for r in rects] + [r[2] for r in rects]
|
||
ys = [p[1] for p in poly] + [r[1] for r in rects] + [r[3] for r in rects]
|
||
x0, x1, y0, y1 = min(xs) - g, max(xs) + g, min(ys) - g, max(ys) + g
|
||
only_p = only_r = 0
|
||
j = 0
|
||
y = y0 + g / 2
|
||
while y < y1:
|
||
x = x0 + g / 2
|
||
while x < x1:
|
||
a = inside(poly, x, y)
|
||
b = any(rx0 <= x <= rx1 and ry0 <= y <= ry1
|
||
for rx0, ry0, rx1, ry1 in rects)
|
||
if a and not b:
|
||
only_p += 1
|
||
elif b and not a:
|
||
only_r += 1
|
||
x += g
|
||
y += g
|
||
j += 1
|
||
return only_p, only_r
|
||
|
||
|
||
def poly_rect_gap(poly, x0, y0, x1, y1):
|
||
u"""多边形到一个轴对齐矩形的最小距离;矩形碰到 / 进到多边形里就是 0。"""
|
||
box = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||
return poly_gap(poly, box)
|
||
|
||
|
||
def poly_seg_gap(poly, x0, y0, x1, y1, w):
|
||
u"""多边形到一条**线段胶囊**(宽 w,圆头端帽)的最小距离。
|
||
|
||
⚠️⚠️ **斜线不能按包围盒算。** `obstacles()` 为了省事把每条走线折成轴对齐包围盒
|
||
(那时板上只有扇出的正交短线,无所谓)。信号布线一上来,板上有两千多条 45° 斜线,
|
||
一条长斜线的包围盒必定罩住半块顶层铜 ⇒ `check_top_clearance` 报「侵入 0.000 mm」,
|
||
而真实净距其实好几毫米。2026-09-06 第二十一轮实测被这个假阳性拦下过一次重铺。
|
||
"""
|
||
if inside(poly, x0, y0) or inside(poly, x1, y1):
|
||
return 0.0
|
||
best = 9e9
|
||
for i in range(len(poly)):
|
||
best = min(best, _seg_gap(poly[i], poly[(i + 1) % len(poly)],
|
||
(x0, y0), (x1, y1)))
|
||
return max(0.0, best - w / 2.0)
|
||
|
||
|
||
def check_top_clearance(chk, obs, pads, segs=()):
|
||
u"""顶层那三块**手画的**铜,离每一个异网的铜必须都 ≥ 0.2 mm。
|
||
|
||
⚠️ 这一条和栅格校核**不是一回事,也替代不了**。栅格是 0.25 mm 一格,
|
||
一处 0.1 mm 的侵入很可能一个格心都不落进去 —— 栅格全绿,铺出来 EDA 自己
|
||
往回缩,于是「板上那块铜」和「这里写的那块铜」不再是同一块,
|
||
后面所有断面数字就都不作数了。这里量的是**真实几何**,不过格子。
|
||
|
||
只对顶层这么严。内层平面本来就是罩住一堆异网通孔、靠反焊盘活着的,
|
||
那里的「挖」是设计意图,不是侵入。
|
||
"""
|
||
tops = [r for r in REGIONS if r['layer'] == L_TOP]
|
||
boxes = []
|
||
for pnet, x, y, hx, hy, th, tag, plane in pads:
|
||
if th or plane == L_TOP:
|
||
boxes.append((pnet, tag, x - hx, y - hy, x + hx, y + hy))
|
||
for onet, olay, a, b, c, d in obs:
|
||
if olay is None: # 过孔:包围盒就是它自己,够准
|
||
boxes.append((onet, u'过孔', a, b, c, d))
|
||
# 走线一律按**真实胶囊**算(见 `poly_seg_gap` 的注释)
|
||
caps = [(l[0], l[2], l[3], l[4], l[5], l[6]) for l in segs if l[1] == L_TOP]
|
||
for r in tops:
|
||
worst, hits = (9e9, ''), []
|
||
for lnet, lx0, ly0, lx1, ly1, lw in caps:
|
||
if lnet == r['net']:
|
||
continue
|
||
g = poly_seg_gap(r['poly'], lx0, ly0, lx1, ly1, lw)
|
||
if g < worst[0]:
|
||
worst = (g, u'走线(%s)' % lnet)
|
||
if g < CLEAR - 1e-6:
|
||
hits.append((g, u'走线', lnet))
|
||
for bnet, tag, a, b, c, d in boxes:
|
||
if bnet == r['net']:
|
||
continue
|
||
if a > max(p[0] for p in r['poly']) + 2 or c < min(p[0] for p in r['poly']) - 2 \
|
||
or b > max(p[1] for p in r['poly']) + 2 or d < min(p[1] for p in r['poly']) - 2:
|
||
continue
|
||
g = poly_rect_gap(r['poly'], a, b, c, d)
|
||
if g < worst[0]:
|
||
worst = (g, u'%s(%s)' % (tag, bnet))
|
||
if g < CLEAR - 1e-6:
|
||
hits.append((g, tag, bnet))
|
||
chk(not hits,
|
||
u'%-13s 离最近的异网铜 %s 有 %.2f mm(下限 %.2f)'
|
||
% (r['name'], worst[1], worst[0], CLEAR),
|
||
u'%s 侵入 %d 处,最近的:%s'
|
||
% (r['name'], len(hits),
|
||
' '.join(u'%s(%s) %.3f mm' % (t, n, g)
|
||
for g, t, n in sorted(hits)[:5])))
|
||
|
||
|
||
def anchors_in(r, pads, obs):
|
||
u"""这一块铺铜里有没有「本网的落脚点」—— 没有就会被整块当孤岛删光。
|
||
|
||
⚠️ **判据按层分**:内层上贴片焊盘根本不在那一层,只有通孔和过孔算;
|
||
外层上本层的贴片焊盘就是落脚点(8 A 那三段全靠它们活着,一个通孔都没有的
|
||
`VINFUSED-TOP` 也不例外)。第一版照抄内层的写法,会把顶层三块全判成「必被删」。
|
||
"""
|
||
outer = r['layer'] in (L_TOP, L_BOT)
|
||
th, smd = [], []
|
||
for pnet, x, y, hx, hy, is_th, tag, plane in pads:
|
||
if pnet != r['net'] or not inside(r['poly'], x, y):
|
||
continue
|
||
(th if (is_th or (outer and plane == r['layer'])) else smd).append(tag)
|
||
for onet, olay, x0, y0, x1, y1 in obs:
|
||
if onet == r['net'] and olay is None \
|
||
and inside(r['poly'], (x0 + x1) / 2.0, (y0 + y1) / 2.0):
|
||
th.append(u'过孔')
|
||
return th, smd
|
||
|
||
|
||
def report(pads, nth, nth_board, obs=()):
|
||
bad = []
|
||
|
||
def chk(ok, good, msg):
|
||
print((u' [OK] ' if ok else u' [!!] ') + (good if ok else msg))
|
||
if not ok:
|
||
bad.append(msg)
|
||
|
||
print(u'── 载流换算(IPC-2221, ΔT=20 °C)──')
|
||
print(u' 内层 0.5 oz: 8A→%.1f 6.4A→%.1f 4A→%.2f 2.8A→%.2f 2A→%.2f 1.6A→%.2f 0.5A→%.2f mm'
|
||
% tuple(width_for(i) for i in (8, 6.4, 4, 2.8, 2, 1.6, 0.5)))
|
||
print(u' 外层 1 oz : 8A→%.2f 4A→%.2f 2.8A→%.2f 0.5A→%.2f mm (同电流只要内层的约 1/4)'
|
||
% tuple(width_for(i, inner=False) for i in (8, 4, 2.8, 0.5)))
|
||
print(u' 通孔焊盘 %d / %d 配上(只有通孔在内层挖反焊盘,贴片对内层透明)' % (nth, nth_board))
|
||
print()
|
||
|
||
# 每个 (网络, 层) 一张栅格
|
||
keys, grids = [], {}
|
||
for r in REGIONS:
|
||
k = (r['net'], r['layer'])
|
||
if k not in grids:
|
||
keys.append(k)
|
||
grids[k] = [x for x in REGIONS if (x['net'], x['layer']) == k]
|
||
G_ = {}
|
||
print(u'── 区域与连通域 ──')
|
||
for k in keys:
|
||
g = Grid(k[0], k[1], grids[k], pads, obs)
|
||
G_[k] = g
|
||
area = sum(g.sizes.values()) * G * G
|
||
comps = sorted(g.sizes.values(), reverse=True)
|
||
lay = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}[k[1]]
|
||
print(u' %-6s %-8s %2d 块区域 / 面积 %6.0f mm² / 挖 %3d 焊盘 + %3d 过孔线 / **%d 个连通域**%s'
|
||
% (lay, k[0], len(grids[k]), area, g.carved, g.carved_obs, g.ncomp,
|
||
u'(碎片 %s mm²)' % ' '.join('%.0f' % (c*G*G) for c in comps[1:6])
|
||
if g.ncomp > 1 else u''))
|
||
print()
|
||
|
||
grid_checks(G_, pads, chk)
|
||
|
||
# 5) 孤岛规则:区域里没有本网通孔焊盘 / 过孔,铺铜会把整块当孤岛删光
|
||
print()
|
||
print(u'── 孤岛规则(载板 pcb2_pour 文件头第 4 条)──')
|
||
print(u' preserveSilos=False + 区域内没有本网焊盘/过孔 = **整块被删光**')
|
||
print(u' (边框还在、nPoured 是 0)⇒ 只靠扇出过孔活着的区域,必须先扇出再铺。')
|
||
for r in REGIONS:
|
||
th_in, smd_in = anchors_in(r, pads, obs)
|
||
if th_in:
|
||
print(u' [OK] %-13s %d 个本网落脚点,自己就活得下来(%s…)'
|
||
% (r['name'], len(th_in), ' '.join(sorted(th_in)[:3])))
|
||
elif smd_in:
|
||
print(u' [--] %-13s 0 个通孔 / %d 个贴片焊盘 ⇒ **必须先扇出**,否则整块被删'
|
||
% (r['name'], len(smd_in)))
|
||
else:
|
||
chk(False, '', u'%s 里一个本网焊盘都没有 —— 铺出来必被删光' % r['name'])
|
||
|
||
# 6) 异网区域之间的净距
|
||
print()
|
||
print(u'── 顶层宽铜到异网铜的真实净距(不过格子)──')
|
||
check_top_clearance(chk, obs, pads, line_segs(False))
|
||
|
||
print()
|
||
print(u'── 手写多边形 vs 它该等于的矩形并集 ──')
|
||
n = 0
|
||
for r in REGIONS:
|
||
if not r.get('rects'):
|
||
continue
|
||
n += 1
|
||
op, orr = poly_vs_rects(r['poly'], r['rects'])
|
||
chk(op == 0 and orr == 0,
|
||
u'%s 的 %d 个点和 %d 个矩形的并集逐格一致'
|
||
% (r['name'], len(r['poly']), len(r['rects'])),
|
||
u'%s 对不上:多边形多出 %d 格、矩形并集多出 %d 格(0.05 mm/格)'
|
||
% (r['name'], op, orr))
|
||
if not n:
|
||
print(u' (没有带 rects 的区域)')
|
||
|
||
print()
|
||
print(u'── 区域间净距(同层异网)──')
|
||
for layer, why in ((L_PWR, u'Inner2 分割电源面'), (L_TOP, u'顶层 8 A 输入链')):
|
||
worst = (9e9, '', '')
|
||
R = [r for r in REGIONS if r['layer'] == layer and r['net'] != 'GND']
|
||
for a in range(len(R)):
|
||
for b in range(a + 1, len(R)):
|
||
if R[a]['net'] == R[b]['net']:
|
||
continue
|
||
d = poly_gap(R[a]['poly'], R[b]['poly'])
|
||
if d < worst[0]:
|
||
worst = (d, R[a]['name'], R[b]['name'])
|
||
if worst[1] == '':
|
||
continue
|
||
chk(worst[0] >= CLEAR - 1e-6,
|
||
u'%s:最近的一对异网区域 %s ↔ %s 相距 %.2f mm(下限 %.2f)'
|
||
% (why, worst[1], worst[2], worst[0], CLEAR),
|
||
u'%s:%s ↔ %s 只相距 %.2f mm,小于 %.2f'
|
||
% (why, worst[1], worst[2], worst[0], CLEAR))
|
||
return bad
|
||
|
||
|
||
def _seg_gap(p1, p2, q1, q2):
|
||
u"""两条线段之间的最小距离。"""
|
||
def pt_seg(px, py, ax, ay, bx, by):
|
||
dx, dy = bx - ax, by - ay
|
||
L = dx * dx + dy * dy
|
||
t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L))
|
||
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
|
||
# 相交则为 0
|
||
d1 = (p2[0]-p1[0]) * (q1[1]-p1[1]) - (p2[1]-p1[1]) * (q1[0]-p1[0])
|
||
d2 = (p2[0]-p1[0]) * (q2[1]-p1[1]) - (p2[1]-p1[1]) * (q2[0]-p1[0])
|
||
d3 = (q2[0]-q1[0]) * (p1[1]-q1[1]) - (q2[1]-q1[1]) * (p1[0]-q1[0])
|
||
d4 = (q2[0]-q1[0]) * (p2[1]-q1[1]) - (q2[1]-q1[1]) * (p2[0]-q1[0])
|
||
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
|
||
return 0.0
|
||
return min(pt_seg(q1[0], q1[1], p1[0], p1[1], p2[0], p2[1]),
|
||
pt_seg(q2[0], q2[1], p1[0], p1[1], p2[0], p2[1]),
|
||
pt_seg(p1[0], p1[1], q1[0], q1[1], q2[0], q2[1]),
|
||
pt_seg(p2[0], p2[1], q1[0], q1[1], q2[0], q2[1]))
|
||
|
||
|
||
def poly_gap(a, b):
|
||
u"""两个简单多边形之间的最小间距。
|
||
|
||
⚠️ **不能用包围盒估**。3V3-SE 是个 L 形,它的包围盒 (73,9)-(148,86.5) 把
|
||
12V-FAN (96.5,66.5)-(112,88) 整个罩进去,按包围盒算出来是 0.00 mm ——
|
||
看着像一条严重违规,实际两块相距 0.5 mm。第一版就是这么误报的。
|
||
"""
|
||
if any(inside(b, x, y) for x, y in a) or any(inside(a, x, y) for x, y in b):
|
||
return 0.0
|
||
best = 9e9
|
||
for i in range(len(a)):
|
||
for j in range(len(b)):
|
||
best = min(best, _seg_gap(a[i], a[(i + 1) % len(a)],
|
||
b[j], b[(j + 1) % len(b)]))
|
||
return best
|
||
|
||
|
||
def ascii_map_top(pads, obs=()):
|
||
u"""0.5 mm 一格,画北左角顶层那三段 8 A 宽铜。
|
||
|
||
这张图是**给人眼看缩腰在哪**的 —— 断面校核只报一个数字,
|
||
看不出「是被 GND 扇出孔啃的还是被自己的边界切的」。
|
||
"""
|
||
g = 0.5
|
||
# 2026-09-08:窗口跟着输入首级西移 —— 旧值 (1,31,65,95) 现在只剩右半条链。
|
||
# 链路实际占 x −17~30.5 / y 70~95.3,取 0.5 mm/格 一行 96 字,够宽。
|
||
x0, x1, y0, y1 = -18.0, 32.0, 69.0, 96.0
|
||
sym = [(VIN_RAW, 'R'), (VIN_FUSED, 'F'), (VIN_SW, 'S')]
|
||
grids = [(s, Grid(net, L_TOP,
|
||
[r for r in REGIONS if r['layer'] == L_TOP and r['net'] == net],
|
||
pads, obs)) for net, s in sym]
|
||
# 障碍(异网焊盘 / 过孔 / 走线)单独标一个 'x',好认出缩腰是被谁啃的
|
||
block = []
|
||
for pnet, px, py, hx, hy, th, tag, plane in pads:
|
||
if th or plane == L_TOP:
|
||
block.append((pnet, px - hx, py - hy, px + hx, py + hy))
|
||
for onet, olay, a, b, c, d in obs:
|
||
if olay is None or olay == L_TOP:
|
||
block.append((onet, a, b, c, d))
|
||
print()
|
||
print(u'── 顶层 8 A 输入链,0.5 mm / 格(R=VIN_RAW F=VIN_FUSED S=VIN_SW '
|
||
u'x=别的网的铜 .=空)──')
|
||
print(' ' + ''.join(str(int(x0 + i * g) % 10) if (i * g) % 5 == 0 else ' '
|
||
for i in range(int((x1 - x0) / g))))
|
||
for j in range(int((y1 - y0) / g) - 1, -1, -1):
|
||
y = y0 + (j + .5) * g
|
||
row = ''
|
||
for i in range(int((x1 - x0) / g)):
|
||
x = x0 + (i + .5) * g
|
||
ch = '.'
|
||
for s, gr in grids:
|
||
ii, jj = gi(x), gj(y)
|
||
if 0 <= ii < gr.nx and 0 <= jj < gr.ny and gr.c[jj][ii]:
|
||
ch = s
|
||
break
|
||
if ch == '.':
|
||
for _n, a, b, c, d in block:
|
||
if a <= x <= c and b <= y <= d:
|
||
ch = 'x'
|
||
break
|
||
row += ch
|
||
print('%5.1f %s' % (y0 + j * g, row))
|
||
|
||
|
||
def ascii_map(pads, obs=()):
|
||
u"""2 mm 一格,画 Inner2 的分块。"""
|
||
g = 2.0
|
||
nx, ny = int(BW / g), int(BH / g)
|
||
sym = {'+12V': '2', '+3V3': '3', 'GND': '.'}
|
||
grids = []
|
||
for net in ('+12V', '+3V3', 'GND'):
|
||
rs = [r for r in REGIONS if r['layer'] == L_PWR and r['net'] == net]
|
||
grids.append((net, Grid(net, L_PWR, rs, pads, obs)))
|
||
print()
|
||
print(u'── Inner2(L3)分块图,2 mm / 格 ──')
|
||
print(' ' + ''.join(str((i * 2 // 10) % 10) for i in range(nx)))
|
||
print(' ' + ''.join(str((i * 2) % 10) for i in range(nx)))
|
||
for j in range(ny - 1, -1, -1):
|
||
row = ''
|
||
for i in range(nx):
|
||
x, y = BX0 + (i + .5) * g, BY0 + (j + .5) * g
|
||
ch = ' '
|
||
for net, gr in grids:
|
||
ii, jj = gi(x), gj(y)
|
||
if 0 <= ii < gr.nx and 0 <= jj < gr.ny and gr.c[jj][ii]:
|
||
ch = sym[net]
|
||
break
|
||
row += ch
|
||
print('%3d %s' % (j * 2, row))
|
||
print(u' 2=+12V 3=+3V3 .=GND 底填 空=反焊盘/禁铺/板外')
|
||
|
||
|
||
# ══════════════════ 下发 ══════════════════
|
||
# 整套流程照抄载板 `pcb2_pour.py`(那边已经跑通两轮),三条硬约定原样保留:
|
||
# 1. **顺序即优先级**:`REGIONS` 的次序就是同层内谁先铺、谁占地盘,GND 底填必须最后。
|
||
# `create()` 的第 7 个参数 `pourPriority` 传什么都没用,也**不能**事后
|
||
# `modify(id, {pourPriority})` 去补 —— 不生效,还会把客户端弄进「后台任务卡在
|
||
# 零进度」的状态(skill §9j / §9k)。
|
||
# 2. **`create()` 成功了也会抛**,返回值和 try/catch 都不能当判据;只能建完之后
|
||
# `getAll()` 回读名字来确认。
|
||
# 3. 多边形点列**末尾要把首点再写一遍**,不闭合的话 `create()` 什么也不建。
|
||
PCB = '7e9415a16a909c51'
|
||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||
"await new Promise(r=>setTimeout(r,1800));" % PCB)
|
||
|
||
|
||
def call(js, timeout=120):
|
||
from eda import run
|
||
r = run(OPEN + js, timeout=timeout)
|
||
if not r.get('success'):
|
||
sys.exit(u'失败: ' + json.dumps(r, ensure_ascii=False)[:500])
|
||
return r['result']
|
||
|
||
|
||
def polysrc(pts):
|
||
u"""点列(mm)-> TPCB_PolygonSourceArray(mil,全直边,首点补在末尾)。"""
|
||
src = [pts[0][0] * MIL, pts[0][1] * MIL, 'L']
|
||
for x, y in list(pts[1:]) + [pts[0]]:
|
||
src += [x * MIL, y * MIL]
|
||
return src
|
||
|
||
|
||
JS_DEL = u"""
|
||
let n = 0;
|
||
for (const o of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(o.primitiveId); n++; }
|
||
return {deleted: n};
|
||
"""
|
||
|
||
JS_MAKE = u"""
|
||
const PLAN = %s;
|
||
// 顺序即优先级:PLAN 已经按「同层内谁该赢谁在前」排好,**不要重排这个循环**。
|
||
for (const p of PLAN) {
|
||
const poly = await eda.pcb_MathPolygon.createComplexPolygon([p.src]);
|
||
if (!poly) continue;
|
||
try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false, p.name, p.prio, 10); }
|
||
catch (e) {}
|
||
}
|
||
return (await eda.pcb_PrimitivePour.getAll())
|
||
.map(x => [String(x.getState_PourName()||'').toUpperCase(), x.getState_PourPriority()]);
|
||
"""
|
||
|
||
# 工具 -> 铺铜管理器... -> 重建所有(菜单项是 React 元素,按钮要派发真事件)
|
||
JS_REBUILD = u"""
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||
const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; };
|
||
const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t);
|
||
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
|
||
const leaf = t => { for (const el of document.querySelectorAll('*'))
|
||
if (el.children.length===0 && (el.textContent||'').trim()===t) return el; return null; };
|
||
const btn = t => { for (const b of document.querySelectorAll('button'))
|
||
if ((b.innerText||'').trim()===t && b.offsetParent!==null) return b; return null; };
|
||
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
|
||
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
|
||
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
|
||
const cancel = btn('取消'); if (cancel) { nat(cancel); await sleep(1200); }
|
||
if (!fire(leaf('工具'))) return {err: 'no 工具'};
|
||
await sleep(900);
|
||
if (!fire(leaf('铺铜管理器...'))) return {err: 'no 铺铜管理器'};
|
||
await sleep(2500);
|
||
const b = btn('重建所有');
|
||
if (!b) return {err: 'no 重建所有'};
|
||
nat(b);
|
||
return {fired: true};
|
||
"""
|
||
|
||
JS_POLL = u"return {poured: (await eda.pcb_PrimitivePoured.getAll()).length};"
|
||
|
||
JS_DONE = u"""
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
|
||
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
|
||
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
|
||
for (const b of document.querySelectorAll('button'))
|
||
if ((b.innerText||'').trim()==='确认' && b.offsetParent!==null) { nat(b); break; }
|
||
await sleep(1500);
|
||
await eda.pcb_Document.save();
|
||
await sleep(2000);
|
||
const fills = [];
|
||
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
|
||
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
|
||
fills.push(f.length);
|
||
}
|
||
return {fills, pour: (await eda.pcb_PrimitivePour.getAll())
|
||
.map(p => [p.getState_PourName(), p.getState_Net(), p.getState_Layer()])};
|
||
"""
|
||
|
||
# 回读**真正铺出来的铜**(不是边框)。坐标单位是 10 mil/单位,在 JS 侧就换成 mm。
|
||
JS_FILLS = u"""
|
||
const U = 3.9370078740157477;
|
||
const byId = {};
|
||
for (const p of await eda.pcb_PrimitivePour.getAll())
|
||
byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''),
|
||
p.getState_PourPriority(), p.getState_Layer(),
|
||
p.getState_Net()];
|
||
const out = [];
|
||
let arcs = 0;
|
||
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
|
||
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
|
||
const info = byId[q.getState_PourPrimitiveId()] || ['?', '?', '?', '?'];
|
||
const rings = [];
|
||
for (const seg of f) for (const ring of ((seg.path && seg.path.complexPolygon) || [])) {
|
||
// 环是 `x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...`
|
||
// **'ARC' 后面第一个数是角度,不是坐标** —— 按奇偶分 x/y 会整体错位。
|
||
const pts = [];
|
||
let i = 0;
|
||
while (i < ring.length) {
|
||
const v = ring[i];
|
||
if (v === 'L') { i++; continue; }
|
||
if (v === 'ARC') { arcs++; i += 2; continue; }
|
||
pts.push(Math.round(ring[i] / U * 1000) / 1000, Math.round(ring[i+1] / U * 1000) / 1000);
|
||
i += 2;
|
||
}
|
||
if (pts.length >= 6) rings.push(pts);
|
||
}
|
||
out.push({name: info[0], prio: info[1], layer: info[2], net: info[3],
|
||
segs: f.length, rings: rings});
|
||
}
|
||
const vias = (await eda.pcb_PrimitiveVia.getAll())
|
||
.map(v => [v.net || '', Math.round(v.x / 39.3700787401575 * 1000) / 1000,
|
||
Math.round(v.y / 39.3700787401575 * 1000) / 1000]);
|
||
return {arcs: arcs, pours: out, vias: vias};
|
||
"""
|
||
|
||
|
||
def _cells_from_rings(rings):
|
||
u"""把一堆环光栅化成布尔格(**奇偶规则**:环套环就是洞)。
|
||
|
||
用扫描线,不要逐格做 point-in-polygon —— 一整层地平面上百个反焊盘环,
|
||
逐格算是 32 万格 x 上百个环,Python 里要跑几分钟。
|
||
"""
|
||
nx, ny = int(BW / G), int(BH / G)
|
||
acc = [[0] * nx for _ in range(ny)]
|
||
for pts in rings:
|
||
xs_ = pts[0::2]
|
||
ys_ = pts[1::2]
|
||
n = len(xs_)
|
||
j0 = max(0, gj(min(ys_)))
|
||
j1 = min(ny - 1, gj(max(ys_)) + 1)
|
||
for j in range(j0, j1 + 1):
|
||
y = gy(j)
|
||
cr = []
|
||
for k in range(n):
|
||
y1, y2 = ys_[k], ys_[(k + 1) % n]
|
||
if (y1 > y) != (y2 > y):
|
||
x1, x2 = xs_[k], xs_[(k + 1) % n]
|
||
cr.append(x1 + (y - y1) * (x2 - x1) / (y2 - y1))
|
||
cr.sort()
|
||
row = acc[j]
|
||
for a in range(0, len(cr) - 1, 2):
|
||
ia = max(0, gi(cr[a]))
|
||
ib = min(nx - 1, gi(cr[a + 1]))
|
||
for i in range(ia, ib + 1):
|
||
xc = gx(i)
|
||
if cr[a] <= xc <= cr[a + 1]:
|
||
row[i] ^= 1
|
||
return [[bool(v) for v in r] for r in acc]
|
||
|
||
|
||
def _grid_from_cells(net, layer, cells):
|
||
g = Grid.__new__(Grid)
|
||
g.net, g.layer = net, layer
|
||
g.ny, g.nx = len(cells), len(cells[0])
|
||
g.c = cells
|
||
g.carved = 0
|
||
g.label()
|
||
return g
|
||
|
||
|
||
FILLS = os.path.join(HERE, '_pcb1_poured.json')
|
||
|
||
|
||
def verify(refresh=True):
|
||
u"""下发之后,按**实测铺出来的铜**再跑一遍连通域 / 断面 / 焊盘覆盖。
|
||
|
||
走的是和下发前完全同一段 `grid_checks()` —— 理由见那个函数的注释。
|
||
"""
|
||
if refresh or not os.path.exists(FILLS):
|
||
d = call(JS_FILLS, timeout=900)
|
||
io.open(FILLS, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False))
|
||
else:
|
||
d = json.load(io.open(FILLS, encoding='utf-8'))
|
||
pours = d['pours']
|
||
print(u'---- 实测铺铜(getState_PourFills 回读)----')
|
||
if d.get('arcs'):
|
||
print(u' 注意:环里有 %d 段圆弧,这里按**弦**处理(反焊盘会算大一丁点)' % d['arcs'])
|
||
cells = {}
|
||
for e in sorted(pours, key=lambda e: (e['layer'], e['name'])):
|
||
npt = sum(len(r) // 2 for r in e['rings'])
|
||
print(u' %-13s net=%-8s layer=%-3s prio=%-3s %3d 段 / %4d 环 / %6d 点'
|
||
% (e['name'], e['net'], e['layer'], e['prio'], e['segs'], len(e['rings']), npt))
|
||
if not e['rings']:
|
||
print(u' [!!] 这一块一个环都没有 —— 铺出来是空的(孤岛被整块删光?)')
|
||
k = (e['net'], e['layer'])
|
||
cells.setdefault(k, []).extend(e['rings'])
|
||
if not pours:
|
||
print(u' [!!] 一块铜都没铺出来')
|
||
return [u'实测铺铜为空']
|
||
print()
|
||
G_ = {}
|
||
for k, rings in cells.items():
|
||
G_[k] = _grid_from_cells(k[0], k[1], _cells_from_rings(rings))
|
||
print(u'-- 实测连通域 --')
|
||
for k in sorted(G_, key=lambda k: (k[1], k[0])):
|
||
g = G_[k]
|
||
area = sum(g.sizes.values()) * G * G
|
||
comps = sorted(g.sizes.values(), reverse=True)
|
||
lay = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}[k[1]]
|
||
print(u' %-6s %-8s 面积 %6.0f mm2 / **%d 个连通域**%s'
|
||
% (lay, k[0], area, g.ncomp,
|
||
u'(碎片 %s mm2)' % ' '.join('%.1f' % (c * G * G) for c in comps[1:6])
|
||
if g.ncomp > 1 else u''))
|
||
print()
|
||
bad = []
|
||
|
||
def chk(ok, good, msg):
|
||
print((u' [OK] ' if ok else u' [!!] ') + (good if ok else msg))
|
||
if not ok:
|
||
bad.append(msg)
|
||
|
||
pads, _n, _nb = load_pads(False)
|
||
|
||
# ⭐ 每一块碎片都要有本网的过孔 / 通孔焊盘,否则那是一块**浮铜**。
|
||
# 铺铜自己的孤岛判定只在「重建」那一刻生效;而它认的是**铺铜对象**,
|
||
# 一个对象铺出好几块的时候(Inner2 的 GND 底填被三块电源面切成 3 段),
|
||
# 单独某一段有没有落脚点它是不管的。浮铜在 DRC 里只会变成几条连接性错误,
|
||
# 混在几百条「还没布线」里根本挑不出来。
|
||
anchors = {} # (网络, 层) -> 落脚点。**层不同,算不算落脚点也不同**(见 anchors_in)
|
||
layers = set(k[1] for k in G_)
|
||
for net, x, y in d.get('vias', ()):
|
||
for lay in layers:
|
||
anchors.setdefault((net, lay), []).append((x, y))
|
||
for pnet, px, py, hx, hy, th, tag, plane in pads:
|
||
for lay in layers:
|
||
if th or (lay in (L_TOP, L_BOT) and plane == lay):
|
||
anchors.setdefault((pnet, lay), []).append((px, py))
|
||
for k in sorted(G_, key=lambda k: (k[1], k[0])):
|
||
g = G_[k]
|
||
live = set()
|
||
for x, y in anchors.get(k, ()):
|
||
i, j = gi(x), gj(y)
|
||
if 0 <= i < g.nx and 0 <= j < g.ny and g.lab[j][i]:
|
||
live.add(g.lab[j][i])
|
||
dead = [(cid, n) for cid, n in g.sizes.items() if cid not in live]
|
||
big = [(c, n) for c, n in dead if n * G * G >= MIN_FLOAT_AREA]
|
||
dust = [(c, n) for c, n in dead if n * G * G < MIN_FLOAT_AREA]
|
||
lay = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}[k[1]]
|
||
if dust:
|
||
print(u' (%s %s 另有 %d 粒 < %.1f mm² 的碎屑,按 MIN_FLOAT_AREA 放过:%s mm²)'
|
||
% (lay, k[0], len(dust), MIN_FLOAT_AREA,
|
||
' '.join('%.2f' % (n * G * G) for _c, n in sorted(dust, key=lambda t: -t[1])[:6])))
|
||
chk(not big,
|
||
u'%s %s 的 %d 块碎片各自都有本网焊盘/过孔落脚' % (lay, k[0], g.ncomp),
|
||
u'%s %s 有 %d 块**浮铜**(没有任何本网落脚点):%s mm²'
|
||
% (lay, k[0], len(big),
|
||
' '.join('%.1f' % (n * G * G) for _c, n in sorted(big, key=lambda t: -t[1])[:6])))
|
||
|
||
grid_checks(G_, pads, chk)
|
||
return bad
|
||
|
||
|
||
def push():
|
||
plan = [(r['name'], r['net'], r['layer'], polysrc(r['poly'])) for r in REGIONS]
|
||
seen, batch = {}, []
|
||
for name, net, layer, src in plan:
|
||
seen[layer] = seen.get(layer, 0) + 1
|
||
batch.append({'name': name, 'net': net, 'layer': layer,
|
||
'src': src, 'prio': seen[layer]})
|
||
print(u'清掉旧铜面: %d 块' % call(JS_DEL)['deleted'])
|
||
made = call(JS_MAKE % json.dumps(batch, ensure_ascii=False), timeout=300)
|
||
print(u'已建: ' + ' '.join(u'%s(prio %s)' % (n, p) for n, p in made))
|
||
got = dict((n, p) for n, p in made)
|
||
miss = [b['name'] for b in batch if b['name'].upper() not in got]
|
||
if miss:
|
||
sys.exit(u'!! 这几块没建出来: ' + ' '.join(miss))
|
||
# 断言**同层内优先级的相对次序**(数值是 1,3,5,... 不连续,不能断言等于名次)
|
||
for layer in sorted(set(b['layer'] for b in batch)):
|
||
seq = [(b['name'], got[b['name'].upper()]) for b in batch if b['layer'] == layer]
|
||
for (na, pa), (nb, pb) in zip(seq, seq[1:]):
|
||
if not pa < pb:
|
||
sys.exit(u'!! L%d 优先级次序乱了:%s(%s) 应排在 %s(%s) 前面 —— '
|
||
u'重叠的铜面会按错的顺序裁剪,先修好再往下走' % (layer, na, pa, nb, pb))
|
||
print(u' 优先级次序 OK:' + ' | '.join(
|
||
u'L%d %s' % (layer, ' < '.join(u'%s(%s)' % (b['name'], got[b['name'].upper()])
|
||
for b in batch if b['layer'] == layer))
|
||
for layer in sorted(set(b['layer'] for b in batch))))
|
||
r = call(JS_REBUILD, timeout=300)
|
||
if r.get('err'):
|
||
sys.exit(u'!! 打不开铺铜管理器: ' + r['err'])
|
||
for _i in range(60):
|
||
time.sleep(4)
|
||
n = call(JS_POLL, timeout=60)['poured']
|
||
print(u' 填充 %d / %d' % (n, len(batch)))
|
||
if n >= len(batch):
|
||
break
|
||
else:
|
||
sys.exit(u'!! 填充一直上不去。看窗口最底下是不是有条卡在 0% 的进度条 —— '
|
||
u'那是客户端卡在后台任务上,location.reload() 之后从 --push 头上重跑'
|
||
u'(重载会丢掉已建的边框)。')
|
||
d = call(JS_DONE, timeout=300)
|
||
print(u'完成:铜面 %d 块,各自填充 %s' % (len(d['pour']), d['fills']))
|
||
|
||
|
||
def main():
|
||
refresh = '--refresh' in sys.argv
|
||
pads, nth, nthb = load_pads(refresh)
|
||
obs = obstacles(False)
|
||
print(u'下板 PCB1 铺铜计划(Inner1=整板地 / Inner2=分割电源面 / Top=8 A 输入链宽铜)')
|
||
print(u' Inner2 区域 %d 块:%s'
|
||
% (len([r for r in REGIONS if r['layer'] == L_PWR]),
|
||
' '.join(r['name'] for r in REGIONS if r['layer'] == L_PWR)))
|
||
print(u' Top 区域 %d 块:%s'
|
||
% (len([r for r in REGIONS if r['layer'] == L_TOP]),
|
||
' '.join(r['name'] for r in REGIONS if r['layer'] == L_TOP)))
|
||
print(u' 8 A 输入链的网名(当场反查,没写死):%s'
|
||
% ' '.join(u'%s=%s' % (nm, net) for net, nm in CHAIN))
|
||
print(u' 板上已有的铜:过孔 %d / 走线 %d(顶层 %d)'
|
||
% (sum(1 for o in obs if o[1] is None), sum(1 for o in obs if o[1] is not None),
|
||
sum(1 for o in obs if o[1] == L_TOP)))
|
||
print()
|
||
for r in REGIONS:
|
||
lay = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}[r['layer']]
|
||
print(u' %-6s %-8s %-13s %s' % (lay, r['net'], r['name'], r['note']))
|
||
print()
|
||
if '--map' in sys.argv:
|
||
ascii_map_top(pads, obs)
|
||
ascii_map(pads, obs)
|
||
return
|
||
if '--verify' in sys.argv: # 只回读实测铜,不改板
|
||
bad = verify('--cache' not in sys.argv)
|
||
_done(bad, u'实测铺铜和计划对得上。')
|
||
return
|
||
bad = report(pads, nth, nthb, obs)
|
||
if '--push' not in sys.argv:
|
||
ascii_map_top(pads, obs)
|
||
ascii_map(pads, obs)
|
||
print()
|
||
# ⚠️ 计划没跑绿就不许下发。铺铜下去之后再发现算错,要连扇出一起推倒重来。
|
||
_done(bad, u'✅ 计划全部通过。' + (u'' if '--push' in sys.argv else
|
||
u'要真的铺到板上加 --push(它自己会先把这一整套再跑一遍)。'
|
||
u'⚠️ 动过摆位的话,先跑扇出三步再回来。'))
|
||
if '--push' not in sys.argv:
|
||
return
|
||
print()
|
||
push()
|
||
print()
|
||
_done(verify(), u'实测铺铜和计划对得上。')
|
||
|
||
|
||
def _done(bad, ok_msg):
|
||
if bad:
|
||
print(u'❌ %d 项没过:' % len(bad))
|
||
for b in bad:
|
||
print(u' · ' + b)
|
||
sys.exit(1)
|
||
print(ok_msg)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|