# -*- coding: utf-8 -*- u"""R2 底板 **A1-INPUT** 页:12V 母线储能 + 输入侧检流 + 母线泄放。 /c/Python314/python .claude/r2/a1_input.py # 只算 + 本地自检 /c/Python314/python .claude/r2/a1_input.py --push # 清页 + 重画(幂等) M3 的第一页。做法同 `b1_power.py`:**搬 R1 的版图,不重排** —— R1 的 `P1-INPUT`(`9a6096762e036e23`)整页读回在 `_p1_live.json`, 本脚本按下面几条改,其余坐标一个字不动。 | | 内容 | |---|---| | **删** | `U19` 3V3 Buck 那一块(`U19` `C7` `L2` `C9` `C10` `R2` `R3` `C8`,8 件)→ 挪到 **`A2-AUX`**,和 `U30` 5V Buck 放一页 | | **删(D16)** | 整条旧入口 `J1` DC 座 / `F1` / `Q1` 反接 / `DZ1` `R1` `C1` 缓启动 → 新入口在 **`A1B-PD`**(USB-C PD 20V → LM61495 → 12V)| | **加** | 母线节点的网络名 `VIN_SW`(降压输出 → Rs6);D16 之前另有 `VIN_RAW` / `VIN_FUSED`,现在都在 A1B-PD | | **换** | 文字整套重写(见下) | 位号沿用 R1(`D1` `C2~C6` `U13` `Rs6` `U29` `R4~R6` `Q3` `R9A~E` `C11`), 方案文档 §4 的 BOM 归属表就是这么写的。`R2`/`R3` 留给 `A2-AUX`;`J1` `F1` 这两个位号 D16 起在 A1B-PD。 ## D16(2026-09-11):入口挪走之后这一页剩什么 `D1` + `C2~C6` 那一排挂在 `VIN_SW` 母线上不动;母线左端原来是 Q1 漏极(x=430 那几段), 现在删掉,母线从 D1 起。`VIN_SW` 旗是和 A1B-PD 的唯一接点(那边 L4 之后同名的两面旗)。 旧入口挂着的导线组(`VIN_RAW` / `VIN_FUSED` / `GATE`、J1 两只地脚、DZ1/C1/R1 的地)整组删, `VIN_SW` 组只摘掉 `D16_SEGS` 那五段,删完断言没有任何导线端点还压在被删件的引脚上。 ## 为什么要给母线节点起名 R1 上这一段是**无名网络**,PCB 侧只能按 `$1N236` 认 —— 第三十四轮那次「+12V 从 4 个碎片炸成 23 个」的假象就出在这一带。 底板的这一段要过约 8 A、要铺铜 + 断面校核(方案文档 §6.4),按名字认比按 `$1N…` 认稳得多。 ## ⚠ 一条从 R1 带过来、必须知道的 **`BLD_REF` 端口与 `R4.1` 同坐标**(`(80,235)`)—— CLAUDE.md 点名的那类坑。 这里是安全的:`R4.1`→`R5.2` 之间**有一段真导线**,端口落在它的端点上,不是零长度。 脚本里有断言盯着这一处,重画之后仍然要看 `verify_sch` 的 `BLD_REF` 那一行。 """ 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 # noqa: E402 PAGE, NAME = '9d86e72abb1a6571', 'A1-INPUT' R2_PROJECT = '05a65c22f14846b8b95360ef8d3c1e2e' SRC = os.path.join(HERE, '_p1_live.json') # ── 挪到 A2-AUX 的 3V3 Buck 那一块 ────────────────────────────────── DROP = set(['U19', 'C7', 'L2', 'C9', 'C10', 'R2', 'R3', 'C8']) # ── D16:挪到 A1B-PD 的旧入口 ──────────────────────────────────────── D16_DROP = set(['J1', 'F1', 'Q1', 'DZ1', 'R1', 'C1']) # VIN_SW 组里属于 Q1 漏极的四段竖线 + 母线从 x 430 到 D1 的那一小段(删完母线从 D1 起) D16_SEGS = [((430, 700), (430, 690)), ((430, 710), (430, 700)), ((430, 720), (430, 710)), ((430, 770), (430, 720)), ((480, 770), (430, 770))] # ── 母线节点:起名 + 挂旗的位置(支线从既有导线上分叉出来)────── # (网名, 分叉点, 旗点, 旗 rot);rot 180 = Power 旗的符号朝下 NEW_FLAGS = [ ('VIN_SW', (830, 770), (830, 690), 180), ] # 用一个「标志引脚」认出它属于哪个既有导线组 SIG_PIN = {'VIN_SW': 'Rs6.1'} # U13 IN± 对调要删的三段 R1 导线(见 build() 的 2b):10 脚直通 Rs6.1、9 脚短竖线、9 脚并到 VBUS 横线那一小段 IN_SWAP_DEL = [((880, 770), (880, 670)), ((890, 720), (890, 670)), ((890, 720), (900, 720))] # ⚠ 文字全部塞进「删掉 3V3 Buck 之后空出来的那一带」(x 60~760 / y 330~570)。 # R1 原来把说明压在元件行上(y 700~790 与 y 190~270),本页元件位置一个没动, # 照抄文字坐标就会盖在 D1/C2 和 R4/R5/R6/U29 上 —— 已经踩过一次。 TEXTS = [ (60, 795, u'【A1-INPUT】底板 12V 母线:储能 → 检流(U13)→ +12V;外加 12V 母线泄放。入口在 A1B-PD(D16)', 14), (60, 560, u'【A】入口已挪到 A1B-PD(D16,2026-09-11):USB-C PD 20V → F1 → LM61495 → 12V,经 VIN_SW 旗接进本页', 11), (60, 543, u'原 DC-005 圆座 / Q1 反接保护 / DZ1·R1·C1 缓启动已删:USB-C 不会插反,缓启动由降压的软启动 + EN 门限代替', 10), (60, 482, u'【E】12V 母线储能与钳位:D1 SMBJ13A 钳位 21.5V;C2/C3 470µF + C4/C5 10µF + C6 100nF', 11), (60, 465, u'VIN_SW = 降压输出(A1B-PD 的 L4 之后)→ Rs6 → +12V;最坏约 8A(USB-C 20V/5A = 100W 封顶)', 10), (60, 448, u' 这一段要铺铜 + 断面校核(方案 §6.4);A1B-PD 的 VIN_FUSED(20V、≤5A)另算', 10), (60, 421, u'【B】12V 母线泄放(防 USB 反灌抬高母线)', 11), (60, 404, u'阈值 = 2.495V x (R4+R5)/R5 = 2.495 x 5.32 = 13.27V;TL431 导通时阴极 2.0~2.2V → Q3 Vgs = -9.8V 全导通', 10), (60, 387, u'R9A~R9E: 5 x 2Ω 2512 串联 = 10Ω/10W,高 0.8mm(立式水泥电阻站起来超高度预算)', 10), (60, 370, u'泄放为突发额定:13.3V 下 1.33A / 17.7W,持续故障由固件关 VBUS 兜底', 10), (60, 343, u'【C】3V3 辅助电源(U19/L2/C7/C9/C10/R2/R3/C8)已挪到 A2-AUX,和 12V→5V 的 U30 放同一页', 11), # ⚠ IN± 对调的说明第一次放在 (60, 326),压住了 R6 那面 +12V 旗的**标签**(标签画在旗引脚上方, # text_clash 只比引脚点,查不出来,是截图看出来的)⇒ 并进右下【D】这一组,整组上移一行 (620, 262, u'【D】输入侧电流 / 电压监测(整板功率预算)', 11), (620, 245, u'Rs6 5mΩ:满板约 8A → 40mV(INA226 满量程 81.92mV / 分辨率 0.5mA)', 10), (620, 228, u'U13 地址 A1=GND、A0=SCL → 0x43(地址表不是顺排)。挂 I2C 主干,不经 TCA9548A', 10), (620, 211, u'⚠ IN+ 接 Rs6 下游 +12V、IN- 接上游 VIN_SW(与 R1 相反,PCB 上两条 Kelvin 不交叉)', 10), (620, 194, u'读数因此为负,固件 ina226_set_reversed(0x43) 取反;体电容在 Rs6 上游', 10), ] def load(): u"""读回 R1 的 P1-INPUT,拆成「导线组 → 网络名」。""" src = json.load(io.open(SRC, encoding='utf-8')) pts = src['wires'] segs = [((int(round(pts[i][0])), int(round(pts[i][1]))), (int(round(pts[i + 1][0])), int(round(pts[i + 1][1])))) for i in range(0, len(pts), 2)] for a, b in segs: assert a != b, u'源页有零长度导线 %s(skill §8a)' % (a,) assert (a[0] == b[0]) != (a[1] == b[1]), u'源页有斜线 %s-%s' % (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 pins = {} # 位号 -> {脚号: (x,y)} pinat = {} # (x,y) -> '位号.脚号' pinname = {} # 位号 -> {脚号: 脚名} for p in src['parts']: d = p[0] pins[d] = dict((str(q[0]), (int(round(q[2])), int(round(q[3])))) for q in p[9]) pinname[d] = dict((str(q[0]), q[1]) for q in p[9]) for q in p[9]: pinat[(int(round(q[2])), int(round(q[3])))] = '%s.%s' % (d, q[0]) # 组 -> 名字 / 组 -> 引脚 gname, gpins = defaultdict(set), defaultdict(set) for it, key in [(src['flags'], 1), (src['ports'], 1)]: for f in it: xy = (int(round(f[2])), int(round(f[3]))) hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])] assert hit, u'旗/端口 %s @%s 不在任何导线上' % (f[key], xy) gname[find(hit[0])].add(f[key]) for d, ps in pins.items(): for num, xy in ps.items(): hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])] assert hit, u'源页悬空引脚 %s.%s @%s' % (d, num, xy) gpins[find(hit[0])].add('%s.%s' % (d, num)) return src, segs, find, pins, pinat, pinname, gname, gpins def build(): src, segs, find, pins, pinat, pinname, gname, gpins = load() # ── 1. 判断每个导线组是留是删(必须整组同进退)─────────────────── keep_grp = {} for r_, ps in gpins.items(): owners = set(p.split('.')[0] for p in ps) kept, dropped = owners - DROP, owners & DROP assert not (kept and dropped), \ u'导线组同时挂着留下的 %s 和删掉的 %s,不能整组同进退' % (sorted(kept), sorted(dropped)) keep_grp[r_] = bool(kept) for i in range(len(segs)): assert find(i) in keep_grp, u'有导线组一个引脚都不挂:%s' % (segs[i],) # ── 1b. D16:只挂旧入口件的组整组删;和留下的件混挂的只允许 VIN_SW 那一组 ── rs6_grp = [r_ for r_, ps in gpins.items() if SIG_PIN['VIN_SW'] in ps] assert len(rs6_grp) == 1 drop16 = set() for r_, ps in gpins.items(): if not keep_grp[r_]: continue owners = set(p.split('.')[0] for p in ps) - DROP if owners <= D16_DROP: drop16.add(r_) elif owners & D16_DROP: assert r_ == rs6_grp[0], \ u'D16:组 %s 同时挂着旧入口件和留下的件,只有 VIN_SW 那一组允许这样' % sorted(ps) seg_set = set(segs) | set((b, a) for a, b in segs) for a, b in D16_SEGS: assert (a, b) in seg_set, u'D16 要摘的导线 %s-%s 不在 R1 源页上(源页变了?)' % (a, b) assert find(segs.index((a, b)) if (a, b) in segs else segs.index((b, a))) == rs6_grp[0] # ── 2. 给每个留下的组定网络名 ─────────────────────────────────── sig2net = dict((v, k) for k, v in SIG_PIN.items()) gnet = {} for r_, keep in keep_grp.items(): if not keep or r_ in drop16: continue names = gname.get(r_, set()) forced = [sig2net[p] for p in gpins[r_] if p in sig2net] if forced: assert not names, u'组 %s 本来就有名字 %s,不该再起名' % (sorted(gpins[r_]), names) assert len(set(forced)) == 1, forced gnet[r_] = forced[0] elif not names: # R1 上本来就无名的短节点(泄放 MOS 漏极、R9A~E 之间的 4 个串联节点)。 # 这些段只走 1.33A 突发、走线两三毫米,不铺铜也不做断面校核, # 沿用 R1 的「让 EasyEDA 自动起名」。`#` 开头的名字只给本地自检用,不下发。 gnet[r_] = '#' + '+'.join(sorted(gpins[r_])[:2]) else: assert len(names) == 1, \ u'组 %s 的名字不唯一:%s' % (sorted(gpins[r_])[:4], sorted(names)) gnet[r_] = sorted(names)[0] s = Sheet(PAGE, NAME) d16_segs = set(D16_SEGS) | set((b, a) for a, b in D16_SEGS) for i, (a, b) in enumerate(segs): r_ = find(i) if keep_grp[r_] and r_ not in drop16 and (a, b) not in d16_segs: s.w(gnet[r_], a, b) # ── 2b. U13 的 IN+ / IN− 对调(2026-09-11,用户拍板;R1 不变)───────── # R1:U13.10(IN+)→Rs6.1(VIN_SW,上游)、U13.9(IN−)→Rs6.2(+12V,下游)。底板 PCB 上 U13.10 在上、Rs6.1 在南, # 两条只走 L4 的 Kelvin 必然交叉 —— IN− 绕了 37 mm、把 U13 东列的 SDA / INA_ALERT 圈死(PROGRESS 2026-09-11)。 # 对调后 U13.10(IN+)=+12V、U13.9(IN−)=VIN_SW,读数为负,固件对这一片取反(ina226_set_reversed)。 # 原理图上照样会交叉,所以 10 脚改挂 +12V 旗,9 脚画折线进 Rs6.1。 assert (pins['U13']['9'], pins['U13']['10'], pins['Rs6']['1']) == ((890, 670), (880, 670), (880, 770)), \ u'U13 / Rs6 的引脚坐标和 R1 源页对不上,IN± 对调补丁要重算' n0 = len(s.W) s.W[:] = [(a, b, n) for (a, b, n) in s.W if (a, b) not in IN_SWAP_DEL and (b, a) not in IN_SWAP_DEL] assert n0 - len(s.W) == len(IN_SWAP_DEL), \ u'IN± 对调要删的 %d 段只删到 %d 段(R1 源页变了?)' % (len(IN_SWAP_DEL), n0 - len(s.W)) s.w('VIN_SW', (890, 670), (890, 745), (880, 745), (880, 770)) s.w('+12V', (880, 670), (880, 685), (860, 685), (860, 700)) s.pwr('+12V', 860, 700, 0) # D16 删完的兜底:任何一段导线的端点都不许还压在被删件的引脚上(否则那一头是悬空的残段) dead = set(xy for d in D16_DROP for xy in pins[d].values()) left = [(a, b, n) for a, b, n in s.W if a in dead or b in dead] assert not left, u'D16:还有导线压在旧入口件的引脚上:%s' % left[:4] # ── 3. 旗 / 端口:删掉挂在被删组上的 ───────────────────────────── def kept_at(xy): hit = [i for i in range(len(segs)) if _on(xy, segs[i][0], segs[i][1])] if not hit: return False r_ = find(hit[0]) return keep_grp[r_] and r_ not in drop16 for f in src['flags']: xy = (int(round(f[2])), int(round(f[3]))) if kept_at(xy): # 读回来的符号名是 'Ground-GND' / 'Power-5V',createNetFlag 只认前缀 s.F.append([f[0].split('-')[0], f[1], xy[0], xy[1], f[4]]) for p in src['ports']: xy = (int(round(p[2])), int(round(p[3]))) if kept_at(xy): s.PT.append([p[0].replace('Netport-', ''), p[1], xy[0], xy[1], p[4]]) # ── 4. 母线节点起名:从既有导线上分叉出一小段,挂 Power 旗 ── for net, tap, flag, rot in NEW_FLAGS: sig = SIG_PIN[net] onseg = [i for i in range(len(segs)) if _on(tap, segs[i][0], segs[i][1])] assert onseg, u'%s 的分叉点 %s 不在任何导线上' % (net, tap) assert gnet.get(find(onseg[0])) == net, \ u'%s 的分叉点 %s 落在了 %s 上' % (net, tap, gnet.get(find(onseg[0]))) assert sig in gpins[find(onseg[0])], u'%s 的组里没有 %s' % (net, sig) assert tap not in pinat, u'%s 的分叉点压在引脚 %s 上' % (net, pinat[tap]) s.w(net, tap, flag) s.pwr(net, flag[0], flag[1], rot) for t in TEXTS: s.txt(t[0], t[1], t[2], t[3]) place, exp = [], {} from schcheck import PARTS byname = {} for k, v in PARTS.items(): byname.setdefault(v['name'], k) for p in src['parts']: d, nm = p[0], p[1] if d in DROP or d in D16_DROP: 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])), int(round(p[4])), p[5]]) exp[d] = dict((str(q[0]), (int(round(q[2])), int(round(q[3])))) for q in p[9]) # CLAUDE.md 点名的那一处:端口与引脚同坐标,靠一段真导线兜着 assert ('BLD_REF', 80, 235) in [(x[1], x[2], x[3]) for x in s.PT], \ u'BLD_REF @(80,235) 那个端口丢了' assert any(_on((80, 235), a, b) and a != b for a, b, _ in s.W), \ u'BLD_REF @(80,235) 底下没有真导线 —— 就是 CLAUDE.md 说的零长度那类坑' return s, place, exp, pinname def text_clash(s, exp): u"""文字压在导线 / 引脚 / 旗端口上 —— schcheck 只查右边界,这一条是本页新加的。 宽度按 skill §12 的经验值(全角 = 字号,半角 = 半个字号), 高度保守地取 ±0.8 字号。宁可误报,也不要再出现「说明盖住 J1/F1/Q1」那种图。 """ 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(): if num == '?': for p in xy: pts.append((p[0], p[1], u'引脚 %s' % d)) continue 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"""两面旗/端口的**符号**叠在一起 —— crossings() 查不到(旗不是导线)。 判据按符号尺寸定,不是按标签:一格(10)以内上下叠 **且** 横向也挨着才算叠。 同一个 x 上下差一格排开(R1 的 `BLD_REF`/`BLD_A` 就是这么放的)是正常写法,不报。 """ 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 main(): push = '--push' in sys.argv s, place, exp, _pinname = build() s.assert_grid() errs = s.netcheck(exp, {}, set(['GND', '+12V', '+3V3', 'BLD_A', 'BLD_REF'])) errs += text_clash(s, exp) + flag_clash(s) print(u'搬 R1 P1-INPUT:%d 件(删掉 3V3 Buck 那 %d 件 + D16 旧入口 %d 件)' % (len(place), len(DROP), len(D16_DROP))) 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() # 存盘:新放的元件存盘之后才分到 uniqueId —— 不存的话 uid_sync_base.py 读回全是空, # 「从原理图导入变更」也配不上对(2026-09-11,IN± 对调那次踩到;a4_hub.py 早就这么做了) call(u"return await eda.sch_Document.save();") print(u'接着跑 verify_sch.py --nets,再 uid_sync_base.py') if __name__ == '__main__': main()