文件
phonerail/.claude/r2/a2_aux.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

356 行
15 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()