文件
phonerail/.claude/pinmap.py
T
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

263 行
12 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""按几何重排「逻辑信号 -> 物理引脚」的三张映射,并把改动落到原理图上。
/c/Python314/python .claude/pinmap.py # 只打计划 + 读回现状对比
/c/Python314/python .claude/pinmap.py --push # 真改(删端口/建端口/删线/建线)
/c/Python314/python .claude/pinmap.py --check # 只读回板上现状,逐条核对
## 为什么能随便换脚
* **ESP32-S3**:除 `USB_DM/DP`(IO19/IO20 是 USB PHY 硬连线)和两路 NTC(要 ADC1 =
IO1~IO10)以外,全部信号都经 **GPIO matrix** 出去 —— I²C、LEDC(FAN_PWM)、
PCNT(FAN_TACH)、普通 GPIO 都不挑脚。IOMUX 直连只在 >40 MHz 的 SPI/LCD 上才必须,
这块板上一根都没有。
* **DAC63204**:4 个通道完全对等,`VDACn` 挂哪个通道只是固件里的通道号。
* **74HC595 x3**:三片串成一条 24 位链,哪个信号落在哪一位只是固件移位的字节序。
⚠️⚠️ **`setState_Net` 对 netport 是坏的**(2026-09-06 实测,和 skill §3b 的
`setState_X/Y` 同一个毛病):调用之后网络名变成**空字符串**,标签也没了。
⇒ 端口只能 **删掉重建**(`createNetPort`)。
✅ 但 skill §8b 那句「端口的 dir 读不回来」是**错的**:
`getState_Component().name` 就是 `Netport-BI` / `Netport-IN` / `Netport-OUT`。
本脚本重建前先读它,原样传回去。
## 三张映射怎么算出来的
代价 = 从「出线点」(焊盘往芯片外挪 1.7 mm 的那个点)到本网最近焊盘的
**绕开芯片本体的直角最短路**,再跑匈牙利算法求最小总和。
HPWL 量不出「东边的脚要去西边」有多贵 —— 线得先爬出芯片再横穿整个封装。
`--push` 之前的那份计划见 §26.18。
"""
import io
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
PAGE = {'P3': '12f74666e70167e4', 'P3B': 'f42285b3728794d0', 'P6': 'fd00f13e59336392'}
# ── 端口改名 / 删除 / 新建 ────────────────────────────────────────────
# (x, y) 是端口引脚坐标;None = 删掉这个端口连同它那根短线
RENAME = {
'P3': {
# ESP32-S3。左列 = 模块脚号,注释里的 IOxx 是 WROOM-1 的官方脚位
(500, 710): 'KEY_RCLK', # 脚17 IO9 原 VBUS_EN1
(500, 680): 'KEY_SRCLK', # 脚20 IO12 原 VBUS_EN4
(340, 670): 'KEY_SER', # 脚21 IO13 原 VBUS_EN5
(340, 640): 'FAN_TACH', # 脚9 IO16 原 KEY_SER
(420, 630): 'VBUS_EN5', # 脚10 IO17 原 KEY_SRCLK
(500, 620): 'FAN_PWM', # 脚11 IO18 原 KEY_RCLK
(340, 480): 'UP_VBUS_DET', # 脚24 IO47 原 FAN_PWM
(340, 470): 'EFUSE_EN2', # 脚25 IO48 原 FAN_TACH
(340, 760): None, # 脚4 IO4 原 EFUSE_EN1 -> 空出来
(420, 750): None, # 脚5 IO5 原 EFUSE_EN2 -> 空出来
(340, 730): None, # 脚7 IO7 原 EFUSE_EN4 -> 空出来
(200, 480): None, # 脚36 IO44 原 UP_VBUS_DET -> 空出来
},
'P3B': {
# DAC63204:纯置换,一根线都不用新画(只用本来就接好的 5 个通道)
(390, 270): 'VDAC2', # U6.OUT0 东 原 VDAC1
(480, 260): 'VDAC4', # U6.OUT1 东 原 VDAC2
(130, 295): 'VDAC1', # U6.OUT2 西 原 VDAC3
(920, 270): 'VDAC3', # U7.OUT0 东 原 VDAC4
# 74HC595 U21 / U22
(340, 755): 'EDL_EN4', # U21.QA 脚15 原 K1_PWR
(195, 765): 'K1_PWR', # U21.QB 脚1 原 K1_VUP
(195, 755): 'K1_VUP', # U21.QC 脚2 原 K1_VDN
(195, 745): 'K1_VDN', # U21.QD 脚3 原 K2_PWR
(195, 735): None, # U21.QE 脚4 原 K2_VUP -> 空出来
(195, 725): 'EDL_EN5', # U21.QF 脚5 原 K2_VDN
(195, 715): 'EDL_EN3', # U21.QG 脚6 原 K3_PWR
(195, 705): 'EDL_EN2', # U21.QH 脚7 原 K3_VUP
(340, 585): None, # U22.QA 脚15 原 K3_VDN -> 空出来
(195, 565): None, # U22.QE 脚4 原 K5_PWR -> 空出来
(195, 555): 'K5_VDN', # U22.QF 脚5 原 K5_VUP
(195, 545): 'K5_VUP', # U22.QG 脚6 原 K5_VDN
(195, 535): 'K5_PWR', # U22.QH 脚7 原 HUB_RST
},
'P6': {
(170, 635): 'K2_VUP', # U33.QB 脚1 原 EDL_EN2
(170, 625): 'K2_VDN', # U33.QC 脚2 原 EDL_EN3
(170, 615): 'K2_PWR', # U33.QD 脚3 原 EDL_EN4
(170, 605): 'K3_VDN', # U33.QE 脚4 原 EDL_EN5
},
}
# 新建:(端口x, 端口y, 网络, rot, dir, 导线另一端x)
ADD = {
'P3': [
(420, 540, 'EFUSE_EN1', 0, 'BI', 295), # 脚32 IO39(原留给 JTAG MTCK)
(500, 530, 'VBUS_EN1', 0, 'BI', 295), # 脚33 IO40(MTDO)
(340, 520, 'EFUSE_EN4', 0, 'BI', 295), # 脚34 IO41(MTDI)
(420, 510, 'VBUS_EN4', 0, 'BI', 295), # 脚35 IO42(MTMS)
],
'P3B': [],
'P6': [
(170, 595, 'K3_VUP', 180, 'BI', 210), # U33.QF 脚5
(170, 585, 'K3_PWR', 180, 'BI', 210), # U33.QG 脚6
(170, 575, 'HUB_RST', 180, 'BI', 210), # U33.QH 脚7
],
}
TOL = 0.01
def near(a, b):
return abs(a[0] - b[0]) < 1.0 and abs(a[1] - b[1]) < 1.0
JS_READ = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1200));
const cs = await eda.sch_PrimitiveComponent.getAll();
const ports = [];
for (const c of cs) {
if (c.getState_ComponentType() !== 'netport') continue;
ports.push([c.getState_PrimitiveId(), c.getState_Net(), c.getState_X(), c.getState_Y(),
c.getState_Rotation(), c.getState_Component().name]);
}
const ws = await eda.sch_PrimitiveWire.getAll();
return {ports: ports, wires: ws.map(w => [w.getState_PrimitiveId(), w.getState_Line()])};
"""
def read(page):
r = run(JS_READ % PAGE[page], timeout=300)
assert r.get('success'), json.dumps(r, ensure_ascii=False)[:400]
return r['result']
def stub_of(wires, px, py):
u"""找到端口 (px,py) 那根短线(两点段,一端就是端口引脚)。"""
hit = []
for pid, line in wires:
pts = list(zip(line[0::2], line[1::2]))
if any(near(p, (px, py)) for p in pts):
hit.append((pid, line))
return hit
def plan():
out = {}
for pg in ('P3', 'P3B', 'P6'):
st = read(pg)
by = {}
for pid, net, x, y, rot, comp in st['ports']:
by[(round(x), round(y))] = (pid, net, rot, comp)
acts = {'ren': [], 'del': [], 'add': list(ADD[pg]), 'miss': []}
for (x, y), newnet in RENAME[pg].items():
k = None
for kk in by:
if near(kk, (x, y)):
k = kk
break
if k is None:
acts['miss'].append((x, y, newnet))
continue
pid, net, rot, comp = by[k]
if newnet is None:
acts['del'].append((pid, net, k[0], k[1], stub_of(st['wires'], x, y)))
elif net != newnet: # 幂等:已经是目标名就不动
acts['ren'].append((pid, net, newnet, k[0], k[1], rot, comp))
# 幂等:已经建过的端口不再建第二个(否则同一坐标会叠两个)
acts['add'] = [a for a in acts['add']
if not any(near((round(px), round(py)), (a[0], a[1]))
for _i, _n, px, py, _r, _c in st['ports'])]
out[pg] = (st, acts)
return out
def show(P):
for pg, (st, a) in P.items():
print(u'══ %s ══ 端口 %d' % (pg, len(st['ports'])))
for pid, old, new, x, y, rot, comp in a['ren']:
print(u' 改名 (%4d,%4d) %-12s -> %-12s rot=%-4s %s' % (x, y, old, new, rot, comp))
for pid, old, x, y, stubs in a['del']:
print(u' 删除 (%4d,%4d) %-12s 连带 %d 根短线' % (x, y, old, len(stubs)))
for x, y, net, rot, d, wx in a['add']:
print(u' 新建 (%4d,%4d) %-12s rot=%-4s dir=%s 线 -> x=%d' % (x, y, net, rot, d, wx))
if a['miss']:
print(u' ⚠️ 找不到的端口:%s' % a['miss'])
JS_APPLY = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1500));
const DELP = %s, DELW = %s, MK = %s, MKW = %s;
if (DELP.length) await eda.sch_PrimitiveComponent.delete(DELP);
if (DELW.length) await eda.sch_PrimitiveWire.delete(DELW);
for (const w of MKW) await eda.sch_PrimitiveWire.create(w);
for (const p of MK) await eda.sch_PrimitiveComponent.createNetPort(p[0], p[1], p[2], p[3], p[4], false);
await eda.sch_Document.save();
await new Promise(r => setTimeout(r, 800));
return {delp: DELP.length, delw: DELW.length, mk: MK.length, mkw: MKW.length};
"""
def push(P):
for pg, (st, a) in P.items():
delp = [r[0] for r in a['ren']] + [r[0] for r in a['del']]
delw = []
for pid, old, x, y, stubs in a['del']:
delw += [s[0] for s in stubs]
mk = [['BI' if c.endswith('BI') else ('IN' if c.endswith('IN') else 'OUT'),
new, x, y, rot] for _pid, _old, new, x, y, rot, c in a['ren']]
mk += [[d, net, x, y, rot] for x, y, net, rot, d, _wx in a['add']]
mkw = [[x, y, wx, y] for x, y, _n, _r, _d, wx in a['add']]
r = run(JS_APPLY % (PAGE[pg], json.dumps(delp), json.dumps(delw),
json.dumps(mk, ensure_ascii=False), json.dumps(mkw)), timeout=600)
assert r.get('success'), json.dumps(r, ensure_ascii=False)[:500]
print(u' %s: 删端口 %d / 删线 %d / 新建端口 %d / 新建线 %d'
% (pg, r['result']['delp'], r['result']['delw'], r['result']['mk'], r['result']['mkw']))
def check():
u"""读回板上现状,和目标表逐条比对。"""
tgt = {}
for pg in RENAME:
t = {}
st = read(pg)
for (x, y), n in RENAME[pg].items():
t[(x, y)] = n
for x, y, n, _r, _d, _w in ADD[pg]:
t[(x, y)] = n
have = {}
for pid, net, x, y, rot, comp in st['ports']:
have[(round(x), round(y))] = net
bad = []
for (x, y), want in t.items():
got = None
for k in have:
if near(k, (x, y)):
got = have[k]
break
if want is None:
if got is not None:
bad.append(u'(%d,%d) 应该删掉,实际还是 %s' % (x, y, got))
elif got != want:
bad.append(u'(%d,%d) 应该是 %s,实际 %s' % (x, y, want, got))
print(u' %s: %s' % (pg, u'✅ 全对' if not bad else u'❌ %d 处不符' % len(bad)))
for b in bad:
print(u' ' + b)
tgt[pg] = bad
return tgt
if __name__ == '__main__':
if '--check' in sys.argv:
check()
else:
P = plan()
show(P)
if '--push' in sys.argv:
print()
push(P)
print()
check()
else:
print(u'\n(未加 --push,没有改动)')