- 规则:铜层 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
356 行
15 KiB
Python
356 行
15 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""R2 底板 **A2-AUX** 页:两路辅助电源 12V→3V3(U19)与 12V→5V(U30)。
|
||
|
||
/c/Python314/python .claude/r2/a2_aux.py # 只算 + 本地自检
|
||
/c/Python314/python .claude/r2/a2_aux.py --push # 清页 + 重画(幂等)
|
||
|
||
M3 的第二页。**两块都是从 R1 搬过来的**,各自整块平移,块内坐标一个字不动:
|
||
|
||
| 块 | 来源 | 平移 |
|
||
|---|---|---|
|
||
| 3V3(`U19` `C7` `C8` `C9` `C10` `L2` `R2` `R3`)| R1 `P1-INPUT`,正是 `a1_input.py` 里 `DROP` 掉的那 8 件 | 上移 230 |
|
||
| 5V(`U30` `C731~C733` `L3` `C734/C735` `R706/R707`)| R1 `P1B-5V` 整页 | 下移 110 |
|
||
|
||
两块原来的 y 都在 390 附近、x 有 460~795 的重叠段,**不平移就是叠在一起**。
|
||
平移量按两块的实测 bbox 算出来(3V3 块 x100~795 / y390~535,5V 块 x460~900 / y390~665),
|
||
脚本里 `assert_no_overlap()` 每次都重算一遍,改坐标改出重叠会当场终止。
|
||
|
||
## 一处改名:`BST` → `BST3V3`
|
||
|
||
R1 的 3V3 块把三个内部节点用端口起了名:`SW3V3` / `FB3V3` / **`BST`**。
|
||
前两个带了后缀,第三个没有。现在**两个 Buck 同页**,隔壁 5V 那颗的自举节点就在旁边,
|
||
`BST` 这个名字会变成一个等着人踩的坑(下一页随手写个 `BST` 就静默并网了)。⇒ 加后缀。
|
||
|
||
5V 那三个内部节点(SW / BST / FB)**沿用 R1 不起名**:那一块是紧凑画法、
|
||
三个节点各自只有两三厘米导线,不跨页也不铺铜,没有 `A1-INPUT` 里 `VIN_*` 那种理由。
|
||
|
||
## 本页不含的两件事
|
||
|
||
- `U31` AMS1117-3.3(Hub 专用 3V3)在 **`A4-HUB`** —— R1 的 P1B-5V 页上有两行讲它的文字,
|
||
是**说明**不是电路,本页把这两行改写成「它不在这页」。
|
||
- `+5V` 的去处(Hub `U32` + 五个槽的 VBUS)在 `A4-HUB` / `A5-SLOTS`。
|
||
"""
|
||
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, PARTS # noqa: E402
|
||
|
||
PAGE, NAME = '3361444ab04474c5', 'A2-AUX'
|
||
R2_PROJECT = '05a65c22f14846b8b95360ef8d3c1e2e'
|
||
|
||
SRC_3V3 = os.path.join(HERE, '_p1_live.json') # R1 P1-INPUT,只取 3V3 那一块
|
||
SRC_5V = os.path.join(HERE, '_p1b_live.json') # R1 P1B-5V,整页
|
||
KEEP_3V3 = set(['U19', 'C7', 'L2', 'C9', 'C10', 'R2', 'R3', 'C8'])
|
||
|
||
OFF_3V3 = (0, 230) # 3V3 块整体上移,坐到页面上半
|
||
OFF_5V = (0, -110) # 5V 块整体下移
|
||
NET_RENAME = {'BST': 'BST3V3'}
|
||
|
||
# ── 5V 块唯一一处挪位:C733 左移 40(平移之后的坐标)──────────────────
|
||
# R1 那一页上 C733 紧贴 U30 左边,**「100nF」这个值标签正好压在「U30」位号上**,
|
||
# 渲染出来是「100uF0」—— 自举电容的值被读错就是另一颗器件了,属于必须修的那种叠字。
|
||
# 只动这一颗和它自己的两根短脚线,U30 / C731 / C732 / L3 全部原地不动。
|
||
# ⚠ 不要顺手把 (550,490) 也remap 掉:那是 SW 竖线的分叉点,
|
||
# 挪了它 SW 与 +12V 的交叉点会从离 C731.1 的 10 变成离 C732.1 的 10,更糟。
|
||
C733_DX = -40
|
||
WIRE_EDITS = [ # (旧段, 新段),必须一一命中
|
||
(((550, 500), (550, 490)), ((510, 500), (510, 490))),
|
||
(((550, 490), (565, 490)), ((510, 490), (565, 490))),
|
||
(((550, 540), (550, 550)), ((510, 540), (510, 550))),
|
||
(((660, 550), (550, 550)), ((660, 550), (510, 550))),
|
||
]
|
||
|
||
# ── 3V3 块同类的一处叠字:`BST3V3` / `FB3V3` 两个端口的标签压住 C7 的「2.2uF」──
|
||
# 端口的符号 + 标签往锚点**左边**伸出去约 70 个单位(实测),而 C7 的标签在 x 113~134。
|
||
# ⇒ 把两个端口往右挪到 225、顺带把夹在中间的 +12V 旗挪到 245(三者互相让开 ≥20)。
|
||
# 导线只是缩短,U19 侧的端点一个没动。
|
||
EDITS_3V3_W = [
|
||
(((265, 690), (175, 690)), ((265, 690), (225, 690))),
|
||
(((265, 710), (195, 710)), ((265, 710), (225, 710))),
|
||
(((265, 700), (235, 700)), ((265, 700), (245, 700))),
|
||
]
|
||
EDITS_3V3_PT = {('BST3V3', 175, 690): 225, ('FB3V3', 195, 710): 225}
|
||
EDITS_3V3_F = {('+12V', 235, 700): 245}
|
||
|
||
TEXTS = [
|
||
(45, 795, u'【A2-AUX】底板辅助电源:12V→3V3(U19,给 ESP32 与全部逻辑)+ 12V→5V(U30,给 Hub 与手机 VBUS)', 14),
|
||
(45, 545, u'【A】12V → 3V3 / 500mA', 11),
|
||
(45, 528, u'U19 LMR51430YDDCR,Vref 0.6V,', 10),
|
||
(45, 511, u'R2/R3 = 100k/22.1k → 3.315V', 10),
|
||
(45, 494, u'EN 绝对最大 = VIN+0.3V,可直接接 VIN', 10),
|
||
(45, 477, u'Y 后缀 = 1.1MHz(已复核 SLUSEF4A)', 10),
|
||
(45, 450, u'【B】12V → 5V / 2A', 11),
|
||
(45, 433, u'U30 与 U19 同料号,Vref 0.6V,', 10),
|
||
(45, 416, u'R706/R707 = 100k/13.7k → 4.98V', 10),
|
||
(45, 399, u'L3 = SPM6530T-4R7M(与刀片 L1 同料)', 10),
|
||
(45, 382, u'1.1MHz 下 ΔIL = 0.56A,2A 负载', 10),
|
||
(45, 365, u'峰值 2.28A << Isat 6.2A', 10),
|
||
(45, 265, u'5V 负载:正常 <0.5A(关充电后手机基本不从 VBUS 取电);最坏 2.8A(五路 input_suspend 全失效)', 10),
|
||
(45, 248, u'⚠ 两条轨都直接吃 +12V —— 是 Q1 反接保护之后、Rs6 检流之后的那一段,不依赖任何一路手机输出', 10),
|
||
(45, 231, u'⚠ Hub 专用的 U31 AMS1117-3.3 不在本页,在 A4-HUB:它只给 U32 供电(满配约 420mA),挂不到 U19 这条 0.5A 轨上', 10),
|
||
(45, 214, u'内部节点:3V3 侧用端口起了名(SW3V3 / BST3V3 / FB3V3);5V 侧沿用 R1 不起名 —— 紧凑画法,三个节点各自只有两三厘米导线', 10),
|
||
]
|
||
|
||
|
||
def load(path, keep, off):
|
||
u"""读回一页,按 keep 过滤,整块平移。返回 (导线[(a,b,net)], 旗, 端口, place, expect)。
|
||
|
||
⚠ 过滤的判据是「导线组里的引脚属于谁」,不是坐标框 —— 坐标框会把别人的线一起切走。
|
||
"""
|
||
src = json.load(io.open(path, encoding='utf-8'))
|
||
dx, dy = off
|
||
pts = src['wires']
|
||
segs = [((int(round(pts[i][0])) + dx, int(round(pts[i][1])) + dy),
|
||
(int(round(pts[i + 1][0])) + dx, int(round(pts[i + 1][1])) + dy))
|
||
for i in range(0, len(pts), 2)]
|
||
for a, b in segs:
|
||
assert a != b, u'%s 有零长度导线 %s(skill §8a)' % (path, (a,))
|
||
assert (a[0] == b[0]) != (a[1] == b[1]), u'%s 有斜线 %s-%s' % (path, 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
|
||
|
||
gname, gowner, gpins = defaultdict(set), defaultdict(set), defaultdict(set)
|
||
for lst, strip in ((src['flags'], False), (src['ports'], True)):
|
||
for f in lst:
|
||
xy = (int(round(f[2])) + dx, int(round(f[3])) + dy)
|
||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||
assert hit, u'%s 的旗/端口 %s 不在任何导线上' % (path, f[1])
|
||
gname[find(hit[0])].add(NET_RENAME.get(f[1], f[1]))
|
||
for p in src['parts']:
|
||
for q in p[9]:
|
||
xy = (int(round(q[2])) + dx, int(round(q[3])) + dy)
|
||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||
assert hit, u'%s 源页悬空引脚 %s.%s' % (path, p[0], q[0])
|
||
gowner[find(hit[0])].add(p[0])
|
||
gpins[find(hit[0])].add('%s.%s' % (p[0], q[0]))
|
||
|
||
keepgrp = {}
|
||
for r_, owners in gowner.items():
|
||
inside, outside = owners & keep, owners - keep
|
||
assert not (inside and outside), \
|
||
u'%s 的导线组同时挂着 %s 和 %s,切不开' % (path, sorted(inside), sorted(outside))
|
||
keepgrp[r_] = bool(inside)
|
||
|
||
W, F, PT = [], [], []
|
||
for r_, ok in keepgrp.items():
|
||
if not ok:
|
||
continue
|
||
names = gname.get(r_, set())
|
||
assert len(names) <= 1, u'组 %s 名字不唯一:%s' % (sorted(gpins[r_])[:3], sorted(names))
|
||
# 无名的短节点(5V 那颗的 SW / BST / FB)沿用 R1 的自动命名,
|
||
# `#` 开头的名字只给本地自检用,不下发。
|
||
net = sorted(names)[0] if names else '#' + '+'.join(sorted(gpins[r_])[:2])
|
||
for i in range(n):
|
||
if find(i) == r_:
|
||
W.append((segs[i][0], segs[i][1], net))
|
||
|
||
def kept_at(xy):
|
||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||
return keepgrp[find(hit[0])] if hit else False
|
||
|
||
for f in src['flags']:
|
||
xy = (int(round(f[2])) + dx, int(round(f[3])) + dy)
|
||
if kept_at(xy):
|
||
F.append([f[0].split('-')[0], NET_RENAME.get(f[1], f[1]), xy[0], xy[1], f[4]])
|
||
for p in src['ports']:
|
||
xy = (int(round(p[2])) + dx, int(round(p[3])) + dy)
|
||
if kept_at(xy):
|
||
PT.append([p[0].replace('Netport-', ''), NET_RENAME.get(p[1], p[1]),
|
||
xy[0], xy[1], p[4]])
|
||
|
||
byname = {}
|
||
for k, v in PARTS.items():
|
||
byname.setdefault(v['name'], k)
|
||
place, exp = [], {}
|
||
for p in src['parts']:
|
||
d, nm = p[0], p[1]
|
||
if d not in keep:
|
||
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])) + dx, int(round(p[4])) + dy, p[5]])
|
||
exp[d] = dict((str(q[0]), (int(round(q[2])) + dx, int(round(q[3])) + dy))
|
||
for q in p[9])
|
||
return W, F, PT, place, exp
|
||
|
||
|
||
def bbox(W, F, PT, exp):
|
||
xs, ys = [], []
|
||
for a, b, _ in W:
|
||
xs += [a[0], b[0]]
|
||
ys += [a[1], b[1]]
|
||
for it in F + PT:
|
||
xs.append(it[2])
|
||
ys.append(it[3])
|
||
for ps in exp.values():
|
||
for xy in ps.values():
|
||
xs.append(xy[0])
|
||
ys.append(xy[1])
|
||
return min(xs), max(xs), min(ys), max(ys)
|
||
|
||
|
||
def assert_no_overlap(b1, b2, gap=40):
|
||
u"""两块的包围盒必须分开 —— 平移量改错了当场终止,别等截图。"""
|
||
ax0, ax1, ay0, ay1 = b1
|
||
bx0, bx1, by0, by1 = b2
|
||
sep_x = ax1 + gap <= bx0 or bx1 + gap <= ax0
|
||
sep_y = ay1 + gap <= by0 or by1 + gap <= ay0
|
||
assert sep_x or sep_y, \
|
||
u'两块重叠:3V3 x%d..%d y%d..%d vs 5V x%d..%d y%d..%d' % (
|
||
ax0, ax1, ay0, ay1, bx0, bx1, by0, by1)
|
||
|
||
|
||
def text_clash(s, exp):
|
||
u"""文字压在导线 / 引脚 / 旗端口上(同 a1_input.py,本页照用)。"""
|
||
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"""两面旗/端口的符号叠在一起(同 a1_input.py)。"""
|
||
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 edit_wires(W, edits):
|
||
u"""按 (旧段 → 新段) 表改导线,**每一条都必须命中**(改错坐标当场终止)。"""
|
||
norm = lambda s: (min(s[0], s[1]), max(s[0], s[1]))
|
||
todo = dict((norm(o), n) for o, n in edits)
|
||
out = []
|
||
for a, b, net in W:
|
||
k = norm((a, b))
|
||
if k in todo:
|
||
na, nb = todo.pop(k)
|
||
out.append((na, nb, net))
|
||
else:
|
||
out.append((a, b, net))
|
||
assert not todo, u'导线改动表有没命中的段:%s' % sorted(todo)
|
||
return out
|
||
|
||
|
||
def tweak_c733(W, place, exp):
|
||
u"""把 C733 连同它自己的两根短脚线左移 40,解开与 U30 位号的叠字。"""
|
||
out = edit_wires(W, WIRE_EDITS)
|
||
for it in place:
|
||
if it[0] == 'C733':
|
||
it[2] += C733_DX
|
||
exp['C733'] = dict((k, (v[0] + C733_DX, v[1])) for k, v in exp['C733'].items())
|
||
return out
|
||
|
||
|
||
def tweak_3v3(W, F, PT):
|
||
u"""把 BST3V3 / FB3V3 两个端口和夹在中间的 +12V 旗往右挪开 C7 的标签。"""
|
||
out = edit_wires(W, EDITS_3V3_W)
|
||
for lst, table in ((PT, EDITS_3V3_PT), (F, EDITS_3V3_F)):
|
||
hit = 0
|
||
for it in lst:
|
||
key = (it[1], it[2], it[3])
|
||
if key in table:
|
||
it[2] = table[key]
|
||
hit += 1
|
||
assert hit == len(table), u'旗/端口挪位表只命中 %d/%d' % (hit, len(table))
|
||
return out
|
||
|
||
|
||
def build():
|
||
a = load(SRC_3V3, KEEP_3V3, OFF_3V3)
|
||
a = (tweak_3v3(a[0], a[1], a[2]),) + a[1:]
|
||
src5 = json.load(io.open(SRC_5V, encoding='utf-8'))
|
||
all5 = set(p[0] for p in src5['parts'])
|
||
b = load(SRC_5V, all5, OFF_5V)
|
||
b = (tweak_c733(b[0], b[3], b[4]),) + b[1:]
|
||
assert_no_overlap(bbox(*a[:3], exp=a[4]), bbox(*b[:3], exp=b[4]))
|
||
|
||
dup = set(a[4]) & set(b[4])
|
||
assert not dup, u'两块位号撞车:%s' % sorted(dup)
|
||
|
||
s = Sheet(PAGE, NAME)
|
||
for W, F, PT, _pl, _ex in (a, b):
|
||
for seg in W:
|
||
s.W.append(seg)
|
||
s.F += F
|
||
s.PT += PT
|
||
for t in TEXTS:
|
||
s.txt(t[0], t[1], t[2], t[3])
|
||
|
||
place = a[3] + b[3]
|
||
exp = dict(a[4])
|
||
exp.update(b[4])
|
||
return s, place, exp
|
||
|
||
|
||
def main():
|
||
push = '--push' in sys.argv
|
||
s, place, exp = build()
|
||
s.assert_grid()
|
||
# 两块之间只靠 +12V / GND 两条电源旗成网,导线上天然分块
|
||
errs = s.netcheck(exp, {}, set(['GND', '+12V', '+3V3', 'SW3V3', 'FB3V3', 'BST3V3']))
|
||
errs += text_clash(s, exp) + flag_clash(s)
|
||
print(u'3V3 块 %d 件 + 5V 块 %d 件 = %d 件'
|
||
% (len(KEEP_3V3), len(place) - len(KEEP_3V3), len(place)))
|
||
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()
|