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

445 行
20 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 刀片 **B2-CTRL** 页:PCA9555 + 侧键 MOS ×3 + 本槽 I²C 上拉。
/c/Python314/python .claude/r2/b2_ctrl.py # 只算 + 本地自检
/c/Python314/python .claude/r2/b2_ctrl.py --push # 清页 + 重画(幂等)
## 这一页画什么
| 位号 | 器件 | 作用 |
|---|---|---|
| `U4` | PCA9555PW(0x20)| 3 个侧键 + `EDL_EN` + `VBUS_EN` 输出;4 路状态输入 |
| `Q3/Q4/Q5` | 2N7002 | 侧键低边 MOS(R1 §13.3 原样:1k 串阻 + 100k 栅极下拉)|
| `R40/R41` | 4.7k | 本槽 mux 分支的 I²C 上拉(每片刀片各自一套)|
| `R42~R47` | 1k ×3 + 100k ×3 | 三个栅极的串阻与下拉 |
| `C40` | 100nF | PCA9555 去耦 |
## ⚠️ 2026-09-08 改:**DAC 挪到底板**(推翻 D11 的「一片刀片一颗」)
原来这一页还有 `U4 DAC63204`(只用 CH0)。用户拍板改成**底板 2 颗 DAC63204 喂 5 路**
—— 就是 R1 的 U6/U7 那套原样搬,固件 A4 驱动、NVM 预置 0.548V、
§4.5 的三条外围硬要求、通道映射表全部现成,不引入任何新料。
**为什么敢让 `VDAC` 跨连接器**(设计文档 §4.4 的失效表已经算过):
| 故障 | Vd | Vout |
|---|---|---|
| 金手指断了 / 空槽 | 悬空 | **3.890 V** ✅ 正好是目标值 |
| 对地短路 | 0 V | 4.010 V ✅ |
| 短到 +3V3 | 3.3 V | 3.35 V ✅ 安全侧(手机关机)|
注入增益只有 **0.2007**,线上噪声和两板之间的地差到输出都**衰减 5 倍**;
而 §4.5 要求的 `R8 1k + C12 10nF` 低通留在刀片上(Rinj 的 DAC 侧),
正好把长线拾到的东西全滤掉 —— **B1-POWER 一个字节不用改**。
⇒ 位号顺移:PCA9555 从 `U5` 变 **`U4`**,B3-PHONE 的 TPS22918 / TS5A3157
相应变成 `U5` / `U6`(方案 §7.2b 已同步)。
## 本页不放的四样(别在这里找)
- **DAC** -> 底板 `A3-CTRL`(2 × DAC63204,0x4A / 0x4B)
- `EDL_EN` / `VBUS_EN` 的 100k 下拉 -> **B3-PHONE**(贴各自的负载,R1 R741/R701 就在那边)
- `INT#` 的上拉 -> **底板 A3-CTRL**(五槽 wire-OR 共用一颗 10k,刀片上放会并成 2k)
- `EFUSE_EN` -> **不经 PCA9555**,走金手指专用针直连底板 ESP32(方案 §7.3 的框)
## 几何约定
skill §6b/§6c:`create()` 的 rotation 语义同一个料号在不同页都可能相反,
所以放完逐个 `getAllPins()` 读回、和 `EXPECT` 比对,对不上就翻 180 重试(最多 3 轮)。
两脚件按**坐标集合**比(非极性件翻转只换脚号,无害)。
"""
import io
import json
import os
import sys
from collections import defaultdict
HERE = os.path.dirname(os.path.abspath(__file__))
# ⚠ `.claude` 下有和 r2 同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place)。
# 只插父目录的话,**后面任何一个 `import pcb_rules` 都会拿到 R1 那一份**,
# 而且不报错,只在调到 R2 专有的函数时才炸(2026-09-08 踩过)。
# 规矩:两行都要写,HERE 后插(在 sys.path 最前)。
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
PAGE = '4f458d678c50f7a4' # BLADE / B2-CTRL
PAGE_NAME = 'B2-CTRL'
PARTS = json.load(io.open(os.path.join(os.path.dirname(HERE), 'eda-parts.json'),
encoding='utf-8'))['parts']
PCA, MOS = 'C2688893', 'C8545'
R4K7, R1K, R100K, C100N = 'C25900', 'C11702', 'C25741', 'C1525'
U4 = (420, 640) # PCA9555,rot 0:输出在左、输入与 I²C 在右
PU = [(700, 'SDA'), (770, 'SCL')] # 两颗 4.7k 上拉的 x
BX = [250, 550, 850] # 三路侧键 MOS 的中心 x
KEYS = ['PWR', 'VUP', 'VDN']
# ── 引脚期望(相对中心的偏移,来自 R1 P3B-EXPAND / C4-SITE1 实测)──────────
PCA_REL = dict([(str(i + 1), (-50, 55 - 10 * i)) for i in range(12)]
+ [(str(13 + i), (50, -55 + 10 * i)) for i in range(12)])
MOS_REL = {'1': (-20, 0), '2': (10, -20), '3': (10, 20)} # G / S / D
NC = {} # 本页没有有意不接的脚
ALLOW_SPLIT = set(['GND', '+3V3', 'SDA', 'SCL'] +
['KEY_' + k for k in KEYS]) # 靠端口连起来的
def call(js, timeout=180):
r = run(js, timeout=timeout)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:900])
return r['result']
def plan():
W, F, PT = [], [], []
def w(net, *pts):
for a, b in zip(pts, pts[1:]):
if a != b:
W.append((a, b, net))
def gnd(x, y, rot):
F.append(['Ground', 'GND', x, y, rot])
def pwr(x, y, rot):
F.append(['Power', '+3V3', x, y, rot])
def port(d, net, x, y, rot):
PT.append([d, net, x, y, rot])
# ── I²C 上拉(本槽 mux 分支各自一套;INA226 在 B1-POWER,也吃这两颗)──
for x, net in PU:
w(net, (x, 700), (x, 675))
port('BI', net, x, 675, 0)
w('+3V3', (x, 740), (x, 765))
pwr(x, 765, 0)
# ── U4 PCA9555 ───────────────────────────────────────────────────────
px, py = U4
PL, PR = px - 50, px + 50
w('INT#', (PL, py + 55), (330, py + 55), (330, 745)) # 1 INT#
port('OUT', 'INT#', 330, 745, 0)
w('GND', (PL, py + 45), (300, py + 45)) # 2 A1
w('GND', (PL, py + 35), (300, py + 35)) # 3 A2
w('GND', (300, py + 35), (300, 725))
gnd(300, 725, 180)
OUTS = [('KEY_PWR', 25), ('KEY_VUP', 15), ('KEY_VDN', 5),
('EDL_EN', -5), ('VBUS_EN', -15)] # IO0_0 ~ IO0_4
for net, dy in OUTS:
w(net, (PL, py + dy), (300, py + dy))
port('OUT', net, 300, py + dy, 180)
for dy in (-25, -35, -45, -55): # IO0_5~7 + VSS
w('GND', (PL, py + dy), (320, py + dy))
w('GND', (320, py - 25), (320, 555))
gnd(320, 555, 0)
INS = [('EFUSE_FLT', -55), ('BUCK_PG', -45),
('INA_ALERT', -35), ('EFUSE_PG', -25)] # IO1_0 ~ IO1_3
for net, dy in INS:
w(net, (PR, py + dy), (510, py + dy))
port('IN', net, 510, py + dy, 180)
for dy in (-15, -5, 5, 15, 25): # IO1_4~7 + A0
w('GND', (PR, py + dy), (570, py + dy))
w('GND', (570, py - 15), (570, 720))
gnd(570, 720, 180)
w('SCL', (PR, py + 35), (490, py + 35))
port('BI', 'SCL', 490, py + 35, 0)
w('SDA', (PR, py + 45), (490, py + 45))
port('BI', 'SDA', 490, py + 45, 0)
w('+3V3', (PR, py + 55), (520, py + 55)) # 24 VDD -> C40 下脚
w('+3V3', (490, py + 55), (490, 730))
pwr(490, 730, 0)
w('GND', (520, 735), (520, 760))
gnd(520, 760, 180)
# ── 侧键 MOS ×3(R1 §13.3:栅极 1k 串阻 + 100k 下拉,源极接地)────────
for k, bx in enumerate(BX):
g, kg = 'KEY_' + KEYS[k], 'KG_' + KEYS[k]
w(kg, (bx - 20, 400), (bx - 60, 400)) # 栅极
w(kg, (bx - 60, 420), (bx - 60, 400)) # 1k 下脚
w(kg, (bx - 60, 380), (bx - 60, 400)) # 100k 上脚
w(g, (bx - 60, 460), (bx - 60, 500)) # 1k 上脚 -> 端口
port('IN', g, bx - 60, 500, 0)
w('GND', (bx - 60, 340), (bx - 60, 315))
gnd(bx - 60, 315, 0)
w('GND', (bx + 10, 380), (bx + 10, 340)) # 源极
gnd(bx + 10, 340, 0)
w('KPAD_' + KEYS[k], (bx + 10, 420), (bx + 10, 465)) # 漏极 -> 中框焊盘
port('OUT', 'KPAD_' + KEYS[k], bx + 10, 465, 0)
TX = [
(45, 790, u'【B2-CTRL】刀片控制页:PCA9555 + 侧键 MOS ×3(DAC 已挪到底板)', 14),
(45, 275, u'U4 PCA9555(A0/A1/A2 全接地 ⇒ 0x20):P0.0~P0.2 三个侧键 / P0.3 EDL_EN / '
u'P0.4 VBUS_EN 为输出;P1.0~P1.3 收 EFUSE_FLT / BUCK_PG / INA_ALERT / EFUSE_PG。', 10),
(45, 257, u'⚠ 备用脚 P0.5~P0.7 / P1.4~P1.7 直接接地:固件严禁把它们配成输出,'
u'否则输出高电平直接对地短路(R1 §17.2 同款约束)。', 10),
(45, 239, u'⚠ 复位安全(D9):PCA9555 上电为高阻输入 + 栅极 100k 下拉 ⇒ '
u'三个侧键必然松开、VBUS 必然断开,不依赖任何软件动作。', 10),
(45, 221, u'2026-09-08 改:DAC63204 挪到底板(2 颗喂 5 路,沿用 R1 的 U6/U7 与通道映射表)。'
u'VDAC 从金手指进来,直接去 B1-POWER 的 R8/C12 低通 + Rinj。', 10),
(45, 203, u'⚠ VDAC 跨连接器是安全的(设计文档 §4.4):断线 = Rinj 悬空 = 3.890V;对地短路 = 4.010V。'
u'注入增益 0.2007,线上噪声与两板地差到输出衰减 5 倍。', 10),
(45, 185, u'⚠ EDL_EN / VBUS_EN 的 100k 下拉在 B3-PHONE;INT# 的上拉在底板(五槽 wire-OR)。', 10),
(45, 167, u'⚠ EFUSE_EN 不经 PCA9555,直连底板 ESP32(方案 §7.3)。', 10),
(45, 149, u'4.7k I²C 上拉每片刀片自带;INA226 仍在 B1-POWER。', 10),
]
# 文字越界自检(skill §12 的宽度经验值 + §8c 的标题栏范围)——
# 这一条几何验证抓不到:文字压在标题栏上,六项检查全过,只有截图看得见。
for x, y, s, size in TX:
cjk = sum(1 for ch in s if ord(ch) > 0x2E7F)
right = x + size * (cjk + 0.5 * (len(s) - cjk))
limit = 455 if y < 192 else 1145
assert right <= limit, u'文字右边界 %.0f 越过 %d(y=%d): %s' % (right, limit, y, s[:24])
# ── 下发前的几何断言(skill §3 / §6d / §8a-2b)────────────────────────
for a, b, net in W:
assert (a[0] == b[0]) != (a[1] == b[1]), u'%s 段不是横平竖直: %s->%s' % (net, a, b)
for v in (a[0], a[1], b[0], b[1]):
assert v % 5 == 0, u'%s 坐标 %d 不是 5 的倍数' % (net, v)
for it in F + PT:
assert it[2] % 5 == 0 and it[3] % 5 == 0, u'旗/端口 %s 坐标不是 5 的倍数' % (it[1],)
assert 20 <= it[2] <= 1145 and 10 <= it[3] <= 812, u'旗/端口 %s 出框' % (it[1],)
return W, F, PT, TX
# ── 本地几何自检(skill §10b)─────────────────────────────────────────────
def _on(p, a, b):
if a[0] == b[0]:
return p[0] == a[0] and min(a[1], b[1]) <= p[1] <= max(a[1], b[1])
return p[1] == a[1] and min(a[0], b[0]) <= p[0] <= max(a[0], b[0])
def netcheck(W, F, PT, pins):
errs = []
n = len(W)
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, _ = W[i]
b1, b2, _ = W[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
grp = defaultdict(list)
for i in range(n):
grp[find(i)].append(i)
pieces = defaultdict(int)
for root, idx in grp.items():
names = sorted(set(W[i][2] for i in idx))
if len(names) > 1:
errs.append(u'短路: ' + ' + '.join(names) + u' :: '
+ ' | '.join('%s%s-%s' % (W[i][2], W[i][0], W[i][1]) for i in idx[:6]))
pieces[names[0]] += 1
for name, cnt in sorted(pieces.items()):
if cnt > 1 and name not in ALLOW_SPLIT:
errs.append(u'网络 %s 被拆成 %d 块(漏连)' % (name, cnt))
for d, pd in pins.items():
for num, xy in pd.items():
if num in NC.get(d, []):
continue
if not any(_on(tuple(xy), s[0], s[1]) for s in W):
errs.append(u'悬空引脚: %s.%s @ %s' % (d, num, tuple(xy)))
# 旗/端口:必须落在导线**端点**,且不能是合并后母线的中段(skill §8a-2 / §8a-3)
for kind, lst in ((u'旗', F), (u'端口', PT)):
for it in lst:
p = (it[2], it[3])
through = any((s[0][0] == s[1][0] and p[0] == s[0][0]
and min(s[0][1], s[1][1]) < p[1] < max(s[0][1], s[1][1]))
or (s[0][1] == s[1][1] and p[1] == s[0][1]
and min(s[0][0], s[1][0]) < p[0] < max(s[0][0], s[1][0]))
for s in W)
ends = [s for s in W if s[0] == p or s[1] == p]
if through:
errs.append(u'%s %s @ %s 压在导线中段' % (kind, it[1], p))
elif not ends:
errs.append(u'%s %s @ %s 不在任何导线端点上' % (kind, it[1], p))
else:
ax = set(('V' if s[0][0] == s[1][0] else 'H') for s in ends)
if len(ends) > 1 and len(ax) == 1:
errs.append(u'%s %s @ %s 两段共线,合并后会变成中段' % (kind, it[1], p))
return errs
def crossings(W):
out = []
for i in range(len(W)):
for j in range(i + 1, len(W)):
a1, a2, na = W[i]
b1, b2, nb = W[j]
if na == nb:
continue
ha, hb = a1[1] == a2[1], b1[1] == b2[1]
if ha == hb:
continue
h, v = ((a1, a2), (b1, b2)) if ha else ((b1, b2), (a1, a2))
y, x = h[0][1], v[0][0]
if (min(h[0][0], h[1][0]) < x < max(h[0][0], h[1][0])
and min(v[0][1], v[1][1]) < y < max(v[0][1], v[1][1])):
out.append((na, nb, x, y))
return out
def placement():
px, py = U4
out = [['U4', PCA, px, py, 0], ['C40', C100N, 520, 715, 270],
['R40', R4K7, PU[0][0], 720, 270], ['R41', R4K7, PU[1][0], 720, 270]]
for k, bx in enumerate(BX):
out += [['Q%d' % (k + 3), MOS, bx, 400, 0],
['R%d' % (42 + 2 * k), R1K, bx - 60, 440, 270],
['R%d' % (43 + 2 * k), R100K, bx - 60, 360, 270]]
return out
def expect(place):
u"""期望引脚坐标。IC 逐脚;两脚件只比坐标集合(非极性件翻转只换脚号)。"""
exp = {}
for d, lcsc, x, y, rot in place:
if lcsc == PCA:
exp[d] = dict((k, (x + dx, y + dy)) for k, (dx, dy) in PCA_REL.items())
elif lcsc == MOS:
exp[d] = dict((k, (x + dx, y + dy)) for k, (dx, dy) in MOS_REL.items())
else:
exp[d] = {'?': set([(x, y + 20), (x, y - 20)])}
return exp
def main():
push = '--push' in sys.argv
place = placement()
for it in place:
assert it[1] in PARTS, u'eda-parts.json 里没有 %s' % it[1]
exp = expect(place)
pins = {}
for d, want in exp.items():
if '?' in want:
a, b = sorted(want['?'])
pins[d] = {'1': a, '2': b}
else:
pins[d] = dict(want)
W, F, PT, TX = plan()
errs = netcheck(W, F, PT, pins)
print(u'元件 %d 导线 %d 旗 %d 端口 %d 文字 %d'
% (len(place), len(W), len(F), len(PT), len(TX)))
xs = crossings(W)
print(u'交叉点(不相连,供人工确认): %d %s' % (len(xs), xs if xs else ''))
if errs:
print(u'本地自检 %d 条问题:' % len(errs))
for e in errs:
print(u' ! ' + e)
raise SystemExit(1)
print(u'本地自检通过(短路 0 / 漏连 0 / 悬空 0 / 旗端口全在端点)')
if not push:
return
n = call("""
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1200));
const nm=(await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
if (nm!=='%s') throw new Error('page='+nm);
let k=0;
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()==='sheet') continue;
await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); k++;
}
for (const w of await eda.sch_PrimitiveWire.getAll()) await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId());
for (const t of await eda.sch_PrimitiveText.getAll()) await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId());
return k;
""" % (PAGE, PAGE_NAME))
print(u'%s 已清空(删掉 %d 个图元)' % (PAGE_NAME, n))
put = [[d, PARTS[l]['uuid'], x, y, rot, PARTS[l]['lib']] for d, l, x, y, rot in place]
for it in put:
assert len(it) == 6 and it[5], u'placement 少了 libraryUuid: %s' % (it,) # skill §9c-2
for i in range(0, len(put), 4):
call("""
for (const [d,uuid,x,y,rot,lib] of %s){
await eda.sch_PrimitiveComponent.create({libraryUuid:lib, uuid}, x, y, undefined, rot, false, true, true);
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()!=='part') continue;
if (Math.round(c.getState_X())!==x || Math.round(c.getState_Y())!==y) continue;
await c.toAsync().setState_Designator(d).done();
}
}
return 1;
""" % json.dumps(put[i:i + 4]))
print(u'已放置 %d 个元件' % len(put))
READ = """
const out={};
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()!=='part') continue;
out[c.getState_Designator()] = (await c.getAllPins()).map(
p=>[p.getState_PinNumber(), Math.round(p.getState_X()), Math.round(p.getState_Y())]);
}
return out;
"""
rots = dict((d, rot) for d, _l, _x, _y, rot in place)
for rnd in range(4):
got = call(READ)
bad = []
for d, want in exp.items():
if d not in got:
raise SystemExit(u'读不到元件 %s' % d)
have = dict((p, (x, y)) for p, x, y in got[d])
if '?' in want:
if set(have.values()) != want['?']:
bad.append((d, sorted(want['?']), sorted(have.values())))
else:
for p, xy in sorted(want.items()):
if have.get(p) != xy:
bad.append((d, p, xy, have.get(p)))
break
if not bad:
print(u'引脚坐标逐个核对通过(%d 个元件)' % len(exp))
break
if rnd == 3:
raise SystemExit(u'引脚坐标对不上:\n '
+ '\n '.join(repr(b) for b in bad))
names = sorted(set(b[0] for b in bad))
print(u'第 %d 轮:翻转 %d 件重试 %s' % (rnd + 1, len(names), names))
call("""
for (const [d,rot] of %s){
for (const c of await eda.sch_PrimitiveComponent.getAll()){
if (c.getState_ComponentType()!=='part') continue;
if (c.getState_Designator()!==d) continue;
await c.toAsync().setState_Rotation(rot).done();
}
}
return 1;
""" % json.dumps([[d, (rots[d] + 180 * (rnd + 1)) % 360] for d in names]))
def batch(items, size, js):
for i in range(0, len(items), size):
call(js % json.dumps(items[i:i + size], ensure_ascii=False))
batch([[a[0], a[1], b[0], b[1]] for a, b, _ in W], 40,
"for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;")
batch(PT, 25, "for (const p of %s) await eda.sch_PrimitiveComponent.createNetPort(p[0],p[1],p[2],p[3],p[4],false); return 1;")
batch(F, 25, "for (const f of %s) await eda.sch_PrimitiveComponent.createNetFlag(f[0],f[1],f[2],f[3],f[4],false); return 1;")
batch([list(t) for t in TX], 12,
"for (const t of %s) await eda.sch_PrimitiveText.create(t[0],t[1],t[2],0,null,null,t[3],false,false,false,0); return 1;")
json.dump(NC, io.open(os.path.join(HERE, 'b2-nc.json'), 'w', encoding='utf-8'),
ensure_ascii=False, indent=1)
print(u'推送完成;接着跑 verify_sch.py --nets')
if __name__ == '__main__':
main()