# -*- coding: utf-8 -*- u"""P5-HARNESS -> P5-BTB:板对板排针接口页(两条 2x20)。 下板侧装公头 **C55020601**(AZ-PZ2.54-V11-2*20P,塑高 2.5 / 配合针长 6, 2026-09-05 定稿,取代占位 C9900001574)。见设计文档 §22.3f。 该符号是 **IDC 奇偶式**编号:第 r 行 = 引脚 2r-1(左列)/ 2r(右列), 第 1 行在中心上方 95 个单位。载板母座 C5307344 **完全一样**, 所以换件之后两块板同一套编号,§22.2 那个「两套编号」的陷阱消失了。 用法: /c/Python314/python .claude/p5_btb.py [--push] """ import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run PAGE = '58bacf3ddd60ed4e' LIB = '0819f05c4eef4c71ace90d822a990e87' HDR = 'c0ab04240dc64fca8d6203d15f8a8a68' # C55020601 AZ-PZ2.54-V11-2*20P # ═══════════ 逐针表(2026-09-05 按载板几何重排,取代 09-04 那版)═══════════ # # 每条排针 20 个位置,(北排网络, 南排网络)。 # **北排 = 奇数脚 2r−1(PCB 上 y = 68.77);南排 = 偶数脚 2r(y = 66.23)。** # # ## 为什么要重排(§25.10) # # 09-04 那版是按「通道号」切的(J24 = CH1+CH2+全局 / J25 = CH3+CH4+CH5), # 当时载板 PCB 还不存在。载板画出来之后发现两件事: # # ① 排母南排前 16 位全是 GND 焊盘,Ø1.70 / 间距 2.54 ⇒ 相邻边距 0.84, # 扣掉两边 0.2 净距,Inner1 上每个缝只剩 **0.44 mm 铜网**。 # 4 A 按 IPC-2221 内层 0.5 oz 要 6.97 mm —— # **4 A 的电源面不可能穿过排母的地针行。** # ② 而 09-04 那版把 VO4/VO5 放在 J32 的**北**排,它们的站却在排母**南**边。 # # ⇒ 规矩:**北排喂北边的站,南排喂南边的站**,一条都不许跨行。 # # ## 载板几何(决定了谁放哪) # # J31 焊盘 x 37.87~86.13 正对 site1(x 5~72,北) 和 site4(x 41~108,南) # J32 焊盘 x 133.87~182.13 正对 site3(x 148~215,北) 和 site5(x 112~179,南) # site2 (x 76.5~143.5,北) 夹在两颗中间 —— 它的 VO2_P 取 J31 北排最右三针(x 81~86), # 正好顶着 site2 最左边的负载 C811(x 86.0) # # 重排后五路 VOn_P **全部同侧**,最远曼哈顿从 ~120 mm(VO4 原来要绕排母端头)降到 ≤42 mm。 # # ## 三条不变的约定 # # · 每路 VOn_P 仍是 3 针,**正对面 3 针 GND** 做本路回流(§15.4) # · +5V 从 3 针加到 **10 针**、分散在两颗排母的南北四处 —— # 不是为了电流(2.8 A / 10 = 0.28 A/针),是为了**让它就近落地、 # 不必在 Inner1 上拉一条横贯全板的脊**(§25.10) # · GND 从 33 针减到 **26 针**,20 A 回流 = 0.77 A/针,仍很宽裕 # ## 2026-09-06 第三版:74HC595 + 15 颗按键 MOS 整窝挪到载板(§26.23) # # 跨排针的不再是 15 条 `POGO*` + 5 条 `EDL_EN*` + `HUB_RST`(21 针), # 而是移位链的 4 根线 `KEY_SER` / `KEY_SRCLK` / `KEY_RCLK` / `KEY_OE#`。 # 净腾 17 针,全部补成 GND(回流从 26 针加到 43 针)。 # 4 根 KEY_* 放在 **J25 北排 3~6 行**(载板 x 139~147):595 摆在载板走廊中段 # (J31 与 J32 之间,x 110~133),从 J32 西端出线最近;下板这头 ESP32 在 (88,18), # U21/U22 腾出来之后 x 88~97 / y 35~55 整片是空的,四根线北上到 J25 西端没有障碍。 # `POGO*` / `EDL_EN*` / `HUB_RST` 从此是**载板内部网络**。 J24 = [ ('VO1_P', 'GND'), ('VO1_P', 'GND'), ('VO1_P', 'GND'), # 1~3 -> site1(北) ('+5V', 'GND'), ('+5V', 'GND'), # 4~5 -> C701/U24(北) ('GND', 'GND'), # 6~9 原 POGO1/POGO4/EDL_EN1/EDL_EN4, ('GND', 'GND'), # 2026-09-06 随 595 上载板腾空 -> GND ('GND', 'GND'), ('GND', 'GND'), ('VBUS_EN1', 'VBUS_EN4'), # 10 ('SDA', '+5V'), ('SCL', '+5V'), # 11~12 南=U27/C704(南) ('GND', 'UP_VBUS_DET'), # 13 R722 在底层 y62.2 ('GND', 'GND'), # 14 ('GND', 'VO4_P'), ('GND', 'VO4_P'), ('GND', 'VO4_P'), # 15~17 -> site4(南) ('VO2_P', 'GND'), ('VO2_P', 'GND'), ('VO2_P', 'GND'), # 18~20 -> site2(北) ] J25 = [ ('+5V', '+5V'), ('+5V', '+5V'), # 1~2 北->C702/U25 南->U28/C705 ('KEY_SER', 'VBUS_EN5'), # 3~6 北=74HC595 移位链 4 根(原 POGO2_* / EDL_EN2) ('KEY_SRCLK', 'GND'), # 南=CH5 的 VBUS 使能(原 EDL_EN5 -> GND) ('KEY_RCLK', 'GND'), ('KEY_OE#', 'GND'), ('VBUS_EN2', 'GND'), # 7 ('GND', 'GND'), # 8 原 HUB_RST -> 载板内部 ('GND', 'VO5_P'), ('GND', 'VO5_P'), ('GND', 'VO5_P'), # 9~11 -> site5(南) ('GND', 'GND'), ('GND', 'GND'), ('GND', 'GND'), # 12~14 原 POGO3_* / POGO5_* -> GND ('VO3_P', 'GND'), ('VO3_P', 'GND'), ('VO3_P', 'GND'), # 15~17 -> site3(北) ('GND', 'GND'), ('VBUS_EN3', 'GND'), # 18~19 原 EDL_EN3 -> GND ('+5V', '+5V'), # 20 -> C703/U26(北) ] # ══════════════════════════════════════════════════════════════════════════════ # ⭐ MATE_SWAP:下板这一侧的网络要**奇偶对调**(2026-09-06 用户拍板,方案 A,§26.5) # # 上面两张表是按**载板**几何写的:(北排, 南排) = (载板 y 68.77, 载板 y 66.23), # 而载板的北排 = 奇数脚 2r−1。`c1_conn.py` 直接用这两张表生成载板 C1-CONN,**不受本开关影响**。 # # 但下板不一样:母座 J31/J32 装在载板**底层**,顶视图里镜像一次(skill §9w), # 公头 J24/J25 装在下板**顶层**不镜像。两颗封装的焊盘编号又是**同手性**的 # (本地 pin1 都在西南角)。⇒ 对插时下板 pin(2r−1) 压在载板 pin(2r) 上,整体错开一行位。 # 实测(`pcb1_place.py --verify`):80 个焊盘全部落在载板针正下方 0.001 mm,针号全错开。 # # 两颗都是**无防呆的 2×20**,物理完全对称,"pin1 在哪一排"纯粹是封装画法约定, # 不存在谁画错。所以修法就是把下板这一侧的网络按针号对调一次: # 下板 pin(2r−1) := 载板 pin(2r) 的网络,下板 pin(2r) := 载板 pin(2r−1) 的网络。 # # ⚠️ 关掉这个开关 = 下板 VO1_P 顶在载板 GND 上(4 A 直接对地)。 MATE_SWAP = True def board_rows(rows): u"""把「按载板写的 (北, 南)」翻成「下板符号的 (奇数脚, 偶数脚)」。""" return [(s, n) for n, s in rows] if MATE_SWAP else list(rows) CONNS = [('J24', 300, 500, board_rows(J24)), ('J25', 800, 500, board_rows(J25))] STUB = 40 # 引脚向外引出的长度 ROW0 = 95 # 第 1 行相对元件中心的 y 偏移(实测,公母两颗一样) DY = 10 def call(js, timeout=180): r = run(js, timeout=timeout) if not r.get('success'): raise RuntimeError(json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def plan(): u"""生成导线 / 端口 / 文字。 ⚠️ 2026-09-05 起 **GND 也用 netport,不再画地母线**。 原因:重排之后 GND 不再是「右列从第 1 行起的连续块」,而是散在两列的若干段 (J24 北 13~17 / 南 1~5,14,18~20;J25 北 9~11,20 / 南 5~8,15~19)。 给每一段配母线 + 地旗的话,旗会掉进相邻行的端口里(行距只有 10, 而旗高 15~20,见 skill §7)。端口在 10 的行距上是验证过能排下的 (这一页左列本来就是 20 个端口叠着),所以统一用端口最省事也最不容易错。 """ segs, ports, texts = [], [], [] for desig, cx, cy, rows in CONNS: assert len(rows) == 20, u'%s 必须正好 20 行' % desig lx, rx = cx - 30, cx + 30 lp, rp = lx - STUB, rx + STUB for i, (nnet, snet) in enumerate(rows): y = cy + ROW0 - DY * i segs.append((lx, y, lp, y)) ports.append(('BI', nnet, lp, y, 180)) segs.append((rx, y, rp, y)) ports.append(('BI', snet, rp, y, 0)) texts.append((cx - 60, cy + ROW0 + 45, u'%s 奇数脚(左) = 载板南排 / 偶数脚(右) = 载板北排 —— 已按 §26.5 奇偶对调' % desig, 11)) # 每路 VOn_P 正对面必须是 3 针 GND(§15.4 各路独立回地) for desig, cx, cy, rows in CONNS: for i, (n, sth) in enumerate(rows): if n.startswith('VO') and n.endswith('_P'): assert sth == 'GND', u'%s 第%d行 北=%s 但南不是 GND' % (desig, i + 1, n) if sth.startswith('VO') and sth.endswith('_P'): assert n == 'GND', u'%s 第%d行 南=%s 但北不是 GND' % (desig, i + 1, sth) texts += [ (60, 780, u'【板对板接口】下板 → 载板,两条 2x20 加高排针(2.54mm)', 14), (60, 758, u'公头在下板(C55020601 AZ-PZ2.54-V11-2*20P,塑高 2.5 / 针长 6,直插镀金),母座在载板(C5307344 PC104-2*20,塑高 10.5)。', 10), (60, 738, u'编号:第 r 行 = 引脚 2r-1(左)/ 2r(右)。⚠ 本页的网络已相对载板 C1-CONN **奇偶对调**(§26.5):', 10), (60, 722, u' 母座装在载板底层、顶视图镜像一次,对插时下板 pin(2r-1) 正对载板 pin(2r)。两颗都是无防呆 2x20,编号只是画法约定。', 10), (60, 718, u'分配按载板几何排(§25.10):J31 正对 site1(北)+site4(南),J32 正对 site3(北)+site5(南),site2 取 J31 北排最右三针。', 10), (60, 698, u'⚠ 北排喂北边的站、南排喂南边的站,一路都不许跨行 —— 4A 的电源面穿不过排母的地针行(缝里只剩 0.44mm 铜)。', 10), (60, 215, u'每路 VOn_P 用 3 针并联(4A / 3 = 1.33A 每针,母座额定 2.5A 每针),正对面 3 针地做本路回流(§15.4)。', 10), (60, 195, u'+5V 用 10 针、分散在两颗排母的南北四处:不是为了电流(0.28A 每针),是为了就近落地,免得在 Inner1 上拉一条横贯全板的脊。', 10), (60, 175, u'合计 37 个信号 + 43 个地(2026-09-06 起:74HC595 + 按键 MOS 整窝在载板,跨板只剩 KEY_SER/SRCLK/RCLK/OE# 四根,§26.23)。', 10), (60, 155, u'对插高度 = 2.5 + 10.5 = 13.0mm(塑料对塑料定位),满足 §22.3d 的净空要求。', 10), (60, 135, u'⚠ 下板 C2/C3 罐高必须 <= 11.5mm,且 C2~C6 压在 J24 位置上、PCB 上要挪开(§22.3f)。', 10), ] # ---- 下发前的几何断言(skill §6d 横平竖直 / §8a-2b 坐标吸附到 5 的倍数)---- for a, b, c, e in segs: assert (a == c) != (b == e), u'有一段导线不是横平竖直或零长度: %s' % ((a, b, c, e),) for v in (a, b, c, e): assert v % 5 == 0, u'导线坐标 %d 不是 5 的倍数: %s' % (v, (a, b, c, e)) for it in ports: assert it[2] % 5 == 0 and it[3] % 5 == 0, u'port %s 的坐标 (%d,%d) 不是 5 的倍数' % (it[1], it[2], it[3]) return segs, ports, texts def main(): push = '--push' in sys.argv segs, ports, texts = plan() print(u'导线 %d 端口 %d 文字 %d' % (len(segs), len(ports), len(texts))) names = {} for _, n, _, _, _ in ports: names[n] = names.get(n, 0) + 1 for n in sorted(names): print(u' %-14s %d' % (n, names[n])) if not push: print(u'(未推送,加 --push)') return call(""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,800)); if ((await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name.indexOf('P5')!==0) throw new Error('page'); let n=0; for (const c of await eda.sch_PrimitiveComponent.getAll()){ if (c.getState_ComponentType()==='sheet') continue; await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); n++; } for (const w of await eda.sch_PrimitiveWire.getAll()) { await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId()); n++; } for (const r of await eda.sch_PrimitiveRectangle.getAll()) { await eda.sch_PrimitiveRectangle.delete(r.getState_PrimitiveId()); n++; } for (const p of await eda.sch_PrimitivePolygon.getAll()) { await eda.sch_PrimitivePolygon.delete(p.getState_PrimitiveId()); n++; } for (const t of await eda.sch_PrimitiveText.getAll()) { await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId()); n++; } await eda.dmt_Schematic.modifySchematicPageName('%s','P5-BTB'); return n; """ % (PAGE, PAGE)) print(u'已清空并改名 P5-BTB') for desig, cx, cy, _ in CONNS: call(""" await eda.sch_PrimitiveComponent.create({libraryUuid:'%s', uuid:'%s'}, %d, %d, undefined, 0, false, true, true); for (const c of await eda.sch_PrimitiveComponent.getAll()){ if (c.getState_ComponentType()!=='part') continue; if (Math.round(c.getState_X())!==%d) continue; await c.toAsync().setState_Designator('%s').done(); } return 1; """ % (LIB, HDR, cx, cy, cx, desig)) print(u'已放置 J24 / J25') real = call(""" const out={}; for (const c of await eda.sch_PrimitiveComponent.getAll()){ if (c.getState_ComponentType()!=='part') continue; const d=c.getState_Designator(); const ps=await c.getAllPins(); out[d]=ps.map(p=>[p.getState_PinNumber(),Math.round(p.getState_X()),Math.round(p.getState_Y())]); } return out; """) for desig, cx, cy, rows in CONNS: pm = {p: (x, y) for p, x, y in real[desig]} for i in range(20): y = cy + ROW0 - DY * i lp_, rp_ = str(2 * i + 1), str(2 * i + 2) assert pm[lp_] == (cx - 30, y), u'%s 第%d行左 = pin%s,期望 %s 实测 %s' % ( desig, i + 1, lp_, (cx - 30, y), pm[lp_]) assert pm[rp_] == (cx + 30, y), u'%s 第%d行右 = pin%s,期望 %s 实测 %s' % ( desig, i + 1, rp_, (cx + 30, y), pm[rp_]) print(u'引脚坐标与预期一致') 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([list(s) for s in segs], 40, "for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;") batch([list(p) for p in ports], 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([list(t) for t in texts], 15, "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;") print(u'推送完成') if __name__ == '__main__': main()