# -*- 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,没有改动)')