- 规则:铜层 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
314 行
16 KiB
Python
314 行
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""R2 底板 **A1-INPUT** 页:12V 入口链 + 输入侧检流 + 母线泄放。
|
||
|
||
/c/Python314/python .claude/r2/a1_input.py # 只算 + 本地自检
|
||
/c/Python314/python .claude/r2/a1_input.py --push # 清页 + 重画(幂等)
|
||
|
||
M3 的第一页。做法同 `b1_power.py`:**搬 R1 的版图,不重排** ——
|
||
R1 的 `P1-INPUT`(`9a6096762e036e23`)整页读回在 `_p1_live.json`,
|
||
本脚本按下面三条改,其余坐标一个字不动。
|
||
|
||
| | 内容 |
|
||
|---|---|
|
||
| **删** | `U19` 3V3 Buck 那一块(`U19` `C7` `L2` `C9` `C10` `R2` `R3` `C8`,8 件)→ 挪到 **`A2-AUX`**,和 `U30` 5V Buck 放一页 |
|
||
| **加** | 入口链三个节点的**网络名**:`VIN_RAW`(J1→F1)/ `VIN_FUSED`(F1→Q1 源)/ `VIN_SW`(Q1 漏→Rs6)|
|
||
| **换** | 文字整套重写(见下) |
|
||
|
||
位号沿用 R1(`J1` `F1` `Q1` `D1` `DZ1` `R1` `C1~C6` `U13` `Rs6` `U29` `R4~R6` `Q3` `R9A~E` `C11`),
|
||
方案文档 §4 的 BOM 归属表就是这么写的。`R2`/`R3` 留给 `A2-AUX`。
|
||
|
||
## 为什么要给入口链三个节点起名
|
||
|
||
R1 上这三段是**无名网络**,PCB 侧只能按 `$1N223` / `$1N227` / `$1N236` 认 ——
|
||
第三十四轮那次「+12V 从 4 个碎片炸成 23 个」的假象就出在这一带。
|
||
底板的这一段要过 **9 A**、要铺铜 + 断面校核(方案文档 §6.4),
|
||
按名字认比按 `$1N…` 认稳得多。三面旗都是 `Power` 型,天然单实例,
|
||
已进 `xpage.py` 的 `NAMING_ONLY`。
|
||
|
||
## ⚠ 两条从 R1 带过来、必须知道的
|
||
|
||
1. **`J1` 是圆形 DC 座,不是 XT30**:脚 1 = 中心极正,脚 2/3 = 负极静/动触点,
|
||
两个都接地并联降接触电阻。R1 页上那句 `J1: 1 = GND(-) 2 = +12V(+)` 是**换座之前的旧文字**,
|
||
本页已改正。⚠ DC-005 家族额定 2~5 A 而整机最坏 9.99 A —— 已知风险、用户明确接受。
|
||
2. **`BLD_REF` 端口与 `R4.1` 同坐标**(`(80,235)`)—— CLAUDE.md 点名的那类坑。
|
||
这里是安全的:`R4.1`→`R5.2` 之间**有一段真导线**,端口落在它的端点上,不是零长度。
|
||
脚本里有断言盯着这一处,重画之后仍然要看 `verify_sch` 的 `BLD_REF` 那一行。
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import sys
|
||
from collections import defaultdict
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from schcheck import Sheet, _on, call # noqa: E402
|
||
|
||
PAGE, NAME = '9d86e72abb1a6571', 'A1-INPUT'
|
||
R2_PROJECT = '05a65c22f14846b8b95360ef8d3c1e2e'
|
||
SRC = os.path.join(HERE, '_p1_live.json')
|
||
|
||
# ── 挪到 A2-AUX 的 3V3 Buck 那一块 ──────────────────────────────────
|
||
DROP = set(['U19', 'C7', 'L2', 'C9', 'C10', 'R2', 'R3', 'C8'])
|
||
|
||
# ── 入口链三个节点:起名 + 挂旗的位置(支线从既有导线上分叉出来)──────
|
||
# (网名, 分叉点, 旗点, 旗 rot);rot 180 = Power 旗的符号朝下
|
||
NEW_FLAGS = [
|
||
# ⚠ VIN_RAW 一开始放在 (190,660),正好压在 J1.3 那面 GND 旗 (185,665) 上 ——
|
||
# crossings() 查不出来(旗不是导线),是 flag_clash() 抓到的。
|
||
('VIN_RAW', (225, 710), (225, 650), 180),
|
||
('VIN_FUSED', (390, 650), (390, 610), 180),
|
||
('VIN_SW', (830, 770), (830, 690), 180),
|
||
]
|
||
# 每个新网络用一个「标志引脚」认出它属于哪个既有导线组
|
||
SIG_PIN = {'VIN_RAW': 'F1.1', 'VIN_FUSED': 'F1.2', 'VIN_SW': 'Rs6.1'}
|
||
|
||
# ⚠ 文字全部塞进「删掉 3V3 Buck 之后空出来的那一带」(x 60~760 / y 330~570)。
|
||
# R1 原来把说明压在元件行上(y 700~790 与 y 190~270),本页元件位置一个没动,
|
||
# 照抄文字坐标就会盖在 J1/F1/Q1/D1/C2 和 R4/R5/R6/U29 上 —— 已经踩过一次。
|
||
TEXTS = [
|
||
(60, 795, u'【A1-INPUT】底板 12V 入口:DC 座 → 保险 → 反接保护 → 储能 → 检流;外加 12V 母线泄放', 14),
|
||
(60, 560, u'【A】输入保护 / 缓启动:J1 → F1 12A → Q1 AO4407 反接保护,栅极 RC 缓启动 tau = R1*C1 = 10ms', 11),
|
||
(60, 543, u'J1 是圆形 DC 座 DC-005-5.5*2.5:脚1 = 中心极(正) → F1;脚2/3 = 负极静/动触点,两个都接地并联降接触电阻', 10),
|
||
(60, 526, u'⚠ DC-005 家族额定 2~5A,而整机最坏 9.99A(设计文档 §22.6)—— 已知风险、用户明确接受:', 10),
|
||
(60, 509, u' 发热点在插头内芯与弹片的接触面,板上加铜救不了。装配前用表核脚序:无插头时 2-3 导通、1 对 2/3 断开', 10),
|
||
(60, 482, u'【E】入口储能与钳位:D1 SMBJ13A 钳位 21.5V;C2/C3 470µF + C4/C5 10µF + C6 100nF', 11),
|
||
(60, 465, u'入口链三个节点已命名:VIN_RAW(J1→F1)/ VIN_FUSED(F1→Q1 源)/ VIN_SW(Q1 漏→Rs6);', 10),
|
||
(60, 448, u' 这一段过 9A,PCB 上要铺铜 + 断面校核(方案 §6.4,≥8mm 净铜宽)', 10),
|
||
(60, 421, u'【B】12V 母线泄放(防 USB 反灌抬高母线)', 11),
|
||
(60, 404, u'阈值 = 2.495V x (R4+R5)/R5 = 2.495 x 5.32 = 13.27V;TL431 导通时阴极 2.0~2.2V → Q3 Vgs = -9.8V 全导通', 10),
|
||
(60, 387, u'R9A~R9E: 5 x 2Ω 2512 串联 = 10Ω/10W,高 0.8mm(立式水泥电阻站起来超高度预算)', 10),
|
||
(60, 370, u'泄放为突发额定:13.3V 下 1.33A / 17.7W,持续故障由固件关 VBUS 兜底', 10),
|
||
(60, 343, u'【C】3V3 辅助电源(U19/L2/C7/C9/C10/R2/R3/C8)已挪到 A2-AUX,和 12V→5V 的 U30 放同一页', 11),
|
||
(620, 245, u'【D】输入侧电流 / 电压监测(整板功率预算)', 11),
|
||
(620, 228, u'Rs6 5mΩ:满板约 9A → 45mV(INA226 满量程 81.92mV / 分辨率 0.5mA)', 10),
|
||
(620, 211, u'U13 地址 A1=GND、A0=SCL → 0x43(地址表不是顺排)。挂 I2C 主干,不经 TCA9548A', 10),
|
||
(620, 194, u'体电容在 Rs6 上游;PCB 上 Rs6 放在体电容与五槽分叉点之间,Kelvin 四线焊盘', 10),
|
||
]
|
||
|
||
|
||
def load():
|
||
u"""读回 R1 的 P1-INPUT,拆成「导线组 → 网络名」。"""
|
||
src = json.load(io.open(SRC, encoding='utf-8'))
|
||
pts = src['wires']
|
||
segs = [((int(round(pts[i][0])), int(round(pts[i][1]))),
|
||
(int(round(pts[i + 1][0])), int(round(pts[i + 1][1]))))
|
||
for i in range(0, len(pts), 2)]
|
||
for a, b in segs:
|
||
assert a != b, u'源页有零长度导线 %s(skill §8a)' % (a,)
|
||
assert (a[0] == b[0]) != (a[1] == b[1]), u'源页有斜线 %s-%s' % (a, b)
|
||
|
||
n = len(segs)
|
||
par = list(range(n))
|
||
|
||
def find(i):
|
||
while par[i] != i:
|
||
par[i] = par[par[i]]
|
||
i = par[i]
|
||
return i
|
||
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
a1, a2 = segs[i]
|
||
b1, b2 = segs[j]
|
||
if (_on(a1, b1, b2) or _on(a2, b1, b2)
|
||
or _on(b1, a1, a2) or _on(b2, a1, a2)):
|
||
ri, rj = find(i), find(j)
|
||
if ri != rj:
|
||
par[ri] = rj
|
||
|
||
pins = {} # 位号 -> {脚号: (x,y)}
|
||
pinat = {} # (x,y) -> '位号.脚号'
|
||
pinname = {} # 位号 -> {脚号: 脚名}
|
||
for p in src['parts']:
|
||
d = p[0]
|
||
pins[d] = dict((str(q[0]), (int(round(q[2])), int(round(q[3])))) for q in p[9])
|
||
pinname[d] = dict((str(q[0]), q[1]) for q in p[9])
|
||
for q in p[9]:
|
||
pinat[(int(round(q[2])), int(round(q[3])))] = '%s.%s' % (d, q[0])
|
||
|
||
# 组 -> 名字 / 组 -> 引脚
|
||
gname, gpins = defaultdict(set), defaultdict(set)
|
||
for it, key in [(src['flags'], 1), (src['ports'], 1)]:
|
||
for f in it:
|
||
xy = (int(round(f[2])), int(round(f[3])))
|
||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||
assert hit, u'旗/端口 %s @%s 不在任何导线上' % (f[key], xy)
|
||
gname[find(hit[0])].add(f[key])
|
||
for d, ps in pins.items():
|
||
for num, xy in ps.items():
|
||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||
assert hit, u'源页悬空引脚 %s.%s @%s' % (d, num, xy)
|
||
gpins[find(hit[0])].add('%s.%s' % (d, num))
|
||
return src, segs, find, pins, pinat, pinname, gname, gpins
|
||
|
||
|
||
def build():
|
||
src, segs, find, pins, pinat, pinname, gname, gpins = load()
|
||
|
||
# ── 1. 判断每个导线组是留是删(必须整组同进退)───────────────────
|
||
keep_grp = {}
|
||
for r_, ps in gpins.items():
|
||
owners = set(p.split('.')[0] for p in ps)
|
||
kept, dropped = owners - DROP, owners & DROP
|
||
assert not (kept and dropped), \
|
||
u'导线组同时挂着留下的 %s 和删掉的 %s,不能整组同进退' % (sorted(kept), sorted(dropped))
|
||
keep_grp[r_] = bool(kept)
|
||
for i in range(len(segs)):
|
||
assert find(i) in keep_grp, u'有导线组一个引脚都不挂:%s' % (segs[i],)
|
||
|
||
# ── 2. 给每个留下的组定网络名 ───────────────────────────────────
|
||
sig2net = dict((v, k) for k, v in SIG_PIN.items())
|
||
gnet = {}
|
||
for r_, keep in keep_grp.items():
|
||
if not keep:
|
||
continue
|
||
names = gname.get(r_, set())
|
||
forced = [sig2net[p] for p in gpins[r_] if p in sig2net]
|
||
if forced:
|
||
assert not names, u'组 %s 本来就有名字 %s,不该再起名' % (sorted(gpins[r_]), names)
|
||
assert len(set(forced)) == 1, forced
|
||
gnet[r_] = forced[0]
|
||
elif not names:
|
||
# R1 上本来就无名的短节点(泄放 MOS 漏极、R9A~E 之间的 4 个串联节点)。
|
||
# 这些段只走 1.33A 突发、走线两三毫米,不铺铜也不做断面校核,
|
||
# 沿用 R1 的「让 EasyEDA 自动起名」。`#` 开头的名字只给本地自检用,不下发。
|
||
gnet[r_] = '#' + '+'.join(sorted(gpins[r_])[:2])
|
||
else:
|
||
assert len(names) == 1, \
|
||
u'组 %s 的名字不唯一:%s' % (sorted(gpins[r_])[:4], sorted(names))
|
||
gnet[r_] = sorted(names)[0]
|
||
|
||
s = Sheet(PAGE, NAME)
|
||
for i, (a, b) in enumerate(segs):
|
||
r_ = find(i)
|
||
if keep_grp[r_]:
|
||
s.w(gnet[r_], a, b)
|
||
|
||
# ── 3. 旗 / 端口:删掉挂在被删组上的 ─────────────────────────────
|
||
def kept_at(xy):
|
||
hit = [i for i in range(len(segs)) if _on(xy, segs[i][0], segs[i][1])]
|
||
return keep_grp[find(hit[0])] if hit else False
|
||
|
||
for f in src['flags']:
|
||
xy = (int(round(f[2])), int(round(f[3])))
|
||
if kept_at(xy):
|
||
# 读回来的符号名是 'Ground-GND' / 'Power-5V',createNetFlag 只认前缀
|
||
s.F.append([f[0].split('-')[0], f[1], xy[0], xy[1], f[4]])
|
||
for p in src['ports']:
|
||
xy = (int(round(p[2])), int(round(p[3])))
|
||
if kept_at(xy):
|
||
s.PT.append([p[0].replace('Netport-', ''), p[1], xy[0], xy[1], p[4]])
|
||
|
||
# ── 4. 入口链三个节点起名:从既有导线上分叉出一小段,挂 Power 旗 ──
|
||
for net, tap, flag, rot in NEW_FLAGS:
|
||
sig = SIG_PIN[net]
|
||
onseg = [i for i in range(len(segs)) if _on(tap, segs[i][0], segs[i][1])]
|
||
assert onseg, u'%s 的分叉点 %s 不在任何导线上' % (net, tap)
|
||
assert gnet.get(find(onseg[0])) == net, \
|
||
u'%s 的分叉点 %s 落在了 %s 上' % (net, tap, gnet.get(find(onseg[0])))
|
||
assert sig in gpins[find(onseg[0])], u'%s 的组里没有 %s' % (net, sig)
|
||
assert tap not in pinat, u'%s 的分叉点压在引脚 %s 上' % (net, pinat[tap])
|
||
s.w(net, tap, flag)
|
||
s.pwr(net, flag[0], flag[1], rot)
|
||
|
||
for t in TEXTS:
|
||
s.txt(t[0], t[1], t[2], t[3])
|
||
|
||
place, exp = [], {}
|
||
from schcheck import PARTS
|
||
byname = {}
|
||
for k, v in PARTS.items():
|
||
byname.setdefault(v['name'], k)
|
||
for p in src['parts']:
|
||
d, nm = p[0], p[1]
|
||
if d in DROP:
|
||
continue
|
||
base = nm.split('_C')[0] if '_C' in nm else nm
|
||
k = byname.get(nm) or byname.get(base)
|
||
assert k, u'eda-parts.json 里找不到器件 %r(%s)' % (nm, d)
|
||
assert not p[6], u'%s 是镜像件,Sheet.place 处理不了' % d
|
||
place.append([d, k, int(round(p[3])), int(round(p[4])), p[5]])
|
||
exp[d] = dict((str(q[0]), (int(round(q[2])), int(round(q[3])))) for q in p[9])
|
||
|
||
# CLAUDE.md 点名的那一处:端口与引脚同坐标,靠一段真导线兜着
|
||
assert ('BLD_REF', 80, 235) in [(x[1], x[2], x[3]) for x in s.PT], \
|
||
u'BLD_REF @(80,235) 那个端口丢了'
|
||
assert any(_on((80, 235), a, b) and a != b for a, b, _ in s.W), \
|
||
u'BLD_REF @(80,235) 底下没有真导线 —— 就是 CLAUDE.md 说的零长度那类坑'
|
||
|
||
return s, place, exp, pinname
|
||
|
||
|
||
def text_clash(s, exp):
|
||
u"""文字压在导线 / 引脚 / 旗端口上 —— schcheck 只查右边界,这一条是本页新加的。
|
||
|
||
宽度按 skill §12 的经验值(全角 = 字号,半角 = 半个字号),
|
||
高度保守地取 ±0.8 字号。宁可误报,也不要再出现「说明盖住 J1/F1/Q1」那种图。
|
||
"""
|
||
errs = []
|
||
boxes = []
|
||
for x, y, txt, size in s.TX:
|
||
cjk = sum(1 for ch in txt if ord(ch) > 0x2E7F)
|
||
w = size * (cjk + 0.5 * (len(txt) - cjk))
|
||
boxes.append((x, y - size * 0.8, x + w, y + size * 0.8, txt))
|
||
|
||
pts = []
|
||
for d, ps in exp.items():
|
||
for num, xy in ps.items():
|
||
pts.append((xy[0], xy[1], u'引脚 %s.%s' % (d, num)))
|
||
for it in s.F + s.PT:
|
||
pts.append((it[2], it[3], u'旗/端口 %s' % it[1]))
|
||
|
||
for x0, y0, x1, y1, txt in boxes:
|
||
for px, py, what in pts:
|
||
if x0 <= px <= x1 and y0 <= py <= y1:
|
||
errs.append(u'文字压住 %s @(%d,%d): %s' % (what, px, py, txt[:26]))
|
||
for a, b, net in s.W:
|
||
if a[1] == b[1]: # 横线
|
||
if y0 <= a[1] <= y1 and min(a[0], b[0]) <= x1 and max(a[0], b[0]) >= x0:
|
||
errs.append(u'文字压住导线 %s y=%d: %s' % (net, a[1], txt[:26]))
|
||
else: # 竖线
|
||
if x0 <= a[0] <= x1 and min(a[1], b[1]) <= y1 and max(a[1], b[1]) >= y0:
|
||
errs.append(u'文字压住导线 %s x=%d: %s' % (net, a[0], txt[:26]))
|
||
return sorted(set(errs))
|
||
|
||
|
||
def flag_clash(s, gx=20, gy=10):
|
||
u"""两面旗/端口的**符号**叠在一起 —— crossings() 查不到(旗不是导线)。
|
||
|
||
判据按符号尺寸定,不是按标签:一格(10)以内上下叠 **且** 横向也挨着才算叠。
|
||
同一个 x 上下差一格排开(R1 的 `BLD_REF`/`BLD_A` 就是这么放的)是正常写法,不报。
|
||
"""
|
||
errs, items = [], [(it[1], it[2], it[3]) for it in s.F + s.PT]
|
||
for i in range(len(items)):
|
||
for j in range(i + 1, len(items)):
|
||
(n1, x1, y1), (n2, x2, y2) = items[i], items[j]
|
||
if abs(x1 - x2) < gx and abs(y1 - y2) < gy:
|
||
errs.append(u'旗/端口 %s @(%d,%d) 与 %s @(%d,%d) 挨太近'
|
||
% (n1, x1, y1, n2, x2, y2))
|
||
return sorted(set(errs))
|
||
|
||
|
||
def main():
|
||
push = '--push' in sys.argv
|
||
s, place, exp, _pinname = build()
|
||
s.assert_grid()
|
||
errs = s.netcheck(exp, {}, set(['GND', '+12V', '+3V3', 'GATE',
|
||
'BLD_A', 'BLD_REF']))
|
||
errs += text_clash(s, exp) + flag_clash(s)
|
||
print(u'搬 R1 P1-INPUT:%d 件(删掉 3V3 Buck 那 %d 件)' % (len(place), len(DROP)))
|
||
s.report(len(place), errs)
|
||
if not push:
|
||
return
|
||
call("await eda.dmt_Project.openProject('%s'); await new Promise(r=>setTimeout(r,2500)); return 1;"
|
||
% R2_PROJECT)
|
||
s.clear()
|
||
s.place(place, exp)
|
||
s.push_geometry()
|
||
print(u'接着跑 verify_sch.py --nets')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|