原理图/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
263 行
12 KiB
Python
263 行
12 KiB
Python
# -*- 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,没有改动)')
|