R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发

- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
这个提交包含在:
zhou yang
2026-09-10 21:02:10 +08:00
共同撰写人 Claude Opus 5
父节点 18d3d69856
当前提交 efcba30a0c
共修改 280 个文件,包含 92007 行新增和 15053 行删除
+968
查看文件
@@ -0,0 +1,968 @@
{
"C1": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C2": {
"n": 2,
"w": 5.1,
"h": 1.6,
"rot": -90.00000000000001,
"fp": "CAP-TH_BD8.0-P3.50-D0.6-FD"
},
"C3": {
"n": 2,
"w": 5.1,
"h": 1.6,
"rot": -90.00000000000001,
"fp": "CAP-TH_BD8.0-P3.50-D0.6-FD"
},
"C4": {
"n": 2,
"w": 3.411,
"h": 1.349,
"rot": 0,
"fp": "C0805"
},
"C5": {
"n": 2,
"w": 3.409,
"h": 1.349,
"rot": 0,
"fp": "C0805"
},
"C6": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C7": {
"n": 2,
"w": 3.411,
"h": 1.349,
"rot": 90,
"fp": "C0805"
},
"C8": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C9": {
"n": 2,
"w": 4.671,
"h": 1.727,
"rot": 0,
"fp": "C1206"
},
"C10": {
"n": 2,
"w": 4.671,
"h": 1.727,
"rot": 0,
"fp": "C1206"
},
"C11": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 270,
"fp": "C0402"
},
"C601": {
"n": 2,
"w": 4.671,
"h": 1.727,
"rot": 0,
"fp": "C1206"
},
"C602": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 90,
"fp": "C0402"
},
"C603": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 90,
"fp": "C0402"
},
"C604": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C607": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 270,
"fp": "C0402"
},
"C608": {
"n": 2,
"w": 3.409,
"h": 1.349,
"rot": 180,
"fp": "C0805"
},
"C609": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 90,
"fp": "C0402"
},
"C610": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 270,
"fp": "C0402"
},
"C611": {
"n": 2,
"w": 3.411,
"h": 1.349,
"rot": 90,
"fp": "C0805"
},
"C612": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 90,
"fp": "C0402"
},
"C731": {
"n": 2,
"w": 3.411,
"h": 1.349,
"rot": 270,
"fp": "C0805"
},
"C732": {
"n": 2,
"w": 3.411,
"h": 1.349,
"rot": 180,
"fp": "C0805"
},
"C733": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 180,
"fp": "C0402"
},
"C734": {
"n": 2,
"w": 4.671,
"h": 1.727,
"rot": 0,
"fp": "C1206"
},
"C735": {
"n": 2,
"w": 4.671,
"h": 1.727,
"rot": 0,
"fp": "C1206"
},
"C736": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C737": {
"n": 2,
"w": 3.899,
"h": 1.359,
"rot": 270,
"fp": "CASE-A_3216"
},
"C738": {
"n": 2,
"w": 4.671,
"h": 1.727,
"rot": 180,
"fp": "C1206"
},
"C739": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 180,
"fp": "C0402"
},
"C740": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 180,
"fp": "C0402"
},
"C741": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 180,
"fp": "C0402"
},
"C742": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C743": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C744": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C745": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C746": {
"n": 2,
"w": 1.339,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C747": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C748": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C749": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C750": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 0,
"fp": "C0402"
},
"C751": {
"n": 2,
"w": 3.409,
"h": 1.349,
"rot": 0,
"fp": "C0805"
},
"C752": {
"n": 2,
"w": 3.411,
"h": 1.349,
"rot": 180,
"fp": "C0805"
},
"C753": {
"n": 2,
"w": 1.341,
"h": 0.541,
"rot": 90,
"fp": "C0402"
},
"D1": {
"n": 2,
"w": 6.769,
"h": 2.192,
"rot": 0,
"fp": "SMB_L4.6-W3.6-LS5.3-RD"
},
"D2": {
"n": 2,
"w": 6.398,
"h": 1.999,
"rot": 180,
"fp": "SMA_L4.3-W2.6-LS5.2-RD"
},
"DZ1": {
"n": 2,
"w": 4.219,
"h": 0.841,
"rot": 180,
"fp": "SOD-123_L2.7-W1.7-LS3.8-RD"
},
"DZ3": {
"n": 2,
"w": 4.597,
"h": 0.95,
"rot": 270,
"fp": "SOD-123_L2.7-W1.6-LS3.7-RD-1"
},
"F1": {
"n": 2,
"w": 4.082,
"h": 1.727,
"rot": 0,
"fp": "F1206"
},
"J1": {
"n": 3,
"w": 7.6,
"h": 7.399,
"rot": 90,
"fp": "DC-IN-TH_DC-005-2"
},
"J23": {
"n": 16,
"w": 9.85,
"h": 6.499,
"rot": 0,
"fp": "USB-C_SMD-TYPE-C-31-M-12_1"
},
"J26": {
"n": 4,
"w": 9.22,
"h": 1.6,
"rot": 180,
"fp": "CONN-TH_4P-P2.54_CAX_2510-4A"
},
"J27": {
"n": 4,
"w": 9.22,
"h": 1.6,
"rot": 180,
"fp": "CONN-TH_4P-P2.54_CAX_2510-4A"
},
"J33": {
"n": 5,
"w": 11.859,
"h": 1.699,
"rot": -90.00000000000001,
"fp": "HDR-TH_5P-P2.54-V-M"
},
"J34": {
"n": 66,
"w": 41.999,
"h": 9.197,
"rot": 0,
"fp": "PCIE-SMD_PCIE-64P11L"
},
"J35": {
"n": 66,
"w": 41.999,
"h": 9.197,
"rot": 0,
"fp": "PCIE-SMD_PCIE-64P11L"
},
"J36": {
"n": 66,
"w": 41.999,
"h": 9.197,
"rot": 0,
"fp": "PCIE-SMD_PCIE-64P11L"
},
"J37": {
"n": 66,
"w": 41.999,
"h": 9.197,
"rot": 0,
"fp": "PCIE-SMD_PCIE-64P11L"
},
"J38": {
"n": 66,
"w": 41.999,
"h": 9.197,
"rot": 0,
"fp": "PCIE-SMD_PCIE-64P11L"
},
"L2": {
"n": 2,
"w": 4.801,
"h": 3.5,
"rot": 180,
"fp": "IND-SMD_L4.0-W4.0"
},
"L3": {
"n": 2,
"w": 7.6,
"h": 3.401,
"rot": 180,
"fp": "IND-SMD_L7.1-W6.5"
},
"Q1": {
"n": 8,
"w": 4.384,
"h": 7.569,
"rot": 0,
"fp": "SOP-8_L5.0-W4.0-P1.27-LS6.0-BL"
},
"Q3": {
"n": 8,
"w": 4.384,
"h": 7.572,
"rot": -90.00000000000001,
"fp": "SOP-8_L5.0-W4.0-P1.27-LS6.0-BL"
},
"Q6": {
"n": 3,
"w": 3.0,
"h": 2.55,
"rot": 180,
"fp": "SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR"
},
"Q7": {
"n": 3,
"w": 3.302,
"h": 2.7,
"rot": 0,
"fp": "SOT-23_L2.9-W1.3-P1.90-LS2.4-BR"
},
"Q8": {
"n": 3,
"w": 3.002,
"h": 2.55,
"rot": 180,
"fp": "SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR"
},
"Q9": {
"n": 3,
"w": 3.002,
"h": 2.55,
"rot": 180,
"fp": "SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR"
},
"Q10": {
"n": 3,
"w": 3.0,
"h": 2.55,
"rot": 90,
"fp": "SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR"
},
"R1": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R2": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R3": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R4": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 90,
"fp": "R0402"
},
"R5": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R6": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 90,
"fp": "R0402"
},
"R9A": {
"n": 2,
"w": 7.414,
"h": 3.457,
"rot": 0,
"fp": "R2512"
},
"R9B": {
"n": 2,
"w": 7.414,
"h": 3.457,
"rot": 180,
"fp": "R2512"
},
"R9C": {
"n": 2,
"w": 7.414,
"h": 3.457,
"rot": 0,
"fp": "R2512"
},
"R9D": {
"n": 2,
"w": 7.414,
"h": 3.457,
"rot": 180,
"fp": "R2512"
},
"R9E": {
"n": 2,
"w": 7.414,
"h": 3.457,
"rot": 0,
"fp": "R2512"
},
"R601": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R602": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R603": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R604": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R605": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R606": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R607": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R608": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R609": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R610": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R611": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R614": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 90,
"fp": "R0402"
},
"R615": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R616": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 270,
"fp": "R0402"
},
"R617": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R660": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R661": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R706": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R707": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R708": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R709": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R710": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R711": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R712": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R716": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R717": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 90,
"fp": "R0402"
},
"R718": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 90,
"fp": "R0402"
},
"R719": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R720": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R721": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R722": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R733": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R734": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R746": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R747": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R748": {
"n": 2,
"w": 1.43,
"h": 0.541,
"rot": 90,
"fp": "R0402"
},
"R749": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R750": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R751": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 0,
"fp": "R0402"
},
"R752": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R753": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R754": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R755": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R756": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"R757": {
"n": 2,
"w": 1.433,
"h": 0.541,
"rot": 180,
"fp": "R0402"
},
"Rs6": {
"n": 2,
"w": 7.414,
"h": 3.457,
"rot": 90,
"fp": "R2512"
},
"RT601": {
"n": 2,
"w": 0.864,
"h": 2.316,
"rot": 89.99999999999997,
"fp": "R0603"
},
"RT602": {
"n": 2,
"w": 2.316,
"h": 0.864,
"rot": 270,
"fp": "R0603"
},
"U6": {
"n": 17,
"w": 3.8,
"h": 3.8,
"rot": 180,
"fp": "WQFN-16_L3.0-W3.0-P0.50-TL-EP1.7"
},
"U7": {
"n": 17,
"w": 3.8,
"h": 3.8,
"rot": 180,
"fp": "WQFN-16_L3.0-W3.0-P0.50-TL-EP1.7"
},
"U13": {
"n": 10,
"w": 2.281,
"h": 5.842,
"rot": 180,
"fp": "MSOP-10_L3.0-W3.0-P0.50-LS5.0-BL"
},
"U19": {
"n": 6,
"w": 3.8,
"h": 2.499,
"rot": 180,
"fp": "SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BR"
},
"U20": {
"n": 49,
"w": 19.002,
"h": 18.99,
"rot": -90.00000000000001,
"fp": "WIRELM-SMD_ESP32-S3-WROOM-1"
},
"U29": {
"n": 3,
"w": 3.002,
"h": 2.55,
"rot": 180,
"fp": "SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR"
},
"U30": {
"n": 6,
"w": 3.8,
"h": 2.499,
"rot": 180,
"fp": "SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BR"
},
"U31": {
"n": 4,
"w": 8.36,
"h": 5.7,
"rot": 180,
"fp": "SOT-223-3_L6.5-W3.4-P2.30-LS7.0-BR"
},
"U32": {
"n": 65,
"w": 9.751,
"h": 9.751,
"rot": 0,
"fp": "QFN-64_L9.0-W9.0-P0.50-TL-EP4.8"
},
"U35": {
"n": 24,
"w": 7.546,
"h": 7.696,
"rot": 180,
"fp": "TSSOP-24_L7.8-W4.4-P0.65-LS6.4-BL"
},
"Y1": {
"n": 4,
"w": 3.599,
"h": 2.901,
"rot": 180,
"fp": "CRYSTAL-SMD_4P-L3.2-W2.5-BL"
}
}
@@ -0,0 +1 @@
[]
文件差异因一行或多行过长而隐藏
+692
查看文件
@@ -0,0 +1,692 @@
{
"J38": [
46.5,
25.86,
0
],
"J37": [
46.5,
46.18,
0
],
"J36": [
46.5,
66.5,
0
],
"J35": [
46.5,
86.82,
0
],
"J34": [
46.5,
107.14,
0
],
"U32": [
89.111,
52.903,
0
],
"Y1": [
91.611,
45.903,
180
],
"C740": [
95.111,
45.903,
180
],
"C741": [
88.111,
44.903,
180
],
"C743": [
86.361,
59.403,
0
],
"R708": [
83.861,
46.903,
180
],
"U20": [
93.0,
97.0,
270
],
"J23": [
94.572,
5.123,
0
],
"J1": [
14.48,
10.663,
90
],
"F1": [
24.73,
3.503,
0
],
"Q1": [
30.73,
6.503,
0
],
"D1": [
37.73,
5.003,
0
],
"C2": [
55.245,
6.731,
270
],
"C3": [
46.609,
6.731,
270
],
"C4": [
30.61,
12.703,
0
],
"C5": [
39.73,
13.003,
0
],
"C6": [
26.797,
11.938,
0
],
"Rs6": [
63.75,
4.0,
0
],
"U13": [
63.75,
10.5,
180
],
"J26": [
83.0,
118.5,
180
],
"J27": [
70.0,
118.5,
180
],
"J33": [
107.568,
74.925,
270
],
"U19": [
58.5,
116.5,
180
],
"C8": [
59.944,
118.491,
0
],
"L2": [
53.0,
116.5,
180
],
"R2": [
62.865,
117.475,
0
],
"R3": [
62.75,
115.75,
180
],
"U30": [
45.5,
116.5,
180
],
"C733": [
45.5,
120.5,
180
],
"L3": [
38.5,
116.5,
180
],
"R706": [
46.99,
118.999,
180
],
"R707": [
44.196,
118.999,
180
],
"Q3": [
71.247,
8.763,
270
],
"R9A": [
81.3077,
4.4266,
0
],
"R9B": [
81.3077,
8.9266,
180
],
"R9C": [
88.5,
13.4266,
0
],
"R9D": [
88.5,
17.9266,
180
],
"R9E": [
88.5,
22.4266,
0
],
"U29": [
67.0,
13.0,
180
],
"R4": [
73.406,
12.8504,
90
],
"R5": [
72.136,
12.827,
270
],
"R6": [
66.548,
7.62,
90
],
"U35": [
7.239,
53.975,
180
],
"U6": [
11.513,
77.037,
180
],
"U7": [
12.402,
66.402,
180
],
"C608": [
6.588,
73.041,
180
],
"C611": [
8.001,
61.722,
90
],
"C612": [
13.741,
62.552,
90
],
"C603": [
98.552,
108.458,
90
],
"C736": [
100.5,
85.5,
0
],
"C739": [
98.5,
85.5,
180
],
"C738": [
92.639,
119.706,
180
],
"C742": [
87.611,
46.903,
0
],
"C609": [
13.97,
73.152,
90
],
"R615": [
12.227,
80.029,
0
],
"R617": [
14.478,
69.723,
180
],
"C734": [
31.0,
114.0,
0
],
"C735": [
31.0,
119.0,
0
],
"C737": [
85.5,
67.0,
270
],
"C752": [
6.7111,
66.4429,
180
],
"C753": [
7.9651,
69.5459,
90
],
"C10": [
69.0,
122.5,
0
],
"C7": [
55.75,
120.75,
90
],
"C731": [
49.25,
117.5,
270
],
"C732": [
41.021,
122.174,
180
],
"C9": [
50.0,
122.5,
0
],
"DZ1": [
31.0,
1.65,
0
],
"C1": [
34.0,
1.2,
0
],
"R1": [
36.0,
1.2,
0
],
"R614": [
12.827,
73.152,
90
],
"C601": [
96.774,
111.379,
0
],
"C602": [
96.647,
108.966,
90
],
"R601": [
94.7972,
108.5832,
270
],
"R609": [
87.5518,
108.5598,
270
],
"R608": [
89.662,
108.585,
270
],
"R607": [
91.44,
108.585,
270
],
"R606": [
93.3092,
108.5832,
270
],
"R610": [
76.5,
116.25,
0
],
"R604": [
90.0,
85.5,
0
],
"R605": [
88.4126,
84.23,
180
],
"R611": [
95.0,
85.5,
0
],
"R710": [
101.111,
51.403,
0
],
"R711": [
101.111,
52.903,
0
],
"R712": [
101.111,
54.403,
0
],
"R720": [
101.111,
55.903,
0
],
"R716": [
101.111,
57.903,
0
],
"C604": [
12.722,
50.516,
0
],
"R602": [
12.722,
52.016,
0
],
"R603": [
12.722,
53.516,
0
],
"C607": [
10.5801,
73.1911,
270
],
"C610": [
11.43,
62.484,
270
],
"R616": [
12.573,
62.517,
270
],
"C744": [
96.111,
48.903,
0
],
"C745": [
96.111,
50.403,
0
],
"C746": [
96.111,
51.903,
0
],
"C747": [
98.111,
51.903,
0
],
"C748": [
98.111,
53.403,
0
],
"C749": [
98.111,
54.903,
0
],
"C750": [
98.111,
56.403,
0
],
"C751": [
97.111,
59.903,
0
],
"D2": [
84.0,
113.5,
180
],
"R755": [
94.0,
113.5,
180
],
"R757": [
97.603,
116.73,
180
],
"R753": [
78.5,
113.5,
180
],
"R748": [
57.531,
121.285,
90
],
"RT601": [
91.186,
73.787,
90
],
"RT602": [
25.654,
8.509,
270
],
"Q6": [
72.5,
113.5,
180
],
"R733": [
68.0,
113.5,
180
],
"R734": [
76.0,
113.5,
180
],
"Q10": [
102.572,
110.563,
90
],
"R754": [
91.5,
113.5,
180
],
"R756": [
95.103,
116.73,
180
],
"R747": [
100.103,
116.73,
180
],
"R660": [
100.746,
83.933,
180
],
"R661": [
98.298,
83.947,
180
],
"C11": [
11.7231,
73.2399,
270
],
"U31": [
85.746,
73.562,
180
],
"R717": [
88.011,
5.08,
90
],
"R718": [
101.346,
5.461,
90
],
"R719": [
96.111,
55.903,
0
],
"R722": [
76.5,
118.0,
180
],
"R709": [
101.031,
50.167,
180
],
"R721": [
100.865,
48.087,
180
],
"R749": [
58.0,
119.5,
180
],
"R752": [
52.0,
120.0,
180
],
"Q7": [
98.044,
119.634,
0
],
"Q8": [
61.468,
120.904,
180
],
"Q9": [
94.234,
82.296,
180
],
"DZ3": [
101.473,
120.269,
270
],
"R746": [
96.9214,
122.5,
0
],
"R750": [
81.3736,
122.5,
0
],
"R751": [
78.5612,
122.4798,
0
]
}
@@ -0,0 +1,830 @@
{
"C1": [
29.42,
1.123,
0,
1
],
"C10": [
69.0,
122.5,
0,
1
],
"C11": [
11.7231,
73.2399,
270,
1
],
"C2": [
55.245,
6.731,
270,
1
],
"C3": [
46.609,
6.731,
270,
1
],
"C4": [
30.61,
12.703,
0,
1
],
"C5": [
39.73,
13.003,
0,
1
],
"C6": [
26.797,
11.938,
0,
1
],
"C601": [
96.774,
111.379,
0,
1
],
"C602": [
96.647,
108.966,
90,
1
],
"C603": [
98.552,
108.458,
90,
1
],
"C604": [
12.722,
50.516,
0,
1
],
"C607": [
10.5801,
73.1911,
270,
1
],
"C608": [
6.588,
73.041,
180,
1
],
"C609": [
13.97,
73.152,
90,
1
],
"C610": [
11.43,
62.484,
270,
1
],
"C611": [
8.001,
61.722,
90,
1
],
"C612": [
13.741,
62.552,
90,
1
],
"C7": [
55.75,
120.75,
90,
1
],
"C731": [
49.25,
117.5,
270,
1
],
"C732": [
41.021,
122.174,
180,
1
],
"C733": [
45.5,
120.5,
180,
1
],
"C734": [
31.0,
114.0,
0,
1
],
"C735": [
31.0,
119.0,
0,
1
],
"C736": [
100.5,
85.5,
0,
1
],
"C737": [
85.5,
67.0,
270,
1
],
"C738": [
92.639,
119.706,
180,
1
],
"C739": [
98.5,
85.5,
180,
1
],
"C740": [
95.111,
45.903,
180,
1
],
"C741": [
88.111,
44.903,
180,
1
],
"C742": [
87.611,
46.903,
0,
1
],
"C743": [
86.361,
59.403,
0,
1
],
"C744": [
96.111,
48.903,
0,
1
],
"C745": [
96.111,
50.403,
0,
1
],
"C746": [
96.111,
51.903,
0,
1
],
"C747": [
98.111,
51.903,
0,
1
],
"C748": [
98.111,
53.403,
0,
1
],
"C749": [
98.111,
54.903,
0,
1
],
"C750": [
98.111,
56.403,
0,
1
],
"C751": [
97.111,
59.903,
0,
1
],
"C752": [
6.7111,
66.4429,
180,
1
],
"C753": [
7.9651,
69.5459,
90,
1
],
"C8": [
59.944,
118.491,
0,
1
],
"C9": [
50.0,
122.5,
0,
1
],
"D1": [
37.73,
5.003,
0,
1
],
"D2": [
84.0,
113.5,
180,
1
],
"DZ1": [
36.96,
1.653,
180,
1
],
"DZ3": [
101.473,
120.269,
270,
1
],
"F1": [
24.73,
3.503,
0,
1
],
"J1": [
14.48,
10.663,
90,
1
],
"J23": [
94.572,
5.123,
0,
1
],
"J26": [
83.0,
118.5,
180,
1
],
"J27": [
70.0,
118.5,
180,
1
],
"J33": [
107.568,
74.925,
270,
1
],
"J34": [
46.5,
107.14,
0,
1
],
"J35": [
46.5,
86.82,
0,
1
],
"J36": [
46.5,
66.5,
0,
1
],
"J37": [
46.5,
46.18,
0,
1
],
"J38": [
46.5,
25.86,
0,
1
],
"L2": [
53.0,
116.5,
180,
1
],
"L3": [
38.5,
116.5,
180,
1
],
"Q1": [
30.73,
6.503,
0,
1
],
"Q10": [
102.572,
110.563,
90,
1
],
"Q3": [
71.247,
8.763,
270,
1
],
"Q6": [
72.5,
113.5,
180,
1
],
"Q7": [
98.044,
119.634,
0,
1
],
"Q8": [
61.468,
120.904,
180,
1
],
"Q9": [
94.234,
82.296,
180,
1
],
"R1": [
31.92,
1.123,
0,
1
],
"R2": [
62.865,
117.475,
0,
1
],
"R3": [
62.75,
115.75,
180,
1
],
"R4": [
73.406,
12.8504,
90,
1
],
"R5": [
72.136,
12.827,
270,
1
],
"R6": [
66.548,
7.62,
90,
1
],
"R601": [
94.7972,
108.5832,
270,
1
],
"R602": [
12.722,
52.016,
0,
1
],
"R603": [
12.722,
53.516,
0,
1
],
"R604": [
90.0,
85.5,
0,
1
],
"R605": [
88.4126,
84.23,
180,
1
],
"R606": [
93.3092,
108.5832,
270,
1
],
"R607": [
91.44,
108.585,
270,
1
],
"R608": [
89.662,
108.585,
270,
1
],
"R609": [
87.5518,
108.5598,
270,
1
],
"R610": [
76.5,
116.25,
0,
1
],
"R611": [
95.0,
85.5,
0,
1
],
"R614": [
12.827,
73.152,
90,
1
],
"R615": [
12.227,
80.029,
0,
1
],
"R616": [
12.573,
62.517,
270,
1
],
"R617": [
14.478,
69.723,
180,
1
],
"R660": [
100.746,
83.933,
180,
1
],
"R661": [
98.298,
83.947,
180,
1
],
"R706": [
46.99,
118.999,
180,
1
],
"R707": [
44.196,
118.999,
180,
1
],
"R708": [
83.861,
46.903,
180,
1
],
"R709": [
101.031,
50.167,
180,
1
],
"R710": [
101.111,
51.403,
0,
1
],
"R711": [
101.111,
52.903,
0,
1
],
"R712": [
101.111,
54.403,
0,
1
],
"R716": [
101.111,
57.903,
0,
1
],
"R717": [
88.011,
5.08,
90,
1
],
"R718": [
101.346,
5.461,
90,
1
],
"R719": [
96.111,
55.903,
0,
1
],
"R720": [
101.111,
55.903,
0,
1
],
"R721": [
100.865,
48.087,
180,
1
],
"R722": [
76.5,
118.0,
180,
1
],
"R733": [
68.0,
113.5,
180,
1
],
"R734": [
76.0,
113.5,
180,
1
],
"R746": [
96.9214,
122.5,
0,
1
],
"R747": [
100.103,
116.73,
180,
1
],
"R748": [
57.531,
121.285,
90,
1
],
"R749": [
58.0,
119.5,
180,
1
],
"R750": [
81.3736,
122.5,
0,
1
],
"R751": [
78.5612,
122.4798,
0,
1
],
"R752": [
52.0,
120.0,
180,
1
],
"R753": [
78.5,
113.5,
180,
1
],
"R754": [
91.5,
113.5,
180,
1
],
"R755": [
94.0,
113.5,
180,
1
],
"R756": [
95.103,
116.73,
180,
1
],
"R757": [
97.603,
116.73,
180,
1
],
"R9A": [
81.3077,
4.4266,
0,
1
],
"R9B": [
81.3077,
8.9266,
180,
1
],
"R9C": [
88.5,
13.4266,
0,
1
],
"R9D": [
88.5,
17.9266,
180,
1
],
"R9E": [
88.5,
22.4266,
0,
1
],
"RT601": [
91.186,
73.787,
90,
1
],
"RT602": [
25.654,
8.509,
270,
1
],
"Rs6": [
63.75,
4.0,
0,
1
],
"U13": [
63.75,
10.5,
180,
1
],
"U19": [
58.5,
116.5,
180,
1
],
"U20": [
93.0,
97.0,
270,
1
],
"U29": [
67.0,
13.0,
180,
1
],
"U30": [
45.5,
116.5,
180,
1
],
"U31": [
85.746,
73.562,
180,
1
],
"U32": [
89.111,
52.903,
0,
1
],
"U35": [
7.239,
53.975,
180,
1
],
"U6": [
11.513,
77.037,
180,
1
],
"U7": [
12.402,
66.402,
180,
1
],
"Y1": [
91.611,
45.903,
180,
1
]
}
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
+673
查看文件
@@ -0,0 +1,673 @@
# -*- coding: utf-8 -*-
u"""R2 底板 PCB(M8):板框 + 分区 + 插槽阵列 + 安装孔。
/c/Python314/python .claude/r2/pcb_base.py # 只算 + 自检
/c/Python314/python .claude/r2/pcb_base.py --outline # 画/重画板框(幂等)
/c/Python314/python .claude/r2/pcb_base.py --holes # 打安装孔
/c/Python314/python .claude/r2/pcb_base.py --verify # 读回校验
与 `pcb_blade.py` 是同一套接口(`PCB` / `BW` / `BH` / `ZONES` / `mill_cuts()` …),
下游的 `pcb_place` / `router` / `pcb_pour` 用 `--base` 切到这一份。
## 坐标系
**板左下角 = (0, 0),x 向右、y 向上**(EasyEDA PCB 里 y 向上为正,方案 §22.5)。
x = 槽距方向(5 槽并排),y = 槽长方向 / 板深。方案 §6.5 那张分区图可以直接抄。
## ⛔ 板框必须是**折线**不是导线
`pcb_PrimitiveLine` 在嘉立创里是**导线(铜)**。M4b~M6 的刀片板框就是这么画的:
渲染成品红、DRC 干净、四道几何校验全过,而客户端根本不认它是板框
(自动布线一路弹「请先绘制板框!」)。见方案 §27.3。
⇒ 板框只用 `pcb_PrimitivePolyline`。底板文档里现成那 4 条 `Line`(建工程时的默认框)
属于同一个坑,`push_outline()` 会把它们删掉。
## 刀片投影是硬碰撞,不是净距问题
插槽把刀片底边抬到底板上方 ~11 mm ⇒ **投影带内元件 ≤ 8 mm**(方案 §6.6)。
两颗 470 µF 直立电解、DC-005 座、TH 排针 / 风扇座都只能放到投影带**外面**
(`y < PROJ[0]` 或 `y > PROJ[1]`)。`zone_of()` / `height_ok()` 就是干这个的。
"""
import io
import json
import math
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
# ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本,先插 .claude 会 import 到 R1 那一份且不报错。
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import fp_edge # noqa: E402
PCB = '7b9524b6aaf6f235' # BASE-PCB(r2-uuids.json)
MIL = fp_edge.MIL
# ── 板框(mm,出处 方案 §3.1)─────────────────────────────────────────
BW, BH = 110.0, 125.0
CORNER_R = 5.0 # 底板四角 R5(刀片是 R2)
CORNER_SEG = 10
OUTLINE_W = 0.15
# ══ M9 新坐标模型(2026-09-09 用户拍板)══════════════════════════════════
# ⛔ **M8 的分带式布局(槽阵横排 + ctrl 带在北)在两层板上是死的**,
# 根因见方案 §29.1:单颗 Hub 扭出 5 路 D±,在扇出带里形成一道**横贯全板的墙**
# (`USB5`/`USB1` 共用 y=41.4 那条道,两段拼起来 x 24.5~105.1),
# 而墙以北没有横向通道(5 个槽体 + 每槽两端的屏蔽脚把槽阵横向堵死)。
# ⇒ 12V / 5V / 3V3 和 25 条槽上信号全都过不去,实测 173/314 条布不出来,
# 四轮打平(173/174/175/173),换 `BOT_PEN` 无效(178,更差)。
#
# ⇒ **槽阵转 90°**:槽体长轴沿 x,五片刀片沿 y 堆叠。于是
# **D± 走西缘、电源走东缘,走廊里全是平行车道,一个交叉都没有。**
#
# 为什么这条能成立(针位实测,见 §29.2):把槽转成 A1 端**朝东**之后,
# 每只脚落在哪一列 x 是定死的 ——
# x 71.5 +12V(5) ← 紧贴东缘,12V 铜条只进走廊 4.5 mm
# x 66.5 SCL/SDA x 64.0 +3V3(3) x 61.5 EFUSE_EN / INT#
# x 56.0 D± x 54.5 VDAC x 51.5 / 38.5 +5V
# x 39.5 PRSNT#
# **x > 69 只有 `+12V` 一个网** ⇒ 12V 的铜条可以在走廊东端做成「顶天立地」的一堵墙
# (同时够到上下两排焊盘),而不挡任何别的东西:其余全部从西边进来,最远只到 x=67。
SLOT_PITCH = 20.32 # 刀片堆叠间距(沿 y),= 手机+刀片+间隙,机械定死
SLOT_N = 5
SLOT_ROT = 0 # 2026-09-10 用户要求:五个插槽从 rot 180 再转 180°
# ⚠ 槽体长轴中心**不在板中心**:西区要塞下 ESP32 的「天线 7.5 + 焊盘 19 = 26.5」,
# 所以整条阵列往东挪 3 mm。刀片 98 沿 x ⇒ 盖住 x 9~107,两边各留 3 mm。
# ⛔ 再东移 2 mm(58→60):D± 通道要塞下**三条**并行车道。一对 1.2 mm 的线在占用图里
# 要 `1.41 + 2×0.81 = 3.03 mm`,三条 = 9.6,8 mm 的通道装不下 —— 现象是每对永远
# 只进得去一根(「DP 通、DM 出不来」,七对里六对)。刀片随之盖住 x 11~109。
# ⛔ **2026-09-09 用户拍板 A 方案**:通道从 10 加宽到 13.5,槽阵再东移 3.5(60→63.5)。
# 账:一对 1.0 mm 的 D± 在占用图里要 `1.21 + 2×0.71 = 2.63`;车道还要能被一根
# 0.25 的逃逸线横穿(吃掉 1.67)而仍装得下一对 ⇒ 车道 4.5,三条 = 13.5。
# 代价:刀片(98 宽)盖住 x 14.5~112.5,**伸出东板边 2.5 mm**(用户已确认)。
SLOT_CX = 46.5 # 插槽翻转后整体西移,东侧留 13.5 mm D± 通道
# ⚠ 阵列中心比板中心(62.5)**往北挪 1 mm**:南带要塞下 DC-005 的 14.5 mm 桶身,外加 0.6 净距,
# 17.26 差 1 mm。北带少 1 mm 不影响(风扇块全是小件)。
# M13:整组北移 3 mm,为最南槽 USB5 与 9 A 输入铜之间留出独立顶层走廊。
# 槽距不变,并保留北侧辅助 Buck / 风扇服务带。
ARRAY_CY = 66.5
SLOT_BODY_L = 42.0 # 槽体沿 **x**(实测 `PCIE-64P11L` 焊盘外框)
SLOT_BODY_W = 9.2 # 槽体沿 **y**
# 两排焊盘:y = 槽心 ± 3.6;单盘 0.7(x) × 2.0(y),脚距 1.0
PAD_ROW_DY = 3.6
PAD_L, PAD_W = 0.7, 2.0
# 长轴上的针位(u = 距槽体中心,正 = 西)。A1 端朝东 ⇒ 板上 x = SLOT_CX − u。
PIN_U = {'+12V': (-16.5, -14.5), 'I2C': (-12.5, -11.5), '+3V3': (-9.5, -7.5),
'EN': (-6.5, -6.5), 'DIFF': (-1.5, -0.5), 'VDAC': (0.5, 0.5),
'+5V': (3.5, 16.5), 'PRSNT': (15.5, 15.5)}
def pin_x(u):
u"""长轴坐标 u(封装)→ 板上 x。rot 0 与 rot 180 互为镜像。"""
return round(SLOT_CX + u if SLOT_ROT == 0 else SLOT_CX - u, 3)
def slot_y(i):
u"""槽 i(1 = 最南)的中心 y。"""
return round(ARRAY_CY + (i - (SLOT_N + 1) / 2.0) * SLOT_PITCH, 3)
def slot_ys():
return [slot_y(i) for i in range(1, SLOT_N + 1)]
def slot_body(i):
y = slot_y(i)
return (SLOT_CX - SLOT_BODY_L / 2, y - SLOT_BODY_W / 2,
SLOT_CX + SLOT_BODY_L / 2, y + SLOT_BODY_W / 2)
# ── 走廊:槽体之间那 11.12 mm,外加阵列两头 ────────────────────────────────
# 走廊 k(0 = 阵列最南那条,5 = 最北)。内部四条各服务上下两个槽的一排焊盘。
def corridor(k):
y0 = 0.0 if k == 0 else slot_y(k) + SLOT_BODY_W / 2
y1 = BH if k == SLOT_N else slot_y(k + 1) - SLOT_BODY_W / 2
return (0.0, round(y0, 3), BW, round(y1, 3))
def corridors():
return [corridor(k) for k in range(SLOT_N + 1)]
# ── 三条纵向分区(沿 x)───────────────────────────────────────────────────
# 西区:12V 干道;中区:5 个槽体;东区:D± 通道 + Hub / 控制器。
WEST = (0.0, 25.5) # 西侧 12V 服务区
MID = (SLOT_CX - SLOT_BODY_L / 2, SLOT_CX + SLOT_BODY_L / 2) # 34 ~ 76
EAST = (81.0, BW) # 东侧 Hub / ESP32 / mux / DAC 功能区
# 逻辑 USB 号 -> 插槽位号 / 物理槽位(南起 1..5)。
# 插槽翻转后物理顺序与 USB 号反向;混用会让五对 D± 全交叉。
USB_SLOT_REF = {1: 'J34', 2: 'J35', 3: 'J36', 4: 'J37', 5: 'J38'}
USB_SLOT_POS = {i: SLOT_N + 1 - i for i in range(1, SLOT_N + 1)}
def usb_corridor(i):
u"""USB i 的 D± 所在插槽南侧走廊。"""
return corridor(USB_SLOT_POS[i] - 1)
# ── 刀片投影:每片刀片盖住 x 6~104,沿 y 占 ±7.35 ──────────────────────────
# 保守起见整条阵列带按投影处理(限高 8 mm,方案 §6.6)。
BLADE_W = 98.0 # 刀片沿 x
BLADE_X0 = round(SLOT_CX - BLADE_W / 2, 2) # 6.0
PROJ = (round(slot_y(1) - SLOT_BODY_W / 2, 2),
round(slot_y(SLOT_N) + SLOT_BODY_W / 2, 2)) # 17.26 ~ 107.74
H_PROJ = 8.0
def blade_x_to_base_x(bx):
u"""刀片自身坐标 x → 底板 x(刀片 98 沿 x 铺开,居中对齐槽体)。"""
return round(BLADE_X0 + bx, 3)
# ── 分区(y 方向只剩三条:南带 / 阵列带 / 北带)────────────────────────────
ZONES = {
'input': (0.0, 0.0, BW, PROJ[0]), # 投影外,可放高件:DC 座 / F1 / Q1 / Φ8 电解
'array': (0.0, PROJ[0], BW, PROJ[1]), # 槽体 + 走廊 + 西区器件 + 东区高速路
'fan': (0.0, PROJ[1], BW, BH), # 投影外:风扇座 ×2 / 驱动 / J33 应急口
}
# ── 12V(9 A):南带进料 → 西侧高速路 → 每条走廊分段进槽 ────────────────
# M12 当前布局把 U35/U6/U7 放在 x=10~15;主干只能使用它们与槽体之间
# x=15.5~24.5 的连续通道。实测无异网焊盘侵入,9 mm 净宽仍高于 8 mm 要求。
# ⛔ 北端只到 y=110:再往北是风扇驱动 `Q6`/`Q7`(它们本来就该贴着风扇座),
# 高速路铺过去会被它们的非 12V 脚掐到 **5.70 mm**(断面校核实测)。
# 那一段只带风扇 + 两颗 Buck(~3 A,5.7 mm 够),但校核按 9 A 一刀切 ⇒
# 与其放宽判据,不如让干道在 110 收口,北边的 12V 由走廊 5 那堵铜墙供。
# ⛔ 铜墙必须**伸进上下两个槽体 2.0 mm**:焊盘中心距槽心 3.6、盘高 2.0,也就是占了槽体边缘往里 0~2.0 那一段。
# 只画走廊本身的话**一只 12V 脚都碰不到**,而铺铜管理器 / DRC 都不会响
# (只有 `board_check` 从板上读回铜做并查集才看得见)。
RIB_OVER = 1.4
# 旧 BUS_S 与定稿后的 J1/F1/Q1 输入链重叠,会把 VIN_RAW 直接短到 +12V。
# 干道改从投影带南沿开始,Rs6 -> 干道由路由器单独连接。
# 南端避开 y=16.25 的 2 mm VIN_SW 引线;向东加宽后,扣除其净距仍超过 8 mm。
# 东界距 x=27、宽 3 mm 的槽外壳 GND 焊盘保留 0.25 mm 净距。
# RIB_X 保持从 x=24.5 起,与主干重叠 0.75 mm,并继续避开 x=33 的 GND 针。
BUS_W = (15.5, 18.75, 25.25, BH - 0.5)
RIB_X = (24.5, 32.4)
def rib_rects():
u"""五个插槽的 12V 分段铜墙。
下排是 x=30/31/32,上排是 x=31/32;上排 x=30 紧邻 GND,槽心 x=27
还有巨大的外壳 GND 盘。所以不能再用「整条走廊顶天立地」的矩形。
每排用窄指覆盖电源脚,再在排外用宽帽接回西侧干道。
"""
out = []
c = 0.25
row_o = PAD_ROW_DY + PAD_W / 2
row_i = PAD_ROW_DY - PAD_W / 2
x_lower = round(pin_x(-16.5) - PAD_L / 2 - 0.05, 3) # x=30 焊盘左侧
x_upper = round(pin_x(-15.5) - PAD_L / 2 - 0.05, 3) # x=31 焊盘左侧
for i in range(1, SLOT_N + 1):
cy = slot_y(i)
cap_lo = round(cy - row_o - RIB_OVER, 3)
cap_hi = round(cy - row_o - c, 3)
# 最南宽帽直接接到主干,并停在 J38 外壳 GND 的净距边界以内。
if i == 1:
cap_lo = BUS_W[1]
top_hi = round(cy + row_o + RIB_OVER, 3)
# 北移后的 J34 靠近 C734;北帽只需覆盖针排并接到主干,不再向北多伸。
if i == SLOT_N:
top_hi = round(cy + row_o + 0.9, 3)
out += [
(RIB_X[0], cap_lo, RIB_X[1], cap_hi),
(x_lower, round(cy - row_o - c - 0.05, 3), RIB_X[1],
round(cy - row_i + c + 0.05, 3)),
(x_upper, round(cy + row_i - c - 0.05, 3), RIB_X[1],
round(cy + row_o + c + 0.05, 3)),
(RIB_X[0], round(cy + row_o + c, 3), RIB_X[1], top_hi),
]
return out
def bus_rects():
return [BUS_W] + rib_rects()
# ── +3V3 铜墙(M9b,2026-09-10 用户拍板)───────────────────────────────────
# `+3V3` 是全板最大的网(47 只脚),A* 当一棵树布 ⇒ **整网崩**(M9 实测 46/47 没布通)。
# 办法与 12V 同:把脚位那一段直接做成铜,脚从 A* 里摘掉。
#
# ⛔ 但 3V3 的墙**不能像 12V 那样顶天立地**,因为它不在东缘尽头,而在信号中间:
# 南排 … EN(70) 3V3(71) 3V3(72) NC(73) NC(74) NC(75) NC(76) GND(77) 12V(78,79) …
# 北排 … INT#(70) NC(71) NC(72) 3V3(73) GND(74) SDA(75) SCL(76) GND(77) 12V(78~80)
# 每个槽的 `SDA_i`/`SCL_i` 在**北排 x 75/76**,要一路往西去 `U35`(x≈10~16) ——
# 任何一条从 x>73 一直连到 x<70 的**顶层**南北向铜,都会把这十根线拦腰截断。
# ⇒ 墙只在**槽体那一段**(顶层,够着三只脚),走廊那一段**走底层桥**(0.8 mm + 两个过孔)。
# 代价是地平面上多五道 9.5 mm 长的窄缝(都在 x≈72,D± 全在 x<66,不碰 §6.2 第 1 条)。
#
# ⛔ 墙的 x 是**逐排不同的**(同一个矩形盖不住两排):
# 南排的 3V3 在 71/72、北排的在 73,而它们各自旁边就是**没有网的 NC 针**
# (南排 73、北排 71/72)。一个 70.6~73.4 的方块会同时压上 NC 针 ——
# 铺铜会绕开它们、把墙在排上切碎,而铺铜管理器和 DRC 都只说「有铜」。
# ⇒ 拆成「南窄 / 中宽 / 北窄」三段同网矩形(同网重叠是允许的,12V 那几块也是这么接的)。
W3V3_CLR = 0.25 # 到异网针的净距(规则底线 0.20,留 0.05)
W3V3_OVER = 1.4 # 墙伸进走廊多少:给底层桥的过孔留地方
W3V3_BRIDGE_W = 0.8 # 走廊底层桥的线宽(3V3 全板 ~0.3 A,0.8 够 2 A)
W3V3_VIA_DY = 5.4 # 过孔离槽心多远(针排外沿 4.6 + 0.55 环 + 余量)
def wall3v3_rects():
u"""五块 `+3V3` 槽墙,每块三段(南窄 / 中宽 / 北窄)+ 两块走廊帽。
x 的来历(全部从 `PIN_U` 算,不手抄):
西边界 = `EN` 针右沿 + 净距;东边界 = `GND(74)` 针左沿 − 净距;
南段东界 = 南排 NC(73) 针左沿 − 净距;北段西界 = 北排 NC(72) 针右沿 − 净距。
"""
c = W3V3_CLR
# 新方向是旧铜墙相对槽心的水平镜像。
x_w = round(SLOT_CX - 9.9, 3)
x_e = round(SLOT_CX - 7.1, 3)
x_s_e = round(SLOT_CX - 9.1, 3)
x_n_w = round(SLOT_CX - 8.9, 3)
row_o = PAD_ROW_DY + PAD_W / 2 # 针排外沿 4.6
row_i = PAD_ROW_DY - PAD_W / 2 # 针排内沿 2.6
out = []
for i in range(1, SLOT_N + 1):
cy = slot_y(i)
out += [
# 走廊帽(排外,没有任何针):整宽,给桥的过孔一个圆整的落点
(x_w, round(cy - row_o - W3V3_OVER, 3), x_e, round(cy - row_o - c, 3)),
# 南段:从走廊穿过南排(盖住 71/72)进到两排之间
(x_w, round(cy - row_o - c - 0.05, 3), x_s_e, round(cy - row_i + c + 0.05, 3)),
# 中段:两排之间那 5.2 mm,整宽
(x_w, round(cy - row_i + c, 3), x_e, round(cy + row_i - c, 3)),
# 北段:穿过北排(盖住 73)
(x_n_w, round(cy + row_i - c - 0.05, 3), x_e, round(cy + row_o + c + 0.05, 3)),
(x_w, round(cy + row_o + c, 3), x_e, round(cy + row_o + W3V3_OVER, 3)),
]
return out
def wall3v3_bridges():
u"""走廊里的**底层**桥:[(x, y0, y1, 宽)],以及两端的过孔 [(x, y)]。
只连相邻两块槽墙(4 段)。北端到 `U19`/`L2` 那一段交给 A*:
它是一条普通的短网,不像槽脚那样被十根 I²C 挡着。
"""
x = round(SLOT_CX - 8.5, 3) # 38.0,新方向的 3V3 墙中心
segs, vias = [], []
for i in range(1, SLOT_N):
y0 = round(slot_y(i) + W3V3_VIA_DY, 3)
y1 = round(slot_y(i + 1) - W3V3_VIA_DY, 3)
segs.append((x, y0, y1, W3V3_BRIDGE_W))
vias += [(x, y0), (x, y1)]
return segs, vias
# ── D± 竖向扇出通道(西区):`U32` 在西区中部,五对沿 y 散开再往东进走廊 ────
# ⛔ 这条带里**一件都不许放**(同 M8 的 FANOUT,理由一样:排布器会把去耦塞满,
# 然后布线器报「某一对布了 0 段」,而摆位自检 / DRC / 几何验证一条都不响)。
DP_CHAN = (67.5, PROJ[0], 81.0, PROJ[1])
# Hub USB6 -> ESP32 原生 USB 的北向短通道。U20/U32 是端点,其他元件不得进入。
USB6_CHAN = (80.5, 78.0, 84.8, 107.2)
# ⛔ 阵列**北边**还要留一条 4 mm 的过道:最北那个槽(槽 5)的 D± 焊盘在它的北排,
# 北侧走廊就是风扇带本身。不留的话逃逸落点会掉进风扇块里 ——
# 实测 `J38.launch` 起步格点 **0**,现象是「差分对 USB5 布了 0 段」。
DP_N_LANE = (MID[0], PROJ[1], BW, PROJ[1] + 4.0)
# ── 走廊「喉口」:西区器件与 D± 通道之间那一段,**一件都不许放** ────────────
# ⛔ 2026-09-09 用户看出来的:`R605~R608` / `C9` / `C731` 那一窝落在走廊 2 的
# y 段、x 20~28 上 —— 那正是西区**所有**去往槽 2 的信号(含它那一对 D±)
# 往东挤进通道的唯一喉口。堵在这儿的后果是布线器报「出不来 / 没布通」,
# 而摆位自检、DRC、几何校验一条都不会响。
# ⇒ 每条内走廊在 x 17~29 这一段设成摆位禁区(器件退到槽体那几段 y 上去)。
# ⚠ 宽度是权衡出来的:17~29(12 mm)会把西区挤爆(去耦平均从 22.9 涨到 33.3 mm),
# 而喉口只要够信号穿过去就行 —— 6 mm 在 0.5 间距下可过十根线,够一条走廊的量。
THROAT_X = (DP_CHAN[2], 87.0)
# `U32` 东沿到 D± 通道之间那条**逃逸扇形带**:十根 0.15 的细线从 QFN 脚扇到车道,
# 一件都不许放(放了就是 DRC「焊盘到导线」,实测 22 条压在 C602/C607 上)。
HUB_ESC = (DP_CHAN[2], 55.0, 86.0, 78.0)
def throat_rects():
out = []
for k in range(1, SLOT_N):
c = corridor(k)
out.append((THROAT_X[0], c[1], THROAT_X[1], c[3]))
return out
def keepout_rects():
u"""摆位禁区(不是铺铜禁区):12V 高速路 + 铜墙 + 3V3 槽墙 + D± 竖向通道 + 天线缺口。"""
return (bus_rects() + wall3v3_rects()
+ [DP_CHAN, USB6_CHAN, DP_N_LANE, ANT_NOTCH, HUB_ESC] + throat_rects())
# ── ESP32 天线缺口(西板边)────────────────────────────────────────────────
# 见 §28.14:`U20` 转 rot=90 天线朝西、天线那 7.5 mm 下方板材整块挖掉。
# 新布局里 `U20` 挪到西北(靠近风扇带的 FAN_*/NTC 和 J33),缺口跟着走。
ANT_NOTCH = (BW - 7.5, 86.0, BW, 108.0)
def notch_rect():
return ANT_NOTCH
# ── 安装孔(M3 尼龙立柱,NPTH)────────────────────────────────────────
HOLE_D = 3.2
HOLE_KEEP = 3.2
HOLE_INSET = 4.0
MOUNT_HOLES = {
'H1': (HOLE_INSET, HOLE_INSET),
'H2': (BW - HOLE_INSET, HOLE_INSET),
'H3': (HOLE_INSET, BH - HOLE_INSET),
'H4': (BW - HOLE_INSET, BH - HOLE_INSET),
}
# ── 下游脚本要的空壳 ─────────────────────────────────────────────────────
FLEX_W = {}
SLOT_Y = None
PHONE_ORG = None
PHONE_WH = None
def phone_holes():
u"""两层都不许压的孔:四个 M3 安装孔(它们是 `pcb_PrimitivePad`,不在 pads.json 里)。"""
return dict(MOUNT_HOLES)
def mill_cuts():
return []
def notch_break():
return None
def corner(cx, cy, a0, a1):
pts = []
for k in range(CORNER_SEG + 1):
a = math.radians(a0 + (a1 - a0) * k / CORNER_SEG)
pts.append((cx + CORNER_R * math.cos(a), cy + CORNER_R * math.sin(a)))
return pts
def outline_pts():
u"""板框点列(逆时针,从底边左端 (r,0) 起,闭合回 (r,0))。东边带天线缺口。"""
r = CORNER_R
pts = [(r, 0.0), (BW - r, 0.0)]
pts += corner(BW - r, r, 270, 360)
pts += [(BW, ANT_NOTCH[1]), (ANT_NOTCH[0], ANT_NOTCH[1]),
(ANT_NOTCH[0], ANT_NOTCH[3]), (BW, ANT_NOTCH[3]),
(BW, BH - r)]
pts += corner(BW - r, BH - r, 0, 90)
pts += [(r, BH)]
pts += corner(r, BH - r, 90, 180)
pts += [(0.0, r)]
pts += corner(r, r, 180, 270)
out = [pts[0]]
for q in pts[1:]:
if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9:
out.append(q)
assert abs(out[0][0] - out[-1][0]) < 1e-9 and abs(out[0][1] - out[-1][1]) < 1e-9, u'板框点列没有闭合'
return out
def outline_polygon():
return outline_pts()
def zone_of(y):
for k, z in ZONES.items():
if z[1] <= y < z[3]:
return k
return None
def height_ok(y, h, x=None):
u"""该位置允许 h mm 高的件吗(刀片投影是二维硬碰撞)。"""
in_y = PROJ[0] <= y <= PROJ[1]
in_x = x is None or BLADE_X0 <= x <= BLADE_X0 + BLADE_W
return not (in_x and in_y and h > H_PROJ)
def selfcheck():
bad = []
ys = slot_ys()
# 阵列必须居中、间距是机械定死的 20.32
for a, b in zip(ys, ys[1:]):
if abs((b - a) - SLOT_PITCH) > 1e-6:
bad.append(u'槽间距 %.3f != %.2f' % (b - a, SLOT_PITCH))
if abs((ys[0] + ys[-1]) / 2.0 - ARRAY_CY) > 1e-6:
bad.append(u'阵列没居中: %s' % ys)
# 槽体不许出板边(南北两头)
if ys[0] - SLOT_BODY_W / 2 < 2.0 or ys[-1] + SLOT_BODY_W / 2 > BH - 2.0:
bad.append(u'槽体贴板边太近: %.2f / %.2f'
% (ys[0] - SLOT_BODY_W / 2, ys[-1] + SLOT_BODY_W / 2))
# 槽体长轴不许出板边(东西两头),且要给东区高速路让出地方
if SLOT_CX - SLOT_BODY_L / 2 < WEST[1] - 1e-6 or SLOT_CX + SLOT_BODY_L / 2 > EAST[0] + 1e-6:
bad.append(u'槽体 x %.1f~%.1f 越过了西/东区边界 %.1f / %.1f'
% (SLOT_CX - SLOT_BODY_L / 2, SLOT_CX + SLOT_BODY_L / 2, WEST[1], EAST[0]))
# 走廊必须首尾相接、铺满板
cs = corridors()
for a, b in zip(cs, cs[1:]):
if a[3] > b[1] + 1e-6:
bad.append(u'走廊重叠: %s / %s' % (a, b))
if abs(cs[0][1]) > 1e-6 or abs(cs[-1][3] - BH) > 1e-6:
bad.append(u'走廊没铺满 0~%.1f' % BH)
for k in range(1, SLOT_N):
w = cs[k][3] - cs[k][1]
if abs(w - (SLOT_PITCH - SLOT_BODY_W)) > 1e-6:
bad.append(u'走廊 %d 净宽 %.2f != %.2f' % (k, w, SLOT_PITCH - SLOT_BODY_W))
# 分区必须首尾相接
ks = sorted(ZONES, key=lambda k: ZONES[k][1])
for a, b in zip(ks, ks[1:]):
if abs(ZONES[a][3] - ZONES[b][1]) > 1e-6:
bad.append(u'分区 %s 顶 %.1f != %s 底 %.1f' % (a, ZONES[a][3], b, ZONES[b][1]))
if abs(ZONES[ks[0]][1]) > 1e-6 or abs(ZONES[ks[-1]][3] - BH) > 1e-6:
bad.append(u'分区没铺满 0~%.1f' % BH)
if abs(PROJ[0] - ZONES['input'][3]) > 1e-6 or abs(PROJ[1] - ZONES['fan'][1]) > 1e-6:
bad.append(u'刀片投影 %s 与 input/fan 带边界对不上' % (PROJ,))
# ⛔ 这条是新布局能成立的**判据本身**:x > RIB_X[0] 只许有 +12V。
# 别的网只要有一只脚落到铜墙的 x 区间里,那堵墙就不能顶天立地了。
for tag, (u0, u1) in PIN_U.items():
if tag == '+12V':
continue
for u in (u0, u1):
if pin_x(u) < RIB_X[1] + 0.5:
bad.append(u'%s 的针落在 x=%.1f,侵入 12V 铜墙区(x<%.1f 只许有 +12V)'
% (tag, pin_x(u), RIB_X[1]))
# 12V 铜墙要够得着 12V 针
if not (RIB_X[0] < pin_x(PIN_U['+12V'][0]) < RIB_X[1]):
bad.append(u'12V 铜墙 x %.1f~%.1f 够不着 12V 针 x=%.1f'
% (RIB_X[0], RIB_X[1], pin_x(PIN_U['+12V'][0])))
# 东缘高速路净宽(1 oz 外层 9 A ΔT20K 要 ≥8 mm)
if BUS_W[2] - BUS_W[0] < 8.0:
bad.append(u'西缘 12V 高速路只有 %.1f mm,9 A 要 ≥8' % (BUS_W[2] - BUS_W[0]))
if [USB_SLOT_REF[i] for i in range(1, SLOT_N + 1)] != ['J34', 'J35', 'J36', 'J37', 'J38']:
bad.append(u'USB -> 插槽位号映射被改坏')
for i in range(1, SLOT_N + 1):
c = usb_corridor(i)
if c[0] >= c[2] or c[1] >= c[3]:
bad.append(u'USB%d 的南侧走廊无效: %s' % (i, c))
# 安装孔:不许出板 / 压槽体 / 被圆角切 / 落进天线缺口
for n, (hx, hy) in MOUNT_HOLES.items():
if not (HOLE_KEEP < hx < BW - HOLE_KEEP and HOLE_KEEP < hy < BH - HOLE_KEEP):
bad.append(u'安装孔 %s 出板或贴边: (%.1f, %.1f)' % (n, hx, hy))
for i in range(1, SLOT_N + 1):
b0, b1, b2, b3 = slot_body(i)
if b0 - HOLE_KEEP < hx < b2 + HOLE_KEEP and b1 - HOLE_KEEP < hy < b3 + HOLE_KEEP:
bad.append(u'安装孔 %s 压到槽 %d' % (n, i))
for cx, cy in ((CORNER_R, CORNER_R), (BW - CORNER_R, CORNER_R),
(CORNER_R, BH - CORNER_R), (BW - CORNER_R, BH - CORNER_R)):
out_x = hx < cx if cx < BW / 2 else hx > cx
out_y = hy < cy if cy < BH / 2 else hy > cy
if out_x and out_y and math.hypot(hx - cx, hy - cy) > CORNER_R - HOLE_KEEP:
bad.append(u'安装孔 %s 被 R%.0f 圆角切到' % (n, CORNER_R))
if (ANT_NOTCH[0] - HOLE_KEEP < hx < ANT_NOTCH[2] + HOLE_KEEP and
ANT_NOTCH[1] - HOLE_KEEP < hy < ANT_NOTCH[3] + HOLE_KEEP):
bad.append(u'安装孔 %s 落进 ESP32 天线缺口 (%.1f, %.1f)' % (n, hx, hy))
sep = chr(10) + ' '
assert not bad, u'底板自检不过:' + sep + sep.join(bad)
return True
# ── 下发 ──────────────────────────────────────────────────────────────
def call(js, timeout=180):
r = run(js, timeout=timeout)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800])
return r['result']
def mil(v):
return round(v * MIL, 4)
def polygon_src(pts):
a = [mil(pts[0][0]), mil(pts[0][1])]
for q in pts[1:]:
a += ['L', mil(q[0]), mil(q[1])]
if pts[0] != pts[-1]:
a += ['L', mil(pts[0][0]), mil(pts[0][1])]
return a
def push_outline():
u"""板框 = 一条闭合折线;顺带删掉建工程时留下的 4 条层 11 **导线**(§27.3 的坑)。"""
src = polygon_src(outline_polygon())
js = ("await eda.dmt_EditorControl.openDocument('%s');" % PCB +
"await new Promise(r=>setTimeout(r,2500));" +
"const oldL = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11)"
".map(l=>l.primitiveId);" +
"if (oldL.length) await eda.pcb_PrimitiveLine.delete(oldL);" +
"const oldP = (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11).map(p=>p.getState_PrimitiveId());" +
"if (oldP.length) await eda.pcb_PrimitivePolyline.delete(oldP);" +
"const poly = await eda.pcb_MathPolygon.createPolygon(%s);" % json.dumps(src) +
"const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, %s);" % mil(OUTLINE_W) +
"await eda.pcb_Document.save();" +
"return {delLine: oldL.length, delPoly: oldP.length, made: !!r,"
" now: (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11).length,"
" line11: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11).length};")
r = call(js, timeout=300)
print(u'板框折线: %s(删旧导线 %d / 旧折线 %d);层 11 现有折线 %d / 导线 %d'
% (u'建好' if r['made'] else u'失败', r['delLine'], r['delPoly'], r['now'], r['line11']))
if not r['made'] or r['now'] != 1:
raise SystemExit(u'!! 板框没画成')
return r
PUSH_HOLES_JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,2500));
const H = %s, D = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const num = p.getState_PadNumber();
if (typeof num === 'string' && /^H[1-9]$/.test(num))
await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (const [name, x, y] of H) {
// layer 12 = 多层;末位 metallization=false ⇒ NPTH。参数位序照抄 pcb_blade.push_holes
await eda.pcb_PrimitivePad.create(12, name, x, y, 0, ['ELLIPSE', D, D], '',
['ROUND', D], 0, 0, 0, false);
k++;
}
await eda.pcb_Document.save();
return {made: k, now: (await eda.pcb_PrimitivePad.getAll())
.filter(p => /^H[1-9]$/.test(String(p.getState_PadNumber()))).length};
"""
def push_holes():
arr = [[k, mil(v[0]), mil(v[1])] for k, v in sorted(MOUNT_HOLES.items())]
r = call(PUSH_HOLES_JS % (PCB, json.dumps(arr), mil(HOLE_D)), timeout=300)
print(u'安装孔: 下发 %d / 读回 %d,NPTH Φ%.1f(M3 尼龙立柱)' % (r['made'], r['now'], HOLE_D))
for k, v in sorted(MOUNT_HOLES.items()):
print(u' %-3s (%6.2f, %6.2f)' % (k, v[0], v[1]))
return r
def verify():
r = call("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2000));"
"const ps = (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11);"
"const line11 = (await eda.pcb_PrimitiveLine.getAll()||[])"
".filter(l=>l.layer===11).length;"
"const holes = (await eda.pcb_PrimitivePad.getAll())"
".filter(p=>/^H[1-9]$/.test(String(p.getState_PadNumber())))"
".map(p=>[String(p.getState_PadNumber()), p.getState_X(), p.getState_Y()]);"
"return {n: ps.length, poly: ps.length ? ps[0].getState_Polygon() : null,"
" line11, holes, comps: (await eda.pcb_PrimitiveComponent.getAll()).length};" % PCB,
timeout=180)
bad = []
if r['n'] != 1:
bad.append(u'层 11 上有 %d 条折线(应为 1)' % r['n'])
if r['line11']:
bad.append(u'层 11 上还有 %d 条**导线**(板框画成铜的老坑,§27.3)' % r['line11'])
pts = []
src = r['poly']
if isinstance(src, dict):
src = src.get('pointArr') or src.get('points') or src.get('polygon') or src
if isinstance(src, list):
i = 0
while i < len(src):
if src[i] in ('L', 'l'):
i += 1
continue
if isinstance(src[i], (int, float)) and i + 1 < len(src) and isinstance(src[i + 1], (int, float)):
pts.append((src[i] / MIL, src[i + 1] / MIL))
i += 2
else:
i += 1
if pts:
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
if abs(min(xs)) > 0.01 or abs(max(xs) - BW) > 0.01 or \
abs(min(ys)) > 0.01 or abs(max(ys) - BH) > 0.01:
bad.append(u'外接矩形 %.2f~%.2f x %.2f~%.2f != 0~%.0f x 0~%.0f'
% (min(xs), max(xs), min(ys), max(ys), BW, BH))
if abs(pts[0][0] - pts[-1][0]) > 0.01 or abs(pts[0][1] - pts[-1][1]) > 0.01:
bad.append(u'折线首尾不重合: %s vs %s' % (pts[0], pts[-1]))
if abs(len(pts) - len(outline_polygon())) > 1:
bad.append(u'点数 %d != %d' % (len(pts), len(outline_polygon())))
print(u'读回板框折线 1 条 / %d 点,外接 %.2f~%.2f x %.2f~%.2f;层 11 遗留导线 %d'
% (len(pts), min(xs), max(xs), min(ys), max(ys), r['line11']))
else:
bad.append(u'读不回折线点列(原始值 %s)' % str(r['poly'])[:120])
got = {h[0]: (round(h[1] / MIL, 3), round(h[2] / MIL, 3)) for h in r['holes']}
for n, (hx, hy) in sorted(MOUNT_HOLES.items()):
if n not in got:
bad.append(u'安装孔 %s 没打' % n)
elif abs(got[n][0] - hx) > 0.02 or abs(got[n][1] - hy) > 0.02:
bad.append(u'安装孔 %s 位置 %s != (%.1f, %.1f)' % (n, got[n], hx, hy))
print(u'安装孔读回 %d 个;板上元件 %d 件' % (len(got), r['comps']))
if bad:
print(u'[X] 校验不过:')
for b in bad:
print(u' ' + b)
else:
print(u'[OK] 板框是一条闭合折线、外接矩形对上、安装孔齐')
return bad
if __name__ == '__main__':
selfcheck()
print(u'底板 %.0f x %.0f,四角 R%.0f;PCB=%s' % (BW, BH, CORNER_R, PCB))
print(u'槽阵 5 槽 @%.2f(沿 y 堆叠,长轴沿 x,rot %d): %s'
% (SLOT_PITCH, SLOT_ROT, u' / '.join('%.2f' % y for y in slot_ys())))
print(u'槽体 x %.1f~%.1f;刀片投影 y %.1f~%.1f(带内元件 ≤ %.0f mm)'
% (MID[0], MID[1], PROJ[0], PROJ[1], H_PROJ))
print(u'西区 x %.0f~%.0f | D± 通道 %.0f~%.0f | 中区 %.0f~%.0f | 东区 %.0f~%.0f'
% (WEST[0], WEST[1], DP_CHAN[0], DP_CHAN[2], MID[0], MID[1], EAST[0], EAST[1]))
print(u'12V:Rs6 单独接入 → 西缘高速路 %s → %d 堵铜墙'
% (BUS_W, len(rib_rects())))
if '--outline' in sys.argv:
push_outline()
verify()
elif '--holes' in sys.argv:
push_holes()
verify()
elif '--verify' in sys.argv:
verify()
文件差异内容过多而无法显示 加载差异
+889
查看文件
@@ -0,0 +1,889 @@
# -*- coding: utf-8 -*-
u"""R2 底板 PCB 的缝合过孔 + 铺铜 + 断面校核(M8e)。
/c/Python314/python .claude/r2/pour_base.py # 只算 + 自检 + 断面校核
/c/Python314/python .claude/r2/pour_base.py --stitch # 打 GND 缝合过孔
/c/Python314/python .claude/r2/pour_base.py --push # 清旧铜面 + 重建 + 重填
/c/Python314/python .claude/r2/pour_base.py --rebuild # 只重填
⛔ **顺序**(刀片 M7 §27.10 的结论,两层板同样成立):
摆位 → 布线 → 下发 → **缝合过孔** → 斜切 → **铺铜**。
重跑布线器要把缝合过孔和斜切一起重跑。
## 叠层(方案 §6.1)
| 设计 | EasyEDA | id | 内容 |
|---|---|---|---|
| L1 | Top Layer | 1 | 全部元件 + 全部信号 + 12V 宽铜 + GND 底填 |
| L2 | Bottom Layer | 2 | **完整地平面**(D± 走廊下方一刀不许割,§6.2 第 1 条)|
## 两个只有这块板才有的约束
1. **`U20` 天线净空**:ESP32-S3-WROOM-1 的天线那一头(焊盘北沿外 7.5 mm)
**两层都不许有铜**。铜面不能带洞(`createComplexPolygon` 传多个环会把客户端挂死,
刀片 §24 的坑),⇒ 用**若干块单环矩形**绕开它,同网接壤会自然连成一片。
2. **入口链 9 A 靠铺铜**:`VIN_RAW`/`VIN_FUSED`/`VIN_SW`/`+12V` 在 input 带里各自
铺一块顶层铜(优先级比 GND 高,先占地盘),断面要 ≥ 方案 §6.4 的要求。
⚠ 这四个是**串联的四段**,不是一个网 —— 每段各铺各的,中间隔着 F1 / Q1 / Rs6。
## 断面校核(`--cuts`,办法沿用 R1 `pcb1_pour.py`)
在铺铜上画一条断面线,数「这条线上属于本网、且没有被别人的焊盘/过孔/导线占掉」的
长度。⚠ 它算的是**计划的铜面**,不是板上填出来的铜 —— 填的时候还会被反焊盘再吃掉一点,
所以判据留了 15% 余量。真正的判据仍是「铺完截图 + DRC」。
"""
import io
import json
import math
import os
import sys
import time
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import pcb_base as B # noqa: E402
import place_base as PL # noqa: E402
PCB = B.PCB
MIL = B.MIL
L_TOP, L_BOT = 1, 2
INSET = 0.5 # 铜面离板边
CLR = 0.25 # 断面校核里别人的铜要让开多少
VIA_D, VIA_HOLE = 0.6, 0.3
STITCH_PITCH = 4.0
STITCH_CLR = 0.5 # 缝合过孔到任何焊盘 / 导线的净距
PADS = os.path.join(HERE, '_base_pads.json')
ROUTES = os.path.join(HERE, '_base_routes.json')
ROUTE_SCHEMA = 7
# 入口链四段的铺铜:从各自的焊盘外框算,不手抄坐标(摆位一改就跟着走)
POWER_NETS = [('VIN_RAW', 1), ('VIN_FUSED', 2), ('VIN_SW', 3)]
# ⚠ `+12V` **不按焊盘外框算** —— 它不是入口链里的一段,是遍布全板的母线。
# 三段几何写在 `pcb_base.BUS_IN/BUS_UP/BUS_SLOT`(那里是唯一事实来源,
# `place_base` 也拿它当摆位禁区)。
# ⛔ 入口链的铜只能从 y=6.3 往上:**输入带最南那 5 mm 归 12V 横脊**(`pcb_base.SPINE`)。
# 两者同层不同网,搭上就把 `Rs6` 短路了。
# M9:南带整条都归入口链(横脊没了 —— 12V 不再需要穿过 D±,它全在东缘)
POWER_BAND = (0.0, 0.8, B.BW, B.ZONES['input'][3])
POWER_GROW = {'VIN_RAW': 1.2, 'VIN_FUSED': 1.2, 'VIN_SW': 1.2}
# Q1 栅极网络(DZ1 钳位 / C1 栅源电容 / R1 下拉)。它们的焊盘**不撑大**入口链铜面
# (和 U13.10 同理:不是 9 A 主电流焊盘),而且整团从铜面里挖出一个缺口给 GATE 走线。
# ⛔ 不挖的话 Q1.4 北、西、南三面是 VIN_FUSED、东面是 VIN_SW,中间只剩 0.25 mm 的缝 ——
# 布线器报「GATE: Q1.4 出不来」,看着像拥塞,其实是铜面把栅极脚封死了。
GATE_PARTS = ('DZ1', 'C1', 'R1')
NOTCH_CLR = 0.3 # 缺口在 GATE 焊盘外框外再让多少
NOTCH_HEAD = 1.5 # 缺口在栅极件北侧留多高:GATE 要从 C1 北边绕过去
# 落在硬功率铜面里 / GATE 缺口里的 **GND 贴片焊盘**:(位号, 脚, 过孔在哪一侧, 是否挖地岛)。
# ⛔ GND 不走线、只靠铺铜 + 过孔。这几只脚四周全是 VIN_SW 铜(或是缺口里的一小块孤立地铜),
# 没有专用过孔的话它们根本接不到地 —— D1 是 TVS,浪涌电流全靠这一只脚回地。
# 通孔地脚(C2/C3 的电解、J1)自己穿到底层地平面,不在此列。
# 地岛从铜面里挖一条缝通到铜面外(S = 向南到板边,N = 向北出 VIN_SW 顶边),过孔排在开口那一侧。
GND_PAD_VIAS = [('D1', '2', 'S', True), ('C5', '1', 'N', True), ('R1', '1', 'N', False)]
ISL_M = 0.6 # 地岛在焊盘外框外让多少
ISL_VIA_GAP = 0.25 # 过孔铜边到焊盘边(别让焊锡流进孔里)
ISL_VIA_PITCH = 0.9 # 同排过孔中心距(≥ 0.6 的同网孔距)
# 断面校核(方案 §6.4)。入口链电流方向是 +x ⇒ 断面是**竖直**的,量 y 方向的净铜高,
# 而且要取沿途的**最小值**(第一版取了最大值,等于没查)。
# (名字, 网, 是否强制)
# (铜面名, 标签, 网, 沿哪个轴流 'x'/'y', 要求净铜, 是否强制)
CUTS = [
('PWR-VIN_RAW', u'J1→F1 入口', 'VIN_RAW', 'x', 8.0, False),
('PWR-VIN_FUSED', u'F1→Q1', 'VIN_FUSED', 'x', 8.0, False),
# ⚠ `VIN_SW` 的最窄点在 `Rs6`/`U13` 那一头(Kelvin 要求它们贴在一起)。
# 6.5 mm 在 9 A 下 ΔT≈25 K 而不是 20 K,只有分流电阻前后 2~3 mm 一小段,可接受。
('PWR-VIN_SW', u'Q1→储能→Rs6', 'VIN_SW', 'x', 6.5, True),
# 西缘高速路的电流是**沿 y** 流的 ⇒ 断面是水平的,量 x 方向净铜宽。
# 写成 'x' 的话量到的是它 105 mm 的长度,永远「通过」,等于没查。
('PWR-12V-0', u'12V 西缘高速路', '+12V', 'y', 8.0, True),
]
CUT_MIN_W = 8.0 # IPC-2221 1 oz 外层 9 A ΔT20K ⇒ ≥8 mm 净铜宽
# ── 低压轨的「软铺铜」(2026-09-09 用户拍板:先按分布把电源网铺铜)────────────
# ⛔ **为什么单独一类、名字不叫 `PWR-`**:
# `PWR-` 是**硬铺铜** —— 几何算出来、要过断面校核,而且会当障碍喂给布线器
# (`route_base.pour_blocks()`),别的网一律不许穿过去。12V 母线被一根信号线
# 切断 = 9 A 没了,而铺铜管理器 / DRC / 连通性三个都只说「有铜」。
# `RAIL-` 是**软铺铜**:只是「这一带的余铜归这个网」,铺铜时自然绕开已布的信号线,
# **不喂给布线器** —— 喂了的话 ctrl 带 57 个件的信号一根都出不去。
# 判据也换一个:硬铺铜看断面,软铺铜看 **`board_check` 从板上读回来的连通性**,
# 因为它被信号切碎成孤岛是完全可能的,而那只有读铜才看得见。
#
# 依据是 `_base_pads.json` 实测的分布,不是画出来的(见 §28.15):
# `+3V3` 47 只脚里 **26 只**在 ctrl 带 x 28~86 / y 86~112(其余 15 只在槽上、4 只在风扇带)
# `+3V3_HUB` 21 只脚分成 U32/去耦组与 U31/C737 两簇
# `+5V` ctrl 带东头 2 只(`R706` / `L3`,5V Buck 那一窝);槽上那 10 只要走走廊,不在这
# ⚠ 三块互不重叠(不同网同层重叠 = 短路),`selfcheck()` 会查。
# ⚠ M9 暂时清空:`+3V3` 与 `+3V3_HUB` 的脚在西区互相穿插(排布器按「离目标最近」摆的),
# 两个网划不出互不重叠的矩形。新拓扑没有 M8 那道墙,先让布线器直接布,
# 看失败分布再决定要不要为它们改摆位 + 加软铺铜。
RAILS = [
# U31 与 U32 相距较远,分成两块局部软铜,中间由 A* 明确连接。
# 这比一块 24x60 mm 大面更不容易被控制信号切成孤岛。
# M12 采用当前手工布局后旧坐标已失效:U31 移到 y=73.6,U32 移到 y=52.9。
('RAIL-3V3HUB-LDO', '+3V3_HUB', (77.8, 71.5, 90.2, 75.7)),
('RAIL-3V3HUB-HUB', '+3V3_HUB', (84.0, 47.8, 102.0, 60.7)),
# ⛔ **`+3V3` 试过铺两块,反而更差**(45 → 58 条):软铺铜会逼每只**区外**的脚
# 各自去够铜面,而原来布线器可以把它们串成一条链。
# ⇒ 判据:软铺铜只在「大部分脚都在区内」时划算(`+3V3_HUB` 21 只脚全在西区 ⇒ 值),
# `+3V3` 47 只脚里 15 只在槽上、6 只在风扇带 ⇒ 不值,留给布线器。
]
# ⚠ `VIN_RAW` / `VIN_FUSED` 两段**不强制**:它们的瓶颈是 DC-005 的三只脚和 1206 保险
# 本身(座子额定 2~5 A、这块板要过 9 A,是用户 2026-09-08 明确接受的已知风险,
# 设计文档 §22.6)。板上再宽的铜也救不了插头内芯与弹片的接触面。
# 真正必须够的是它们之间那两段长铜:`VIN_SW`(储能到检流)和 `+12V`(母线)。
def mil(v):
return round(v * MIL, 4)
def call(js, timeout=180):
r = run(js, timeout=timeout)
if not r.get('success'):
print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:600])
sys.exit(1)
return r['result']
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2500));" % PCB)
_PADS = [None]
def pads():
u"""底板焊盘,**按 `place_base.ROUTE_FIX` 的计划位置**(与 `route_base` 的路由模型一致)。
⛔ 以前这里读的是实测焊盘,而布线器读的是改位后的:DZ1 从 x=38.6 挪到 29.3 之后,
实测算出的 VIN_FUSED/VIN_SW 分界是 32.9,改位后算出的是 30.5(VIN_SW 盖住 Q1.3)——
布线器看到的铜面和摆位下发后真正铺出来的对不上,而且哪一边都不报错。
"""
if _PADS[0] is None:
_PADS[0], _moved = PL.route_fixed_pads(json.load(io.open(PADS, encoding='utf-8')))
return _PADS[0]
def routes():
if not os.path.exists(ROUTES):
return [], []
c = json.load(io.open(ROUTES, encoding='utf-8'))
if c.get('schema') != ROUTE_SCHEMA or c.get('slot_rot') != B.SLOT_ROT:
return [], []
return c['segs'], c['vias']
# ── 铜面几何 ─────────────────────────────────────────────────────────────
def keepouts():
u"""两层都不许铺铜的地方:`U20` 天线净空 + **天线缺口** + 四个 M3 安装孔。
⛔ 缺口那一块**板材是挖掉的**,而这份脚本的铜面几何是按 `BW × BH` 的矩形算的
(`tiles()` 只认矩形,不认板框折线)。不把缺口喂进来,GND 瓦片会照样铺到
板外的空气里 —— 板框对不上铜面,DRC 报「铜到板边」,而铺铜管理器不报。
"""
sz = PL.sizes()
n = B.notch_rect()
out = [PL.antenna_keepout(sz),
(n[0] - INSET, n[1] - INSET, n[2] + INSET, n[3] + INSET)]
r = B.HOLE_D / 2.0 + 0.6
for (hx, hy) in B.MOUNT_HOLES.values():
out.append((hx - r, hy - r, hx + r, hy + r))
return out
def tiles(x0, y0, x1, y1, cuts):
u"""把矩形按 `cuts` 切成若干块**单环**矩形。
⛔ 不能用带洞的复杂多边形:`createComplexPolygon()` 传多个环会把客户端挂死
(刀片 §24,请求 30 s 超时、进度恒 0/0,和「按钮失灵」分不清)。
同网的矩形接壤会自然连成一片铜,效果一样。
"""
xs = sorted(set([x0, x1] + [v for c in cuts for v in (c[0], c[2])
if x0 < v < x1]))
out = []
for xa, xb in zip(xs, xs[1:]):
if xb - xa < 0.6:
continue
# ⛔ **禁区的 y 区间必须先合并再配对**。原来是「按 c[1] 排序后把
# [起, 止] 一对一对塞进 ys,再 `zip(ys[0::2], ys[1::2])`」——
# 那套只在禁区互不重叠时成立。天线缺口 (84.5~107.5) 和天线净空 (86~106)
# 是**嵌套**的,塞进去就变成 [.., 84.5, 107.5, 86.0, 106.0, ..],
# 奇偶配对当场错位 ⇒ 瓦片照样盖住缺口,而**只有 `selfcheck` 那一条
# 「压进净空区」看得出来**(铺铜管理器和 DRC 都不会响)。
iv = []
for c in cuts:
if c[0] < xb - 1e-9 and xa < c[2] - 1e-9:
iv.append([max(y0, c[1]), min(y1, c[3])])
iv.sort()
merged = []
for a, b in iv:
if merged and a <= merged[-1][1] + 1e-9:
merged[-1][1] = max(merged[-1][1], b)
else:
merged.append([a, b])
cur = y0
for a, b in merged + [[y1, y1]]:
if a - cur > 0.6:
out.append((round(xa, 3), round(cur, 3), round(xb, 3), round(a, 3)))
cur = max(cur, b)
return out
def power_rect(net):
u"""入口链某一段的铜面:这个网在 input 带里全部焊盘的外框 + `POWER_GROW`。"""
z = POWER_BAND
xs, ys = [], []
for p in pads():
if p[7] != net or not (z[1] <= p[3] <= z[3]):
continue
# U13.10 是 Rs6 上游的 Kelvin/监测脚,不是 9 A 主电流焊盘。
# 把它纳入包围盒会让 VIN_SW 铜面跨过 Rs6 的 +12V 端。
if net == 'VIN_SW' and p[0] == 'U13':
continue
if p[0] in GATE_PARTS:
continue
xs += [p[2] - p[4] / 2, p[2] + p[4] / 2]
ys += [p[3] - p[5] / 2, p[3] + p[5] / 2]
if not xs:
return None
g = POWER_GROW[net]
return (max(INSET, min(xs) - g), max(z[1] + INSET, min(ys) - g),
min(B.BW - INSET, max(xs) + g), min(z[3], max(ys) + g))
def clip_chain(chain):
u"""入口链四段是**串联**的(中间隔着 F1 / Q1 / Rs6),铜面搭上就把器件短路了。
`chain` = [(net, rect)],已按 x 排好。相邻两段重叠时竖切一刀,两边各退 CLR/2。
⛔ 刀口**按焊盘空隙定,不按重叠区中点**:在两块铜 y 方向重叠的那一带里,取
「西段网最东的焊盘」与「东段网最西的焊盘」之间的中点。
取重叠区中点的旧写法在 Q1 这里是错的 —— Q1 是南北两排脚(南排源极、北排漏极),
VIN_SW 的外框又被北边的 C4/C6 拉到 x=25.8,中点直接落进 Q1 里面(x=30.6,
VIN_SW 盖住 Q1.3)。以前没出事,只是因为 DZ1 旧位置把 VIN_FUSED 撑到了 x=40。
⚠ 不能靠铺铜优先级解决:优先级只保证「低的那块被让开」,让出来的形状是不可预期的
细条,断面校核就没意义了。
"""
out = [r for _n, r in chain]
ps = pads()
def band(net, y0, y1):
return [p for p in ps if p[7] == net and p[0] not in GATE_PARTS
and not (net == 'VIN_SW' and p[0] == 'U13')
and p[3] - p[5] / 2 < y1 and p[3] + p[5] / 2 > y0]
for i in range(len(out) - 1):
a, b = out[i], out[i + 1]
if a[2] <= b[0]:
continue
y0, y1 = max(a[1], b[1]), min(a[3], b[3])
pa, pb = band(chain[i][0], y0, y1), band(chain[i + 1][0], y0, y1)
ea = max([p[2] + p[4] / 2 for p in pa] or [None])
wb = min([p[2] - p[4] / 2 for p in pb] or [None])
if ea is not None and wb is not None and ea < wb:
m = (ea + wb) / 2.0
else:
m = (a[2] + b[0]) / 2.0 # 没有干净的焊盘空隙时退回旧规则
out[i] = (a[0], a[1], round(min(a[2], m - CLR / 2), 3), a[3])
out[i + 1] = (round(max(b[0], m + CLR / 2), 3), b[1], b[2], b[3])
return out
def gate_notch():
u"""GATE 缺口(矩形列表,喂给 `tiles()`)。
主块:GATE 焊盘 + 三颗栅极件的外框,向外让 NOTCH_CLR、北侧留 NOTCH_HEAD,南到板边。
竖条:比主块更高的 GATE 焊盘(Q1.4 是 2 mm 长的 SOIC 脚)整只让出来 ——
只挖主块的话 Q1.4 北半截仍在 VIN_FUSED 铜面里,模型里等于异网铜压在焊盘上。
"""
ps = pads()
g = [p for p in ps if p[7] == 'GATE' and p[3] <= POWER_BAND[3]]
sm = [p for p in ps if p[0] in GATE_PARTS]
if not g or not sm:
return []
y1 = round(max(p[3] + p[5] / 2 for p in sm) + NOTCH_HEAD, 3)
out = [(round(min(p[2] - p[4] / 2 for p in g) - NOTCH_CLR, 3), 0.0,
round(max(p[2] + p[4] / 2 for p in g + sm) + NOTCH_CLR, 3), y1)]
for p in g:
if p[3] + p[5] / 2 + NOTCH_CLR > y1:
out.append((round(p[2] - p[4] / 2 - NOTCH_CLR, 3), 0.0,
round(p[2] + p[4] / 2 + NOTCH_CLR, 3),
round(p[3] + p[5] / 2 + NOTCH_CLR, 3)))
return out
def gnd_pad_vias():
u"""`GND_PAD_VIAS` 的几何:(要从入口链铜面里挖掉的地岛矩形, 专用 GND 过孔坐标)。"""
ps = pads()
islands, vias = [], []
for ref, pin, side, cut in GND_PAD_VIAS:
hit = [p for p in ps if p[0] == ref and str(p[1]) == pin and p[6] == L_TOP]
if len(hit) != 1 or hit[0][7] != 'GND':
raise RuntimeError(u'%s.%s 应是唯一的顶层 GND 贴片焊盘' % (ref, pin))
p = hit[0]
x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2
x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2
n = int((x1 - x0) / ISL_VIA_PITCH) + 1
vy = y0 - ISL_VIA_GAP - VIA_D / 2 if side == 'S' else y1 + ISL_VIA_GAP + VIA_D / 2
for k in range(n):
vias.append((round(p[2] + (k - (n - 1) / 2.0) * ISL_VIA_PITCH, 3), round(vy, 3)))
if cut:
# 缝一直通到板边 / 板顶:`tiles()` 只拿它切入口链那几块铜,切不到别的东西
islands.append((round(x0 - ISL_M, 3), 0.0 if side == 'S' else round(y0 - ISL_M, 3),
round(x1 + ISL_M, 3), round(y1 + ISL_M, 3) if side == 'S' else B.BH))
return islands, vias
def drain_piece(outs):
u"""Q1 漏极排(北排四脚,VIN_SW)的铜面。
⛔ 入口链是按 x 串起来切的,而 Q1 是**南北两排**:南排源极归 VIN_FUSED,北排漏极
在 VIN_SW 那块的西边界之外 —— 9 A 只能靠 A* 的 2 mm 走线从四只漏极脚爬进铜面
(2 mm 1 oz 外层只带 ~4 A)。这里按漏极排外框补一块,东头与 VIN_SW 已有的块搭接,
南沿让开 VIN_FUSED。西沿只让 CLR:再往西是死头,断面校核会把它量成瓶颈。
"""
ps = [p for p in pads() if p[0] == 'Q1' and p[7] == 'VIN_SW']
if not ps:
return None
g = POWER_GROW['VIN_SW']
x0 = min(p[2] - p[4] / 2 for p in ps) - CLR
x1 = max(p[2] + p[4] / 2 for p in ps) + g
y0 = min(p[3] - p[5] / 2 for p in ps) - g
y1 = max(p[3] + p[5] / 2 for p in ps) + g
vf = [r for (_n, net, _l, r, _p) in outs if net == 'VIN_FUSED' and r[0] < x1 and x0 < r[2]]
y0 = max([y0] + [r[3] + CLR for r in vf])
sw = [r for (_n, net, _l, r, _p) in outs
if net == 'VIN_SW' and r[1] < y1 and y0 < r[3] and r[0] >= x0]
if sw:
x1 = max(x1, min(r[0] for r in sw) + 0.6) # 保证与已有 VIN_SW 块搭接
return (round(x0, 3), round(y0, 3), round(x1, 3), round(y1, 3))
def plan():
u"""[(name, net, layer, rect, priority)],priority 小的先铺、先占地盘。"""
outs = []
chain = [(net, prio, power_rect(net)) for net, prio in POWER_NETS]
chain = [c for c in chain if c[2]]
chain.sort(key=lambda c: (c[2][0] + c[2][2]) / 2.0)
clipped = clip_chain([(c[0], c[2]) for c in chain])
cuts = gate_notch() + gnd_pad_vias()[0]
for (net, prio, _r), r in zip(chain, clipped):
parts = tiles(r[0], r[1], r[2], r[3], cuts) if cuts else [r]
# 最大一块沿用原名(CUTS 按名找),其余加序号;check_cuts 会把同名前缀的块一起算
parts.sort(key=lambda t: -(t[2] - t[0]) * (t[3] - t[1]))
for k, t in enumerate(parts):
outs.append(('PWR-%s%s' % (net.strip('+'), '-%d' % k if k else ''),
net, L_TOP, t, prio))
dp = drain_piece(outs)
if dp:
outs.append(('PWR-VIN_SW-Q1', 'VIN_SW', L_TOP, dp, dict(POWER_NETS)['VIN_SW']))
for k, r in enumerate(B.bus_rects()):
outs.append(('PWR-12V-%d' % k, '+12V', L_TOP, r, 4))
# `+3V3` 五块槽墙(几何与判据见 `pcb_base.wall3v3_rects` / `wall_check`)。
# 与 12V 同为**硬铺铜**:喂给布线器当障碍,且脚从 A* 里摘掉。
for k, r in enumerate(B.wall3v3_rects()):
outs.append(('PWR-3V3-%d' % k, '+3V3', L_TOP, r, 4))
# 低压轨的软铺铜:排在硬铺铜之后、GND 之前
for name, net, r in RAILS:
outs.append((name, net, L_TOP, r, 5))
# ⚠ `PWR-12V-*` 里 0=输入带母线 1=横脊 2~7=五根齿(3 号槽那根是两块拼的 L)
ko = keepouts()
d = INSET
for tag, lay in (('TOP', L_TOP), ('BOT', L_BOT)):
for k, t in enumerate(tiles(d, d, B.BW - d, B.BH - d, ko)):
outs.append(('GND-%s-%d' % (tag, k), 'GND', lay, t, 9))
return outs
def rrect_src(x0, y0, x1, y1):
pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)] # ⚠ 必须显式闭合
a = [mil(pts[0][0]), mil(pts[0][1])]
for p in pts[1:]:
a += ['L', mil(p[0]), mil(p[1])]
return a
def wall_check():
u"""`+3V3` 槽墙的判据(几何算不出来,只有拿实测焊盘表比才算数)。
⛔ 三条都要过,缺一条墙就是白做:
① **15 只槽脚全被盖住** —— 与 12V 那条「10 排焊盘全被盖住」同理:
少盖一只,铺铜管理器和 DRC 都只说「有铜」,而那只脚是断的。
② **一只异网针都不许压到**(`NC` 空网也算异网):南排 NC(73)、北排 NC(71/72)
就贴在 3V3 针旁边,方块一大就压上去,铺铜会绕开它们**把墙在针排上切碎**。
③ **桥的过孔要落在墙里**,否则过孔与铜面之间靠铺铜「碰巧」相接。
"""
bad = []
rects = B.wall3v3_rects()
ps = pads()
c = B.W3V3_CLR - 0.05 # 判据比构造留的净距松 0.05
def inside(x0, y0, x1, y1):
return any(r[0] - 1e-6 <= x0 and x1 <= r[2] + 1e-6 and
r[1] - 1e-6 <= y0 and y1 <= r[3] + 1e-6 for r in rects)
own = 0
for p in ps:
px0, py0 = p[2] - p[4] / 2, p[3] - p[5] / 2
px1, py1 = p[2] + p[4] / 2, p[3] + p[5] / 2
if p[7] == '+3V3':
if inside(px0, py0, px1, py1):
own += 1
continue
for r in rects:
if px0 - c < r[2] and r[0] < px1 + c and py0 - c < r[3] and r[1] < py1 + c:
bad.append(u'3V3 槽墙压到异网针 %s.%s(%s) @%.2f,%.2f'
% (p[0], p[1], p[7] or 'NC', p[2], p[3]))
break
slots = set(PL.SLOT_ORDER)
slot_own = sum(1 for p in ps if p[7] == '+3V3' and p[0] in slots)
if own != slot_own:
bad.append(u'3V3 槽脚 %d 只,墙只盖住 %d 只' % (slot_own, own))
_segs, vias = B.wall3v3_bridges()
for (vx, vy) in vias:
r = VIA_D / 2.0
if not inside(vx - r, vy - r, vx + r, vy + r):
bad.append(u'3V3 桥的过孔 (%.2f, %.2f) 不在墙里' % (vx, vy))
return bad, own, slot_own
def bus_check():
u"""12V 干道/铜墙必须覆盖全部槽脚,且不能压到异网焊盘。"""
rects = B.bus_rects()
ps = pads()
slots = set(B.USB_SLOT_REF.values())
clear = CLR - 0.05
def inside(p):
x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2
x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2
return any(r[0] - 1e-6 <= x0 and x1 <= r[2] + 1e-6 and
r[1] - 1e-6 <= y0 and y1 <= r[3] + 1e-6 for r in rects)
own = [p for p in ps if p[0] in slots and p[7] == '+12V']
missed = [p for p in own if not inside(p)]
bad = [u'12V 铜墙没盖住 %s.%s @%.2f,%.2f' % (p[0], p[1], p[2], p[3])
for p in missed]
for p in ps:
if p[7] == '+12V':
continue
px0, py0 = p[2] - p[4] / 2, p[3] - p[5] / 2
px1, py1 = p[2] + p[4] / 2, p[3] + p[5] / 2
if any(px0 - clear < r[2] and r[0] < px1 + clear and
py0 - clear < r[3] and r[1] < py1 + clear for r in rects):
bad.append(u'12V 干道/铜墙压到异网针 %s.%s(%s) @%.2f,%.2f'
% (p[0], p[1], p[7] or 'NC', p[2], p[3]))
return bad, len(own) - len(missed), len(own)
def selfcheck(ps):
bad = []
ko = [k for k in keepouts() if k not in B.bus_rects()]
for name, net, lay, r, _p in ps:
if r[2] - r[0] < 0.6 or r[3] - r[1] < 0.6:
bad.append(u'%s 太小: %s' % (name, r))
if r[0] < INSET - 1e-6 or r[1] < INSET - 1e-6 or \
r[2] > B.BW - INSET + 1e-6 or r[3] > B.BH - INSET + 1e-6:
bad.append(u'%s 出了内缩线: %s' % (name, r))
for k in ko:
if r[0] < k[2] - 1e-6 and k[0] < r[2] - 1e-6 and \
r[1] < k[3] - 1e-6 and k[1] < r[3] - 1e-6:
bad.append(u'%s 压进净空区 %s' % (name, tuple(round(v, 1) for v in k)))
# 入口链各段不许互相重叠(同层,靠 F1/Q1/Rs6 断开)。
# ⚠ **只判不同网**:`+12V` 自己那几块(母线 / 横脊)是故意相接的,重叠才对。
pw = [(n, net, r) for (n, net, _l, r, _p) in ps
if n.startswith('PWR-') or n.startswith('RAIL-')]
for i, (na, na_net, a) in enumerate(pw):
for (nb, nb_net, b) in pw[i + 1:]:
if na_net == nb_net:
continue
if a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]:
bad.append(u'%s(%s) 与 %s(%s) 在顶层重叠 —— 串联的两段铜搭上就把器件短路了'
% (na, na_net, nb, nb_net))
return bad
# ── 断面校核 ─────────────────────────────────────────────────────────────
def cut_height(net, rect, x):
u"""在 x 处竖切一刀,`rect` 这块铜面上属于 `net` 的**最宽一段**连续净铜高(mm)。
别人的焊盘 / 过孔 / 导线按 `CLR` 膨胀之后从这条线上扣掉。
⚠ 取「最宽一段」而不是总和:被挖成两条 4 mm 的铜带,不等于一条 8 mm 的。
"""
if not (rect[0] <= x <= rect[2]):
return 0.0
spans = [(rect[1], rect[3])]
blocks = []
for p in pads():
if p[7] == net or p[6] not in (1, 12):
continue
if p[2] - p[4] / 2 - CLR <= x <= p[2] + p[4] / 2 + CLR:
blocks.append((p[3] - p[5] / 2 - CLR, p[3] + p[5] / 2 + CLR))
segs, vias = routes()
for s in segs:
if s[6] == net or s[0] != 1:
continue
x0, x1 = sorted((s[1], s[3]))
hw = s[5] / 2.0 + CLR
if x0 - hw <= x <= x1 + hw:
y0, y1 = sorted((s[2], s[4]))
blocks.append((y0 - hw, y1 + hw))
for v in vias:
if len(v) > 2 and v[2] == net:
continue
r = VIA_D / 2.0 + CLR
if abs(v[0] - x) <= r:
blocks.append((v[1] - r, v[1] + r))
for (b0, b1) in blocks:
nxt = []
for (a0, a1) in spans:
if b1 <= a0 or b0 >= a1:
nxt.append((a0, a1))
continue
if b0 > a0:
nxt.append((a0, b0))
if b1 < a1:
nxt.append((b1, a1))
spans = nxt
return max([q[1] - q[0] for q in spans] or [0.0])
def cut_width_y(net, rect, y):
u"""在 y 处横切一刀,最宽一段连续净铜宽(mm)。"""
if not (rect[1] <= y <= rect[3]):
return 0.0
spans = [(rect[0], rect[2])]
blocks = []
for p in pads():
if p[7] == net or p[6] not in (1, 12):
continue
if p[3] - p[5] / 2 - CLR <= y <= p[3] + p[5] / 2 + CLR:
blocks.append((p[2] - p[4] / 2 - CLR, p[2] + p[4] / 2 + CLR))
segs, vias = routes()
for q in segs:
if q[6] == net or q[0] != 1:
continue
y0, y1 = sorted((q[2], q[4]))
hw = q[5] / 2.0 + CLR
if y0 - hw <= y <= y1 + hw:
x0, x1 = sorted((q[1], q[3]))
blocks.append((x0 - hw, x1 + hw))
for v in vias:
if len(v) > 2 and v[2] == net:
continue
rr = VIA_D / 2.0 + CLR
if abs(v[1] - y) <= rr:
blocks.append((v[0] - rr, v[0] + rr))
for (b0, b1) in blocks:
nxt = []
for (a0, a1) in spans:
if b1 <= a0 or b0 >= a1:
nxt.append((a0, a1)); continue
if b0 > a0:
nxt.append((a0, b0))
if b1 < a1:
nxt.append((b1, a1))
spans = nxt
return max([q[1] - q[0] for q in spans] or [0.0])
def series_windows(net):
u"""这个网自己串联元件占的坐标窗口(本体的焊盘外框)。
⛔ 断面校核**必须跳过这些窗口**:最窄的一刀永远落在串联元件身上
(2512 分流电阻自己就只有 3.2 mm 宽、DC 座三只脚挤在 5 mm 里),
量到的是元件的宽度不是铜的宽度,判据就没意义了。
元件本身的载流由选型保证(`Rs6` 是 2512 3 W、`F1` 是 12 A 保险),
这里要查的是**元件之间那段铜**。
"""
# ⛔ 窗口要按**整颗器件**的焊盘外框算,不是只按它属于本网的那只脚。
# 只按那只脚的话,`D1`(TVS:一端 VIN_SW、一端 GND)只有阴极那半边被跳过,
# 阳极的 GND 盘照样把断面掐断 —— 实测 `VIN_SW` 被判成 4.35 mm。
# 物理上电流是**穿过器件**的,整颗器件占的那一段本来就不该算铜宽。
ps = pads()
own = set(p[0] for p in ps if p[7] == net)
by = {}
for p in ps:
if p[0] not in own:
continue
b = by.setdefault(p[0], [1e9, 1e9, -1e9, -1e9])
b[0] = min(b[0], p[2] - p[4] / 2); b[2] = max(b[2], p[2] + p[4] / 2)
b[1] = min(b[1], p[3] - p[5] / 2); b[3] = max(b[3], p[3] + p[5] / 2)
return by
def check_cuts(ps):
print(u'')
print(u'载流断面校核(逐刀切,取**最窄**一刀,跳过串联元件本身):')
bad = []
byname = {}
for (n, _net, _l, r, _p) in ps:
byname.setdefault(n, []).append(r)
for key, label, net, axis, need, must in CUTS:
# 挖了 GATE 缺口之后一段铜会被切成几块(`PWR-X`、`PWR-X-1`…):每一刀取包含它的那块
rs = [r for n, v in byname.items() if n == key or n.startswith(key + '-') for r in v]
if not rs:
print(u' %-18s %-9s [X] 这一段没有铜面' % (label, net))
bad.append(label)
continue
r = (min(q[0] for q in rs), min(q[1] for q in rs),
max(q[2] for q in rs), max(q[3] for q in rs))
skip = []
for ref, b in series_windows(net).items():
if b[0] > r[2] or b[2] < r[0] or b[1] > r[3] or b[3] < r[1]:
continue
skip.append((ref, (b[0] - CLR, b[2] + CLR) if axis == 'x' else (b[1] - CLR, b[3] + CLR)))
lo, hi = (r[0], r[2]) if axis == 'x' else (r[1], r[3])
hs = []
t = lo + 0.25
while t <= hi - 0.25:
if not any(a <= t <= b for _ref, (a, b) in skip):
f = cut_height if axis == 'x' else cut_width_y
hs.append((max(f(net, q, t) for q in rs), t))
t += 0.25
worst = min(hs) if hs else (0.0, 0.0)
ok = worst[0] >= need
print(u' %-18s %-9s 铜面 %5.1f×%5.1f 最窄 %5.2f mm (%s=%.2f,要 %.1f) %s'
% (label, net, r[2] - r[0], r[3] - r[1], worst[0], axis, worst[1], need,
u'[OK]' if ok else (u'[X] 不够' if must else u'[~] 瓶颈在连接器,不强制')))
if skip:
print(u' (跳过串联元件 %s)' % ' '.join(sorted(ref for ref, _w in skip)))
if not ok and must:
bad.append(u'%s %s 最窄只有 %.2f mm' % (label, net, worst[0]))
return bad
# ── 缝合过孔 ─────────────────────────────────────────────────────────────
def board_tracks():
return call(OPEN + "return (await eda.pcb_PrimitiveLine.getAll()||[])"
".filter(l=>l.layer===1||l.layer===2)"
".map(l=>[l.startX/%s,l.startY/%s,l.endX/%s,l.endY/%s,"
"l.lineWidth/%s,String(l.net||'')]);" % (MIL, MIL, MIL, MIL, MIL),
timeout=300)
def _seg_d(px, py, x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
L = dx * dx + dy * dy
t = 0.0 if L < 1e-12 else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L))
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
def stitch_points(tracks=None):
u"""GND 缝合过孔的坐标。
⛔ **顶层的地铜被信号切成很多块岛,每块岛至少要有一个缝合过孔**,
否则「移除孤岛」把它整块删掉 —— 而 `rebuildCopperRegion()` /
铺铜管理器 / DRC **三个地方都不报错**(刀片 §24.4)。
4 mm 网格是为了让最小的岛也有机会摊上一个。
"""
d = pads()
if tracks is None:
tracks = board_tracks()
r = VIA_D / 2.0 + STITCH_CLR
ko = keepouts()
# ⛔ **硬功率铜面里不打缝合孔**:那里顶层没有地铜,GND 孔只会在 9 A 铜上掏洞
# (4 mm 网格一刀能掏掉 3~4 mm 净铜),而断面校核看不到缝合孔。
ko += [rr for (n, _net, _l, rr, _p) in plan() if n.startswith('PWR-')]
# 专用 GND 过孔(地岛 / 缺口里的地脚)永远在,网格点让开它们
own = gnd_pad_vias()[1]
out = list(own)
ko += [(vx - VIA_D, vy - VIA_D, vx + VIA_D, vy + VIA_D) for (vx, vy) in own]
y = INSET + 2.0
while y <= B.BH - INSET - 2.0:
x = INSET + 2.0
while x <= B.BW - INSET - 2.0:
ok = True
for k in ko:
if k[0] - r < x < k[2] + r and k[1] - r < y < k[3] + r:
ok = False
break
if ok:
for p in d:
# ⛔ **通孔地脚不能当「自己人」跳过**:缝合过孔落在它身上,
# DRC 报「钻孔到钻孔 0 mil」(实测 J1_3 / C2_2 / J33_1 三处)。
# 同网也要留钻孔间距,这是工艺规则不是电气规则。
pass
if p[2] - p[4] / 2 - r < x < p[2] + p[4] / 2 + r and \
p[3] - p[5] / 2 - r < y < p[3] + p[5] / 2 + r:
ok = False
break
if ok:
for t in tracks:
if t[5] == 'GND':
continue
if _seg_d(x, y, t[0], t[1], t[2], t[3]) < r + t[4] / 2.0:
ok = False
break
if ok:
out.append((round(x, 3), round(y, 3)))
x += STITCH_PITCH
y += STITCH_PITCH
return out
def stitch():
pts = stitch_points()
print(u'GND 缝合过孔 %d 个(%g mm 网格)' % (len(pts), STITCH_PITCH))
JS = OPEN + u"""
const MIL = %s, P = %s, D = %s, H = %s;
let old = 0;
for (const v of (await eda.pcb_PrimitiveVia.getAll()||[]))
if (String(v.net) === 'GND') { await eda.pcb_PrimitiveVia.delete(v.primitiveId); old++; }
let n = 0;
for (const p of P) {
try { await eda.pcb_PrimitiveVia.create('GND', p[0]*MIL, p[1]*MIL, H*MIL, D*MIL); n++; }
catch (e) {}
}
await eda.pcb_Document.save();
return {deleted: old, made: n, now: (await eda.pcb_PrimitiveVia.getAll()||[]).length};
""" % (MIL, json.dumps(pts), VIA_D, VIA_HOLE)
print(call(JS, timeout=900))
# ── 下发铺铜 ─────────────────────────────────────────────────────────────
JS_DEL = OPEN + u"""
let n = 0;
for (const old of await eda.pcb_PrimitivePour.getAll()) {
await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; }
await eda.pcb_Document.save();
return {deleted: n};
"""
JS_MAKE = OPEN + u"""
const PLAN = %s;
for (const p of PLAN) {
const poly = await eda.pcb_MathPolygon.createComplexPolygon([p.src]);
if (!poly) continue;
try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false,
p.name, p.prio, 10); } catch (e) {}
}
return {made: (await eda.pcb_PrimitivePour.getAll())
.map(x => String(x.pourName || '').toUpperCase())};
"""
JS_REBUILD = OPEN + u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; };
const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t);
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
const leaf = (txt) => { for (const el of document.querySelectorAll('*'))
if (el.children.length===0 && (el.textContent||'').trim()===txt) return el; return null; };
const btn = (txt) => { for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()===txt && b.offsetParent!==null) return b; return null; };
if (!fire(leaf('工具'))) return {err: 'no 工具'};
await sleep(900);
if (!fire(leaf('铺铜管理器...'))) return {err: 'no 铺铜管理器'};
await sleep(2500);
const b = btn('重建所有');
if (!b) return {err: 'no 重建所有'};
return {fired: fire(b)};
"""
JS_POLL = u"return {poured: (await eda.pcb_PrimitivePoured.getAll()).length};"
JS_DONE = u"""
const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; };
const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const q=propsOf(t);
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
let ok = null;
for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()==='确认' && b.offsetParent!==null) { ok = b; break; }
if (ok) fire(ok);
await new Promise(r => setTimeout(r, 1500));
await eda.pcb_Document.save();
return {poured: (await eda.pcb_PrimitivePoured.getAll()).length,
pour: (await eda.pcb_PrimitivePour.getAll()).length};
"""
def rebuild():
print(u'重建所有铺铜(走 GUI —— `rebuildCopperRegion()` 是坏的,刀片 §24 已查清)…')
print(call(JS_REBUILD, timeout=180))
last, same = -1, 0
for _ in range(40):
time.sleep(6)
n = call(JS_POLL, timeout=120)['poured']
print(u' 已填 %d 块' % n)
same = same + 1 if n == last else 0
last = n
if same >= 2 and n > 0:
break
print(call(JS_DONE, timeout=180))
def push(ps):
print(call(JS_DEL, timeout=300))
arr = [{'name': n, 'net': net, 'layer': l, 'prio': p, 'src': rrect_src(*r)}
for (n, net, l, r, p) in ps]
made = []
for k in range(0, len(arr), 12):
made += call(JS_MAKE % json.dumps(arr[k:k + 12], ensure_ascii=False), timeout=600)['made']
want = set(n.upper() for (n, _net, _l, _r, _p) in ps)
got = set(made)
print(u'铜面建了 %d / %d 块' % (len(want & got), len(want)))
if want - got:
print(u' [!] 没建上: %s' % ' '.join(sorted(want - got)))
rebuild()
if __name__ == '__main__':
# 铜面按 ROUTE_FIX 的计划位置算(见 pads());板端还没挪件就铺,铜和件对不上。
if ('--push' in sys.argv or '--stitch' in sys.argv) and PL.route_fix_moves():
sys.exit(u'!! ROUTE_FIX 还有 %d 件没下发到板端(%s);先 place_base --push 再 '
u'--adopt-live --dump,然后才能铺铜' % (len(PL.route_fix_moves()),
' '.join(sorted(PL.route_fix_moves()))))
if '--clear' in sys.argv:
print(call(JS_DEL, timeout=300))
sys.exit(0)
ps = plan()
bad = selfcheck(ps)
print(u'计划建 %d 块铜面:' % len(ps))
for name, net, lay, r, prio in ps:
if not name.startswith('GND-') or name.endswith('-0'):
print(u' %-12s net=%-9s L%d x %6.2f~%6.2f y %6.2f~%6.2f prio=%d'
% (name, net, lay, r[0], r[2], r[1], r[3], prio))
print(u' (GND 瓦片 %d 块,绕开天线净空与四个 M3 孔)'
% sum(1 for p in ps if p[0].startswith('GND-')))
if bad:
print(u'[X] 自检 %d 条不过:' % len(bad))
for t in bad:
print(u' ' + t)
else:
print(u'[OK] 自检全过(不出内缩线 / 不压净空 / 入口链四段互不重叠)')
wbad, wown, wtot = wall_check()
bs, bv = B.wall3v3_bridges()
print(u'3V3 槽墙:%d 块矩形,盖住 %d/%d 只槽脚;走廊底层桥 %d 段 / 过孔 %d'
% (len(B.wall3v3_rects()), wown, wtot, len(bs), len(bv)))
if wbad:
print(u'[X] 3V3 槽墙自检 %d 条不过:' % len(wbad))
for t in wbad:
print(u' ' + t)
bad += wbad
else:
print(u'[OK] 3V3 槽墙:槽脚全盖住 / 不压异网针 / 桥的过孔都在墙里')
bbad, bown, btot = bus_check()
print(u'12V 干道/铜墙:盖住 %d/%d 只插槽电源脚' % (bown, btot))
if bbad:
print(u'[X] 12V 干道/铜墙自检 %d 条不过:' % len(bbad))
for t in bbad:
print(u' ' + t)
bad += bbad
else:
print(u'[OK] 12V 干道/铜墙:槽脚全盖住 / 不压异网针')
cbad = check_cuts(ps)
bad += cbad
if '--stitch' in sys.argv:
stitch()
if '--push' in sys.argv:
if bad:
sys.exit(u'!! 自检不过,不下发')
push(ps)
elif '--rebuild' in sys.argv:
rebuild()
+163
查看文件
@@ -0,0 +1,163 @@
param(
[string]$Server = '10.5.5.13',
[int]$Passes = 1,
[switch]$PriorityCache,
[switch]$DpOnly,
# 服务器并行试算:每个元素是一组额外参数(如 '--repair --bot-pen-w 1'),
# 各自在独立副本里跑,取未完成最少的那个走下面同一套校验 / 接受逻辑。
[string[]]$Variants = @(),
[int]$PollSec = 60,
# 服务器上跑布线的解释器;PyPy 见 PROGRESS(纯 Python 的 A*,JIT 通常快数倍)
[string]$Interp = 'python3'
)
$ErrorActionPreference = 'Stop'
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
$Repo = Resolve-Path (Join-Path $Here '..\..')
$Python = 'C:\Python314\python.exe'
$Ssh = 'C:\Windows\System32\OpenSSH\ssh.exe'
$Scp = 'C:\Windows\System32\OpenSSH\scp.exe'
# 临时文件带 PID:允许同时起两批(快的单轮变体 + 慢的拆线重布),互不覆盖
$Archive = Join-Path ([System.IO.Path]::GetTempPath()) "dcdc-remote-route-$PID.tgz"
$RunScript = Join-Path ([System.IO.Path]::GetTempPath()) "dcdc-remote-run-$PID.sh"
$RemoteArchive = Split-Path -Leaf $Archive
$Incoming = Join-Path $Here "_base_routes.server.$PID.json"
$Cache = Join-Path $Here '_base_routes.json'
$Backup = Join-Path $Here '_base_routes.pre-server.json'
$Rejected = Join-Path $Here '_base_routes.rejected.json'
$remoteRoot = $null
if ($Passes -lt 1) {
throw 'Passes must be at least 1.'
}
if ($DpOnly -and $Variants.Count -gt 0) {
throw '-DpOnly cannot be combined with -Variants.'
}
Push-Location $Repo
try {
& $Python '.claude\r2\route_base.py' --snapshot-vias
if ($LASTEXITCODE -ne 0) { throw 'Failed to refresh board-via snapshot.' }
$remoteRoot = (& $Ssh $Server 'mktemp -d /tmp/dcdc-route.XXXXXX').Trim()
if ($LASTEXITCODE -ne 0 -or $remoteRoot -notmatch '^/tmp/dcdc-route\.[A-Za-z0-9]+$') {
throw "Unexpected remote work directory: $remoteRoot"
}
& tar.exe --exclude=*.png --exclude=*.bak --exclude=__pycache__ `
--exclude=_remote_route.tgz -czf $Archive `
'.claude\r2' '.claude\eda.py' '.claude\pcb_pour.py' '.claude\eda-parts.json'
if ($LASTEXITCODE -ne 0) { throw 'Failed to build routing bundle.' }
& $Scp $Archive "${Server}:${remoteRoot}/"
if ($LASTEXITCODE -ne 0) { throw 'Failed to upload routing bundle.' }
& $Ssh $Server "tar -xzf ${remoteRoot}/${RemoteArchive} -C ${remoteRoot}"
if ($LASTEXITCODE -ne 0) { throw 'Failed to extract routing bundle.' }
$priority = if ($PriorityCache) { ' --priority-cache' } else { '' }
$scope = if ($DpOnly) { ' --dp-only' } else { '' }
$timeFmt = "/usr/bin/time -f 'REMOTE_TIME=%e sec CPU=%P RSS=%M KB'"
$remoteResult = "${remoteRoot}/.claude/r2/_base_routes.json"
if ($Variants.Count -eq 0) {
$command = "DCDC_ROUTE_SERVER=1 $timeFmt $Interp " +
"${remoteRoot}/.claude/r2/route_base.py --offline --replan --passes $Passes$priority$scope"
& $Ssh $Server $command
if ($LASTEXITCODE -ne 0) { throw 'Remote routing failed.' }
}
else {
# 变体各自一份副本(缓存文件同名,不能共用目录),脱离 ssh 会话在后台跑:
# 拆线重布可能要跑很久,ssh 断线不能把计算带走。
$lines = @('#!/bin/bash', "cd ${remoteRoot}")
for ($k = 0; $k -lt $Variants.Count; $k++) {
$v = $Variants[$k]
$lines += "mkdir -p v$k && cp -r .claude v$k/"
$lines += "( DCDC_ROUTE_SERVER=1 $timeFmt $Interp ${remoteRoot}/v$k/.claude/r2/route_base.py " +
"--offline --replan --passes $Passes$priority $v > v$k.log 2>&1; echo `$? > v$k.rc ) &"
}
$lines += 'wait'
$lines += 'touch done'
[System.IO.File]::WriteAllText($RunScript, (($lines -join "`n") + "`n"))
& $Scp $RunScript "${Server}:${remoteRoot}/run.sh"
if ($LASTEXITCODE -ne 0) { throw 'Failed to upload run script.' }
& $Ssh $Server "setsid nohup bash ${remoteRoot}/run.sh > /dev/null 2>&1 < /dev/null &"
if ($LASTEXITCODE -ne 0) { throw 'Failed to start remote variants.' }
for ($k = 0; $k -lt $Variants.Count; $k++) { Write-Host "v${k}: $($Variants[$k])" }
$start = Get-Date
$poll = 0
while ($true) {
Start-Sleep -Seconds $PollSec
$poll++
$state = (& $Ssh $Server "test -f ${remoteRoot}/done && echo DONE || echo RUN").Trim()
if ($state -eq 'DONE') { break }
if ($poll % 5 -eq 0) {
$mins = [int]((Get-Date) - $start).TotalMinutes
Write-Host "--- $mins min ---"
& $Ssh $Server "cd ${remoteRoot} && for f in v*.log; do echo `"`$f: `$(tail -n 1 `$f)`"; done"
}
}
$summary = & $Ssh $Server ("cd ${remoteRoot} && for k in `$(seq 0 $($Variants.Count - 1)); do " +
"echo `"v`$k rc=`$(cat v`$k.rc) `$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); " +
"print(len(d[\`"logs\`"]), len(d[\`"segs\`"]), len(d[\`"vias\`"]))' " +
"v`$k/.claude/r2/_base_routes.json 2>/dev/null || echo NA)`"; done")
$best = $null
foreach ($row in $summary) {
Write-Host $row
if ($row -match '^v(\d+) rc=0 (\d+) (\d+) (\d+)$') {
$cand = @{ K = [int]$Matches[1]; Logs = [int]$Matches[2]; Vias = [int]$Matches[4] }
if (-not $best -or $cand.Logs -lt $best.Logs -or
($cand.Logs -eq $best.Logs -and $cand.Vias -lt $best.Vias)) {
$best = $cand
}
}
}
for ($k = 0; $k -lt $Variants.Count; $k++) {
Write-Host "===== v${k}: $($Variants[$k]) ====="
& $Ssh $Server "grep -E '拆线重布|第 [0-9]+ 轮|REMOTE_TIME|Traceback|Error' ${remoteRoot}/v$k.log | tail -n 25"
}
if (-not $best) { throw 'No remote variant finished successfully.' }
Write-Host "Best variant: v$($best.K) ($($Variants[$best.K])) with $($best.Logs) unfinished."
$remoteResult = "${remoteRoot}/v$($best.K)/.claude/r2/_base_routes.json"
}
if ($DpOnly) {
Write-Host 'Server-side D+/- diagnostic completed; local route cache was not changed.'
}
else {
& $Scp "${Server}:${remoteResult}" $Incoming
if ($LASTEXITCODE -ne 0) { throw 'Failed to download routing result.' }
$result = Get-Content -LiteralPath $Incoming -Raw | ConvertFrom-Json
if ($result.schema -ne 7 -or $result.slot_rot -ne 0 -or
$result.usb_slot_ref.'1' -ne 'J34' -or $result.usb_slot_ref.'5' -ne 'J38') {
throw 'Downloaded routing cache failed topology validation.'
}
$current = if (Test-Path -LiteralPath $Cache) {
Get-Content -LiteralPath $Cache -Raw | ConvertFrom-Json
} else { $null }
if ($current -and $result.logs.Count -gt $current.logs.Count) {
Move-Item -LiteralPath $Incoming -Destination $Rejected -Force
Write-Host "Rejected worse server result: $($result.logs.Count) unfinished > current $($current.logs.Count)."
}
else {
if ($current) {
Copy-Item -LiteralPath $Cache -Destination $Backup -Force
}
Move-Item -LiteralPath $Incoming -Destination $Cache -Force
Write-Host "Accepted server result: $($result.segs.Count) segments, $($result.vias.Count) vias, $($result.logs.Count) unfinished."
}
}
}
finally {
Pop-Location
foreach ($f in @($Archive, $RunScript)) {
if (Test-Path -LiteralPath $f) { Remove-Item -LiteralPath $f -Force }
}
if ($remoteRoot -and $remoteRoot -match '^/tmp/dcdc-route\.[A-Za-z0-9]+$') {
# ⛔ 模式里的 `(v[0-9]+/)?[.]claude` 是故意的:直接写目录串的话,执行这条命令的
# 远程 shell 命令行里也含这个串,pkill 先把自己杀了(ssh 返回 255),后面的 rm
# 根本不执行。按路径匹配而不是按 `python3`,换成 pypy3 也杀得到。
& $Ssh $Server "pkill -f '${remoteRoot}/(v[0-9]+/)?[.]claude/r2/route_base' ; rm -rf -- '${remoteRoot}'"
}
}
文件差异内容过多而无法显示 加载差异
文件差异内容过多而无法显示 加载差异
+359
查看文件
@@ -0,0 +1,359 @@
# -*- coding: utf-8 -*-
u"""R2 底板 PCB 的设计规则(M8b)。
/c/Python314/python .claude/r2/rules_base.py # 只反查 + 打印,不下发
/c/Python314/python .claude/r2/rules_base.py --dump # 重抓「位号.引脚 → 网名」
/c/Python314/python .claude/r2/rules_base.py --push # 下发规则 + 差分对 + 网络指派
/c/Python314/python .claude/r2/rules_base.py --audit # 读回审计
做法与 `pcb_rules.py`(刀片)同源,**那一份的文件头是唯一的说明书**,四个坑一个没变:
· `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装
· `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状**
· 下发之后**别再去 GUI 换「配置」下拉框** —— 换一次整套重置,网络指派全部悬空
· `overwriteNetRules` / `overwriteNetByNetRules` 都是**整表覆盖**,先读回再合并
## 和刀片那份不同的四处
1. **两层板**,当前配置是 `JLCPCB Capability(Two Layers Board)`:
`Track` / `Safe Spacing` 下只有 `copperThickness1oz` / `2oz` 两条,
**没有内层条目**,所以刀片那段「补 0.5 oz 内层」整块删掉。
2. **入口链是 9 A**(方案 §6.4),不是刀片的 1.45 A。线宽规则给到
`strokeWidthMax = 12`,真正的载流靠 `pour_base` 的断面校核保证,不靠 DRC。
3. **七对差分**:五个槽 + ESP32 原生 USB(D13)+ 上行 USB-C。
刀片那组 `W 0.28 / S 0.18` 是**四层薄介质**的数,两层 1.6 mm 板照搬会变成 ~137 Ω。
按方案 §6.3 取 **W 1.2 / S 0.2 ⇒ ~94 Ω**。
⚠ `min` 必须留到 0.25:1.2 的线接不进 U32 那颗 0.5 mm 间距 QFN-64 的焊盘,
末端一定要缩颈,min 卡在 1.2 会把整条线判成违规。
4. **自动命名网一个都不指派**:底板上 22 个 `$xNyy` 全是小信号
(泄放电阻串、Buck 反馈、DAC 的 RC、风扇 MOS 栅极),走默认线宽即可。
⇒ 没有「按位号.引脚反查自动网名」那一步,也就没有那一类静默失效。
"""
import io
import json
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import pcb_base as B # noqa: E402
PCB = B.PCB
LAYERS = 2
NETS_FILE = os.path.join(HERE, '_base_pcb_nets.json')
# ── 线宽规则(mm,外层 1 oz)────────────────────────────────────────────
# 载流靠灌铜断面保证、不靠 DRC;min 一律压到 0.15~0.3,否则 0402 焊盘扇不出去。
TRACK_RULES = {
'PWR9A': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.30, 'strokeWidthMax': 12.0},
'PWR12V': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.30, 'strokeWidthMax': 12.0},
'FAN12V': {'strokeWidthDefault': 0.8, 'strokeWidthMin': 0.25, 'strokeWidthMax': 3.0},
'PWR5V': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.20, 'strokeWidthMax': 3.0},
'PWR3V3': {'strokeWidthDefault': 0.5, 'strokeWidthMin': 0.15, 'strokeWidthMax': 3.0},
}
# ── 有名字的网络 → 线宽规则 ────────────────────────────────────────────
# ⚠ `+12V` 是**一个网**:从 Rs6 下游一路到 5 个槽的 12V 针(43 只脚)。
# 干路 9 A 那一段和「母线 → 单槽」1.5 A 那一段线宽不同,DRC 只能按一条规则判 ⇒
# 取干路的 `PWR9A`(default 2.0 只是画线的起手宽度),断面由 `pour_base` 逐段校核。
NET_TRACK = {
'VIN_RAW': 'PWR9A', # J1 中心极 → F1
'VIN_FUSED': 'PWR9A', # F1 → Q1 反接保护
'VIN_SW': 'PWR9A', # Q1 → 储能 → Rs6 上游
'+12V': 'PWR9A', # Rs6 下游 → 母线 → 5 槽 + 两颗 Buck + 风扇
'FAN_12V_SW': 'FAN12V', # 风扇下管开关点(R1 §22.6 定的 0.8)
'+5V': 'PWR5V', # U30 → 5 槽(每槽给刀片 U5 TPS22918)
'+3V3': 'PWR3V3', # U19 → ESP32 / mux / DAC / 5 槽逻辑
'+3V3_HUB': 'PWR3V3', # U31 AMS1117 → U32(与 +3V3 只共地)
}
# ── 铺铜焊盘全连接(理由同刀片:不是怕十字花烧,是**铺完之后断面要量得准**)──
PLANE_RULE = 'PLANE-DIRECT'
PLANE_CONNECT_DIRECT = 1
PLANE_NETS = ['VIN_RAW', 'VIN_FUSED', 'VIN_SW', '+12V']
# ── 差分对(mm)── 方案 §6.3:两层 1.6 mm 微带,W 1.2 / S 0.2 ⇒ ~94 Ω ──
DIFF = {'strokeWidthDefault': 1.2, 'strokeWidthMin': 0.25, 'strokeWidthMax': 1.6,
'differentailPairSpacingDefault': 0.20, 'differentailPairSpacingMin': 0.15,
'differentailPairLenTolerMax': 1.0} # intra-pair skew 目标 ≤1 mm(§6.3)
PAIR_RULE = 'USBPAIR'
PAIR_CLEARANCE = 0.20
PAIRS = [('USB1', 'USB1_DP', 'USB1_DM'),
('USB2', 'USB2_DP', 'USB2_DM'),
('USB3', 'USB3_DP', 'USB3_DM'),
('USB4', 'USB4_DP', 'USB4_DM'),
('USB5', 'USB5_DP', 'USB5_DM'),
('USB6', 'USB6_DP', 'USB6_DM'), # D13:Hub 第 6 口 → ESP32 原生 USB
('USBUP', 'UP_DP', 'UP_DM')] # J23 USB-C → U32 上行
# ⚠ 0.2,不是刀片的 0.15。两层板走 JLC 标准工艺(0.2/0.2)最便宜,
# 而底板上最紧的封装是 U32 QFN-64(0.5 间距)与 J23 USB-C,实测都过得去。
# 真刷出「贴片焊盘到贴片焊盘」的假错(全落在某个买来的封装内部)再降到 0.15。
MIN_CLEARANCE = 0.20
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2000));" % PCB)
def call(body, timeout=300):
r = run(OPEN + body, timeout=timeout)
if not r.get('success'):
print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:600])
sys.exit(1)
return r['result']
def dump_nets():
u"""重抓「位号.引脚 → 网名」。**每次导入变更之后必跑**。"""
r = call("const out = {};"
"for (const c of await eda.pcb_PrimitiveComponent.getAll()) {"
" const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId("
" c.getState_PrimitiveId());"
" out[c.designator] = ps.map(p=>[p.padNumber, p.net]); }"
"return out;")
io.open(NETS_FILE, 'w', encoding='utf-8').write(json.dumps(r, ensure_ascii=False))
print(u'已写 _base_pcb_nets.json:%d 件' % len(r))
return r
def nets():
if not os.path.exists(NETS_FILE):
return dump_nets()
return json.load(io.open(NETS_FILE, encoding='utf-8'))
def check_names():
u"""下发前先确认:要指派的网名在板上真的存在(写错名字 = 规则悬空,DRC 不报)。"""
have = set()
for _ref, pads in nets().items():
for _pn, net in pads:
if net:
have.add(net)
miss = [n for n in sorted(set(list(NET_TRACK) + PLANE_NETS)) if n not in have]
pmiss = [p for t in PAIRS for p in t[1:] if p not in have]
if miss or pmiss:
print(u' [!!] 板上没有这些网,**不下发**:线宽/铺铜 %s;差分 %s' % (miss, pmiss))
sys.exit(1)
return True
CAP_JS = u"""
const D = eda.pcb_Drc;
const cur = await D.getCurrentRuleConfiguration();
const c = JSON.parse(JSON.stringify(cur.config));
const TRACKS = %s, DW = %s, MINC = %s, PAIRRULE = %s, PAIRC = %s;
const PLANERULE = %s, PLANEMODE = %s;
const setTrack = (e, mn, df, mx) => {
for (const k of Object.keys(e.form.data))
Object.assign(e.form.data[k], {minValue: mn, defaultValue: df, maxValue: mx});
};
const T = c['Physics']['Track'], SS = c['Spacing']['Safe Spacing'];
setTrack(T['copperThickness1oz'], 0.127, 0.254, 2.54);
Object.assign(c['Physics']['Via Size']['viaSize'].form,
{viaOuterdiameterMin: 0.4, viaInnerdiameterMin: 0.3});
for (const name of Object.keys(TRACKS)) {
const f = TRACKS[name];
const e = JSON.parse(JSON.stringify(T['copperThickness1oz']));
e.editName = name; e.isSetDefault = false; e.unit = 'mm';
setTrack(e, f.strokeWidthMin, f.strokeWidthDefault, f.strokeWidthMax);
T[name] = e;
}
const dp = c['Physics']['Differential Pair']['differentialPair'].form;
for (const k of Object.keys(dp.strokeWidthTables.data))
Object.assign(dp.strokeWidthTables.data[k],
{minValue: DW.strokeWidthMin, defaultValue: DW.strokeWidthDefault,
maxValue: DW.strokeWidthMax});
for (const k of Object.keys(dp.diffPairSpacingTables.data))
Object.assign(dp.diffPairSpacingTables.data[k],
{minValue: DW.differentailPairSpacingMin,
defaultValue: DW.differentailPairSpacingDefault});
dp.differentailPairLenTolerMax = DW.differentailPairLenTolerMax;
if (!SS[PAIRRULE]) {
const e = JSON.parse(JSON.stringify(SS['copperThickness1oz']));
e.editName = PAIRRULE; e.isSetDefault = false;
for (const tk of Object.keys(e.tables || {}))
for (const row of (e.tables[tk].content || []))
for (let i = 0; i < row.length; i++)
if (typeof row[i] === 'number') row[i] = PAIRC;
SS[PAIRRULE] = e;
}
// 安全间距全表**归一到 MINC**:≤0.2 的格子一律赋值成 MINC(不是「只抬不降」);
// 0.254 / 0.3 那几格是孔/板边相关的,不动。
let bumped = 0;
for (const key of Object.keys(SS))
if (key !== PAIRRULE)
for (const tk of Object.keys(SS[key].tables || {}))
for (const row of (SS[key].tables[tk].content || []))
for (let i = 0; i < row.length; i++)
if (typeof row[i] === 'number' && row[i] <= 0.2001 &&
Math.abs(row[i] - MINC) > 1e-6) { row[i] = MINC; bumped++; }
const CZ = c['Plane']['Copper Zone'];
if (!CZ[PLANERULE]) {
const e = JSON.parse(JSON.stringify(CZ['copperRegion']));
e.editName = PLANERULE; e.isSetDefault = false;
CZ[PLANERULE] = e;
}
for (const m of ['singleLayerPadModel', 'multiLayerPadModel'])
for (const k of Object.keys(CZ[PLANERULE].form[m].data))
CZ[PLANERULE].form[m].data[k].connectMode = PLANEMODE;
const ok = await D.overwriteCurrentRuleConfiguration(c);
return {ok: ok === true, bumped: bumped, name: cur.name,
track: Object.keys(T), spacing: Object.keys(SS), plane: Object.keys(CZ)};
"""
def push():
check_names()
r = call(CAP_JS % (json.dumps(TRACK_RULES), json.dumps(DIFF), MIN_CLEARANCE,
json.dumps(PAIR_RULE), PAIR_CLEARANCE,
json.dumps(PLANE_RULE), PLANE_CONNECT_DIRECT))
if not r.get('ok'):
print(u' [!!] overwriteCurrentRuleConfiguration 返回非 true,没生效')
sys.exit(1)
print(u' 规则配置已下发:线宽条目 %s' % ' '.join(r['track']))
print(u' 安全间距归一 %d 格 → %.2f mm;配置名 "%s"' % (r['bumped'], MIN_CLEARANCE, r['name']))
n = call("""
const P = %s;
let k = 0;
for (const d of await eda.pcb_Drc.getAllDifferentialPairs())
await eda.pcb_Drc.deleteDifferentialPair(d.name);
for (const [name, p, m] of P) { if (await eda.pcb_Drc.createDifferentialPair(name, p, m)) k++; }
return k;
""" % json.dumps(PAIRS))
print(u' 差分对 %d / %d 对' % (n, len(PAIRS)))
if n != len(PAIRS):
print(u' [!!] 有对没建上 —— 网络名在这块板上不存在')
n = call("""
const T = %s, PN = %s, PR = %s;
const pset = new Set(PN);
const rules = await eda.pcb_Drc.getNetRules();
let k = 0, p = 0;
const seen = [];
for (const r of rules) {
if (r.type !== 'net') continue;
if (T[r.name]) { r['Track'] = T[r.name]; k++; seen.push(r.name); }
if (pset.has(r.name)) { r['Copper Zone'] = PR; p++; }
}
await eda.pcb_Drc.overwriteNetRules(rules);
return {track: k, plane: p, seen: seen};
""" % (json.dumps(NET_TRACK, ensure_ascii=False), json.dumps(PLANE_NETS, ensure_ascii=False),
json.dumps(PLANE_RULE)))
print(u' 线宽指派 %d / %d 个网络' % (n['track'], len(NET_TRACK)))
if n['track'] != len(NET_TRACK):
print(u' [!!] 板上找不到: %s' % ' '.join(sorted(set(NET_TRACK) - set(n['seen']))))
print(u' 铺铜全连接指派 %d / %d(%s)' % (n['plane'], len(PLANE_NETS), ' '.join(PLANE_NETS)))
n = call("""
const PAIRS = %s, RULE = %s;
const cur = await eda.pcb_Drc.getNetByNetRules();
const old = (cur && cur['Safe Spacing']) ? cur['Safe Spacing'] : [];
const key = e => [e.net1.name, e.net2.name].sort().join('|');
const want = new Set(PAIRS.map(([n, dp, dm]) => [dp, dm].sort().join('|')));
const list = [];
let dropped = 0;
for (const e of old) {
if (e.rule === RULE && !want.has(key(e))) { dropped++; continue; }
list.push(e);
}
const have = new Set(list.map(key));
let k = 0;
for (const [name, dp, dm] of PAIRS) {
const e = {net1: {type: 'net', name: dp}, net2: {type: 'net', name: dm},
rule: RULE, copperRuleName: null, planeRuleName: null};
if (have.has(key(e))) continue;
list.push(e); k++;
}
const ok = await eda.pcb_Drc.overwriteNetByNetRules(
{'Safe Spacing': list, 'Creepage Distance': (cur && cur['Creepage Distance']) || []});
const back = await eda.pcb_Drc.getNetByNetRules();
return {added: k, dropped: dropped, ok: ok === true,
now: (back['Safe Spacing'] || []).length};
""" % (json.dumps(PAIRS), json.dumps(PAIR_RULE)))
print(u' 对内间距:新增 %d / 清掉过期 %d,现共 %d 条(%s = %.2f mm)'
% (n['added'], n['dropped'], n['now'], PAIR_RULE, PAIR_CLEARANCE))
call("await eda.pcb_Document.save(); return 1;")
print(u' 已保存')
print(u'')
print(u'⚠ 别去 GUI 的 设计 → 设计规则… 里换「配置」下拉框:换一次整套重置,')
print(u' %d 个网络的线宽指派立刻悬空,而 DRC 不会报错。' % len(NET_TRACK))
print(u'⚠ 再导入一次变更之后顺序是:--dump → --push,少跑 --dump 就是拿旧网名指派。')
AUDIT_JS = u"""
const c = await eda.pcb_Drc.getCurrentRuleConfiguration();
const rules = await eda.pcb_Drc.getNetRules();
const nbn = await eda.pcb_Drc.getNetByNetRules();
return {
cfgName: c && c.name,
layers: await eda.pcb_Layer.getTheNumberOfCopperLayers(),
track: Object.keys(c.config.Physics['Track']),
pairs: (await eda.pcb_Drc.getAllDifferentialPairs()).map(d => d.name).sort(),
assigned: rules.filter(r => r['Track'] && r['Track'] !== 'default')
.map(r => [r.name, r['Track']]).sort(),
planeAssigned: rules.filter(r => r['Copper Zone'] && r['Copper Zone'] !== 'default')
.map(r => [r.name, r['Copper Zone']]).sort(),
nbn: ((nbn && nbn['Safe Spacing']) || []).length,
};
"""
def audit():
r = call(AUDIT_JS)
bad = []
def chk(cond, good, msg):
print((u' [OK] ' if cond else u' [!!] ') + (good if cond else msg))
if not cond:
bad.append(msg)
print(u'──── 审计(配置 "%s")────' % r['cfgName'])
chk(r['layers'] == LAYERS, u'铜层数 %d' % LAYERS, u'铜层数是 %s,应为 %d' % (r['layers'], LAYERS))
miss = [k for k in TRACK_RULES if k not in r['track']]
chk(not miss, u'%d 条线宽条目都在' % len(TRACK_RULES), u'缺线宽条目 %s' % miss)
got = dict(r['assigned'])
wrong = [(n, got.get(n), v) for n, v in sorted(NET_TRACK.items()) if got.get(n) != v]
chk(not wrong, u'%d 个网络的线宽指派全对' % len(NET_TRACK), u'指派不对: %s' % wrong[:6])
chk(sorted(r['pairs']) == sorted([p[0] for p in PAIRS]),
u'差分对 %d 对: %s' % (len(r['pairs']), ' '.join(r['pairs'])),
u'差分对是 %s,应为 %s' % (r['pairs'], [p[0] for p in PAIRS]))
chk(r['nbn'] == len(PAIRS), u'对内间距 %d 条' % r['nbn'],
u'对内间距 %d 条,应为 %d' % (r['nbn'], len(PAIRS)))
gotp = dict(r['planeAssigned'])
missp = [n for n in PLANE_NETS if gotp.get(n) != PLANE_RULE]
chk(not missp, u'铺铜全连接 %d 个网络' % len(PLANE_NETS), u'没指派上: %s' % missp)
print(u' ' + (u'[OK] 全部通过' if not bad else u'[X] %d 条不过' % len(bad)))
return bad
if __name__ == '__main__':
print(u'底板 PCB=%s(%d 层)' % (PCB, LAYERS))
if '--dump' in sys.argv:
dump_nets()
if '--push' in sys.argv:
push()
audit()
elif '--audit' in sys.argv:
audit()
elif '--dump' not in sys.argv:
check_names()
print(u'线宽规则:')
for k, v in sorted(TRACK_RULES.items()):
print(u' %-8s 默认 %.2f 最小 %.2f 最大 %.2f'
% (k, v['strokeWidthDefault'], v['strokeWidthMin'], v['strokeWidthMax']))
print(u'网络指派:')
for k, v in sorted(NET_TRACK.items()):
print(u' %-12s → %s' % (k, v))
print(u'差分对 %d:%s' % (len(PAIRS), ' '.join(p[0] for p in PAIRS)))
print(u'安全间距 %.2f;铺铜全连接 %s' % (MIN_CLEARANCE, ' '.join(PLANE_NETS)))