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 行删除
+60 -22
查看文件
@@ -28,32 +28,26 @@
"EFUSE_EN2": 5,
"EFUSE_EN3": 6,
"EFUSE_EN4": 7,
"SCL": 8,
"EFUSE_EN5": 12,
"FAN_PWM": 24,
"FAN_TACH": 25,
"IOEXP_INT#": 31,
"KEY_OE#": 23,
"KEY_RCLK": 11,
"KEY_SER": 9,
"KEY_SRCLK": 10,
"KEY_RCLK": 11,
"EFUSE_EN5": 12,
"NTC1": 39,
"NTC2": 38,
"SCL": 8,
"SDA": 22,
"UP_VBUS_DET": 36,
"VBUS_EN1": 17,
"VBUS_EN2": 18,
"VBUS_EN3": 19,
"VBUS_EN4": 20,
"VBUS_EN5": 21,
"SDA": 22,
"KEY_OE#": 23,
"FAN_PWM": 24,
"FAN_TACH": 25,
"IOEXP_INT#": 31,
"UP_VBUS_DET": 36,
"NTC2": 38,
"NTC1": 39
"VBUS_EN5": 21
},
"pin2io": {
"4": "IO4",
"5": "IO5",
"6": "IO6",
"7": "IO7",
"8": "IO15",
"9": "IO16",
"10": "IO17",
"11": "IO18",
"12": "IO8",
@@ -67,12 +61,56 @@
"24": "IO47",
"25": "IO48",
"31": "IO38",
"36": "IO44",
"38": "IO2",
"39": "IO1",
"32": "IO39",
"33": "IO40",
"34": "IO41",
"35": "IO42"
"35": "IO42",
"36": "IO44",
"38": "IO2",
"39": "IO1",
"4": "IO4",
"5": "IO5",
"6": "IO6",
"7": "IO7",
"8": "IO15",
"9": "IO16"
},
"r2_base": {
"EFUSE_EN1": 21,
"EFUSE_EN2": 20,
"EFUSE_EN3": 19,
"EFUSE_EN4": 18,
"EFUSE_EN5": 17,
"FAN_EN": 7,
"FAN_PWM": 6,
"FAN_TACH_IN": 5,
"FAN_TACH_OUT": 4,
"HUB_RST": 12,
"INA_ALERT": 31,
"INT#": 32,
"IO0": 27,
"MUX_RST#": 33,
"NTC1": 39,
"NTC2": 38,
"PRSNT1#": 11,
"PRSNT2#": 10,
"PRSNT3#": 9,
"PRSNT4#": 8,
"PRSNT5#": 22,
"SCL": 35,
"SDA": 34,
"UP_VBUS_DET": 23,
"USB6_DM": 13,
"USB6_DP": 14
},
"r2_base_nc": {
"15": "IO3 strapping(JTAG_SEL)",
"16": "IO46 strapping + input-only",
"24": "IO47 备用",
"25": "IO48 备用",
"26": "IO45 strapping(VDD_SPI)",
"28": "IO35 留给 R8 版八线 PSRAM",
"29": "IO36 同上",
"30": "IO37 同上"
}
}
+30 -1
查看文件
@@ -1,6 +1,35 @@
{
"opened": true,
"seq": [],
"seq": [
{
"top": 0,
"txt": "DZ3\n\t\t\t\t\n\n\t修改封装\tSOD-123_L2.7-W1.6-LS3.7-RD-1:封装\tSOD-123_L2.7-W1.6-LS3.7-RD-1\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR\n\nJ1\n\t\t\t\t\n\n\t移除元件\tJ1\tJ1\t\n\nJ26\n\t\t\t\t\n\n\t修改封装\tCONN-TH_4P-P2.54_CAX_2510-4A:封装\tCONN-TH_4P-P2.54_CAX_2510-4A\tR0402\n\nJ27\n\t\t\t\t\n\n\t修改属性\tJ26:通道ID\t$17I175\t$17I174\n\n\t修改属性\tJ26:位号\tJ27\tJ26\n\n\t修改网络\tJ26:3\tFAN_OUT_TACH_RAW\tFAN_IN_TACH_RAW\n\nQ7\n\t\t\t\t\n\n\t修改封装\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR\tCONN-TH_4P-P2.54_CAX_2510-4A\n\nQ8\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402"
},
{
"top": 188,
"txt": "DZ3\n\t\t\t\t\n\n\t修改封装\tSOD-123_L2.7-W1.6-LS3.7-RD-1:封装\tSOD-123_L2.7-W1.6-LS3.7-RD-1\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR\n\nJ1\n\t\t\t\t\n\n\t移除元件\tJ1\tJ1\t\n\nJ26\n\t\t\t\t\n\n\t修改封装\tCONN-TH_4P-P2.54_CAX_2510-4A:封装\tCONN-TH_4P-P2.54_CAX_2510-4A\tR0402\n\nJ27\n\t\t\t\t\n\n\t修改属性\tJ26:通道ID\t$17I175\t$17I174\n\n\t修改属性\tJ26:位号\tJ27\tJ26\n\n\t修改网络\tJ26:3\tFAN_OUT_TACH_RAW\tFAN_IN_TACH_RAW\n\nQ7\n\t\t\t\t\n\n\t修改封装\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR\tCONN-TH_4P-P2.54_CAX_2510-4A\n\nQ8\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402\n\nQ9\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402\n\nQ10\n\t\t\t\t\n\n\t修改属性\tQ9:通道ID\t$17I223\t$17I222\n\n\t修改属性\tQ9:位号\tQ10\tQ9\n\n\t修改网络\tQ9:1\t$17N233\t$17N226\n\n\t修改网络\tQ9:3\tFAN_TACH_OUT\tFAN_TACH_IN\n\n\t增加元件\tQ10\t-\tQ10\n\nR746\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOD-123_L2.7-W1.6-LS3.7-RD-1\n\nR747\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tDC-IN-TH_DC-005-2\n\nR748\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\n\nR749\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1003TCE\t0402WGF1001TCE\n\nR750\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1003TCE"
},
{
"top": 376,
"txt": "\t修改封装\tCONN-TH_4P-P2.54_CAX_2510-4A:封装\tCONN-TH_4P-P2.54_CAX_2510-4A\tR0402\n\nJ27\n\t\t\t\t\n\n\t修改属性\tJ26:通道ID\t$17I175\t$17I174\n\n\t修改属性\tJ26:位号\tJ27\tJ26\n\n\t修改网络\tJ26:3\tFAN_OUT_TACH_RAW\tFAN_IN_TACH_RAW\n\nQ7\n\t\t\t\t\n\n\t修改封装\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23_L2.9-W1.3-P1.90-LS2.4-BR\tCONN-TH_4P-P2.54_CAX_2510-4A\n\nQ8\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402\n\nQ9\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402\n\nQ10\n\t\t\t\t\n\n\t修改属性\tQ9:通道ID\t$17I223\t$17I222\n\n\t修改属性\tQ9:位号\tQ10\tQ9\n\n\t修改网络\tQ9:1\t$17N233\t$17N226\n\n\t修改网络\tQ9:3\tFAN_TACH_OUT\tFAN_TACH_IN\n\n\t增加元件\tQ10\t-\tQ10\n\nR746\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOD-123_L2.7-W1.6-LS3.7-RD-1\n\nR747\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tDC-IN-TH_DC-005-2\n\nR748\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\n\nR749\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1003TCE\t0402WGF1001TCE\n\nR750\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1003TCE\n\nR751\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR752\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR753\n\t\t\t\t\n\n\t修改属性\tR752:通道ID\t$17I168\t$17I166\n\n\t修改属性\tR752:位号\tR753\tR752\n\n\t修改网络\tR752:1\tFAN_TACH_IN\tGND"
},
{
"top": 564,
"txt": "Q8\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402\n\nQ9\n\t\t\t\t\n\n\t修改封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR:封装\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\tR0402\n\nQ10\n\t\t\t\t\n\n\t修改属性\tQ9:通道ID\t$17I223\t$17I222\n\n\t修改属性\tQ9:位号\tQ10\tQ9\n\n\t修改网络\tQ9:1\t$17N233\t$17N226\n\n\t修改网络\tQ9:3\tFAN_TACH_OUT\tFAN_TACH_IN\n\n\t增加元件\tQ10\t-\tQ10\n\nR746\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOD-123_L2.7-W1.6-LS3.7-RD-1\n\nR747\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tDC-IN-TH_DC-005-2\n\nR748\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\n\nR749\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1003TCE\t0402WGF1001TCE\n\nR750\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1003TCE\n\nR751\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR752\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR753\n\t\t\t\t\n\n\t修改属性\tR752:通道ID\t$17I168\t$17I166\n\n\t修改属性\tR752:位号\tR753\tR752\n\n\t修改网络\tR752:1\tFAN_TACH_IN\tGND\n\n\t修改网络\tR752:2\t+3V3\t$17N226\n\nR754\n\t\t\t\t\n\n\t修改属性\tR753:通道ID\t$17I169\t$17I168\n\n\t修改属性\tR753:位号\tR754\tR753\n\n\t修改网络\tR753:1\tFAN_OUT_TACH_RAW\tFAN_TACH_IN\n\n\t修改网络\tR753:2\tFAN_12V_SW\t+3V3\n\nR755\n\t\t\t\t"
},
{
"top": 752,
"txt": "\t修改网络\tQ9:3\tFAN_TACH_OUT\tFAN_TACH_IN\n\n\t增加元件\tQ10\t-\tQ10\n\nR746\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOD-123_L2.7-W1.6-LS3.7-RD-1\n\nR747\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tDC-IN-TH_DC-005-2\n\nR748\n\t\t\t\t\n\n\t修改封装\tR0402:封装\tR0402\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\n\nR749\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1003TCE\t0402WGF1001TCE\n\nR750\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1003TCE\n\nR751\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR752\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR753\n\t\t\t\t\n\n\t修改属性\tR752:通道ID\t$17I168\t$17I166\n\n\t修改属性\tR752:位号\tR753\tR752\n\n\t修改网络\tR752:1\tFAN_TACH_IN\tGND\n\n\t修改网络\tR752:2\t+3V3\t$17N226\n\nR754\n\t\t\t\t\n\n\t修改属性\tR753:通道ID\t$17I169\t$17I168\n\n\t修改属性\tR753:位号\tR754\tR753\n\n\t修改网络\tR753:1\tFAN_OUT_TACH_RAW\tFAN_TACH_IN\n\n\t修改网络\tR753:2\tFAN_12V_SW\t+3V3\n\nR755\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR756\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR757\n\t\t\t\t\n\n\t修改属性\tR756:通道ID\t$17I173\t$17I171\n\n\t修改属性\tR756:位号\tR757\tR756\n\n\t修改网络\tR756:1\tFAN_TACH_OUT\tGND\n\n\t修改网络\tR756:2\t+3V3\t$17N233"
},
{
"top": 940,
"txt": "\t修改封装\tR0402:封装\tR0402\tSOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR\n\nR749\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1003TCE\t0402WGF1001TCE\n\nR750\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1003TCE\n\nR751\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR752\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR753\n\t\t\t\t\n\n\t修改属性\tR752:通道ID\t$17I168\t$17I166\n\n\t修改属性\tR752:位号\tR753\tR752\n\n\t修改网络\tR752:1\tFAN_TACH_IN\tGND\n\n\t修改网络\tR752:2\t+3V3\t$17N226\n\nR754\n\t\t\t\t\n\n\t修改属性\tR753:通道ID\t$17I169\t$17I168\n\n\t修改属性\tR753:位号\tR754\tR753\n\n\t修改网络\tR753:1\tFAN_OUT_TACH_RAW\tFAN_TACH_IN\n\n\t修改网络\tR753:2\tFAN_12V_SW\t+3V3\n\nR755\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR756\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR757\n\t\t\t\t\n\n\t修改属性\tR756:通道ID\t$17I173\t$17I171\n\n\t修改属性\tR756:位号\tR757\tR756\n\n\t修改网络\tR756:1\tFAN_TACH_OUT\tGND\n\n\t修改网络\tR756:2\t+3V3\t$17N233"
},
{
"top": 1000,
"txt": "R750\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1003TCE\n\nR751\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR752\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR753\n\t\t\t\t\n\n\t修改属性\tR752:通道ID\t$17I168\t$17I166\n\n\t修改属性\tR752:位号\tR753\tR752\n\n\t修改网络\tR752:1\tFAN_TACH_IN\tGND\n\n\t修改网络\tR752:2\t+3V3\t$17N226\n\nR754\n\t\t\t\t\n\n\t修改属性\tR753:通道ID\t$17I169\t$17I168\n\n\t修改属性\tR753:位号\tR754\tR753\n\n\t修改网络\tR753:1\tFAN_OUT_TACH_RAW\tFAN_TACH_IN\n\n\t修改网络\tR753:2\tFAN_12V_SW\t+3V3\n\nR755\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1502TCE\t0402WGF1002TCE\n\nR756\n\t\t\t\t\n\n\t替换器件\tR0402:器件\t0402WGF1002TCE\t0402WGF1502TCE\n\nR757\n\t\t\t\t\n\n\t修改属性\tR756:通道ID\t$17I173\t$17I171\n\n\t修改属性\tR756:位号\tR757\tR756\n\n\t修改网络\tR756:1\tFAN_TACH_OUT\tGND\n\n\t修改网络\tR756:2\t+3V3\t$17N233"
}
],
"buttons": [
"刷新",
"新增",
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
+1 -1
查看文件
@@ -1 +1 @@
[{"net": "GND", "x": 151.9259, "y": 48.8, "num": "A1B12", "layer": 1, "pts": [6006.923, 1933.068, "L", 6006.923, 1921.258, 6006.929, 1921.258, 6006.929, 1909.447, 5955.747, 1909.447, 5955.747, 1921.257, 5955.741, 1921.257, 5955.741, 1933.068, 6006.923, 1933.068]}, {"net": "GND", "x": 151.9259, "y": 55.2, "num": "B1A12", "layer": 1, "pts": [5955.741, 2161.413, "L", 5955.741, 2173.224, 5955.741, 2173.225, 5955.741, 2185.036, 6006.921, 2185.036, 6006.921, 2173.225, 6006.921, 2173.224, 6006.921, 2161.413, 5955.741, 2161.413]}, {"net": "", "x": 151.9259, "y": 54.4002, "num": "B4A9", "layer": 1, "pts": [6006.921, 2153.546, "L", 6006.921, 2141.735, 6006.921, 2141.734, 6006.921, 2129.923, 5955.741, 2129.923, 5955.741, 2141.734, 5955.741, 2141.735, 5955.741, 2153.546, 6006.921, 2153.546]}, {"net": "", "x": 151.9259, "y": 49.5998, "num": "A4B9", "layer": 1, "pts": [6006.927, 1964.564, "L", 6006.927, 1952.755, 6006.928, 1952.755, 6006.928, 1940.943, 5955.748, 1940.943, 5955.748, 1952.752, 5955.747, 1952.752, 5955.747, 1964.564, 6006.927, 1964.564]}, {"net": "GND", "x": 16.8001, "y": 27.5001, "num": "15", "layer": 1, "pts": [621.424, 1122.676, "L", 645.676, 1122.676, 645.676, 1167.715, 655.125, 1167.715, 655.125, 1139.369, 667.723, 1139.369, 667.723, 1167.715, 677.172, 1167.715, 677.172, 1122.676, 701.424, 1122.676, 701.424, 1042.676, 677.172, 1042.676, 677.172, 997.637, 667.723, 997.637, 667.723, 1025.983, 655.125, 1025.983, 655.125, 997.637, 645.676, 997.637, 645.676, 1042.676, 621.424, 1042.676, 621.424, 1122.676]}, {"net": "GND", "x": 51.0891, "y": 98.0001, "num": "15", "layer": 1, "pts": [2051.376, 3818.274, "L", 2027.124, 3818.274, 2027.124, 3773.235, 2017.675, 3773.235, 2017.675, 3801.581, 2005.077, 3801.581, 2005.077, 3773.235, 1995.628, 3773.235, 1995.628, 3818.274, 1971.376, 3818.274, 1971.376, 3898.274, 1995.628, 3898.274, 1995.628, 3943.313, 2005.077, 3943.313, 2005.077, 3914.967, 2017.675, 3914.967, 2017.675, 3943.313, 2027.124, 3943.313, 2027.124, 3898.274, 2051.376, 3898.274, 2051.376, 3818.274]}, {"net": "GND", "x": 139.4691, "y": 98.0001, "num": "15", "layer": 1, "pts": [5530.906, 3818.274, "L", 5506.654, 3818.274, 5506.654, 3773.235, 5497.205, 3773.235, 5497.205, 3801.581, 5484.607, 3801.581, 5484.607, 3773.235, 5475.158, 3773.235, 5475.158, 3818.274, 5450.906, 3818.274, 5450.906, 3898.274, 5475.158, 3898.274, 5475.158, 3943.313, 5484.607, 3943.313, 5484.607, 3914.967, 5497.205, 3914.967, 5497.205, 3943.313, 5506.654, 3943.313, 5506.654, 3898.274, 5530.906, 3898.274, 5530.906, 3818.274]}, {"net": "GND", "x": 48.469, "y": 27.5001, "num": "15", "layer": 1, "pts": [1868.234, 1122.676, "L", 1892.486, 1122.676, 1892.486, 1167.715, 1901.935, 1167.715, 1901.935, 1139.369, 1914.533, 1139.369, 1914.533, 1167.715, 1923.982, 1167.715, 1923.982, 1122.676, 1948.234, 1122.676, 1948.234, 1042.676, 1923.982, 1042.676, 1923.982, 997.637, 1914.533, 997.637, 1914.533, 1025.983, 1901.935, 1025.983, 1901.935, 997.637, 1892.486, 997.637, 1892.486, 1042.676, 1868.234, 1042.676, 1868.234, 1122.676]}, {"net": "GND", "x": 128, "y": 27.5001, "num": "15", "layer": 1, "pts": [4999.374, 1122.676, "L", 5023.626, 1122.676, 5023.626, 1167.715, 5033.075, 1167.715, 5033.075, 1139.369, 5045.673, 1139.369, 5045.673, 1167.715, 5055.122, 1167.715, 5055.122, 1122.676, 5079.374, 1122.676, 5079.374, 1042.676, 5055.122, 1042.676, 5055.122, 997.637, 5045.673, 997.637, 5045.673, 1025.983, 5033.075, 1025.983, 5033.075, 997.637, 5023.626, 997.637, 5023.626, 1042.676, 4999.374, 1042.676, 4999.374, 1122.676]}, {"net": "GND", "x": 79.5801, "y": 38.01, "num": "17", "layer": 1, "pts": [3166.541, 1529.919, "L", 3166.541, 1462.989, 3099.611, 1462.989, 3099.611, 1516.139, 3113.391, 1529.919, 3166.541, 1529.919]}, {"net": "GND", "x": 109.5701, "y": 34.71, "num": "17", "layer": 1, "pts": [4347.251, 1399.999, "L", 4347.251, 1333.069, 4280.321, 1333.069, 4280.321, 1386.219, 4294.101, 1399.999, 4347.251, 1399.999]}]
[{"net": "GND", "x": 181.9258, "y": 48.8, "num": "A1B12", "layer": 1, "pts": [7188.023, 1933.068, "L", 7188.023, 1921.258, 7188.029, 1921.258, 7188.029, 1909.447, 7136.847, 1909.447, 7136.847, 1921.257, 7136.841, 1921.257, 7136.841, 1933.068, 7188.023, 1933.068]}, {"net": "GND", "x": 181.9258, "y": 55.2, "num": "B1A12", "layer": 1, "pts": [7136.841, 2161.413, "L", 7136.841, 2173.224, 7136.841, 2173.225, 7136.841, 2185.036, 7188.021, 2185.036, 7188.021, 2173.225, 7188.021, 2173.224, 7188.021, 2161.413, 7136.841, 2161.413]}, {"net": "", "x": 181.9258, "y": 54.4002, "num": "B4A9", "layer": 1, "pts": [7188.021, 2153.546, "L", 7188.021, 2141.735, 7188.021, 2141.734, 7188.021, 2129.923, 7136.841, 2129.923, 7136.841, 2141.734, 7136.841, 2141.735, 7136.841, 2153.546, 7188.021, 2153.546]}, {"net": "", "x": 181.9258, "y": 49.5998, "num": "A4B9", "layer": 1, "pts": [7188.027, 1964.564, "L", 7188.027, 1952.755, 7188.028, 1952.755, 7188.028, 1940.943, 7136.848, 1940.943, 7136.848, 1952.752, 7136.847, 1952.752, 7136.847, 1964.564, 7188.027, 1964.564]}, {"net": "GND", "x": 16.8001, "y": 27.5001, "num": "15", "layer": 1, "pts": [621.424, 1122.676, "L", 645.676, 1122.676, 645.676, 1167.715, 655.125, 1167.715, 655.125, 1139.369, 667.723, 1139.369, 667.723, 1167.715, 677.172, 1167.715, 677.172, 1122.676, 701.424, 1122.676, 701.424, 1042.676, 677.172, 1042.676, 677.172, 997.637, 667.723, 997.637, 667.723, 1025.983, 655.125, 1025.983, 655.125, 997.637, 645.676, 997.637, 645.676, 1042.676, 621.424, 1042.676, 621.424, 1122.676]}, {"net": "GND", "x": 51.0891, "y": 98.0001, "num": "15", "layer": 1, "pts": [2051.376, 3818.274, "L", 2027.124, 3818.274, 2027.124, 3773.235, 2017.675, 3773.235, 2017.675, 3801.581, 2005.077, 3801.581, 2005.077, 3773.235, 1995.628, 3773.235, 1995.628, 3818.274, 1971.376, 3818.274, 1971.376, 3898.274, 1995.628, 3898.274, 1995.628, 3943.313, 2005.077, 3943.313, 2005.077, 3914.967, 2017.675, 3914.967, 2017.675, 3943.313, 2027.124, 3943.313, 2027.124, 3898.274, 2051.376, 3898.274, 2051.376, 3818.274]}, {"net": "GND", "x": 139.4691, "y": 98.0001, "num": "15", "layer": 1, "pts": [5530.906, 3818.274, "L", 5506.654, 3818.274, 5506.654, 3773.235, 5497.205, 3773.235, 5497.205, 3801.581, 5484.607, 3801.581, 5484.607, 3773.235, 5475.158, 3773.235, 5475.158, 3818.274, 5450.906, 3818.274, 5450.906, 3898.274, 5475.158, 3898.274, 5475.158, 3943.313, 5484.607, 3943.313, 5484.607, 3914.967, 5497.205, 3914.967, 5497.205, 3943.313, 5506.654, 3943.313, 5506.654, 3898.274, 5530.906, 3898.274, 5530.906, 3818.274]}, {"net": "GND", "x": 48.469, "y": 27.5001, "num": "15", "layer": 1, "pts": [1868.234, 1122.676, "L", 1892.486, 1122.676, 1892.486, 1167.715, 1901.935, 1167.715, 1901.935, 1139.369, 1914.533, 1139.369, 1914.533, 1167.715, 1923.982, 1167.715, 1923.982, 1122.676, 1948.234, 1122.676, 1948.234, 1042.676, 1923.982, 1042.676, 1923.982, 997.637, 1914.533, 997.637, 1914.533, 1025.983, 1901.935, 1025.983, 1901.935, 997.637, 1892.486, 997.637, 1892.486, 1042.676, 1868.234, 1042.676, 1868.234, 1122.676]}, {"net": "GND", "x": 128, "y": 27.5001, "num": "15", "layer": 1, "pts": [4999.374, 1122.676, "L", 5023.626, 1122.676, 5023.626, 1167.715, 5033.075, 1167.715, 5033.075, 1139.369, 5045.673, 1139.369, 5045.673, 1167.715, 5055.122, 1167.715, 5055.122, 1122.676, 5079.374, 1122.676, 5079.374, 1042.676, 5055.122, 1042.676, 5055.122, 997.637, 5045.673, 997.637, 5045.673, 1025.983, 5033.075, 1025.983, 5033.075, 997.637, 5023.626, 997.637, 5023.626, 1042.676, 4999.374, 1042.676, 4999.374, 1122.676]}, {"net": "GND", "x": 79.5801, "y": 38.01, "num": "17", "layer": 1, "pts": [3166.541, 1529.919, "L", 3166.541, 1462.989, 3099.611, 1462.989, 3099.611, 1516.139, 3113.391, 1529.919, 3166.541, 1529.919]}, {"net": "GND", "x": 109.5701, "y": 34.71, "num": "17", "layer": 1, "pts": [4347.251, 1399.999, "L", 4347.251, 1333.069, 4280.321, 1333.069, 4280.321, 1386.219, 4294.101, 1399.999, 4347.251, 1399.999]}]
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
+31 -31
查看文件
@@ -1,31 +1,31 @@
# -*- coding: utf-8 -*-
u"""刷新 `_pcb2_pads_full.json` / `_pcb2_vias.json`(inner1_* 三个校核脚本的输入)。"""
import json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
from eda import run
JS = u"""
await eda.dmt_EditorControl.openDocument('f126ffb29c92f9b2');
await new Promise(r=>setTimeout(r,1800));
const name = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll())
name[c.getState_PrimitiveId()] = c.getState_Designator();
const ids = Object.keys(name).sort((a,b)=>b.length-a.length);
const pads = [];
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const pid = p.getState_PrimitiveId(); let hit='';
for (const id of ids) if (pid.length>id.length && pid.indexOf(id)===0){hit=name[id];break;}
pads.push([hit, p.getState_PadNumber(), p.getState_Net(), p.getState_Layer(),
p.getState_X(), p.getState_Y(), p.getState_Pad(), p.getState_Hole()]);
}
const vias = [];
for (const o of await eda.pcb_PrimitiveVia.getAll())
vias.push([o.getState_Net(), o.getState_X(), o.getState_Y(), o.getState_Diameter(), o.getState_HoleDiameter()]);
return {pads: pads, vias: vias};
"""
r = run(JS, timeout=300)
if not r.get('success'):
sys.exit(u'失败: ' + json.dumps(r, ensure_ascii=False)[:400])
d = r['result']
json.dump(d['pads'], open(os.path.join(HERE, '_pcb2_pads_full.json'), 'w', encoding='utf-8'), ensure_ascii=False)
json.dump(d['vias'], open(os.path.join(HERE, '_pcb2_vias.json'), 'w', encoding='utf-8'), ensure_ascii=False)
print(u'焊盘 %d / 过孔 %d' % (len(d['pads']), len(d['vias'])))
# -*- coding: utf-8 -*-
u"""刷新 `_pcb2_pads_full.json` / `_pcb2_vias.json`(inner1_* 三个校核脚本的输入)。"""
import json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
from eda import run
JS = u"""
await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');
await new Promise(r=>setTimeout(r,1800));
const name = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll())
name[c.getState_PrimitiveId()] = c.getState_Designator();
const ids = Object.keys(name).sort((a,b)=>b.length-a.length);
const pads = [];
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const pid = p.getState_PrimitiveId(); let hit='';
for (const id of ids) if (pid.length>id.length && pid.indexOf(id)===0){hit=name[id];break;}
pads.push([hit, p.getState_PadNumber(), p.getState_Net(), p.getState_Layer(),
p.getState_X(), p.getState_Y(), p.getState_Pad(), p.getState_Hole()]);
}
const vias = [];
for (const o of await eda.pcb_PrimitiveVia.getAll())
vias.push([o.getState_Net(), o.getState_X(), o.getState_Y(), o.getState_Diameter(), o.getState_HoleDiameter()]);
return {pads: pads, vias: vias};
"""
r = run(JS, timeout=300)
if not r.get('success'):
sys.exit(u'失败: ' + json.dumps(r, ensure_ascii=False)[:400])
d = r['result']
json.dump(d['pads'], open(os.path.join(HERE, '_pcb2_pads_full.json'), 'w', encoding='utf-8'), ensure_ascii=False)
json.dump(d['vias'], open(os.path.join(HERE, '_pcb2_vias.json'), 'w', encoding='utf-8'), ensure_ascii=False)
print(u'焊盘 %d / 过孔 %d' % (len(d['pads']), len(d['vias'])))
-23
查看文件
@@ -1,23 +0,0 @@
await eda.dmt_EditorControl.openDocument('7e9415a16a909c51');
await new Promise(r => setTimeout(r, 1200));
const M = 39.3701;
const R = (x0,y0,x1,y1) => [x0*M,y0*M,'L',x1*M,y0*M,x1*M,y1*M,x0*M,y1*M];
const tries = [
['rect-L3-+12V', '+12V', 16, R(100,20,120,30)],
['rect-L3-$net', '$1N223', 16, R(100,35,120,45)],
['rect-L4-VO1_P', 'VO1_P', 2, R(100,50,120,60)],
];
const out = [];
for (const [name, net, layer, src] of tries) {
const row = {name};
try {
const poly = await eda.pcb_MathPolygon.createComplexPolygon([src]);
row.poly = !!poly;
const pour = await eda.pcb_PrimitivePour.create(net, layer, poly, 'solid', false, name, 3, 10);
row.pour = !!pour;
if (pour) await eda.pcb_PrimitivePour.delete(pour.primitiveId);
} catch (e) { row.err = String(e).slice(0, 140); }
out.push(row);
}
out.nPourNow = (await eda.pcb_PrimitivePour.getAll()).length;
return out;
-25
查看文件
@@ -1,25 +0,0 @@
await eda.dmt_EditorControl.openDocument('7e9415a16a909c51');
await new Promise(r => setTimeout(r, 1200));
const M = 39.3701;
const R = (x0,y0,x1,y1) => [x0*M,y0*M,'L',x1*M,y0*M,x1*M,y1*M,x0*M,y1*M];
const Rc= (x0,y0,x1,y1) => [x0*M,y0*M,'L',x1*M,y0*M,x1*M,y1*M,x0*M,y1*M,x0*M,y0*M]; // 显式闭合
const Rr= (x0,y0,x1,y1) => ['R',x0*M,y0*M,x1*M,y1*M,0,0]; // R 矩形模式
const cases = [
['L15-rect', 'GND', 15, R(100,20,120,30)],
['L16-rect-close','+12V',16, Rc(100,35,120,45)],
['L16-Rmode', '+12V',16, Rr(100,50,120,60)],
['L1-rect', '+12V', 1, R(100,65,120,75)],
];
const out = [];
for (const [name, net, layer, src] of cases) {
const row = {name};
try {
const poly = await eda.pcb_MathPolygon.createComplexPolygon([src]);
row.poly = !!poly;
const pour = await eda.pcb_PrimitivePour.create(net, layer, poly, 'solid', false, name, 3, 10);
row.pour = !!pour;
if (pour) await eda.pcb_PrimitivePour.delete(pour.primitiveId);
} catch (e) { row.err = String(e).slice(0, 90); }
out.push(row);
}
return out;
+63 -19
查看文件
@@ -3,7 +3,8 @@ u"""载板 C4-SITE1 ~ C4-SITE5:每台手机一页。
一页里三块:
【A】电池排线 4 焊盘直焊座 J8n(P+/P−/TEMP/ID,2026-09-06 取代 TP8n1/2)+ TEMP 10k / ID 68k + §6.3 电容组(六颗)
【B】四根侧键顶针(PWR / VUP / VDN + J604 三键公共地)
【B】四根侧键顶针(PWR / VUP / VDN + J604 三键公共地)+ 三颗按键 MOS(2N7002 + 1k 栅阻 + 100k 下拉)
—— 2026-09-06 随 74HC595 从下板 P2 搬来(§26.23),位号沿用 Q%d03~05 / R%d20~25
【C】EDL:1.25mm 2P 座 + TS5A3157 模拟开关 + 100k 下拉 + 去耦
用法: /c/Python314/python .claude/c4_site.py [--push] [--only N]
@@ -50,6 +51,8 @@ P = {
'CONN2P': '6461e6e5749d421bb997627984866425', # C225104 A1251WV-S-2P
'SW': '5acafd8eb5a641fcaf05190902930635', # C353034 TS5A3157DBVR
'R100K': 'acc294b6661948399f609b41c77ed19f', # C25741 100k 0402
'NMOS': 'f8007837564a4f1fa0e6b57b81dccb0d', # C8545 2N7002(按键 MOS,从下板搬来)
'R1K': '65abbad5fa864933b552e0f016ece0db', # C11702 1k 0402(栅极串阻)
}
# ---- 版面常量 ----
@@ -63,9 +66,14 @@ J8 = (225, 630) # 排线直焊座:符号 4 脚朝**左**(实
COL = {'1': 100, '2': 130, '3': 170, '4': 190}
R78, R79 = (170, 575), (190, 575) # TEMP / ID 电阻(竖放),下端直接落在 GND 母线上
POGO_X = [220, 320, 420, 520] # 【B】四根顶针的元件中心
Y_POGO = 380
POGO_PORT_Y = [330, 300, 270, 240]
# 【B】三个按键列(PWR / VUP / VDN),每列:顶针在上、按键 MOS 在下、K 端口从最底下进来。
# 列内相对顶针中心 x_p:MOS (x_p-25, 360) → G(x_p-45,360) S(x_p-15,340) D(x_p-15,380);
# 栅阻 R%20/22/24 (x_p-45, 330) 竖放;下拉 R%21/23/25 (x_p-75, 330) 竖放;
# K 端口 (x_p-45, 250);POGO 端口挂在漏极竖线旁的横支线末端 (x_p+5, 410)(给 PCB 网络起名)。
# 列距 120:一列占 x_p-85 ~ x_p+85(POGO 标签),最东一列的标签 515 < 【C】那根 x=530 的竖线。
KEY_X = [190, 310, 430]
GND_POGO_X = 80 # 第四根顶针(J604 公共地)
Y_POGO = 440
J6, U4 = (660, 400), (880, 400) # 【C】
R74, C76 = (975, 370), (1060, 370)
@@ -88,6 +96,9 @@ def names(n):
'j8': 'J8%d' % n, 'r78': 'R78%d' % n, 'r79': 'R79%d' % n,
'pogo': ['J%d' % (41 + (n - 1) * 4 + i) for i in range(4)],
'j6': 'J6%d' % n, 'u4': 'U4%d' % n, 'r74': 'R74%d' % n, 'c76': 'C76%d' % n,
'q': ['Q%d0%d' % (n, k) for k in (3, 4, 5)], # PWR / VUP / VDN
'rg': ['R%d2%d' % (n, k) for k in (0, 2, 4)], # 1k 栅阻
'rp': ['R%d2%d' % (n, k) for k in (1, 3, 5)], # 100k 下拉
}
@@ -102,8 +113,12 @@ def placement(n):
# 和 skill §6 那张表相反 —— 所以传 90。下面 EXPECT 会再断言一次。
rot = 90 if key == 'C2200U' else 270
out.append([d['caps'][i], P[key], CAP_X[i], Y_CAP, rot, LIB_POGO if key == 'C2200U' else LIB])
for i, x in enumerate(POGO_X):
for i, x in enumerate(KEY_X):
out.append([d['pogo'][i], P['POGO'], x, Y_POGO, 0, LIB_POGO])
out.append([d['q'][i], P['NMOS'], x - 25, 360, 0, LIB])
out.append([d['rg'][i], P['R1K'], x - 45, 330, 90, LIB])
out.append([d['rp'][i], P['R100K'], x - 75, 330, 90, LIB])
out.append([d['pogo'][3], P['POGO'], GND_POGO_X, Y_POGO, 0, LIB_POGO])
out += [[d['j6'], P['CONN2P'], J6[0], J6[1], 0, LIB],
[d['u4'], P['SW'], U4[0], U4[1], 0, LIB],
[d['r74'], P['R100K'], R74[0], R74[1], 270, LIB],
@@ -153,16 +168,41 @@ def wiring(n, pins):
w(top[0], top[1], top[0], Y_VO)
w(bot[0], bot[1], bot[0], Y_GND)
# ---------- 【B】侧键顶针 ----------
labels = ['POGO%d_PWR' % n, 'POGO%d_VUP' % n, 'POGO%d_VDN' % n, None]
for i in range(4):
# ---------- 【B】侧键顶针 + 按键 MOS ----------
for i, key in enumerate(('PWR', 'VUP', 'VDN')):
px, py = pins[d['pogo'][i]]['1']
yy = POGO_PORT_Y[i]
w(px, py, px, yy)
if labels[i]:
PT.append(['BI', labels[i], px, yy, 180])
else:
F.append(['Ground', 'GND', px, yy, 0])
q = pins[d['q'][i]]
g, sr, dr = q['1'], q['2'], q['3'] # 2N7002:1=G 2=S 3=D(下面 build_page 里断言过)
assert px == dr[0] == sr[0] and dr[1] < py, (d['q'][i], q, px, py)
# 漏极 -> 顶针一根直线;POGO 端口挂在旁边一根 20 长的横支线末端(给 PCB 网络起名)。
# ⚠️ 不能把端口放在直线的中点:两段共线导线会被 EasyEDA 合并成一段,端口就落在中段 =
# 真断线(skill §8a-2,2026-09-06 第一版就是这么被 port_audit 抓到 15 处的)。
mid = dr[1] + 30
w(dr[0], dr[1], px, py)
w(dr[0], mid, dr[0] + 20, mid)
PT.append(['BI', 'POGO%d_%s' % (n, key), dr[0] + 20, mid, 0])
# 源极 -> 地
w(sr[0], sr[1], sr[0], 300)
F.append(['Ground', 'GND', sr[0], 300, 0])
# 栅极 <- 1k <- K 端口(从底下进来)
rg = pins[d['rg'][i]]
gt = max(rg.values(), key=lambda v: v[1])
gb = min(rg.values(), key=lambda v: v[1])
assert gt[0] == g[0] and gt[1] < g[1], (d['rg'][i], rg, g)
w(g[0], g[1], gt[0], gt[1])
w(gb[0], gb[1], gb[0], 250)
PT.append(['IN', 'K%d_%s' % (n, key), gb[0], 250, 180])
# 栅极 100k 下拉(复位期间 / 排针没插时不误按)
rp = pins[d['rp'][i]]
pt = max(rp.values(), key=lambda v: v[1])
pb = min(rp.values(), key=lambda v: v[1])
assert pt[0] < g[0] and pt[1] < g[1], (d['rp'][i], rp, g)
w(g[0], g[1], pt[0], g[1], pt[0], pt[1])
w(pb[0], pb[1], pb[0], 300)
F.append(['Ground', 'GND', pb[0], 300, 0])
px, py = pins[d['pogo'][3]]['1'] # J604 三键公共地
w(px, py, px, 400)
F.append(['Ground', 'GND', px, 400, 0])
# ---------- 【C】EDL ----------
j, u = pins[d['j6']], pins[d['u4']]
@@ -205,11 +245,11 @@ def wiring(n, pins):
% names(n)['j8'], 10),
(45, 772, u' %s 10k 代替保护板上的 NTC(模拟 25 °C),%s 68k 照抄保护板上的 ID 电阻,都对 P−。'
% (names(n)['r78'], names(n)['r79']), 10),
(45, 470, u'【B】四根顶针顶主板反面的 J601~J604(§13.3)。J604 是三键公共地,', 10),
(45, 454, u' 直接接本路的电源返回地。驱动 MOS 在下板,这里只有顶针。', 10),
(45, 438, u'顶针 = YTC1P-2510-01(立创 C5221288):自由高 3.5 / 行程 0.8 / 压到底 2.7 mm。', 10),
(45, 422, u' 配 3.0mm 立柱 → 压缩 0.5mm(62% 行程,约 62g),屏蔽罩净空 1.6mm。', 10),
(45, 406, u' 冠径只有 Ø0.9 球头,±1mm 落点公差靠首件实测校正(§22.3c / §22.3e)。', 10),
(45, 522, u'【B】四根顶针顶主板反面的 J601~J604(§13.3),J604 是三键公共地,直接接本路的电源返回地。按键 = MOS 把触点对地短接(主板上已串 1k)。', 10),
(45, 508, u' %s 2N7002 由 C6-KEYS 的 74HC595 驱动(K%d_PWR/VUP/VDN),栅极 1k 串阻 + 100k 下拉:复位期间 / 排针没插时不会误按(2026-09-06 从下板搬来,§26.23)。'
% ('/'.join(names(n)['q']), n), 10),
(45, 494, u' 顶针 YTC1P-2510-01(C5221288):自由高 3.5 / 行程 0.8 / 压到底 2.7 mm;配 3.0mm 立柱压缩 0.5mm(62% 行程,约 62g),屏蔽罩净空 1.6mm。', 10),
(45, 480, u' 冠径只有 Ø0.9 球头,±1mm 落点公差靠首件实测校正(§22.3c / §22.3e)。', 10),
(45, 205, u'【C】%s 是 1.25mm 2P 座,两根线焊到主板正面的 TP1204 / TP1205(§22.4)。'
% names(n)['j6'], 10),
(45, 189, u' %s 导通即短接两点 → 手机进 9008。串了 1.8k,短路电流只有 1mA。'
@@ -332,6 +372,10 @@ return k;
for k, v in want.items():
if got.get(k, '').upper().replace('VCC', 'V+') != v:
bad.append(u'%s.%s 引脚名期望 %s 实测 %s' % (d['u4'], k, v, got.get(k)))
for qd in d['q']:
q = pins[qd]
if not (sorted(q) == ['1', '2', '3'] and q['1'][0] < q['2'][0] == q['3'][0] and q['2'][1] < q['1'][1] < q['3'][1]):
bad.append(u'%s 2N7002 引脚应是 1=G(左) 2=S(右下) 3=D(右上),实测 %s' % (qd, q))
if bad:
raise SystemExit(pname + u' 引脚核对失败:\n ' + '\n '.join(bad))
+8 -1
查看文件
@@ -43,7 +43,14 @@ sys.path.insert(0, HERE)
from eda import run
# 默认下板;载板加参数:/c/Python314/python .claude/drc_read.py --pcb2
PCB = 'f126ffb29c92f9b2' if '--pcb2' in sys.argv else '7e9415a16a909c51'
# 2026-09-08:加 `--blade` / `--base` 跑 R2 的两块板(UUID 从 .claude/r2/r2-uuids.json 取)
if '--blade' in sys.argv or '--base' in sys.argv:
import json as _j, io as _io, os as _os
_u = _j.load(_io.open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
'r2', 'r2-uuids.json'), encoding='utf-8'))
PCB = _u['boards']['BASE' if '--base' in sys.argv else 'BLADE']['pcb']['uuid']
else:
PCB = '942ed1560d81c1e7' if '--pcb2' in sys.argv else '7e9415a16a909c51'
JS_TREE = u"""
await eda.dmt_EditorControl.openDocument('%s');
+1 -1
查看文件
@@ -38,7 +38,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
# 默认下板;载板用 `python .claude/dump_pads.py f126ffb29c92f9b2 pcb2-copper.json`
# 默认下板;载板用 `python .claude/dump_pads.py 942ed1560d81c1e7 pcb2-copper.json`
PCB = sys.argv[1] if len(sys.argv) > 1 else '7e9415a16a909c51'
MIL = 39.3701
OUT = os.path.join(HERE, sys.argv[2] if len(sys.argv) > 2 else 'pcb-copper.json')
+122 -14
查看文件
@@ -1,6 +1,6 @@
{
"_note": "EasyEDA 器件与工程 UUID 缓存。新会话直接用,不必重新 lib_Device.search。role 里带 ⚠️ 的是踩过的坑。P2 单路取值见设计文档 §16.2。⚠️ 作废:C25907(5.23k)、C25743(107k) 已被 C407675(5.49k)、C25755(150k) 取代,不要再用。 ⚠️ Board3(自制转接板) 已删除,改走方案 D:载板上放 BM24-50DS 座 + 原厂排线。 ⚠️ BM24 座子已改用个人库自建封装版 771dbf00…(见 parts.BM24-HRS-PIN),立创原件 C424561 的封装编号是错的。",
"_generated": "2026-09-05 夜:板对板连接器换件实施完毕 —— 下板 C55020601 公头 / 载板 C5307344 母座(对插 13.0mm、双双插装、公母同一套 IDC 奇偶式编号)。P5-BTB / C1-CONN 两页已重生成,载板 PCB 已重摆/重扇/重铺。",
"_generated": "2026-09-08:R2 底板 M3 新增 4 个器件(TCA9548APWR / NCP18XH103F03RB / PCIE-064-02-F-D-TH 插槽 / 1x5 排针),引脚坐标均为空页实测。",
"library_uuid": "0819f05c4eef4c71ace90d822a990e87",
"project": {
"uuid": "1e5745f4caff4b15a921694d2fd912e4",
@@ -107,7 +107,7 @@
"uuid": "9b2474b02bb94569a00ded395421ea45",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "R0402",
"role": "R 4.7k 0402 — ⚠️ 曾误当 eFuse ILIM 用;TPS25940 的 R_ILIM 合法范围是 16.9~150k,4.7k 非法。现无用途"
"role": "R 4.7k 0402 — R2 刀片的 I²C 上拉(B2-CTRL 的 R40/R41,挂本槽 mux 分支)。⚠️ 曾误当 eFuse ILIM 用,那是非法的(合法范围 16.9~150k)"
},
"C2653873": {
"name": "TPS25940ARVCR",
@@ -526,16 +526,6 @@
"lcsc": "C424561",
"role": "J71~J75 实际用的就是这一条(2026-09-04 自建)。焊盘按 **Hirose 奇偶行编号**:奇数 1,3,5..49 在 +Y 排、偶数 2,4,6..50 在 −Y 排,1 脚在 −X 端;pin51 = 1/2 脚那端的电源触点(x=−6.03),pin52 = 49/50 脚那端(x=+6.03)。几何来自 Hirose 官方样本反算,生成脚本 .claude/fp_bm24.py。⚠ 「哪一排是奇数排」还没有实证(§13.5 的 §MIRROR),打样前必须做那一次实测。"
},
"POGO-D1.9": {
"name": "YTC1P-2510-01 D1.9",
"uuid": "1c8e0ea1e66142a8860d6812b1dbe356",
"lib": "543725e8f0454017a5691e7e570aeef6",
"lib_note": "个人库",
"fp": "POGO-SMD1P_D1.9",
"fp_uuid": "2cbabda783f34133b4fe450e98f282bc",
"lcsc": "C5221288",
"role": "J41~J60 侧键顶针,**实际用的就是这一条**。立创原封装 SMD-1_BD2.0-H3.6 焊盘 Ø2.5,而小米8 四个侧键触点最近的一对(J601↔J604)只隔 2.217 mm → 焊盘会叠在一起把音量+ 永久短路。自建封装缩到 Ø1.9(净间距 0.317 mm)。法兰本来就只有 Ø2.0。见设计文档 §25.3。"
},
"C55020601": {
"name": "AZ-PZ2.54-V11-2*20P",
"uuid": "c0ab04240dc64fca8d6203d15f8a8a68",
@@ -575,6 +565,123 @@
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "R0402",
"role": "R 68k 0402 — R79n 电池 ID 电阻(实测原保护板 ≈68k 对 P−)"
},
"MI8-KEY-PAD": {
"name": "MI8-KEY-PAD D1.5",
"uuid": "1c8e0ea1e66142a8860d6812b1dbe356",
"lib": "543725e8f0454017a5691e7e570aeef6",
"lib_note": "个人库",
"fp": "PAD-D1.5_MI8-KEY",
"fp_uuid": "e106d85830014c088c1fba82f91a26f6",
"sym_uuid": "f524af799aa247f2b91e8f56171fa171",
"lcsc": "",
"role": "J41~J60 侧键触点盘。**2026-09-06 方案改**:顶针取消,改成和中框一样的焊盘 —— 螺丝把主板压在载板上(1 mm 间隙),主板自己的弹片直接压在这个裸铜盘上。器件 uuid 没变(就是原来那颗顶针器件改的),所以**原理图一个字没动**,符号也没换。封装 Ø1.5:四触点实测最小间距 1.78 mm,净距 0.28。⚠️ 旧的 POGO-SMD1P_D1.9 是按「最近一对 2.217 mm」缩的,在真实 1.78 下是负净距。⚠️ 器件属性里的 BOM 字段(Add into BOM=yes / C5221288 / YIYUAN)**API 改不动**,要在客户端手工改成不进 BOM 并清掉料号,否则会照着买 20 颗用不上的顶针。⚠️ 工艺:这是接触盘,**钢网开口 0(不上锡膏)**,表面处理走 ENIG。"
},
"C15127": {
"name": "AO3401A_C15127",
"uuid": "be56508f80684877b262e15e4d50e175",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "SOT-23_L2.9-W1.3-P1.90-LS2.4-BR",
"role": "Q7 P-MOS 高边开关(风扇 FAN_12V_SW)。Vgs 绝对最大 ±12V ⇒ 必须配 DZ3 钳位。⚠ 封装名和 AO3400A 那颗不一样(SOT-23_ 不是 SOT-23-3_),H_FP 要单独加一条"
},
"C2891776": {
"name": "BZT52C8V2",
"uuid": "46e19a04908e4d7f9d40ab2982f52315",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "SOD-123_L2.7-W1.6-LS3.7-RD-1",
"role": "DZ3 8.2V/500mW 稳压管,钳 Q7 的负 Vgs。K 接源极、A 接栅极;create(rot=90) 才是 K 上 A 下"
},
"C25756": {
"name": "0402WGF1502TCE",
"uuid": "6d104587f1cd4b509c6a5fb272570507",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "R0402",
"role": "R 15k 0402 1% — 两路 TACH 接收的栅极串阻(10k 会让高电平不够)"
},
"C2847671": {
"name": "2510-4A",
"uuid": "04ed497706104166a1f88dbf2d16083e",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "CONN-TH_4P-P2.54_CAX_2510-4A",
"role": "J26/J27 四线风扇座(直针 1x4P 2.54)。1=GND 2=FAN_12V_SW 3=TACH 4=PWM,与旧 3 针针序不同。塑壳 11.5mm,下板上只有两个上角和 y 68~92 放得下"
},
"C116575": {
"name": "T491A226K010AT",
"uuid": "950f101b742e488fa85dafd0c53e49eb",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "CASE-A_3216",
"role": "C737 AMS1117-3.3 输出 22uF/10V **固态钽**(手册 §Stability 要求,陶瓷不算数)。⚠ **1 脚是正极**(rot 0 时在上),装配时钽的条纹 = 正极,别按铝电解的习惯"
},
"PCIE-X4-EDGE-64P": {
"name": "PCIE-X4-EDGE-64P",
"uuid": "787309ccc49040b1b6383a339fd8c405",
"lib": "543725e8f0454017a5691e7e570aeef6",
"lib_note": "个人库,不是系统库 —— 放元件时 libraryUuid 要用这个",
"symbol": "1083524aa7614bde8c2394b7d1fe5bf3",
"role": "R2 刀片 J1:PCI-E x4 金手指(板边本身,不采购)。符号借自系统库 PCIE-064-02-F-D-TH,脚号 A1~A32 在左列(自上而下,pitch 10)、B1~B32 在右列,与 PCIe CEM 命名一一对应。⛔ 封装还没建(64 个板边焊盘,1.0 mm 间距,正反两面),属 M4 —— ✅ **T3 已定(2026-09-08):做斜边 20°/1.0 mm,金手指区电镀硬金(不是 ENIG),板厚必须 1.6 mm**,所以现在可以开画。⚠ 斜边会吃掉手指前端约 1.0 mm 的镀层 ⇒ 金手指长度按 PCIe CEM 画,别自己缩短。"
},
"C310990": {
"name": "F1206HC12A0TM",
"uuid": "507cd700b60b451caa0f3237e41baf6d",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "1206",
"role": "下板 / R2 底板 F1:12A 贴片保险丝(35V,分断 150A)。整机最坏 9.99A(设计文档 §22.6),⚠ 与刀片上那颗 F1(2A 慢断,T7 还是 10A 占位件 C310989)不是一回事,别搞混。"
},
"C2570006": {
"name": "RHC2512FT2R00",
"uuid": "3c25870a08a64dc7b809655f380d8ce9",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "2512",
"role": "R9A~R9E:2Ω 2W 2512,5 颗串联 = 10Ω/10W 母线泄放电阻。高 0.8mm —— 原来的立式水泥电阻站起来超高度预算,才拆成 5 颗贴片。"
},
"C9900017043": {
"name": "DC-005-5.5*2.5",
"uuid": "66ed565d4c984ace901de00bbc83af82",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "DC-005",
"role": "J1 圆形 DC 电源座(2026-09-08 用户拍板换掉 XT30PW-M,不降载)。脚 1 = 中心极正 → 保险丝;脚 2/3 = 负极静/动触点,两个都接地并联降接触电阻。⚠ 家族额定 2~5A 而整机最坏 9.99A —— 已知风险、用户明确接受,发热点在插头内芯与弹片的接触面,板上加铜救不了。"
},
"C130026": {
"name": "TCA9548APWR",
"uuid": "98a0404e89ff4ecdb0427f14520ce65a",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "TSSOP-24_L7.8-W4.4-P0.65-LS6.4-BL",
"role": "U35 TCA9548A 8 通道 I²C 多路器(R2 底板,D8:一槽一路)。实测引脚(rot 0,相对中心):左列自上而下 1 A0(-50,+55) 2 A1(+45) 3 RESET#(+35) 4 SD0(+25) 5 SC0(+15) 6 SD1(+5) 7 SC1(-5) 8 SD2(-15) 9 SC2(-25) 10 SD3(-35) 11 SC3(-45) 12 GND(-55);右列自下而上 13 SD4(+50,-55) 14 SC4(-45) 15 SD5(-35) 16 SC5(-25) 17 SD6(-15) 18 SC6(-5) 19 SD7(+5) 20 SC7(+15) 21 A2(+25) 22 SCL(+35) 23 SDA(+45) 24 VCC(+55)。⚠ SD/SC 成对,通道 n 的 SDn/SCn 脚号相邻但**SD 在前 SC 在后**,别按 SCL/SDA 的习惯顺序接反。"
},
"C13564": {
"name": "NCP18XH103F03RB",
"uuid": "26d83c83abc7457cb451b4cf76e659b1",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "R0603",
"role": "RT601/RT602 NTC 10k B=3380K 0603(R1 P3-CONTROL 原件,R2 挪到 A6-FAN)。两脚件,rot 0 时 1 在左(-20,0)、2 在右(+20,0),无极性。"
},
"C4597619": {
"name": "PCIE-064-02-F-D-TH",
"uuid": "3ea1a15bda65451a97a33a1549965ce3",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "插件 P=1mm(SAMTEC)",
"role": "J34~J38:R2 底板的 5 个 PCI-E x4 插槽。**符号与刀片 J1(PCIE-X4-EDGE-64P)同源**,脚号 A1~A32 在左列(-35,自上而下 pitch 10,A1 在 +155)、B1~B32 在右列(+35,同 y)。⛔ **T8 风险**:这是 SAMTEC 的贵料,客户端 lib_Device.search 够不着立创全库里那些几毛钱的PC 标准 PCI-E 4X 插槽(同 T7)。下单前必须换成便宜的同针位插槽并核对封装。"
},
"C50951": {
"name": "2.54mm 1*5P 直排针",
"uuid": "e2ae8b0c17dd425aa2f9a1952b667332",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "HDR-TH_5P-P2.54",
"role": "J33 应急 UART 口(D13 的保命口:GND/TXD0/RXD0/EN/IO0)。rot 0 实测引脚全部朝左:1(-20,+20) 2(-20,+10) 3(-20,0) 4(-20,-10) 5(-20,-20)。位号前缀是 P?,放件后要显式 setState_Designator(\"J33\")。"
},
"C19188869": {
"name": "PCIE-64P11L",
"uuid": "568ba50f7a8e4226aed9ac603b235162",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "PCIE-SMD_PCIE-64P11L",
"role": "J34~J38:R2 底板的 5 个 PCI-E x4 插槽(SOFNG 硕方,PCIE 64PIN 镀金加强 **贴片立式**)。2026-09-08 取代 SAMTEC PCIE-064-02-F-D-TH(C4597619)结掉 T8 —— 用户要 PC 标准便宜料。⚠ **脚号与刀片金手指不一样**:本符号的**脚号是 1~64**(脚名才是 A1~A32/B1~B32),而刀片 PCIE-X4-EDGE-64P 的脚号直接就是 A1~A32/B1~B32。换算:A(n) → 脚号 n,B(n) → 脚号 32+n。引脚**相对坐标与 Samtec 那颗完全一致**(A 排 x=-35、B 排 x=+35,y = 155-10*(n-1)),所以版图一个字没动。⚠ 还多一根 **65 脚 = 外壳/屏蔽,在 (0,+175)**(符号顶端正中),本项目接 GND。⛔ **贴片立式**:保持力只靠焊盘剪切,不如原来那颗直插 —— 五片刀片各驮一块手机主板,M4 布板时要给刀片加固定件(梳齿/压条),不能只靠插槽。"
},
"C310998": {
"name": "F1206SB2000V063TM",
"uuid": "256f75e322a94be48a90613406f07457",
"lib": "0819f05c4eef4c71ace90d822a990e87",
"fp": "1206",
"role": "刀片 B1-POWER 的 F1:**2A 慢断**贴片保险丝(AEM,SB = Slow Blow),1206 / 63V / 分断 50A / 熔断 I²t = 0.63 A²s。2026-09-08 结掉 T7,取代同封装的 10A 占位件 C310989(F1206HC10A0TM)。⚠ **封装 uuid 与占位件不同**(bceff889… vs 66116fae…,10A/150A 分断那颗焊盘更大),刀片 PCB 还没布(M4),所以无影响;原理图侧的导线是按读回引脚坐标算的,自动跟随。⚠ 刀片满载约 1.45 A(16 W 手机 ÷ 92% ÷ 12 V)= 额定的 73%,正压在保险丝「连续 ≤75%」的降额线上,机箱内 40~50 ℃ 还要再降 ——要更大裕量就换 F1206SB2500V032TM(C310999,2.5A/32V/I²t 0.9)。⚠ 别和底板那两颗混:底板 F1 是 C310990 F1206HC12A0TM(12A,整机 9.99 A)。"
}
},
"schematic_boards": {
@@ -586,7 +693,7 @@
},
"Board2": {
"schematic": "302e115180aaa8fc",
"pcb": "f126ffb29c92f9b2",
"pcb": "942ed1560d81c1e7",
"board": "c6594605c7cbe2b3",
"role": "上板 载板(carrier)",
"pages": {
@@ -600,7 +707,8 @@
"C4-SITE5": "e58ef2c0a55e744a",
"C5-J1401A": "967543b084075b4b",
"C5-J1401B": "63413775cfc3aafc",
"C5-AUX": "b1ffbaef65ef82c9"
"C5-AUX": "b1ffbaef65ef82c9",
"C6-KEYS": "f44d1c8b0ad64fad"
}
}
}
+40 -17
查看文件
@@ -1,35 +1,35 @@
{
"frame": "顶视,板左下角原点,mm",
"board_w": 67.09,
"board_h": 44.66,
"board_w": 44.0,
"board_h": 67.0,
"holes": {
"H1": [
54.2,
42.3
1.4,
9.9
],
"H2": [
2.6,
38.2
0.7,
27.3
],
"H3": [
61.9,
33.7
1.7,
38.7
],
"H4": [
62.1,
29.2
41.9,
1.9
],
"H5": [
2.6,
27.0
57.4,
41.2
],
"H6": [
4.0,
10.8
64.6,
28.1
],
"H7": [
42.6,
3.7
64.8,
32.4
]
},
"contacts_up": {
@@ -71,5 +71,28 @@
2.73,
4.38
]
}
},
"hole_d": 2.0,
"_source": "2026-09-06 测量底板扫描件反算(pcb2_measure_read.py,基准点单应,±0.5mm)。板长 67 = 裸板;描的轮廓 70.3 含 SIM 卡托伸出的 3.3mm(卡托在 u 大的那一端)",
"side_key_contacts": {
"PWR": [
4.72,
5.16
],
"VUP": [
3.42,
6.37
],
"VDN": [
3.49,
3.74
],
"GND": [
2.05,
4.99
]
},
"_side_key_note": "2026-09-06 实测:中框金焊盘 + 7 孔配准反算,残差 0.57mm。菱形 相邻1.87/对角2.65,焊盘 Φ1.35。簇心 (3.42,5.07),用户卡尺「H1 孔心到簇心 5mm」判死(另一条 2.88 的解已弃)。身份两条独立描述印证:GND 离短板边最近、PWR 与之正对、VDN 离长板边最近、VUP 次之。",
"frame_boss_to_pad_mm": 1.0,
"_frame_boss_note": "2026-09-06 用户实测:中框螺柱支撑面到侧键金焊盘面 1 mm = 载板面到主板贴合面该有的间隙"
}
+23 -16
查看文件
@@ -56,14 +56,23 @@ HDR = 'c0ab04240dc64fca8d6203d15f8a8a68' # C55020601 AZ-PZ2.54-V11-2*20P
# 不是为了电流(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(北)
('POGO1_PWR', 'POGO4_PWR'), # 6~10 北=CH1 信号
('POGO1_VUP', 'POGO4_VUP'), # 南=CH4 信号
('POGO1_VDN', 'POGO4_VDN'),
('EDL_EN1', 'EDL_EN4'),
('VBUS_EN1', 'VBUS_EN4'),
('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
@@ -72,18 +81,16 @@ J24 = [
]
J25 = [
('+5V', '+5V'), ('+5V', '+5V'), # 1~2 北->C702/U25 南->U28/C705
('POGO2_PWR', 'VBUS_EN5'), # 3~4 南=CH5 的两个使能
('POGO2_VUP', 'EDL_EN5'), # (U28/U45 就在 x133~136)
('POGO2_VDN', 'GND'),
('EDL_EN2', 'GND'),
('VBUS_EN2', 'GND'),
('HUB_RST', 'GND'), # 8
('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(南)
('POGO3_PWR', 'POGO5_PWR'), # 12~14 北=CH3 南=CH5 顶针
('POGO3_VUP', 'POGO5_VUP'),
('POGO3_VDN', 'POGO5_VDN'),
('GND', 'GND'), ('GND', 'GND'), ('GND', 'GND'), # 12~14 原 POGO3_* / POGO5_* -> GND
('VO3_P', 'GND'), ('VO3_P', 'GND'), ('VO3_P', 'GND'), # 15~17 -> site3(北)
('EDL_EN3', 'GND'), ('VBUS_EN3', 'GND'), # 18~19
('GND', 'GND'), ('VBUS_EN3', 'GND'), # 18~19 原 EDL_EN3 -> GND
('+5V', '+5V'), # 20 -> C703/U26(北)
]
@@ -166,7 +173,7 @@ def plan():
(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'合计 54 个信号 + 26 个地(20A 回流 = 0.77A 每针)。', 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),
]
+442 -321
查看文件
@@ -1,343 +1,464 @@
{
"U33": [
[
"1",
210,
635
],
[
"2",
210,
625
],
[
"3",
210,
615
],
[
"4",
210,
605
],
[
"5",
210,
595
],
[
"6",
210,
585
],
[
"7",
210,
575
],
[
"8",
210,
565
],
[
"9",
290,
565
],
[
"10",
290,
575
],
[
"11",
290,
585
],
[
"12",
290,
595
],
[
"13",
290,
605
],
[
"14",
290,
615
],
[
"15",
290,
625
],
[
"16",
290,
635
"Q6": {
"name": "AO3400A_C20917",
"pins": [
[
"3",
"D",
330,
350
],
[
"1",
"G",
300,
330
],
[
"2",
"S",
330,
310
]
]
],
"C739": [
[
"2",
120,
710
],
[
"1",
120,
670
},
"R733": {
"name": "0402WGF1001TCE",
"pins": [
[
"2",
"2",
220,
330
],
[
"1",
"1",
180,
330
]
]
],
"R735": [
[
"2",
430,
680
],
[
"1",
430,
640
},
"R734": {
"name": "0402WGF1003TCE",
"pins": [
[
"2",
"2",
250,
250
],
[
"1",
"1",
250,
290
]
]
],
"R736": [
[
"2",
490,
680
],
[
"1",
490,
640
},
"D2": {
"name": "SS34_C8678",
"pins": [
[
"2",
"A",
430,
640
],
[
"1",
"K",
430,
680
]
]
],
"R737": [
[
"2",
550,
680
],
[
"1",
550,
640
},
"C738": {
"name": "CL31A226KAHNNNE",
"pins": [
[
"1",
"1",
490,
675
],
[
"2",
"2",
490,
645
]
]
],
"R738": [
[
"2",
610,
680
],
[
"1",
610,
640
},
"R747": {
"name": "0402WGF1001TCE",
"pins": [
[
"2",
"2",
150,
680
],
[
"1",
"1",
150,
640
]
]
],
"R739": [
[
"2",
670,
680
],
[
"1",
670,
640
},
"Q8": {
"name": "AO3400A_C20917",
"pins": [
[
"3",
"D",
290,
580
],
[
"1",
"G",
260,
560
],
[
"2",
"S",
290,
540
]
]
],
"Q6": [
[
"3",
710,
350
],
[
"1",
680,
330
],
[
"2",
710,
310
},
"R748": {
"name": "0402WGF1001TCE",
"pins": [
[
"2",
"2",
220,
560
],
[
"1",
"1",
180,
560
]
]
],
"R733": [
[
"2",
620,
330
],
[
"1",
580,
330
},
"R749": {
"name": "0402WGF1003TCE",
"pins": [
[
"2",
"2",
230,
530
],
[
"1",
"1",
230,
490
]
]
],
"R734": [
[
"2",
660,
290
],
[
"1",
660,
250
},
"R750": {
"name": "0402WGF1002TCE",
"pins": [
[
"2",
"2",
960,
740
],
[
"1",
"1",
960,
700
]
]
],
"D2": [
[
"2",
790,
450
],
[
"1",
790,
410
},
"R751": {
"name": "0402WGF1502TCE",
"pins": [
[
"2",
"2",
830,
690
],
[
"1",
"1",
790,
690
]
]
],
"C738": [
[
"1",
880,
605
],
[
"2",
880,
635
},
"R752": {
"name": "0402WGF1002TCE",
"pins": [
[
"2",
"2",
790,
670
],
[
"1",
"1",
790,
630
]
]
],
"R730": [
[
"2",
950,
320
],
[
"1",
950,
280
},
"R753": {
"name": "0402WGF1002TCE",
"pins": [
[
"2",
"2",
710,
770
],
[
"1",
"1",
710,
730
]
]
],
"R731": [
[
"2",
890,
240
],
[
"1",
850,
240
},
"R754": {
"name": "0402WGF1002TCE",
"pins": [
[
"2",
"2",
960,
500
],
[
"1",
"1",
960,
460
]
]
],
"R732": [
[
"2",
950,
220
],
[
"1",
950,
180
},
"R755": {
"name": "0402WGF1502TCE",
"pins": [
[
"2",
"2",
830,
450
],
[
"1",
"1",
790,
450
]
]
],
"J26": [
[
"1",
1065,
710
],
[
"2",
1065,
700
],
[
"3",
1065,
690
},
"R756": {
"name": "0402WGF1002TCE",
"pins": [
[
"2",
"2",
790,
430
],
[
"1",
"1",
790,
390
]
]
],
"J27": [
[
"1",
1065,
630
],
[
"2",
1065,
620
],
[
"3",
1065,
610
},
"R757": {
"name": "0402WGF1002TCE",
"pins": [
[
"2",
"2",
710,
530
],
[
"1",
"1",
710,
490
]
]
],
"J28": [
[
"1",
1065,
550
],
[
"2",
1065,
540
],
[
"3",
1065,
530
},
"J26": {
"name": "2510-4A",
"pins": [
[
"1",
"1",
1065,
710
],
[
"2",
"2",
1065,
700
],
[
"3",
"3",
1065,
690
],
[
"4",
"4",
1065,
680
]
]
],
"J29": [
[
"1",
1065,
470
],
[
"2",
1065,
460
],
[
"3",
1065,
450
},
"J27": {
"name": "2510-4A",
"pins": [
[
"1",
"1",
1065,
470
],
[
"2",
"2",
1065,
460
],
[
"3",
"3",
1065,
450
],
[
"4",
"4",
1065,
440
]
]
],
"J30": [
[
"1",
1065,
390
],
[
"2",
1065,
380
],
[
"3",
1065,
370
},
"Q7": {
"name": "AO3401A_C15127",
"pins": [
[
"3",
"D",
310,
710
],
[
"1",
"G",
280,
730
],
[
"2",
"S",
310,
750
]
]
],
"_page": "fd00f13e59336392"
},
"DZ3": {
"name": "BZT52C8V2",
"pins": [
[
"1",
"K",
150,
770
],
[
"2",
"A",
150,
730
]
]
},
"R746": {
"name": "0402WGF1003TCE",
"pins": [
[
"2",
"2",
240,
770
],
[
"1",
"1",
240,
730
]
]
},
"Q9": {
"name": "AO3400A_C20917",
"pins": [
[
"3",
"D",
710,
710
],
[
"1",
"G",
740,
690
],
[
"2",
"S",
710,
670
]
]
},
"Q10": {
"name": "AO3400A_C20917",
"pins": [
[
"3",
"D",
710,
470
],
[
"1",
"G",
740,
450
],
[
"2",
"S",
710,
430
]
]
}
}
文件差异因一行或多行过长而隐藏
+967 -312
查看文件
文件差异内容过多而无法显示 加载差异
文件差异因一行或多行过长而隐藏
+6 -4
查看文件
@@ -25,7 +25,9 @@ from eda import run
DX, DY = 30.0, 5.0 # 下板原点在载板坐标里的位置
GAP = 13.0 # 两板净空
MARGIN = 1.0 # 装配余量
BW, BH = 160.0, 125.0
# 2026-09-07:跟 pcb1_place 一起改成真板框(220x135,左下角 (-30,-5))。
BX0, BY0, BX1, BY1 = -30.0, -5.0, 190.0, 130.0
BW, BH = BX1 - BX0, BY1 - BY0
LANE_H = 1.0 # 排线(0.2 厚)+ 走线余量,按 1 mm 算
@@ -37,7 +39,7 @@ def shift(x0, y0, x1, y1):
def clip(r):
u"""裁到下板板框内;完全在框外返回 None。"""
x0 = max(r[0], 0.0); y0 = max(r[1], 0.0)
x1 = min(r[2], BW); y1 = min(r[3], BH)
x1 = min(r[2], BX1); y1 = min(r[3], BY1)
if x1 <= x0 or y1 <= y0:
return None
return [round(x0, 2), round(y0, 2), round(x1, 2), round(y1, 2)]
@@ -54,7 +56,7 @@ def header_anchors():
pads = json.load(io.open(os.path.join(HERE, 'pcb2-header-pads.json'), encoding='utf-8'))['pads']
nets = {}
r = run("""
await eda.dmt_EditorControl.openDocument('f126ffb29c92f9b2');
await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');
await new Promise(r=>setTimeout(r,2000));
const out = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
@@ -116,7 +118,7 @@ def build():
data = {
'frame': u'下板 PCB1 坐标系 mm,原点左下角,Y 向上。下板坐标 = 载板坐标 − (30,5)。'
u'hmax = 该矩形内下板元件允许的最大高度 = 13(净空) − h(载板底面元件) − 1(余量)',
'gap': GAP, 'board': [BW, BH],
'gap': GAP, 'board': [BW, BH], 'board_rect': [BX0, BY0, BX1, BY1],
'anchors': anchors, 'zones': zones, 'lanes': lanes, 'slots': slots,
}
out = os.path.join(HERE, 'pcb1-env.json')
+9 -2
查看文件
@@ -156,11 +156,18 @@ def main():
pours = R.pour_rects()
obstacles = R.obstacles_of(pads, vias, lines, pours)
js = R.jobs(pads, want, skip=() if want else R.SKIP)
# ⚠️ 2026-09-06 起 `route_sig` 把出线段也下发了 —— 布不通的焊盘在板上是
# 「一个焊盘 + 一小截自己的出线段」,不再是光焊盘。只认光焊盘会报「0 个」。
# 出线段全长 ≤ 1.2 mm、没有过孔的碎片,一样当孤立焊盘审。
need = set()
for net, frags in js:
for fr in frags:
if len(fr['pads']) == 1 and not fr['vias'] and not fr['lines']:
need.add(fr['pads'][0]['tag'])
if len(fr['pads']) != 1 or fr['vias']:
continue
q = fr['pads'][0]
if all(max(math.hypot(l['x0'] - q['x'], l['y0'] - q['y']),
math.hypot(l['x1'] - q['x'], l['y1'] - q['y'])) <= 1.2 for l in fr['lines']):
need.add(q['tag'])
order = [n for n, _f in js]
stubs, alts = R.plan_stubs(order, pads, obstacles, need)
audit.stubs = stubs
+56 -2
查看文件
@@ -70,7 +70,10 @@ E = RS.E # 引擎(polish / _l_shapes / board_mask 从这
#: **但 2026-09-06 试着加进来没跑通**:它板上的铜整片都在顶层、且集中在控制块那一带,
#: 六条连线里有四条报「L2 源0 / L16 源0」—— 主碎片周围一个合法孔位都没有,扎不下去。
#: 要挪它得先把控制块那一带的顶层拆开,不是改这一行的事。
NETS = ('SCL', 'SDA')
NETS = ('SCL', 'SDA', 'BUCK_PG')
# ── 侧键模拟线 POGO3_PWR / POGO3_VDN / POGO4_VDN / POGO5_PWR / POGO5_VUP(第二十六轮加)
# 2026-09-06 随 74HC595 + 按键 MOS 整窝挪到载板(§26.23)之后,下板上不再有 POGO* 网络,
# 已从候选表拿掉;它们在 Inner2 上的铜由 pcb_prune_nets.py 一并清掉了。
L_TOP, L_BOT, L3 = 1, 2, 16
LAYS = (L_TOP, L_BOT, L3)
@@ -108,6 +111,14 @@ CLUSTERS = {
'U12': (121.5, 45.5, 129.5, 52.5), # CH5 INA226
'U13': (3.0, 90.5, 11.0, 100.0), # 输入侧 INA226
'J24': (30.0, 60.5, 38.5, 67.0), # 排针(通孔针,L3 上直接落脚)
# 五路 Buck 控制器:`BUCK_PG` 在 U*.14 上。**不划框的话整条出线短线也被拆掉**,
# 而那几个脚在 0.5 mm 脚距的 QFN 上,`entry_stub` 重建不回来 ⇒ 整个网络断成 6 片
# (2026-09-06 实测,连通性保险当场拦下)。框只罩住芯片本体 + 0.6 mm。
'U1': (14.4, 25.1, 19.2, 29.9),
'U2': (48.7, 95.6, 53.5, 100.4),
'U3': (137.1, 95.6, 141.9, 100.4),
'U4': (46.1, 25.1, 50.9, 29.9),
'U5': (125.6, 25.1, 130.4, 29.9),
}
@@ -964,6 +975,8 @@ def main():
pads, vias, lines = NL.geom(refresh)
print(u'板上:焊盘 %d / 过孔 %d / 走线 %d' % (len(pads), len(vias), len(lines)))
# ⚠️ 只拆**这一轮要动的**网络(`--net` 限定时尤其重要)
globals()['NETS'] = tuple(nets)
keep_l, drop_l, keep_v, drop_v = split_old(pads, vias, lines)
dlen = sum(math.hypot(lines[i]['x1'] - lines[i]['x0'],
lines[i]['y1'] - lines[i]['y0']) for i in drop_l)
@@ -1016,7 +1029,48 @@ def main():
if worse:
# ⚠️ 这条保险是必需的:本脚本会把这些网络在簇框外的铜**全部拆掉**重来。
# 拆得动、重连不上的话,板子会比动手之前更差,而净距校核照样全绿。
sys.exit(u'⛔ %s 的连通性变差了 —— 不下发' % ' / '.join(worse))
#
# 但**不要因此把整轮毙掉** —— `NETS` 是一张「候选表」,里面难免有挪不动的
# (2026-09-06 实测 `BUCK_PG`:五个分支要去板子四角,而 L3 上 y<34 往东
# 只剩 x115~116 那条 1.0 mm 缝,`+12V` 把别的通道全占了)。
# 把变差的那几个**踢出去重算一遍**,剩下的照常下发。
print(u' → %s 挪进内层会变差,踢出候选表重算一轮' % ' / '.join(worse))
keep = [n for n in nets if n not in worse]
if not keep:
sys.exit(u'⛔ 候选表里一个都留不下 —— 不下发')
# ⚠️ **拆除清单必须跟着重算**。第一版只换了 `run_plan` 的网络表,
# 而 `drop_l/drop_v` 还是按完整候选表算的 ⇒ `--push` 会把被踢掉那个网络的
# 铜删掉、又不给它重布,板子直接变差(净距校核照样全绿)。
globals()['NETS'] = tuple(keep)
keep_l, drop_l, keep_v, drop_v = split_old(pads, vias, lines)
vlines, vvias = virtual(pads, vias, lines, drop_l, drop_v)
print(u' 重算后要拆:%d 段 / %d 孔(%d 个网络)'
% (len(drop_l), len(drop_v), len(keep)))
del BLOCK[:]
routes = run_plan(pads, vvias, vlines, keep)
routes = chamfer(routes, pads, vvias, vlines)
msgs, _r = plane_report(routes, pads_raw, obs_board)
for m in msgs:
print(u' ⚠️ ' + m)
if msgs:
sys.exit(u'⛔ 重算之后仍有孤岛 —— 不下发')
summarize(routes)
bad = verify(routes, pads, vvias, vlines)
for b in bad[:20]:
print(u' ✗ ' + b)
if bad:
sys.exit(u'⛔ 重算之后校核不过 —— 不下发')
nets = keep
worse2 = []
for net, n, det in connectivity(routes, pads, vvias, vlines):
was = len(frags(net, pads, vias, lines))
print(u' 连通性 %s:%d 个碎片 %s%s'
% (net, n, det if n > 1 else u'✅',
u'' if n <= was else u' ⛔ 仍然变差'))
if n > was and net in keep:
worse2.append(net)
if worse2:
sys.exit(u'⛔ %s 重算之后仍然变差 —— 不下发' % ' / '.join(worse2))
if not push:
print(u'\n(只算不下发。要下发加 --push)')
return
+200 -191
查看文件
@@ -1,191 +1,200 @@
# -*- coding: utf-8 -*-
u"""下板 PCB1「从原理图导入变更」。
用法:
python .claude/pcb1_import.py --bom 逐页重抓原理图 BOM,存 _p1_sch_bom.json(对账的基准)
python .claude/pcb1_import.py --probe 只开对话框、把全部变更行读出来存 _p1_import_rows.json
python .claude/pcb1_import.py --apply 读完之后真的点「应用修改」(先自动取消勾选「包含设计规则」)
python .claude/pcb1_import.py --verify 拿 PCB 上的元件表和 _p1_sch_bom.json 对账
⚠️ **改过原理图就要先 `--bom` 再 `--verify`**,否则是拿新 PCB 和旧原理图对账 ——
刚改的那几个位号会被报成「PCB 有原理图没有」,而真正的错混在里面看不出来。
(2026-09-06 之前 `_p1_sch_bom.json` 在仓库里根本没有生产者,只能手工维护。)
⚠️ skill §9o:整页重画过的页(P5-BTB / P6-FAN-EDL)元件 UUID 全是新的,
EasyEDA 会把 PCB 上**将被删掉的**元件复用成新元件(改封装 + 改位号),
所以导入后必须跑 --verify 核对「位号 ↔ 封装」是不是和原理图一致。
"""
import io, json, os, re, sys, time
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
from eda import run
PCB = '7e9415a16a909c51'
HELPERS = 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 p=propsOf(t); if(p&&typeof p.onClick==='function'){p.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
const leaf = (txt) => { for (const el of document.querySelectorAll('*')) if (el.children.length===0 && (el.textContent||'').trim()===txt && el.offsetParent!==null) return el; return null; };
"""
JS_PROBE = HELPERS + u"""
await eda.dmt_EditorControl.openDocument('%s');
await sleep(1800);
const out = {};
fire(leaf('设计')); await sleep(900);
out.opened = fire(leaf('从原理图导入变更'));
await sleep(9000);
let sc = null;
for (const el of document.querySelectorAll('div'))
if (el.scrollHeight > el.clientHeight + 20 && /移除元件|增加元件|修改/.test(el.innerText || ''))
if (!sc || el.innerText.length < sc.innerText.length) sc = el;
const seq = [];
if (sc) {
sc.scrollTop = 0; await sleep(400);
let last = -1;
for (let i = 0; i < 200; i++) {
seq.push({top: sc.scrollTop, txt: sc.innerText});
if (sc.scrollTop === last) break;
last = sc.scrollTop;
if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) break;
sc.scrollTop += Math.max(40, sc.clientHeight / 2);
await sleep(260);
}
}
out.seq = seq;
out.buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null).map(b => (b.innerText||'').trim()).filter(Boolean);
return out;
""" % PCB
JS_APPLY = HELPERS + u"""
const out = {};
let ruleBox = null, ruleTxt = null;
for (const el of document.querySelectorAll('*')) {
if (el.children.length === 0 && /设计规则/.test((el.textContent||'').trim()) && el.offsetParent !== null) {
ruleTxt = (el.textContent||'').trim();
let r = el;
for (let i = 0; i < 6 && r; i++) { const cb = r.querySelector && r.querySelector('input[type=checkbox]'); if (cb) { ruleBox = cb; break; } r = r.parentElement; }
break;
}
}
out.ruleTxt = ruleTxt; out.ruleChecked = ruleBox ? ruleBox.checked : null;
if (ruleBox && ruleBox.checked) { nat(ruleBox); await sleep(500); out.ruleAfter = ruleBox.checked; }
const btn = [...document.querySelectorAll('button')].find(b => b.offsetParent !== null && (b.innerText||'').trim() === '应用修改');
if (!btn) return Object.assign(out, {err: 'no 应用修改'});
nat(btn);
await sleep(15000);
for (const b of document.querySelectorAll('button')) if (b.offsetParent !== null && /^(确定|确认|是)$/.test((b.innerText||'').trim())) { nat(b); await sleep(4000); out.confirmed = (b.innerText||'').trim(); }
await sleep(4000);
const comps = await eda.pcb_PrimitiveComponent.getAll();
out.n = comps.length;
out.buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null).map(b => (b.innerText||'').trim()).filter(Boolean).slice(0,20);
return out;
"""
JS_DUMP = u"""
// ⚠️ skill §9l:每段会读/改文档的 JS 都要自己 openDocument。
// 少了这一行,紧跟在 --bom(最后停在某张原理图上)之后跑 --verify,
// `pcb_PrimitiveComponent.getAll()` 在原理图文档上直接抛
// `Cannot read properties of null (reading 'map')` —— 报错信息完全指不到根因。
await eda.dmt_EditorControl.openDocument('7e9415a16a909c51');
await new Promise(r => setTimeout(r, 1800));
const cs = await eda.pcb_PrimitiveComponent.getAll();
const out = [];
for (const c of cs) out.push({d: c.designator, x: c.x, y: c.y, rot: c.rotation, lay: c.layer,
fp: (c.footprint && c.footprint.name) ? c.footprint.name : (c.footprint || null)});
return out;
"""
JS_BOM = u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
await eda.dmt_EditorControl.openDocument('%s');
await sleep(1300);
const cur = String((await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name || '');
if (cur.toUpperCase() !== '%s') return {err: 'openDocument 打到了 ' + cur};
const list = [];
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
if (c.getState_ComponentType() !== 'part') continue;
let fp = null;
try { const f = c.getState_Footprint(); if (f) fp = {name: String(f.name || f)}; } catch (e) {}
list.push({d: String(c.getState_Designator ? c.getState_Designator() : ''),
uuid: c.getState_PrimitiveId(), fp: fp});
}
return {list: list};
"""
def refresh_bom():
u"""逐页重抓原理图 BOM。
⚠️ **一页一个请求**,不要一次抓 11 页 —— `eda.py` 把返回值截到 20000 字符再打印,
整本 BOM 装不下,截断之后 `json.load` 直接炸在中间(2026-09-06 踩过)。
"""
pages = json.load(io.open(os.path.join(HERE, 'eda-parts.json'), encoding='utf-8'))['pages']
bom, bad = {}, []
for name, uuid in pages.items():
r = run(JS_BOM % (uuid, name.upper()), timeout=300)
if not r.get('success'):
bad.append(u'%s: %s' % (name, json.dumps(r, ensure_ascii=False)[:160]))
continue
res = r['result']
if res.get('err'):
bad.append(u'%s: %s' % (name, res['err']))
continue
bom[name] = res['list']
print(u' %-12s %3d 件' % (name, len(res['list'])))
if bad:
print(u'❌ 这几页没抓到,**没有写缓存**:')
for b in bad:
print(u' · ' + b)
sys.exit(1)
io.open(os.path.join(HERE, '_p1_sch_bom.json'), 'w', encoding='utf-8').write(
json.dumps(bom, ensure_ascii=False))
print(u'已写 _p1_sch_bom.json:%d 页 / %d 件' % (len(bom), sum(len(v) for v in bom.values())))
def dump():
r = run(JS_DUMP, timeout=300)
if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:600])
return r['result']
def verify():
cs = dump()
io.open(os.path.join(HERE, '_pcb1_comps.json'), 'w', encoding='utf-8').write(json.dumps(cs, ensure_ascii=False))
bom = json.load(io.open(os.path.join(HERE, '_p1_sch_bom.json'), encoding='utf-8'))
sch = {}
for pg, v in bom.items():
for c in v:
d = c.get('d')
if d and re.match(r'^[A-Za-z]+\d', d):
sch[d] = ((c.get('fp') or {}).get('name'), pg)
pcb = {c['d']: c for c in cs}
miss = sorted(set(sch) - set(pcb)); extra = sorted(set(pcb) - set(sch))
bad = [(d, pcb[d]['fp'], sch[d][0]) for d in sorted(set(sch) & set(pcb)) if pcb[d]['fp'] != sch[d][0]]
print(u'PCB %d 件 / 原理图 %d 件' % (len(cs), len(sch)))
print(u'原理图有 PCB 没有 %d: %s' % (len(miss), miss[:40]))
print(u'PCB 有原理图没有 %d: %s' % (len(extra), extra[:40]))
print(u'封装对不上 %d:' % len(bad))
for t in bad[:40]: print(u' %s PCB=%s SCH=%s' % t)
return not (miss or extra or bad)
if __name__ == '__main__':
a = sys.argv[1] if len(sys.argv) > 1 else '--verify'
if a == '--bom':
refresh_bom()
elif a == '--probe':
r = run(JS_PROBE, timeout=400)
if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:800])
res = r['result']
io.open(os.path.join(HERE, '_p1_import_rows.json'), 'w', encoding='utf-8').write(json.dumps(res, ensure_ascii=False, indent=1))
print(u'opened=%s 快照 %d 屏' % (res.get('opened'), len(res.get('seq') or [])))
print(res.get('buttons'))
elif a == '--apply':
r = run(JS_APPLY, timeout=400)
print(json.dumps(r, ensure_ascii=False)[:1200])
elif a == '--verify':
sys.exit(0 if verify() else 1)
else:
sys.exit(__doc__)
# -*- coding: utf-8 -*-
u"""下板 PCB1「从原理图导入变更」。
用法:
python .claude/pcb1_import.py --bom 逐页重抓原理图 BOM,存 _p1_sch_bom.json(对账的基准)
python .claude/pcb1_import.py --probe 只开对话框、把全部变更行读出来存 _p1_import_rows.json
python .claude/pcb1_import.py --apply 读完之后真的点「应用修改」(先自动取消勾选「包含设计规则」)
python .claude/pcb1_import.py --verify 拿 PCB 上的元件表和 _p1_sch_bom.json 对账
⚠️ **改过原理图就要先 `--bom` 再 `--verify`**,否则是拿新 PCB 和旧原理图对账 ——
刚改的那几个位号会被报成「PCB 有原理图没有」,而真正的错混在里面看不出来。
(2026-09-06 之前 `_p1_sch_bom.json` 在仓库里根本没有生产者,只能手工维护。)
⚠️ skill §9o:整页重画过的页(P5-BTB / P6-FAN-EDL)元件 UUID 全是新的,
EasyEDA 会把 PCB 上**将被删掉的**元件复用成新元件(改封装 + 改位号),
所以导入后必须跑 --verify 核对「位号 ↔ 封装」是不是和原理图一致。
"""
import io, json, os, re, sys, time
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
from eda import run
PCB = '7e9415a16a909c51'
# 2026-09-06:加 `--board2` 就对载板 PCB2 做同一套事(BOM / 对话框 / 对账),产出文件带 _p2_ 前缀
BOARD2 = '--board2' in sys.argv
if BOARD2:
PCB = '942ed1560d81c1e7'
sys.argv = [a for a in sys.argv if a != '--board2']
BOM_FILE = '_p2_sch_bom.json' if BOARD2 else '_p1_sch_bom.json'
COMPS_FILE = '_pcb2_comps.json' if BOARD2 else '_pcb1_comps.json'
ROWS_FILE = '_p2_import_rows.json' if BOARD2 else '_p1_import_rows.json'
HELPERS = 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 p=propsOf(t); if(p&&typeof p.onClick==='function'){p.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
const leaf = (txt) => { for (const el of document.querySelectorAll('*')) if (el.children.length===0 && (el.textContent||'').trim()===txt && el.offsetParent!==null) return el; return null; };
"""
JS_PROBE = HELPERS + u"""
await eda.dmt_EditorControl.openDocument('%s');
await sleep(1800);
const out = {};
fire(leaf('设计')); await sleep(900);
out.opened = fire(leaf('从原理图导入变更'));
await sleep(9000);
let sc = null;
for (const el of document.querySelectorAll('div'))
if (el.scrollHeight > el.clientHeight + 20 && /移除元件|增加元件|修改/.test(el.innerText || ''))
if (!sc || el.innerText.length < sc.innerText.length) sc = el;
const seq = [];
if (sc) {
sc.scrollTop = 0; await sleep(400);
let last = -1;
for (let i = 0; i < 200; i++) {
seq.push({top: sc.scrollTop, txt: sc.innerText});
if (sc.scrollTop === last) break;
last = sc.scrollTop;
if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) break;
sc.scrollTop += Math.max(40, sc.clientHeight / 2);
await sleep(260);
}
}
out.seq = seq;
out.buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null).map(b => (b.innerText||'').trim()).filter(Boolean);
return out;
""" % PCB
JS_APPLY = HELPERS + u"""
const out = {};
let ruleBox = null, ruleTxt = null;
for (const el of document.querySelectorAll('*')) {
if (el.children.length === 0 && /设计规则/.test((el.textContent||'').trim()) && el.offsetParent !== null) {
ruleTxt = (el.textContent||'').trim();
let r = el;
for (let i = 0; i < 6 && r; i++) { const cb = r.querySelector && r.querySelector('input[type=checkbox]'); if (cb) { ruleBox = cb; break; } r = r.parentElement; }
break;
}
}
out.ruleTxt = ruleTxt; out.ruleChecked = ruleBox ? ruleBox.checked : null;
if (ruleBox && ruleBox.checked) { nat(ruleBox); await sleep(500); out.ruleAfter = ruleBox.checked; }
const btn = [...document.querySelectorAll('button')].find(b => b.offsetParent !== null && (b.innerText||'').trim() === '应用修改');
if (!btn) return Object.assign(out, {err: 'no 应用修改'});
nat(btn);
await sleep(15000);
for (const b of document.querySelectorAll('button')) if (b.offsetParent !== null && /^(确定|确认|是)$/.test((b.innerText||'').trim())) { nat(b); await sleep(4000); out.confirmed = (b.innerText||'').trim(); }
await sleep(4000);
const comps = await eda.pcb_PrimitiveComponent.getAll();
out.n = comps.length;
out.buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null).map(b => (b.innerText||'').trim()).filter(Boolean).slice(0,20);
return out;
"""
JS_DUMP = u"""
// ⚠️ skill §9l:每段会读/改文档的 JS 都要自己 openDocument。
// 少了这一行,紧跟在 --bom(最后停在某张原理图上)之后跑 --verify,
// `pcb_PrimitiveComponent.getAll()` 在原理图文档上直接抛
// `Cannot read properties of null (reading 'map')` —— 报错信息完全指不到根因。
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1800));
const cs = await eda.pcb_PrimitiveComponent.getAll();
const out = [];
for (const c of cs) out.push({d: c.designator, x: c.x, y: c.y, rot: c.rotation, lay: c.layer,
fp: (c.footprint && c.footprint.name) ? c.footprint.name : (c.footprint || null)});
return out;
""" % PCB
JS_BOM = u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
await eda.dmt_EditorControl.openDocument('%s');
await sleep(1300);
const cur = String((await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name || '');
if (cur.toUpperCase() !== '%s') return {err: 'openDocument 打到了 ' + cur};
const list = [];
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
if (c.getState_ComponentType() !== 'part') continue;
let fp = null;
try { const f = c.getState_Footprint(); if (f) fp = {name: String(f.name || f)}; } catch (e) {}
list.push({d: String(c.getState_Designator ? c.getState_Designator() : ''),
uuid: c.getState_PrimitiveId(), fp: fp});
}
return {list: list};
"""
def refresh_bom():
u"""逐页重抓原理图 BOM。
⚠️ **一页一个请求**,不要一次抓 11 页 —— `eda.py` 把返回值截到 20000 字符再打印,
整本 BOM 装不下,截断之后 `json.load` 直接炸在中间(2026-09-06 踩过)。
"""
ep = json.load(io.open(os.path.join(HERE, 'eda-parts.json'), encoding='utf-8'))
pages = ep['schematic_boards']['Board2']['pages'] if BOARD2 else ep['pages']
bom, bad = {}, []
for name, uuid in pages.items():
r = run(JS_BOM % (uuid, name.upper()), timeout=300)
if not r.get('success'):
bad.append(u'%s: %s' % (name, json.dumps(r, ensure_ascii=False)[:160]))
continue
res = r['result']
if res.get('err'):
bad.append(u'%s: %s' % (name, res['err']))
continue
bom[name] = res['list']
print(u' %-12s %3d 件' % (name, len(res['list'])))
if bad:
print(u'❌ 这几页没抓到,**没有写缓存**:')
for b in bad:
print(u' · ' + b)
sys.exit(1)
io.open(os.path.join(HERE, BOM_FILE), 'w', encoding='utf-8').write(
json.dumps(bom, ensure_ascii=False))
print(u'已写 ' + BOM_FILE + u':%d 页 / %d 件' % (len(bom), sum(len(v) for v in bom.values())))
def dump():
r = run(JS_DUMP, timeout=300)
if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:600])
return r['result']
def verify():
cs = dump()
io.open(os.path.join(HERE, COMPS_FILE), 'w', encoding='utf-8').write(json.dumps(cs, ensure_ascii=False))
bom = json.load(io.open(os.path.join(HERE, BOM_FILE), encoding='utf-8'))
sch = {}
for pg, v in bom.items():
for c in v:
d = c.get('d')
if d and re.match(r'^[A-Za-z]+\d', d):
sch[d] = ((c.get('fp') or {}).get('name'), pg)
pcb = {c['d']: c for c in cs}
miss = sorted(set(sch) - set(pcb)); extra = sorted(set(pcb) - set(sch))
bad = [(d, pcb[d]['fp'], sch[d][0]) for d in sorted(set(sch) & set(pcb)) if pcb[d]['fp'] != sch[d][0]]
print(u'PCB %d 件 / 原理图 %d 件' % (len(cs), len(sch)))
print(u'原理图有 PCB 没有 %d: %s' % (len(miss), miss[:40]))
print(u'PCB 有原理图没有 %d: %s' % (len(extra), extra[:40]))
print(u'封装对不上 %d:' % len(bad))
for t in bad[:40]: print(u' %s PCB=%s SCH=%s' % t)
return not (miss or extra or bad)
if __name__ == '__main__':
a = sys.argv[1] if len(sys.argv) > 1 else '--verify'
if a == '--bom':
refresh_bom()
elif a == '--probe':
r = run(JS_PROBE, timeout=400)
if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:800])
res = r['result']
io.open(os.path.join(HERE, ROWS_FILE), 'w', encoding='utf-8').write(json.dumps(res, ensure_ascii=False, indent=1))
print(u'opened=%s 快照 %d 屏' % (res.get('opened'), len(res.get('seq') or [])))
print(res.get('buttons'))
elif a == '--apply':
r = run(JS_APPLY, timeout=400)
print(json.dumps(r, ensure_ascii=False)[:1200])
elif a == '--verify':
sys.exit(0 if verify() else 1)
else:
sys.exit(__doc__)
+313 -307
查看文件
@@ -1,307 +1,313 @@
# -*- coding: utf-8 -*-
u"""下板 PCB1:按**实测铜**算全板每个网络的连通性,列出还欠哪些连接。
/c/Python314/python .claude/pcb1_netlist.py # 全板汇总
/c/Python314/python .claude/pcb1_netlist.py --net +5V # 只看这几个网络
/c/Python314/python .claude/pcb1_netlist.py --todo # 只列还没连通的,按「要拉几条」排序
/c/Python314/python .claude/pcb1_netlist.py --refresh # 重抓板上快照(改过板就要加)
/c/Python314/python .claude/pcb1_netlist.py --json out.json # 把待布线任务写成文件
## 为什么不看 DRC 的「连接性错误」
那一支是按**对象**列的(一个没连上的焊盘可能报好几条),还分页、还要 reload 才准,
数字(594)说明不了「还剩几条线要拉」。这里把焊盘 / 过孔 / 走线 / **实测铺铜**
当成图的节点,两两相触连边,数连通域 —— 一个网络 k 个连通域就是**还差 k−1 条线**。
## 铺铜怎么进这张图(关键)
铺铜不是一个矩形,它是一堆环、还带反焊盘的洞,而且**一个铺铜对象可能铺出好几块**。
所以照 `pcb1_pour.verify()` 的老办法:把同 (网络, 层) 的环光栅化成 0.25 mm 的布尔格、
连通域标号,**每一个连通域各算一个节点**。落脚判据和那里一致:
过孔 / 通孔焊盘落在格子里就算连上;贴片焊盘只在本层(Top/Bottom)算。
⚠️ 走线**不**算落进铺铜(异网铺铜会被挖开,同网铺铜和走线重叠才算),
这里按「走线中点/端点落在本网铺铜格子里」判,偏保守一点没关系 ——
保守的方向是「多报几条要拉的线」,不是「少报」。
"""
import io
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb1_pour as PO # noqa: E402
G = PO.G
EPS = 1e-6
LAYNAME = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}
# ══════════════════ 几何:点/段/矩形 ══════════════════
def seg_pt(x0, y0, x1, y1, px, py):
dx, dy = x1 - x0, y1 - y0
d2 = dx * dx + dy * dy
t = 0.0 if d2 < EPS else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / d2))
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
def seg_seg(a0, a1, b0, b1):
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
d1, d2 = cross(b0, b1, a0), cross(b0, b1, a1)
d3, d4 = cross(a0, a1, b0), cross(a0, a1, b1)
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
return 0.0
return min(seg_pt(a0[0], a0[1], a1[0], a1[1], b0[0], b0[1]),
seg_pt(a0[0], a0[1], a1[0], a1[1], b1[0], b1[1]),
seg_pt(b0[0], b0[1], b1[0], b1[1], a0[0], a0[1]),
seg_pt(b0[0], b0[1], b1[0], b1[1], a1[0], a1[1]))
def seg_rect(p0, p1, cx, cy, hw, hh):
u"""线段到轴对齐矩形的距离(重叠返回 0)。"""
corners = [(cx - hw, cy - hh), (cx + hw, cy - hh), (cx + hw, cy + hh), (cx - hw, cy + hh)]
for p in (p0, p1):
if abs(p[0] - cx) <= hw + EPS and abs(p[1] - cy) <= hh + EPS:
return 0.0
best = 1e9
for k in range(4):
best = min(best, seg_seg(p0, p1, corners[k], corners[(k + 1) % 4]))
return best
# ══════════════════ 读板 ══════════════════
def geom(refresh=False):
u"""返回 (pads, vias, lines),全部 mm。
pads: dict(net,x,y,hw,hh,th,ref,num,plane) plane = 元件所在层(1/2)
vias: dict(net,x,y,r)
lines: dict(net,lay,x0,y0,x1,y1,w)
"""
snap = PO.snapshot(refresh)
raw, _n, _nb = PO.load_pads(False)
pads = []
for net, x, y, hx, hy, th, tag, plane in raw:
ref, _, num = tag.rpartition('.')
pads.append(dict(net=net, x=x, y=y, hw=hx, hh=hy, th=th,
ref=ref, num=num, plane=plane, tag=tag))
vias = [dict(net=n or '', x=x, y=y, r=(od or 0.61) / 2.0)
for n, x, y, od in snap.get('via', ())]
lines = [dict(net=n or '', lay=lay, x0=x0, y0=y0, x1=x1, y1=y1, w=w or 0.2)
for n, lay, x0, y0, x1, y1, w in snap.get('line', ())]
return pads, vias, lines
def pour_grids(refresh=False):
u"""同 (网络, 层) 的实测铺铜 -> 一个 `Grid`(已经标好连通域)。"""
if refresh or not os.path.exists(PO.FILLS):
d = PO.call(PO.JS_FILLS, timeout=900)
io.open(PO.FILLS, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False))
else:
d = json.load(io.open(PO.FILLS, encoding='utf-8'))
rings = {}
for e in d['pours']:
rings.setdefault((e['net'], e['layer']), []).extend(e['rings'])
return {k: PO._grid_from_cells(k[0], k[1], PO._cells_from_rings(v))
for k, v in rings.items()}
def layers_of_pad(p):
return (1, 2, 15, 16) if p['th'] else (p['plane'],)
# ══════════════════ 连通性 ══════════════════
class DSU(object):
def __init__(self, n):
self.p = list(range(n))
def find(self, i):
while self.p[i] != i:
self.p[i] = self.p[self.p[i]]
i = self.p[i]
return i
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a != b:
self.p[a] = b
def analyze(net, pads, vias, lines, pgrids):
u"""返回 (items, groups)。items 是 (kind, layers:set, obj, label)。"""
items = []
for p in pads:
if p['net'] == net:
items.append(('pad', set(layers_of_pad(p)), p, p['tag']))
for v in vias:
if v['net'] == net:
items.append(('via', set((1, 2, 15, 16)), v, u'过孔(%.2f,%.2f)' % (v['x'], v['y'])))
for l in lines:
if l['net'] == net:
items.append(('line', set((l['lay'],)), l, u''))
# 铺铜:每个连通域一个节点
pour_nodes = []
for (pnet, lay), g in pgrids.items():
if pnet != net:
continue
for cid, npx in g.sizes.items():
pour_nodes.append((lay, g, cid))
items.append(('pour', set((lay,)), (lay, g, cid),
u'%s 铺铜 %.0fmm²' % (LAYNAME[lay], npx * G * G)))
n = len(items)
dsu = DSU(n)
def in_pour(node, x, y, hw, hh, lay):
u"""焊盘 / 过孔 / 线段的**外框**有没有碰到这一块铺铜。
⚠️ 不能只测中心那一格:EasyEDA 回读的环里,**同网通孔焊盘的位置是一个洞**
(铜直连时那个洞正好等于焊盘轮廓,焊盘自己把它填上)。按中心格判,
J22.2 / J22.3 / J24.1 这三个明明坐在整块 GND 平面里的通孔脚会被判成孤立
—— 2026-09-06 实测就是这么被骗过一次。
判据照 `pcb1_pour.Grid.hit()`:**外框覆盖到的格子里有一格属于这一块**。
"""
play, g, cid = node
if play != lay:
return False
i0, i1 = int((x - hw) / G), int((x + hw) / G) + 1
j0, j1 = int((y - hh) / G), int((y + hh) / G) + 1
for j in range(max(0, j0), min(g.ny, j1 + 1)):
for i in range(max(0, i0), min(g.nx, i1 + 1)):
if g.lab[j][i] == cid:
return True
return False
def touch(a, b):
ka, la, oa, _ = a
kb, lb, ob, _ = b
common = la & lb
if not common:
return False
if ka == 'pour' or kb == 'pour':
if ka != 'pour':
a, b = b, a
ka, la, oa, _x = a
kb, lb, ob, _y = b
lay = oa[0]
if kb == 'pad':
# 贴片焊盘只在自己那一层算落脚;通孔焊盘哪一层都算
if not (ob['th'] or ob['plane'] == lay):
return False
return in_pour(oa, ob['x'], ob['y'], ob['hw'], ob['hh'], lay)
if kb == 'via':
return in_pour(oa, ob['x'], ob['y'], ob['r'], ob['r'], lay)
if kb == 'line':
if ob['lay'] != lay:
return False
n_ = max(2, int(math.hypot(ob['x1'] - ob['x0'], ob['y1'] - ob['y0']) / 0.2) + 1)
h_ = ob['w'] / 2.0
for s in range(n_ + 1):
t = s / float(n_)
if in_pour(oa, ob['x0'] + t * (ob['x1'] - ob['x0']),
ob['y0'] + t * (ob['y1'] - ob['y0']), h_, h_, lay):
return True
return False
return False # pour-pour:同层不同连通域,本来就不通
if ka == 'line' and kb == 'line':
return seg_seg((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
(ob['x0'], ob['y0']), (ob['x1'], ob['y1'])) \
<= (oa['w'] + ob['w']) / 2 + EPS
if ka == 'line' and kb == 'pad':
return seg_rect((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['w'] / 2 + EPS
if ka == 'line' and kb == 'via':
return seg_pt(oa['x0'], oa['y0'], oa['x1'], oa['y1'],
ob['x'], ob['y']) <= oa['w'] / 2 + ob['r'] + EPS
if ka == 'via' and kb == 'via':
return math.hypot(oa['x'] - ob['x'], oa['y'] - ob['y']) <= oa['r'] + ob['r'] + EPS
if ka == 'via' and kb == 'pad':
return abs(oa['x'] - ob['x']) <= ob['hw'] + oa['r'] + EPS \
and abs(oa['y'] - ob['y']) <= ob['hh'] + oa['r'] + EPS
if ka == 'pad' and kb == 'pad':
return (abs(oa['x'] - ob['x']) <= oa['hw'] + ob['hw'] + EPS
and abs(oa['y'] - ob['y']) <= oa['hh'] + ob['hh'] + EPS)
return touch(b, a)
for i in range(n):
for j in range(i + 1, n):
if touch(items[i], items[j]):
dsu.union(i, j)
groups = {}
for i in range(n):
groups.setdefault(dsu.find(i), []).append(i)
return items, list(groups.values())
def main():
argv = sys.argv[1:]
refresh = '--refresh' in argv
todo_only = '--todo' in argv
want = []
if '--net' in argv:
want = [a for a in argv[argv.index('--net') + 1:] if not a.startswith('--')]
outjson = None
if '--json' in argv:
outjson = argv[argv.index('--json') + 1]
pads, vias, lines = geom(refresh)
pgrids = pour_grids(refresh)
print(u'板上:%d 焊盘 / %d 过孔 / %d 走线 / %d 组铺铜'
% (len(pads), len(vias), len(lines), len(pgrids)))
nets = {}
for p in pads:
if p['net']:
nets.setdefault(p['net'], 0)
nets[p['net']] += 1
todo, done, single = [], 0, 0
tasks = []
for net in sorted(nets):
if want and net not in want:
continue
if nets[net] < 2:
single += 1
continue
items, groups = analyze(net, pads, vias, lines, pgrids)
# 只含走线的碎片不算「要连的点」(它一定挂在别的东西上,否则是垃圾线)
real = [g for g in groups
if any(items[i][0] != 'line' for i in g)]
if len(real) <= 1:
done += 1
continue
frags = []
for g in sorted(real, key=lambda g: -len(g)):
labs = [items[i][3] for i in g if items[i][3]]
pts = []
for i in g:
k, _l, o, _lab = items[i]
if k == 'pad':
pts.append((o['x'], o['y'], sorted(layers_of_pad(o)), o['tag']))
elif k == 'via':
pts.append((o['x'], o['y'], [1, 2, 15, 16], 'via'))
frags.append(dict(n=len(g), labels=labs, pts=pts))
todo.append((len(real) - 1, net, nets[net], frags))
tasks.append(dict(net=net, npads=nets[net], need=len(real) - 1, frags=frags))
todo.sort(key=lambda t: (-t[0], t[1]))
print(u'\n---- 还没连通的网络:%d 个,共欠 %d 条连接 ----'
% (len(todo), sum(t[0] for t in todo)))
for need, net, npads, frags in todo:
print(u' %-14s %3d 焊盘 %2d 个碎片 -> 还差 %2d 条' % (net, npads, len(frags), need))
if not todo_only:
for f in frags[:8]:
labs = f['labels'][:10]
print(u' [%2d] %s%s' % (f['n'], u' '.join(labs),
u' …' if len(f['labels']) > 10 else u''))
print(u'\n已连通 %d 个网络 / 单焊盘网络 %d 个' % (done, single))
if outjson:
io.open(outjson, 'w', encoding='utf-8').write(
json.dumps(tasks, ensure_ascii=False, indent=1))
print(u'待布线任务已写到 %s' % outjson)
return 0
if __name__ == '__main__':
sys.exit(main())
# -*- coding: utf-8 -*-
u"""下板 PCB1:按**实测铜**算全板每个网络的连通性,列出还欠哪些连接。
/c/Python314/python .claude/pcb1_netlist.py # 全板汇总
/c/Python314/python .claude/pcb1_netlist.py --net +5V # 只看这几个网络
/c/Python314/python .claude/pcb1_netlist.py --todo # 只列还没连通的,按「要拉几条」排序
/c/Python314/python .claude/pcb1_netlist.py --refresh # 重抓板上快照(改过板就要加)
/c/Python314/python .claude/pcb1_netlist.py --json out.json # 把待布线任务写成文件
## 为什么不看 DRC 的「连接性错误」
那一支是按**对象**列的(一个没连上的焊盘可能报好几条),还分页、还要 reload 才准,
数字(594)说明不了「还剩几条线要拉」。这里把焊盘 / 过孔 / 走线 / **实测铺铜**
当成图的节点,两两相触连边,数连通域 —— 一个网络 k 个连通域就是**还差 k−1 条线**。
## 铺铜怎么进这张图(关键)
铺铜不是一个矩形,它是一堆环、还带反焊盘的洞,而且**一个铺铜对象可能铺出好几块**。
所以照 `pcb1_pour.verify()` 的老办法:把同 (网络, 层) 的环光栅化成 0.25 mm 的布尔格、
连通域标号,**每一个连通域各算一个节点**。落脚判据和那里一致:
过孔 / 通孔焊盘落在格子里就算连上;贴片焊盘只在本层(Top/Bottom)算。
⚠️ 走线**不**算落进铺铜(异网铺铜会被挖开,同网铺铜和走线重叠才算),
这里按「走线中点/端点落在本网铺铜格子里」判,偏保守一点没关系 ——
保守的方向是「多报几条要拉的线」,不是「少报」。
"""
import io
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb1_pour as PO # noqa: E402
G = PO.G
EPS = 1e-6
LAYNAME = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}
# ══════════════════ 几何:点/段/矩形 ══════════════════
def seg_pt(x0, y0, x1, y1, px, py):
dx, dy = x1 - x0, y1 - y0
d2 = dx * dx + dy * dy
t = 0.0 if d2 < EPS else max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / d2))
return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy))
def seg_seg(a0, a1, b0, b1):
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
d1, d2 = cross(b0, b1, a0), cross(b0, b1, a1)
d3, d4 = cross(a0, a1, b0), cross(a0, a1, b1)
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
return 0.0
return min(seg_pt(a0[0], a0[1], a1[0], a1[1], b0[0], b0[1]),
seg_pt(a0[0], a0[1], a1[0], a1[1], b1[0], b1[1]),
seg_pt(b0[0], b0[1], b1[0], b1[1], a0[0], a0[1]),
seg_pt(b0[0], b0[1], b1[0], b1[1], a1[0], a1[1]))
def seg_rect(p0, p1, cx, cy, hw, hh):
u"""线段到轴对齐矩形的距离(重叠返回 0)。"""
corners = [(cx - hw, cy - hh), (cx + hw, cy - hh), (cx + hw, cy + hh), (cx - hw, cy + hh)]
for p in (p0, p1):
if abs(p[0] - cx) <= hw + EPS and abs(p[1] - cy) <= hh + EPS:
return 0.0
best = 1e9
for k in range(4):
best = min(best, seg_seg(p0, p1, corners[k], corners[(k + 1) % 4]))
return best
# ══════════════════ 读板 ══════════════════
def geom(refresh=False):
u"""返回 (pads, vias, lines),全部 mm。
pads: dict(net,x,y,hw,hh,th,ref,num,plane) plane = 元件所在层(1/2)
vias: dict(net,x,y,r)
lines: dict(net,lay,x0,y0,x1,y1,w)
"""
snap = PO.snapshot(refresh)
raw, _n, _nb = PO.load_pads(False)
pads = []
for net, x, y, hx, hy, th, tag, plane in raw:
ref, _, num = tag.rpartition('.')
pads.append(dict(net=net, x=x, y=y, hw=hx, hh=hy, th=th,
ref=ref, num=num, plane=plane, tag=tag))
vias = [dict(net=n or '', x=x, y=y, r=(od or 0.61) / 2.0)
for n, x, y, od in snap.get('via', ())]
lines = [dict(net=n or '', lay=lay, x0=x0, y0=y0, x1=x1, y1=y1, w=w or 0.2)
for n, lay, x0, y0, x1, y1, w in snap.get('line', ())]
return pads, vias, lines
def pour_grids(refresh=False):
u"""同 (网络, 层) 的实测铺铜 -> 一个 `Grid`(已经标好连通域)。"""
if refresh or not os.path.exists(PO.FILLS):
d = PO.call(PO.JS_FILLS, timeout=900)
io.open(PO.FILLS, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False))
else:
d = json.load(io.open(PO.FILLS, encoding='utf-8'))
rings = {}
for e in d['pours']:
rings.setdefault((e['net'], e['layer']), []).extend(e['rings'])
return {k: PO._grid_from_cells(k[0], k[1], PO._cells_from_rings(v))
for k, v in rings.items()}
def layers_of_pad(p):
return (1, 2, 15, 16) if p['th'] else (p['plane'],)
# ══════════════════ 连通性 ══════════════════
class DSU(object):
def __init__(self, n):
self.p = list(range(n))
def find(self, i):
while self.p[i] != i:
self.p[i] = self.p[self.p[i]]
i = self.p[i]
return i
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a != b:
self.p[a] = b
def analyze(net, pads, vias, lines, pgrids):
u"""返回 (items, groups)。items 是 (kind, layers:set, obj, label)。"""
items = []
for p in pads:
if p['net'] == net:
items.append(('pad', set(layers_of_pad(p)), p, p['tag']))
for v in vias:
if v['net'] == net:
items.append(('via', set((1, 2, 15, 16)), v, u'过孔(%.2f,%.2f)' % (v['x'], v['y'])))
for l in lines:
if l['net'] == net:
items.append(('line', set((l['lay'],)), l, u''))
# 铺铜:每个连通域一个节点
pour_nodes = []
for (pnet, lay), g in pgrids.items():
if pnet != net:
continue
for cid, npx in g.sizes.items():
pour_nodes.append((lay, g, cid))
items.append(('pour', set((lay,)), (lay, g, cid),
u'%s 铺铜 %.0fmm²' % (LAYNAME[lay], npx * G * G)))
n = len(items)
dsu = DSU(n)
def in_pour(node, x, y, hw, hh, lay):
u"""焊盘 / 过孔 / 线段的**外框**有没有碰到这一块铺铜。
⚠️ 不能只测中心那一格:EasyEDA 回读的环里,**同网通孔焊盘的位置是一个洞**
(铜直连时那个洞正好等于焊盘轮廓,焊盘自己把它填上)。按中心格判,
J22.2 / J22.3 / J24.1 这三个明明坐在整块 GND 平面里的通孔脚会被判成孤立
—— 2026-09-06 实测就是这么被骗过一次。
判据照 `pcb1_pour.Grid.hit()`:**外框覆盖到的格子里有一格属于这一块**。
"""
play, g, cid = node
if play != lay:
return False
# ⚠️ 必须走 pcb1_pour 的 gi/gj —— 板框左下角是 (−30, −5),不是 (0, 0)。
# 2026-09-08 踩过:pcb1_pour 把 BW/BH 从假板框 160×125 改成真板框 220×135
# 并加了 BX0/BY0 之后,这里还在 `int(x / G)`,坐标整体偏了 30 mm ——
# 表现是「靠平面连通的网络(+12V/GND/+3V3/VOn_P)忽然全部碎成几十片」,
# 而 pcb1_pour --verify 回读的实测铺铜完全健康。**两边对不上时信实测那一边。**
# 更贵的是 pcb1_orphan_cu 走的就是这套栅格,据此误删过 12 个好过孔。
i0, i1 = PO.gi(x - hw), PO.gi(x + hw) + 1
j0, j1 = PO.gj(y - hh), PO.gj(y + hh) + 1
for j in range(max(0, j0), min(g.ny, j1 + 1)):
for i in range(max(0, i0), min(g.nx, i1 + 1)):
if g.lab[j][i] == cid:
return True
return False
def touch(a, b):
ka, la, oa, _ = a
kb, lb, ob, _ = b
common = la & lb
if not common:
return False
if ka == 'pour' or kb == 'pour':
if ka != 'pour':
a, b = b, a
ka, la, oa, _x = a
kb, lb, ob, _y = b
lay = oa[0]
if kb == 'pad':
# 贴片焊盘只在自己那一层算落脚;通孔焊盘哪一层都算
if not (ob['th'] or ob['plane'] == lay):
return False
return in_pour(oa, ob['x'], ob['y'], ob['hw'], ob['hh'], lay)
if kb == 'via':
return in_pour(oa, ob['x'], ob['y'], ob['r'], ob['r'], lay)
if kb == 'line':
if ob['lay'] != lay:
return False
n_ = max(2, int(math.hypot(ob['x1'] - ob['x0'], ob['y1'] - ob['y0']) / 0.2) + 1)
h_ = ob['w'] / 2.0
for s in range(n_ + 1):
t = s / float(n_)
if in_pour(oa, ob['x0'] + t * (ob['x1'] - ob['x0']),
ob['y0'] + t * (ob['y1'] - ob['y0']), h_, h_, lay):
return True
return False
return False # pour-pour:同层不同连通域,本来就不通
if ka == 'line' and kb == 'line':
return seg_seg((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
(ob['x0'], ob['y0']), (ob['x1'], ob['y1'])) \
<= (oa['w'] + ob['w']) / 2 + EPS
if ka == 'line' and kb == 'pad':
return seg_rect((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['w'] / 2 + EPS
if ka == 'line' and kb == 'via':
return seg_pt(oa['x0'], oa['y0'], oa['x1'], oa['y1'],
ob['x'], ob['y']) <= oa['w'] / 2 + ob['r'] + EPS
if ka == 'via' and kb == 'via':
return math.hypot(oa['x'] - ob['x'], oa['y'] - ob['y']) <= oa['r'] + ob['r'] + EPS
if ka == 'via' and kb == 'pad':
return abs(oa['x'] - ob['x']) <= ob['hw'] + oa['r'] + EPS \
and abs(oa['y'] - ob['y']) <= ob['hh'] + oa['r'] + EPS
if ka == 'pad' and kb == 'pad':
return (abs(oa['x'] - ob['x']) <= oa['hw'] + ob['hw'] + EPS
and abs(oa['y'] - ob['y']) <= oa['hh'] + ob['hh'] + EPS)
return touch(b, a)
for i in range(n):
for j in range(i + 1, n):
if touch(items[i], items[j]):
dsu.union(i, j)
groups = {}
for i in range(n):
groups.setdefault(dsu.find(i), []).append(i)
return items, list(groups.values())
def main():
argv = sys.argv[1:]
refresh = '--refresh' in argv
todo_only = '--todo' in argv
want = []
if '--net' in argv:
want = [a for a in argv[argv.index('--net') + 1:] if not a.startswith('--')]
outjson = None
if '--json' in argv:
outjson = argv[argv.index('--json') + 1]
pads, vias, lines = geom(refresh)
pgrids = pour_grids(refresh)
print(u'板上:%d 焊盘 / %d 过孔 / %d 走线 / %d 组铺铜'
% (len(pads), len(vias), len(lines), len(pgrids)))
nets = {}
for p in pads:
if p['net']:
nets.setdefault(p['net'], 0)
nets[p['net']] += 1
todo, done, single = [], 0, 0
tasks = []
for net in sorted(nets):
if want and net not in want:
continue
if nets[net] < 2:
single += 1
continue
items, groups = analyze(net, pads, vias, lines, pgrids)
# 只含走线的碎片不算「要连的点」(它一定挂在别的东西上,否则是垃圾线)
real = [g for g in groups
if any(items[i][0] != 'line' for i in g)]
if len(real) <= 1:
done += 1
continue
frags = []
for g in sorted(real, key=lambda g: -len(g)):
labs = [items[i][3] for i in g if items[i][3]]
pts = []
for i in g:
k, _l, o, _lab = items[i]
if k == 'pad':
pts.append((o['x'], o['y'], sorted(layers_of_pad(o)), o['tag']))
elif k == 'via':
pts.append((o['x'], o['y'], [1, 2, 15, 16], 'via'))
frags.append(dict(n=len(g), labels=labs, pts=pts))
todo.append((len(real) - 1, net, nets[net], frags))
tasks.append(dict(net=net, npads=nets[net], need=len(real) - 1, frags=frags))
todo.sort(key=lambda t: (-t[0], t[1]))
print(u'\n---- 还没连通的网络:%d 个,共欠 %d 条连接 ----'
% (len(todo), sum(t[0] for t in todo)))
for need, net, npads, frags in todo:
print(u' %-14s %3d 焊盘 %2d 个碎片 -> 还差 %2d 条' % (net, npads, len(frags), need))
if not todo_only:
for f in frags[:8]:
labs = f['labels'][:10]
print(u' [%2d] %s%s' % (f['n'], u' '.join(labs),
u' …' if len(f['labels']) > 10 else u''))
print(u'\n已连通 %d 个网络 / 单焊盘网络 %d 个' % (done, single))
if outjson:
io.open(outjson, 'w', encoding='utf-8').write(
json.dumps(tasks, ensure_ascii=False, indent=1))
print(u'待布线任务已写到 %s' % outjson)
return 0
if __name__ == '__main__':
sys.exit(main())
+165 -42
查看文件
@@ -43,9 +43,20 @@ from eda import run
PCB = '7e9415a16a909c51'
MIL = 39.3700787401575
BW, BH = 160.0, 125.0
# ⚠️⚠️ 2026-09-07 修正:这两个数从 2026-09-06「两板尺寸取大对齐」那一轮起就过期了 ——
# 板上实测的板框是 **220 x 135、左下角 (-30,-5)**(`pcb1_outline.py` 就是这么画的,
# 回读多段线 x -1181.1~7480.3 mil / y -196.9~5118.1 mil 逐条对上)。
# 旧值 (0,0,160,125) 是真板框的**子集**,所以越框检查一直是「偏严但不错」——
# 代价是**左右各 30 mm、上下各 6 mm 的板面从来没进过摆位视野**。
# 板边元件(风扇座、电源座、调试口)按旧值挑位置会挑到板子中间去。
BX0, BY0, BX1, BY1 = -30.0, -5.0, 190.0, 130.0 # 真板框,mm
BW, BH = BX1 - BX0, BY1 - BY0 # 220 x 135
CORNER_R = 5.0
MH = [(5.0, 5.0), (BW - 5.0, 5.0), (5.0, BH - 5.0), (BW - 5.0, BH - 5.0)]
# 2026-09-06 用户指令「两板尺寸取大对齐、四角定位孔对齐」:板框改成和载板一样的 220×135,
# **坐标原点不动**(下板坐标 = 载板坐标 − (30,5),pcb1_env.py 那条约定不变),板框画到 (−30,−5)~(190,130)。
# 定位孔 = 载板 `pcb2_place.MH` 减 (30,5):四角 + 两侧中点。载板中跨那两个 (46.6,74.5)/(118.1,74.5)
# 落在下板 CH2 功率级 / 布好的线上,不打。板框与孔由 `pcb_place.py --outline` 下发(同一张表)。
MH = [(-22.0, 3.0), (182.0, 3.0), (-26.0, 126.0), (186.0, 126.0), (-23.0, 55.0), (183.0, 40.0)]
MH_HOLE, MH_PAD, MH_KEEPOUT = 3.2, 6.0, 6.5
PARTS = {c['d']: c for c in json.load(io.open(os.path.join(HERE, 'pcb1-parts.json'), encoding='utf-8'))}
@@ -63,8 +74,18 @@ FP_BODY = {
# 上面这几条当年是拿放大截图量出来的,六条全部对得上(误差 ≤0.31)。
# ⇒ 新加板边连接器时先跑 `fp_body.py --scan <关键词>`,别再猜 ±4.5。
# ── 2026-09-06 换件:J1 → XT30PW-M,J26~J30 → 2510-3A 直针座 ──
'CONN-TH_XT30PW-M': (-6.65, -7.98, 6.65, 5.97), # 12V XT30 公头(90°板装)
'CONN-TH_2510-3A': (-3.81, -2.88, 3.81, 2.93), # 风扇 3P **直针**座
'CONN-TH_XT30PW-M': (-6.65, -7.98, 6.65, 5.97), # 12V XT30 公头(已换掉,见下一条)
# ── 2026-09-08 换件:J1 → 圆形 DC 电源座 DC-005-5.5*2.5(用户拍板,不降载)──
# ⚠️ 这个数不是量截图量出来的,是 `pcb_Primitive.getPrimitivesBBox([pid])` 直接读的
# (把件建在板外 (200,0) rot 0 读完再删)。**它含丝印/装配层,比塑壳本体大一圈** ——
# 同法读 XT30 得 14.85 x 13.55,而本体表里是 13.30 x 13.95,x 方向多 0.90。
# 偏大的方向是安全的(净距检查偏严),本体真值待打样时拿实物量(§12 待确认项)。
# 本体几乎全在 −X 侧、三个脚挤在 +X 端 ⇒ **桶口朝 −X**,朝西板边用 rot 0,不用转。
'DC-IN-TH_DC-005-2': (-10.68, -7.16, 3.92, 3.22), # 圆形 DC 座 5.5x2.5
'CONN-TH_2510-3A': (-3.81, -2.88, 3.81, 2.93), # 风扇 3P **直针**座(已换掉,见下一条)
# ── 2026-09-07 FAN-R2:五个 3 针风扇座 → 两个 4 针(一进一出,山洋 9G0612P4S001)──
'CONN-TH_4P-P2.54_CAX_2510-4A': (-5.08, -2.79, 5.08, 3.01), # 风扇 4P 直针座(fp_body --scan 读出)
'SOD-123_L2.7-W1.6-LS3.7-RD-1': (-1.93, -0.85, 1.93, 0.85), # DZ3 BZT52C8V2;本体比焊盘框宽 0.4
# 下面两条是换下来的,留着当对照(**别再用**):
'CONN-TH_P5.08_KF128-5.08-2P': (-5.20, -5.20, 5.20, 5.20), # 旧 12V 螺钉端子
# ⚠️ KF2510-3A 是**弯针**座:焊盘一排,塑壳整个往 +Y 伸 11.7 mm,开口在远端。
@@ -113,8 +134,13 @@ H_FP = {
# ⚠️⚠️ 换成立式**省的是板面、付的是高度**:直针座塑壳竖起来。
# `3D Model Transform` 只给 XY,**Z 读不出来** —— 下面两个数是按同系列
# 常规值取的保守估计,**打样前必须拿实物量**(§12 待确认项)。
'CONN-TH_XT30PW-M': 11.0, # XT30 90° 板装公头,本体躺着
'CONN-TH_2510-3A': 9.0, # 2510 直针座,塑壳竖立
'CONN-TH_XT30PW-M': 11.0, # XT30 90° 板装公头,本体躺着(已换掉)
'DC-IN-TH_DC-005-2': 11.0, # 圆形 DC 座:同系列常规值,**打样前拿实物量**
# 放在西条(x −29~−1)上,那一条 hmax = 99(载板底面没东西),所以这个数目前不吃紧
'CONN-TH_2510-3A': 9.0, # 2510 直针座,塑壳竖立(3P,已换掉)
'CONN-TH_4P-P2.54_CAX_2510-4A': 11.5, # 2510 直针 4P:厂家属性 Z-Height of the Board = 11.5
'SOT-23_L2.9-W1.3-P1.90-LS2.4-BR': 1.2, # Q7 AO3401A(和另一个 SOT-23-3 同高,封装名不同)
'SOD-123_L2.7-W1.6-LS3.7-RD-1': 1.2, # DZ3 BZT52C8V2
# 排针本身就是对插件,不参与高度核算
'HDR-TH_40P-P2.54_AZ-PZ2.54-V11-2X20P': None,
}
@@ -238,12 +264,7 @@ CH1 = [
# 放电联锁
('Q%01', 13.8, 35.5, 0), ('R%18', 16.4, 35.5, 0),
('Q%02', 13.8, 39.5, 0), ('R%19', 17.8, 40.0, 90),
# 按键驱动(远离 SW)
('Q%03', 24.0, 12.0, 0), ('Q%04', 28.0, 12.0, 0),
('Q%05', 24.0, 16.0, 0), ('R%20', 28.0, 16.0, 0),
('R%21', 24.0, 19.5, 0), ('R%22', 28.0, 19.5, 0),
('R%23', 24.0, 22.5, 0), ('R%24', 28.0, 22.5, 0),
('R%25', 24.0, 25.5, 0),
# 按键驱动 Q%03~05 / R%20~25:2026-09-06 随 74HC595 整窝挪到载板(§26.23),这一片空出来了
]
@@ -294,7 +315,20 @@ put('J25', 128.0, 62.5, 0)
# **原来 F1 那个位置 (14.5,71.5) 正好被吃掉**。
# 而东边的墙是硬的:北排 CH2 从 **x 29.6** 起,D1(SMB) 再往东就撞上去。
# ⇒ J1 东边只剩 15 mm 给 F1/Q1/D1/DZ1/C1/R1 六件,一行排不下(要 15+ mm),改**两行**。
put('J1', 8.5, 72.0, 270) # 12V XT30 公头,插口朝左板边(rot 270)
# 2026-09-08 ⛔ **硬缺陷修复**:J1 原来在 x=8.5(本体 x 0.5~14.7)——
# 那是按**过期的假板框 160×125**挑的“西板边”。真板框西边在 x=−30,
# 于是这个 **90° 板装** XT30 的插口缩在板内 30.5 mm,上方 13 mm 处还压着载板
# —— **插头根本插不进去**。不是优化问题,是板子能不能用的问题。
# → 整个输入首级(J1/F1/Q1/D1 + Q1 的栅极卫星 DZ1/C1/R1)西移 30 mm。
# 西条 x −29~−1 整条 hmax = 99(载板底面没东西),高件随便放。
# 代价:加权 HPWL +1~3%(源头离负载远了)—— 这是板子可装配的必要代价。
# 2026-09-08 换成圆形 DC 电源座(用户拍板:DC-005 5.5x2.5,**不降载**,接受插座超额定)。
# ⚠️ 整机 12V 最坏 9.99 A(§22.6),而 DC-005 家族额定 2~5 A —— 这是全板唯一一处
# 明知超额定还要装的地方,发热在**插头内芯与插座弹片的接触面**,板上加铜救不了。
# 板端能做的只有一件:pin2 / pin3 两个负极触点**都接地并联**,接触电阻减半(见 p1_dcjack.py)。
# 桶口朝 −X ⇒ **rot 0**(XT30 那颗要 rot 270,别照抄)。本体 x −10.68~+3.92 相对中心,
# 放 x=−18.8 时本体 x −29.48~−14.88,离真西板边 (−30) 0.52 mm,和原来 XT30 的 0.5 一致。
put('J1', -18.8, 72.0, 0) # 12V 圆形 DC 座,桶口朝**真**西板边(本体 x −29.5~−14.9)
# ⚠⚠ **v11 的两行排法把 8 A 链卡死了**(2026-09-06 第二十三轮重排)。
# XT30 rot 270 之后 **GND 在北针 (13.5,74.5)、VIN 在南针 (13.5,69.5)**,
# 而 F1 卵在 (17.5,74.5) 卧着放 ⇒ VIN_RAW 要从南针绕到 GND 盘东侧再北上,
@@ -304,12 +338,12 @@ put('J1', 8.5, 72.0, 270) # 12V XT30 公头,插口朝左板边(rot 270
# ② D1 挪到东边空列 ⇒ J1.2 → F1.1 一片 6.5 × 7.9 mm 的铜,不再有颈;
# ③ C4/C5/C6 一起东移 ⇒ 腾出 y 78.85~83.5 那条 4.65 mm 的 VIN_SW 西行干道
# (干道北提是因为 J1 的机械脚 J1.4 在 (3.5,77.5),占到 y 78.6)。
put('F1', 17.5, 74.5, 90) # 立着放:F1.1(VIN_RAW) 在南、F1.2(VIN_FUSED) 在北
put('Q1', 24.0, 74.5, 270) # SOP-8 P-MOS 防反接
put('D1', 31.0, 74.5, 270) # SMBJ13A TVS(挪出 VIN_RAW 通道,贴在漏极铜东缘)
put('DZ1', 22.0, 68.0, 0) # 栅极钳位
put('C1', 25.5, 68.0, 0)
put('R1', 28.0, 71.5, 0)
put('F1', -12.5, 74.5, 90) # 立着放:F1.1(VIN_RAW) 在南、F1.2(VIN_FUSED) 在北
put('Q1', -6.0, 74.5, 270) # SOP-8 P-MOS 防反接
put('D1', 1.0, 74.5, 270) # SMBJ13A TVS(挪出 VIN_RAW 通道,贴在漏极铜东缘)
put('DZ1', -8.0, 68.0, 0) # 栅极钳位
put('C1', -4.5, 68.0, 0)
put('R1', -2.0, 71.5, 0)
put('C2', 6.5, 84.5, 0) # 470µF/25V
put('C3', 16.0, 84.5, 0)
# C4/C5/C6:rot 180 让 $1N236 那一脚朝西迎着漏极铜,GND 脚朝东好打过孔
@@ -360,28 +394,73 @@ put('R707', 60.0, 71.5, 180)
put('L3', 70.5, 78.0, 0)
put('C734', 77.5, 78.0, 90)
put('C735', 80.0, 78.0, 90)
# 第三片 595(EDL_EN1~5)+ 风扇低边驱动
put('U33', 92.0, 71.0, 0)
# ── 风扇 FAN-R2(2026-09-07):两只 4 线 PWM 风扇,一进一出 ──────────────
# 旧的「低边切回流 + 5 个 3 针座 + TACH 分压」整块作废:R730~R732、J28~J30 已删。
# 现在是「高边默认关电 + 独立 PWM 开漏 + 两路独立测速接收」,见设计文档 §22.6。
# ⚠️ 摆位约束不只是「元件之间不重叠」:这一块北中区 y 64~100 底下压着已布好的
# +12V / +3V3 / GND 铜和过孔,而 `pcb1_route_sig --ripup` **只拆自己画过的信号线**,
# 拆不掉电源铜。第一版把新件摆进 y 66~88 直接刷出 65 条间距错误。
# ⇒ 新件整体上移到 **y 104~121 那条空带**(J28 删掉之后腾出来的),
# 留用件 Q6 / R733 / R734 原地不动(它们原来的位置本来就是干净的)。
# 【A】高边电源开关:+12V --Q7(P-MOS)--> FAN_12V_SW,MCU 高阻时默认断电
put('R746', 91.0, 120.0, 0) # 100k 栅极拉回源极
put('Q7', 95.0, 120.0, 0) # AO3401A
put('DZ3', 99.5, 120.0, 0) # BZT52C8V2 钳位负 Vgs(Q7 的 Vgs 绝对最大 ±12V)
put('C738', 105.0, 120.0, 0) # 22uF/25V,开关后母线本地储能
put('D2', 111.5, 120.0, 0) # SS34:K 接 FAN_12V_SW、A 接 GND(负向钳位,不再是续流)
put('R747', 91.0, 116.0, 0) # 1k 栅极串阻 -> Q8 漏极
put('Q8', 95.0, 116.0, 0) # AO3400A 拉低 Q7 栅极
put('R748', 99.5, 116.0, 0) # 1k FAN_EN(IO5) -> Q8 栅极
put('R749', 103.0, 116.0, 0) # 100k Q8 栅极下拉 = 复位默认断电
# 【B】两路独立测速接收(10k 上拉到 FAN_12V_SW -> 15k/10k 分压 -> N-MOS -> 3V3 上拉)
# 2026-09-08:**跟着各自的风扇座走到真板边**。
# 链路是 J2n.TACH → R75x(上拉到 FAN_12V_SW) → 分压 → Q(N-MOS) → FAN_TACH_xx(3V3 逻辑)。
# `FAN_IN_TACH_RAW` / `FAN_OUT_TACH_RAW` 是**座子到分压器**的本地网络:
# 接收电路留在中间的话,这两个网络要横跨大半块板(121 / 104 mm);
# 挪到座子旁边只剩几毫米。代价是 `FAN_TACH_IN/OUT`(已经是 3.3 V 逻辑、
# 经 N-MOS 整形过)各长约 16 mm —— 净赚一百多毫米,而且换掉的是**未整形的
# 12 V 摆幅裸信号**,抗扰上也是对的。
put('R750', -26.0, 108.0, 0) # 进风:贴着西板边的 J26
put('R751', -21.0, 108.0, 0)
put('Q9', -26.0, 104.0, 0)
put('R752', -21.0, 104.0, 0)
put('R753', -26.0, 100.0, 0)
put('R754', 181.0, 108.0, 0) # 排风:贴着东板边的 J27
put('R755', 186.0, 108.0, 0)
put('Q10', 181.0, 104.0, 0)
put('R756', 186.0, 104.0, 0)
put('R757', 181.0, 100.0, 0)
# 【C】PWM 低边开漏:Q6 / R733 / R734 保持 2026-09-06 的原位(现在只拉风扇 PWM 脚)
put('Q6', 102.0, 69.5, 0)
put('D2', 108.5, 69.5, 0)
put('C738', 102.0, 74.0, 0)
put('C739', 107.0, 74.0, 0)
put('R730', 98.0, 78.0, 0)
put('R731', 101.5, 78.0, 0)
put('R732', 105.0, 78.0, 0)
put('R733', 98.0, 80.5, 0)
put('R733', 98.0, 80.5, 0)
put('R734', 101.5, 80.5, 0)
# 风扇座:4 个侧吸 + 1 个顶排,全部放板边(§22.6)
put('J26', 5.0, 16.0, 0) # 左板边(南)
put('J27', 5.0, 30.0, 0)
put('J29', 154.9, 16.0, 0) # 右板边(南)
put('J30', 154.9, 30.0, 0)
put('J28', 100.0, 112.7, 0) # 顶板边一侧;弯针塑壳往 +Y 伸 11.7,壳顶顶到 y 124.4
# 风扇座:一进一出,分列**真**左右板边(顶排风孔取消,见 THERMAL_TWO_FAN)
# ⚠️ 4P 座比 3P 宽 2.54 ⇒ 半宽 3.81 → 5.08。本体 (-5.08,-2.79)-(5.08,3.01),rot 0。
#
# 2026-09-08 挪位:原来在 (6.3,121) / (151,121) —— 那是按**过期的假板框 160×125** 挑的
# 「两个上角」,在真板框 220×135 / 左下角 (−30,−5) 里其实都在**板子中间**,
# 风扇线缆没法横着出去(直针座是垂直插拔,板边的意义就在于让线缆走得掉)。
#
# 当年退到「上角」的理由是:4P 座塑壳 11.5 mm(厂家 Z-Height),而下板 hmax 由
# **载板底面元件**的投影压出来(pcb1-env.json zones),假板框上 hmax >= 11.5 的
# 板边只有 y 68~92 一段,还被输入链占满。**真板框下这个约束消失**:
# 左条 x −30~0 整条 hmax 无限制,右条 x 160~190 有 84% 无限制。
# 实扫(`_fanpos2.py`):西边 y 2~49 与 61~120、东边 y 2~34 与 67~120 都可用。
#
# ⚠️ 压降复核(原注释担心的那一条):`FAN_12V_SW` 总共 2×0.67 A,但 Q7 居中、
# 两支**各只走 0.67 A**。0.8 mm 线宽下 R = ρL/(Wt):
# 西支 125 mm → 0.077 Ω → 51 mV;东支 95 mm → 0.058 Ω → 39 mV
# 都远在 FAN_CIRCUIT 那 0.2 V 预算之内。当年算的 0.26 V 是「单条 196 mm 走全电流」,
# 分成两支之后不成立。⇒ **这条线要按 0.8 mm 下发,别当普通信号走 0.20。**
put('J26', -23.9, 115.0, 0) # 真西板边 = 进风(本体 x −29.0~−18.8,离板框 1.0)
put('J27', 183.9, 115.0, 0) # 真东板边 = 排风(本体 x 178.8~189.0)
# ══════════════════ 【S-M】P3 / P3B 控制块 ══════════════════
# 南中块 x 69.5~119.5 / y 0.5~59
put('U20', 88.0, 18.2, 180) # ESP32-S3:天线端转到 −Y 朝下板边
put('J22', 154.4, 52.0, 90) # 调试 USB-C,rot 90 = 开口朝 +X 右板边
# 2026-09-08 ⛔ 同一类硬缺陷:J22 原来在 x=154.4(本体到 159.6)= 假板框的东边,
# 真板框东边在 x=190,于是 USB-C 开口缩在板内 30.4 mm,线插不进去。
put('J22', 184.4, 52.0, 90) # 调试 USB-C,rot 90 = 开口朝**真**东板边(本体到 189.6)
# (下板边放不下:C841 罐的 1.5 mm 禁区压到 y 8.4)
put('RT601', 70.8, 36.0, 90) # 电感区测温:贴着 CH4 的电感那一排
put('RT602', 70.8, 26.0, 90) # MOS 区测温
@@ -413,8 +492,7 @@ put('RT602', 70.8, 26.0, 90) # MOS 区测温
DECAP = [
('C602', 'U20', '2', 3.0), # 100 nF 0402,ESP32 高频去耦 —— 先占位
('C601', 'U20', '2', 8.0), # 22 µF 1206,ESP32 储能
('C604', 'U21', '10', 3.0), # 74HC595 #1 VCC
('C605', 'U22', '10', 3.0), # 74HC595 #2 VCC
# C604 / C605(74HC595 去耦)2026-09-06 随 595 挪到载板
('C606', 'U23', '24', 3.0), # PCA9555 VDD
('C607', 'U6', '15', 3.0), # DAC63204 #1 VDD
('C610', 'U7', '15', 3.0), # DAC63204 #2 VDD
@@ -457,8 +535,7 @@ def affine(pagelist, x0, y0, w, h):
# 给走线留通道),结果钉在下面这张表里。附属的 22 个 R/C 不再跟着仿射走,
# 改成 `SAT` 表逐个贴到**它真正的对端引脚**旁边。
CTRL = {
'U21': (92.25, 52.37),
'U22': (92.84, 38.01),
# U21 (92.25,52.37) / U22 (92.84,38.01) 2026-09-06 挪到载板(§26.23)—— x 88~97 / y 35~55 空出来
'U23': (76.50, 49.63),
'U6': (79.58, 38.01),
'U7': (109.57, 34.71),
@@ -489,7 +566,6 @@ SAT = [
('R608', 'U23', '1', 6.0, u'IOEXP_INT# 上拉'),
('R606', 'U23', '9', 6.0, u'BUCK_PG 上拉'),
('R607', 'U23', '10', 6.0, u'INA_ALERT 上拉'),
('R609', 'U21', '13', 7.0, u'KEY_OE# 上拉(三片 595 共用)'),
('C608', 'U6', '13', 4.0, u'DAC #1 内部基准/输出滤波'),
('C609', 'U6', '16', 4.0, u'DAC #1'),
('R614', 'U6', '16', 5.0, u'DAC #1'),
@@ -586,7 +662,7 @@ def _fits(des, cx, cy, rot, clear=0.3):
u"""摆在这里会不会撞人 / 越框 / 压定位孔 / 超高。"""
b = box(des, rot)
ab = (cx + b[0], cy + b[1], cx + b[2], cy + b[3])
if ab[0] < 0.4 or ab[2] > BW - 0.4 or ab[1] < 0.4 or ab[3] > BH - 0.4:
if ab[0] < BX0 + 0.4 or ab[2] > BX1 - 0.4 or ab[1] < BY0 + 0.4 or ab[3] > BY1 - 0.4:
return False
r = MH_KEEPOUT / 2.0
for hx, hy in MH:
@@ -756,7 +832,7 @@ def check():
for d in sorted(POS):
x0, y0, x1, y1 = abs_box(d)
m = 0.4
if x0 < m or x1 > BW - m or y0 < m or y1 > BH - m:
if x0 < BX0 + m or x1 > BX1 - m or y0 < BY0 + m or y1 > BY1 - m:
errs.append(u'越出板框: %-6s 框 %.2f..%.2f / %.2f..%.2f' % (d, x0, x1, y0, y1))
r = MH_KEEPOUT / 2.0
@@ -840,6 +916,53 @@ def mate_report():
% (p, v[2], v[0], v[3], u'✔' if v[2] == v[3] else u'✘'))
# ══════════ 出线道加宽(2026-09-06 第二十六轮,用户拍板)══════════
#: **「够净距」不等于「出得去线」。** `pad_clearance_errs` 判的是焊盘净距 ≥ 0.20,
#: 而一根 0.2 mm 的线要从两个焊盘中间出去,需要 0.20 + 0.20 + 0.20 = **0.60**。
#: U6 / U7(WQFN-16 3×3,四边出脚)周围的卫星件正好卡在 **0.31 ~ 0.38** ——
#: 本地六项全过、DRC 也没有间距错误,可 `pcb1_escape_audit` 报「被围死」、
#: 布线器报「几何上就出不去」,`SCL`(U6.6) / `$6N532`(U6.16) / `$6N547`(U7.16)
#: 三个网络因此永远布不通。这里把堵住出线道的六颗按**背离芯片**的方向各挪 0.30 mm。
#:
#: ⚠️ 挪完 `C607` / `R617` 会分别压到 `SDA` / `SCL` 的换层孔(净距 0.029 / 0.113)——
#: 那两个孔是 `pcb1_i2c_l3.py` 每轮重算的,**挪件之后必须重跑它**,孔会自己让开。
#:
#: ⚠️ §26.22b:门槛不是 0.60 而是 **0.741**(CLR + 半线宽 + 栅格量化余量 QERR,两侧各 0.3707)。
#: 0.30 挪出来的 0.612/0.615/0.676 理论上塞得下、0.1 栅格上塞不下 ⇒ 2026-09-06 第二十八轮
#: 改成 **0.50(东列 0.45)**,三个方向都 ≥ 0.80。板上现在(`_pcb1_place_now.json`)是按 0.30 摆的,
#: 再挪 0.20 / 0.15 会压到的铜见 `.claude/_pcb1_unrouted_report.md` §A(VDAC4 孔、SDA 线+孔,
#: 以及 10 段要按新位置重做的扇出短线)。
#: ⚠️⚠️ **这一手救不了 `U6.16` / `U7.16`**(`pcb1_ch_reroute.py` 本地实测:挪完照样「几何上就出不去」)——
#: 它们被 C609 / C612 的 GND 脚 + GND 扇出孔从西边、C607 / C610 从北边、U6.15 的 +3V3 出线段从东边
#: 三面围死,北挪 C607 只是把北墙推远,西墙东墙不动。真正的解法是下面的 `SAT_FLIP`。
ESCAPE_NUDGE = {
'R615': (0.0, -0.50), # U6 南排(U6.5 / U6.6=SCL) 0.312 -> 0.812
'C607': (0.0, +0.50), # U6 北排(U6.13/15/16) 0.315 -> 0.815
'C608': (+0.45, 0.0), # U6 东列(VDAC2 / VDAC4) 0.376 -> 0.826
'R617': (0.0, -0.50), # U7 南排(U7.5 / U7.6) 0.315 -> 0.815
'C610': (0.0, +0.50), # U7 北排(U7.13/15/16) 0.312 -> 0.812
'C611': (+0.45, 0.0), # U7 东列(VDAC3 / VDAC5) 0.376 -> 0.826
}
for _d, (_dx, _dy) in sorted(ESCAPE_NUDGE.items()):
if _d in POS:
_x, _y, _r = POS[_d]
POS[_d] = (round(_x + _dx, 3), round(_y + _dy, 3), _r)
# ══════════ VREF 电容转 180°(2026-09-06 第二十八轮,A 类唯一真正的几何解)══════════
#: `C609` / `C612` 是 DAC63204 的 VREF 电容(pin 16),`place_sats()` 把它们竖着(rot 90)贴在
#: U6.16 / U7.16 的西北角:**GND 脚在南(离引脚近)、VREF 脚在北(离引脚远)**,
#: GND 扇出孔又打在 GND 脚正南 —— 于是 U6.16 往西北去找 VREF 脚的那条道被自家 GND 孔 + GND 短线堵死。
#: 转 180°(rot 90 -> 270)之后 VREF 脚朝南正对 U6.16(0.8 mm),GND 脚朝北。
#: `pcb1_ch_reroute.py --win 73,32,87,47 --nets '$6N532' --move C609:0:0:180 --drop-via 78.03,39.61`
#: 本地实测:`$6N532` 3.6 mm 一次布通、净距 0;U7 同理。下发时要删 2 段扇出短线 + 补 1 个 GND 孔
#: (见 `_pcb1_unrouted_report.md` §A)。
SAT_FLIP = {'C609': 180, 'C612': 180}
for _d, _dr in sorted(SAT_FLIP.items()):
if _d in POS:
_x, _y, _r = POS[_d]
POS[_d] = (_x, _y, (_r + _dr) % 360)
def summary():
ext = ch_extent()
print(u'母版局部包围盒 x %.2f~%.2f y %.2f~%.2f(%.1f x %.1f)'
+157 -64
查看文件
@@ -71,6 +71,11 @@ J24 焊盘 x 6.97~57.03、J25 x 102.97~153.03,两排 y 61.2 / 63.8,Ø1.8 焊
**南边要 5.6 A**(CH1 + CH4 + CH5 各 1.6 A,加左右四个风扇约 0.4+0.4 A)。
分法:**中桥喂 CH1 + CH4 + 左风扇 = 3.6 A,右通道喂 CH5 + 右风扇 = 2.0 A。**
> ⚠️ 2026-09-07 FAN-R2:五个 3 针风扇座删成两个 4 针,而且挪到了**北边两个上角**,
> 供电也从 `+12V` 改成 Q7 开关之后的 `FAN_12V_SW`。所以**南边现在只有 4.8 A**
> (中桥 3.2 / 右通道 1.6),风扇那 1.34 A 全部走北带经 Q7。上面两个 3.6 / 2.0 的
> 判据没改,现在只是更保守,不用重算。
左通道不用 —— 它的北端要从 J1 (6.0,74) 和 C2.1 (4.8,82) 两个通孔旁边挤过去,
只剩 2.8 mm、带 2.06 A,够不上一路通道的量,而且那一片正好在 8 A 输入链的顶层大铜下面,
留成 GND 底填**比塞一条 12V 细带有用得多**。
@@ -135,7 +140,15 @@ VIN_RAW, VIN_FUSED, VIN_SW = AUTO['VIN_RAW'], AUTO['VIN_FUSED'], AUTO['VIN_SW']
CHAIN = ((VIN_RAW, 'VIN_RAW'), (VIN_FUSED, 'VIN_FUSED'), (VIN_SW, 'VIN_SW'))
MIL = 39.3700787401575
BW, BH = 160.0, 125.0
# ⚠️⚠️ 2026-09-08:这里原来写的是 `BW, BH = 160.0, 125.0`,栅格从 (0,0) 起索引 ——
# 那是**过期的假板框**,和 `pcb1_place.py` 当年那条是同一个病(它已在第三十轮改掉)。
# 真板框是 **220 × 135、左下角 (−30, −5)**(`pcb1_outline.py` 就是这么画的)。
# 后果:**本地校核看不见 x<0 / y<0 那两条边**,落在那里的铜一律算成 0 ——
# 板上的铜不受影响(多边形是原样下发给 EasyEDA 的、由它去填),
# 但**任何摆到负坐标的东西,断面/连通性校核都会静悄悄地给出 0**。
# 第三十四轮把输入首级西移到 x −17~2 之后当场撞上:三块顶层铜全报「碎成 0 块」。
BX0, BY0 = -30.0, -5.0 # 板框左下角
BW, BH = 220.0, 135.0 # 板框尺寸
INSET = 0.5 # 铜面离板边
CLEAR = 0.2 # 安全间距(pcb1_rules.MIN_CLEARANCE)
@@ -167,6 +180,24 @@ def rect(x0, y0, x1, y1):
return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
# ── 毫米 ↔ 栅格:**一律走这四个函数,不要再写 int(x / G)** ──────────────
# 板框左下角不是 (0,0),直接除 G 会把负坐标截成 0(见上面 BX0/BY0 那段)。
def gi(x):
return int((x - BX0) / G)
def gj(y):
return int((y - BY0) / G)
def gx(i):
return BX0 + (i + 0.5) * G
def gy(j):
return BY0 + (j + 0.5) * G
CORNER_R = 5.0
@@ -179,10 +210,10 @@ def board_poly(inset=INSET, seg=6):
"""
r = CORNER_R - inset
pts = []
for cx, cy, a0 in ((BW - CORNER_R, CORNER_R, -90.0),
(BW - CORNER_R, BH - CORNER_R, 0.0),
(CORNER_R, BH - CORNER_R, 90.0),
(CORNER_R, CORNER_R, 180.0)):
for cx, cy, a0 in ((BX0 + BW - CORNER_R, BY0 + CORNER_R, -90.0),
(BX0 + BW - CORNER_R, BY0 + BH - CORNER_R, 0.0),
(BX0 + CORNER_R, BY0 + BH - CORNER_R, 90.0),
(BX0 + CORNER_R, BY0 + CORNER_R, 180.0)):
for k in range(seg + 1):
a = math.radians(a0 + 90.0 * k / seg)
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
@@ -201,12 +232,18 @@ REGIONS = [
# 载板 `pcb2_pour.py` 文件头第 4 条)。
# ⇒ 把中桥并进南西、右通道并进南东、风扇指并进北带,凑成三个 L / T 形多边形,
# 每块各自含 J28.1 / J26.1+J27.1 / J29.1+J30.1,**不依赖扇出就活得下来**。
# ⚠️⚠️ **2026-09-07 FAN-R2 之后上面这句不再成立**:J28/J29/J30 删了,J26/J27 挪到两个上角
# 而且 pin2 已经从 `+12V` 改成开关后的 `FAN_12V_SW`(经 Q7)——
# 三块 +12V 里**一个风扇座的通孔焊盘都不剩了**,现在靠 `pcb_fanout` 打的过孔活着
# (实测三块各填出 1 段,没被当孤岛删)。⇒ **重铺之前必须先扇出**,
# 否则会退回「边框还在、nPoured=0」那个状态。整板重布时把这三块各自含一个本网
# 通孔焊盘的条件重新落实一次,别再依赖扇出的运气。
dict(net='+12V', layer=L_PWR, name='12V-N', poly=[
# 北大块 y 88~124.5 通板 + 风扇指下伸到 y 66.5,**挖掉 U13 那一角**(Kelvin 脚,§10.4)
(0.5, 88.0), (96.5, 88.0), (96.5, 66.5), (112.0, 66.5), (112.0, 88.0),
(159.5, 88.0), (159.5, 124.5), (0.5, 124.5),
(0.5, 102.4), (12.5, 102.4), (12.5, 95.6), (0.5, 95.6)],
note=u'北带(CH2/CH3 的 Cin、U19、U29、J28)+ 风扇指(R730/C738/D2);'
note=u'北带(CH2/CH3 的 Cin、U19、U29;FAN-R2 之后还含 Q7 源极 = 风扇 1.34 A 的入口)+ 风扇指(现已空载);'
u'**源 Rs6.2 的焊盘是 (8.93~10.21, 91.77~95.23)** —— U13 那个缺口原来写成 y 93.0~101.5,'
u'正好把 Rs6.2 的中心咬掉,扇出于是把这颗 8 A 源判成「不在铜面里」一个过孔都没打(2026-09-06 抓到)。'
u'缺口收成 y 95.6~102.4,只罩住 U13 十个脚(96.08~101.92)与 C11'),
@@ -225,11 +262,11 @@ REGIONS = [
# 把 +12V 推到 72.6,缝就没了;离 3V3-SE 还有 0.4,够。
(0.5, 9.0), (72.6, 9.0), (72.6, 88.0), (61.5, 88.0), (61.5, 40.7),
(54.0, 40.7), (54.0, 34.0), (0.5, 34.0)],
note=u'CH1 + CH4 的 Cin、J26/J27 左风扇;中桥穿中间通道下来,喂 3.6 A'),
note=u'CH1 + CH4 的 Cin(FAN-R2 之后南边没有风扇了);中桥穿中间通道下来,喂 3.2 A,校核仍按 3.6 A 卡'),
dict(net='+12V', layer=L_PWR, name='12V-SE+RC', poly=[
# 南东块 + 右通道:L 形。J22 只有两个无网络的壳体通孔在 x=151.9,不挡 x≥153.5
(116.0, 9.0), (159.5, 9.0), (159.5, 88.0), (153.5, 88.0), (153.5, 34.0), (116.0, 34.0)],
note=u'CH5 的 Cin、J29/J30 右风扇;右通道接回北带,喂 2.0 A'),
note=u'CH5 的 Cin(FAN-R2 之后南边没有风扇了);右通道接回北带,喂 1.6 A,校核仍按 2.0 A 卡'),
# ── +3V3:南部两块,各自从北边的源单独引底层干线(见文件头「绕不过去的结论」)──
dict(net='+3V3', layer=L_PWR, name='3V3-SW', poly=rect(0.8, 41.0, 58.5, 58.5),
@@ -273,19 +310,36 @@ REGIONS = [
# ⚠️ 顶层和内层挡路的东西不一样(Grid 的表):这一片里 J1/C2/C3 的通孔、
# D1/C4/C5/C6/R1 的 GND 贴片焊盘、以及扇出给它们打的 9 个 GND 过孔 + 短线,
# **每一样都要躲**。下面每一条边界后面括号里写的就是它躲的那个东西。
# ⚠️⚠️ 2026-09-08 第三十四轮**整段重画**:输入首级(J1/F1/Q1/D1 + 栅极卫星)西移了 30 mm
# (J1 换圆形 DC 座并挪到真西板边)。旧的那一版坐标写在 x 11.9~22.5,
# 那一片现在**一个本网焊盘都没有** —— 铺出来是孤岛、会被整块删掉,
# 而断面校核量到的是空气(报 0.00 mm)。这是本文件第二次踩同一条,
# 第一次是第二十三轮(注释还在下面 CUTS 里)。⇒ **摆位一改就要回来重画这三块。**
#
# 新链路是**单向**的,比旧版顺:J1(−18.8) → F1(−12.5) → Q1(−6) → D1(1) → 东北去 C2/C3/Rs6。
# 旧版是 J1 一路向东到 Q1、再折回西边找 Rs6,多绕了一圈。
dict(net=VIN_RAW, layer=L_TOP, name='VINRAW-TOP',
poly=[(11.9, 65.4), (19.6, 65.4), (19.6, 73.08), (18.58, 73.08),
(18.58, 73.78), (15.0, 73.78), (15.0, 73.0), (11.9, 73.0)],
note=u'J1.2(13.5,69.5) → F1.1(17.5,73.06)。F1 竖着放之后这一段是一片 7.7×7.7 的大铜;'
u'西北那个阶梯是躲 J1.1 的 GND 通孔(Φ2.5, 净距后 r=1.45),'
u'东边 19.6 躲 Q1.4 栅极脚(20.215)与 DZ1.1(19.890),'
u'北边两段台阶各给 VINFUSED 让 0.2'),
poly=[(-17.0, 70.7), (-11.5, 70.7), (-11.5, 73.80),
(-13.65, 73.80), (-13.65, 76.5), (-17.0, 76.5)],
note=u'J1.1(−15.80,74.35,中心正极,通孔盘 1.6×3.8) → F1.1(−12.50,73.06)。'
u'南边 70.7 躲 J1.3 的 GND 通孔盘(顶 70.450);'
u'东边 −11.5 刚好盖住 F1.1(东 −11.637),离 Q1 源极三脚(西 −9.786) 还有 1.7;'
u'x −13.65 那级台阶是躲 F1.2(西 −13.364, 南 75.350) —— 台阶以东只铺到 73.80,'
u'盖住 F1.1(顶 73.651) 并给 VIN_FUSED 让 0.30。'
u'**载流断面在 J1.1 与 F1.1 之间**(x≈−14.2),那里是满高 5.8 mm;'
u'台阶以东那 3.10 mm 不走穿过电流,F1.1 就是终点。'
u'⚠️ 两块的**竖直边**也要拉开:VINRAW 东界 −11.5 与 VINFUSED 的台阶 −11.15 '
u'差 0.35 —— 第一版这两条边只隔 0.20(正好压在下限上),是校核报出来的'),
dict(net=VIN_FUSED, layer=L_TOP, name='VINFUSED-TOP',
poly=[(16.4, 74.0), (18.8, 74.0), (18.8, 73.3), (22.5, 73.3),
(22.5, 78.5), (16.4, 78.5)],
note=u'F1.2(17.5,75.95) → Q1.1~3(源极三脚, x 20.2~22.2)。'
u'南边两段:西半 74.0 躲 F1.1(顶 73.651),东半 73.3 躲 Q1.4(顶 72.882) '
u'并盖住 Q1.3(底 73.578);北边 78.5 给 VIN_SW 西行干道让 0.35'),
poly=[(-13.4, 74.10), (-11.15, 74.10), (-11.15, 73.3),
(-7.5, 73.3), (-7.5, 77.8), (-13.4, 77.8)],
note=u'F1.2(−12.50,75.95) → Q1.1~3(源极三脚, x −9.786~−7.751)。'
u'南边两段:西半 74.10 同时躲 F1.1(顶 73.651) 和 VINRAW 的台阶(顶 73.80,让 0.30),'
u'东半 73.3 躲 Q1.4 栅极脚(顶 72.882) 并盖住 Q1.3(底 73.578);'
u'东边 −7.5 离 Q1 漏极四脚(西 −4.250) 还有 3.25。'
u'断面在 F1.2 与源极之间(x≈−10.7)= 4.5 mm。'
u'⚠️ DZ1.1/C1.1 这两个栅极钳位件同属本网但在 y≈68,**不进这块铜** ——'
u'µA 级,交给 pcb1_route_sig 拉一条细线'),
# ⚠ `VIN_SW` 必须是**一个铺铜对象**,不能拆成几块靠互相重叠连起来。
# 同层同网两块铜重叠时,低优先级那块会被高优先级那块裁掉 —— 裁的时候
@@ -294,16 +348,17 @@ REGIONS = [
# ⇒ 宁可手写 18 个点。下面的 `rects` 是这 18 个点该等于的矩形并集,
# `report()` 里逐格对照,写错一个点当场就报(check_poly_rects)。
dict(net=VIN_SW, layer=L_TOP, name='VINSW-TOP',
poly=[(25.6, 72.1), (29.68, 72.1), (29.68, 73.45), (32.3, 73.45),
(32.3, 78.85), (30.47, 78.85), (30.47, 86.47), (28.6, 86.47),
poly=[(-4.45, 72.10), (-0.30, 72.10), (-0.30, 73.40), (2.40, 73.40),
(2.40, 78.85), (30.46, 78.85), (30.46, 86.47), (28.6, 86.47),
(28.6, 83.49), (16.72, 83.49), (16.72, 85.5), (12.5, 85.5),
(12.5, 83.49), (6.65, 83.49), (6.65, 87.0), (5.3, 87.0),
(5.3, 95.25), (1.5, 95.25), (1.5, 78.85), (25.6, 78.85)],
rects=[(25.6, 72.1, 29.68, 78.85), # Q1 漏极四脚(南边 72.1 躲 R1 的 GND 过孔 26.675,71.5)
(25.6, 73.45, 32.3, 78.85), # D1.1 TVS(南边 73.45 躲 D1.2 的 GND 盘顶 73.163 与它 6 个扇出孔)
(28.6, 78.85, 30.47, 86.47), # C4.2/C5.2/C6.2 去耦(东边 30.47 躲 C6.1 的 GND 盘 30.670)
(1.5, 78.85, 28.6, 83.49), # ★ 8 A 西行干道,y 净宽 4.64 mm;
# 南边 78.85 躲 J1 的机械脚 J1.4(顶 78.401),北边 83.49 躲 C2/C3 的四个通孔(底 83.700)
(5.3, 95.25), (1.5, 95.25), (1.5, 83.49), (-4.45, 83.49)],
rects=[(-4.45, 72.10, -0.30, 78.85), # Q1 漏极四脚(x −4.250~−2.215)。南边 72.10 躲 R1 两脚(顶 71.772),留 0.328
(-0.30, 73.40, 2.40, 78.85), # D1.1 TVS(−0.096~2.096, 75.838~77.885)。
# 南边抬到 73.40 是躲 D1.2 的 GND 盘(顶 73.163) 和它正下方 6 个扇出孔
(-4.45, 78.85, 28.6, 83.49), # ★ 8 A 干道,y 净宽 4.64 mm。**西端由 1.5 延到 −4.45**:
# 旧版西端卡在 1.5 是因为老 J1 的机械脚 J1.4(顶 78.401) 顶着,那颗件已经挪走
(28.6, 78.85, 30.46, 86.47), # C4.2/C5.2/C6.2 去耦(东边 30.46 躲 C6.1 的 GND 盘 30.670)
(12.5, 83.49, 16.72, 85.5), # C3.1 口袋(东边 16.72 躲 C3.2 GND 通孔 16.950)
(1.5, 83.49, 6.65, 87.0), # C2.1 口袋(净宽 5.15 mm)
(1.5, 87.0, 5.3, 95.25)], # 北柱到 Rs6.1(净宽 **3.8 mm** = 8.5 A)
@@ -312,8 +367,11 @@ REGIONS = [
# 顶到 y 95.4 —— 离 U13.2 只剩 0.379 mm,`pcb1_escape_audit` 报「被围死」。
# 收窄之后 U13 南边是一整片 x 5.5~14 / y 91.6~96 的空地。
# (Rs6.2 那两根往西甩的 1 mm 扇出短线也一起没了 —— 见 pcb_fanout.FORCE_ARRAY)
note=u'**8 A 主干**:Q1 漏极 → 北上 → 西行干道 → C3.1/C2.1 → 北柱 → Rs6.1,一路无过孔;'
u'东边挂 D1(TVS) 与 C4/C5/C6 去耦。干道比 v10 北提了 2.8 mm —— XT30 的机械脚占到 y 78.4'),
note=u'**8 A 主干**:Q1 漏极(−3.2) → 东经 D1 → 北上 → **东行**干道 → C2.1/C3.1 → 北柱 → Rs6.1,'
u'一路无过孔;东端挂 C4/C5/C6 去耦。'
u'⚠️ 第三十四轮之后干道是**东行**不是西行 —— 源头 Q1 从 x 24 挪到了 x −6,'
u'而 C2/C3/Rs6/C4~C6 一颗没动,于是电流方向整个翻过来了。'
u'旧注释「干道比 v10 北提 2.8 mm 躲 XT30 机械脚」已作废:那颗 XT30 已经不在这块板上'),
]
# ── 断面校核:每一条都是「这里必须能过多少安」──────────────────────────
@@ -332,7 +390,9 @@ CUTS = [
(u'南西块 → CH1', '+12V', L_PWR, 'x', 33.0, (9.0, 34.0), 2.0),
(u'南西块 → CH4+CH1', '+12V', L_PWR, 'x', 61.0, (9.0, 34.0), 3.6),
(u'南东块 → CH5', '+12V', L_PWR, 'x', 140.0, (9.0, 34.0), 1.6),
(u'风扇指 @接北带', '+12V', L_PWR, 'y', 87.0, (94.0, 114.0), 1.0),
# FAN-R2 之后风扇指下面已经没有 +12V 负载(风扇改吃开关后的 FAN_12V_SW)。
# 这一条留着当**几何回归**:北带往南这一指还在,断面别被后来的布线啃掉。
(u'风扇指 @接北带(现空载)', '+12V', L_PWR, 'y', 87.0, (94.0, 114.0), 1.0),
(u'北大块 @Rs6 东出', '+12V', L_PWR, 'x', 20.0, (86.0, 124.5), 8.0),
(u'3V3 南西块', '+3V3', L_PWR, 'x', 33.0, (41.0, 58.5), 0.5),
(u'3V3 南东块 @控制块', '+3V3', L_PWR, 'x', 95.0, (9.0, 58.5), 0.5),
@@ -341,10 +401,14 @@ CUTS = [
# 8 A 输入链(顶层 1 oz)。J1 → F1 → Q1 → D1/C4~C6 → C3/C2 → Rs6,分叉之前全程 8 A。
# ⚠ 这一排坐标跟着摆位走,**摆位一改就要重定**:2026-09-06 第二十三轮把
# F1 竖过来、D1 与 C4/C5/C6 挪到东列之后,旧的 `x=10` 那一刀量到的是空气(报 0.00 mm)。
(u'RAW J1→F1 颈', VIN_RAW, L_TOP, 'y', 73.4, (14.0, 20.0), 8.0),
(u'RAW J1.2 东出', VIN_RAW, L_TOP, 'x', 15.5, (64.0, 76.0), 8.0),
(u'FUSED F1.2→Q1 源极', VIN_FUSED, L_TOP, 'x', 19.5, (72.0, 80.0), 8.0),
(u'SW Q1 漏极北出', VIN_SW, L_TOP, 'y', 77.5, (24.0, 34.0), 8.0),
# 2026-09-08:输入首级西移 30 mm,下面前四条**全部重定**。
# ⚠️ 断面要卡在「电流真正流过的那一段」,不是随便找个地方量:
# VIN_RAW 的载流段是 J1.1 与 F1.1 **之间**(x≈−14.2);台阶以东那 3.15 mm
# 是 F1.1 的落脚区、不走穿过电流,在那里量会得到一个偏严且没意义的数。
(u'RAW J1.1→F1.1', VIN_RAW, L_TOP, 'x', -14.2, (68.0, 79.0), 8.0),
(u'FUSED F1.2→Q1 源极', VIN_FUSED, L_TOP, 'x', -10.7, (70.0, 80.0), 8.0),
(u'SW Q1 漏极东出', VIN_SW, L_TOP, 'x', -1.5, (69.0, 81.0), 8.0),
(u'SW 过 D1 北上', VIN_SW, L_TOP, 'x', 2.0, (70.0, 86.0), 8.0),
(u'SW 干道 @x24 东端', VIN_SW, L_TOP, 'x', 24.0, (74.0, 86.0), 8.0),
(u'SW 干道 @x12', VIN_SW, L_TOP, 'x', 12.0, (74.0, 86.0), 8.0),
(u'SW 干道拐北 @y84', VIN_SW, L_TOP, 'y', 84.0, (0.5, 12.0), 8.0),
@@ -373,7 +437,6 @@ ROUTED_PADS = {
'U12.9': u'Kelvin(IN−):单独引线到 Rs501.1 的焊盘,不进 VO5_P 小岛',
'C11.1': u'陪 U13 一起挖出去,走顶层到 L2 输出',
'U13.6': u'INA226 的 VS+,陪 U13 一起挖出去(C11 就在旁边)',
'C739.1': u'孤零零落在风扇指里,走 11 mm 顶层线到 U33.10',
# U23 西侧那两颗(v11 控制块退火重排之后落到 x 71.9~72.4):
# 那一条 x 是 `12V-SW+BR`(东界 72.6) 与 `3V3-SE`(西界 73.0) 中间的 0.4 mm 缝,
# 两块内层平面谁都够不着。**贴片焊盘对内层透明,所以不是错误**,走顶层短线即可:
@@ -385,12 +448,24 @@ ROUTED_PADS = {
u'不能从大铜面上随便找个点接',
'DZ1.1': u'Q1 栅极钳位齐纳的上端,µA 级;夹在 F1.1 与 Q1.4 之间只有 3.1 mm,走细线',
'C1.1': u'Q1 栅极 RC 的上端,µA 级;同 DZ1.1',
# 2026-09-08 第三十四轮:两路 TACH 接收跟着风扇座搬到了东西**板边**
# (R753 在 x −25.6、R757 在 x 181,都在 y≈100),离 `3V3-SW`/`3V3-SE` 两块内层
# 平面各有 80~100 mm。**不要为这两颗把平面拉过去** —— 10 kΩ 上拉到 3.3 V
# 只有 **0.33 mA**,外层 0.2 mm 线绰绰有余,而拉一条 100 mm 的内层电源面
# 会把 Inner2 的分割搅乱。它们是「接收电路在信号源头整形」那条设计决定的代价。
'R753.2': u'进风 TACH 的 3.3V 上拉(10k, 0.33 mA),随风扇座在西板边;走顶层细线',
'R757.2': u'排风 TACH 的 3.3V 上拉(10k, 0.33 mA),随风扇座在东板边;走顶层细线',
}
# 整块「有意不进平面」的区域(位号前缀 → 说明)
ROUTED_BLOCKS = [
((14.0, 100.0, 30.0, 124.0), u'北左辅助 Buck 那一窝:12V 与 3V3 在 16×24 mm 里交错,整窝走顶层'),
((36.0, 70.0, 58.0, 84.0), u'CH2 的 +3V3(5 个,50 mA):沿 y≈84 走顶层到中间那块'),
((126.0, 70.0, 146.0, 84.0), u'CH3 的 +3V3(5 个,50 mA):同上'),
# `R518.1`(CH5 的 +3V3):2026-09-06 第二十六轮起不再落在 Inner2 的 +3V3 上 ——
# 它本来就是「短线 + 扇出孔」那一类,只是以前**碰巧**还压着平面所以没被点名。
# 实测通路:R518.1 → 顶层 0.66 mm → +3V3 扇出孔 (134.575, 51.000) → 平面,
# `pcb1_netlist` 里 +3V3 整网连通(不在「还差」名单里)。框只罩这一颗。
((135.0, 50.4, 136.0, 51.6), u'CH5 的 R518.1:顶层 0.66 mm 短线到 (134.575,51.000) 的 +3V3 扇出孔'),
]
@@ -531,6 +606,16 @@ def inside(poly, x, y):
# ══════════════════ 栅格化 + 校核 ══════════════════
G = 0.25 # 栅格(mm)。0.25 下 45 mm 的通道是 180 格,够细
#: 浮铜检查的**最小面积**(mm²)。小于它的当栅格碎屑放过,只打印不判红。
#:
#: ⚠️ 这不是"把检查关小声点",是给它一个说得出理由的下限:布线一多,
#: 过孔的反焊盘之间必然夹出 0.25×0.25(**正好一个栅格**)这种碎屑 ——
#: 2026-09-06 第二十五轮实测两粒:Inner1 (68.9,74.6) 0.06 mm²、Inner2 (138.5,75.2) 0.19 mm²,
#: 都比一个过孔盘还小,电气上没有意义,而且嘉立创铺铜自己也会按死铜阈值清掉。
#: 门槛定在 1.0 mm²(1×1 mm):这一轮真正抓到的两条 —— 南沿那条 **1312.8 mm²** 的
#: GND 底填、`3V3-SW` 里那块 **7.2 mm²** 的孤岛 —— 都远在门槛之上,一条都不会漏。
MIN_FLOAT_AREA = 1.0
class Grid(object):
u"""某一层上某个网络的铜:先按区域填,再挖异网的反焊盘 / 过孔 / 走线。
@@ -554,11 +639,11 @@ class Grid(object):
for r in regions:
xs = [p[0] for p in r['poly']]
ys = [p[1] for p in r['poly']]
i0, i1 = int(min(xs) / G), int(max(xs) / G) + 1
j0, j1 = int(min(ys) / G), int(max(ys) / G) + 1
i0, i1 = gi(min(xs)), gi(max(xs)) + 1
j0, j1 = gj(min(ys)), gj(max(ys)) + 1
for j in range(max(0, j0), min(self.ny, j1)):
for i in range(max(0, i0), min(self.nx, i1)):
if not self.c[j][i] and inside(r['poly'], (i + .5) * G, (j + .5) * G):
if not self.c[j][i] and inside(r['poly'], gx(i), gy(j)):
self.c[j][i] = True
# 反焊盘:异网焊盘(内层只算通孔,外层连贴片一起算)
self.carved = 0
@@ -580,21 +665,21 @@ class Grid(object):
# 天线禁铺(全层)
for j in range(self.ny):
for i in range(self.nx):
if self.c[j][i] and ANT[0] <= (i + .5) * G <= ANT[2] \
and ANT[1] <= (j + .5) * G <= ANT[3]:
if self.c[j][i] and ANT[0] <= gx(i) <= ANT[2] \
and ANT[1] <= gy(j) <= ANT[3]:
self.c[j][i] = False
self.label()
def _carve(self, x0, y0, x1, y1):
u"""挖掉一个矩形(已经把净距算进去了)。返回有没有真挖到东西。"""
hit = False
for j in range(max(0, int(y0 / G)), min(self.ny, int(y1 / G) + 1)):
yc = (j + .5) * G
for j in range(max(0, gj(y0)), min(self.ny, gj(y1) + 1)):
yc = gy(j)
if not (y0 <= yc <= y1):
continue
row = self.c[j]
for i in range(max(0, int(x0 / G)), min(self.nx, int(x1 / G) + 1)):
if row[i] and x0 <= (i + .5) * G <= x1:
for i in range(max(0, gi(x0)), min(self.nx, gi(x1) + 1)):
if row[i] and x0 <= gx(i) <= x1:
row[i] = False
hit = True
return hit
@@ -628,15 +713,15 @@ class Grid(object):
u"""沿一条线量本网铜的总宽与最长连续段(mm)。"""
tot = run_max = run = 0
if axis == 'y':
j = int(coord / G)
for i in range(int(span[0] / G), int(span[1] / G) + 1):
j = gj(coord)
for i in range(gi(span[0]), gi(span[1]) + 1):
v = 0 <= i < self.nx and 0 <= j < self.ny and self.c[j][i]
tot += 1 if v else 0
run = run + 1 if v else 0
run_max = max(run_max, run)
else:
i = int(coord / G)
for j in range(int(span[0] / G), int(span[1] / G) + 1):
i = gi(coord)
for j in range(gj(span[0]), gj(span[1]) + 1):
v = 0 <= i < self.nx and 0 <= j < self.ny and self.c[j][i]
tot += 1 if v else 0
run = run + 1 if v else 0
@@ -645,8 +730,8 @@ class Grid(object):
def hit(self, x, y, hx, hy):
u"""这个焊盘有没有落在本网主连通域上(焊盘框内任一格)。"""
i0, i1 = int((x - hx) / G), int((x + hx) / G) + 1
j0, j1 = int((y - hy) / G), int((y + hy) / G) + 1
i0, i1 = gi(x - hx), gi(x + hx) + 1
j0, j1 = gj(y - hy), gj(y + hy) + 1
for j in range(max(0, j0), min(self.ny, j1 + 1)):
for i in range(max(0, i0), min(self.nx, i1 + 1)):
if self.lab[j][i]:
@@ -1004,7 +1089,9 @@ def ascii_map_top(pads, obs=()):
看不出「是被 GND 扇出孔啃的还是被自己的边界切的」。
"""
g = 0.5
x0, x1, y0, y1 = 1.0, 31.0, 65.0, 95.0
# 2026-09-08:窗口跟着输入首级西移 —— 旧值 (1,31,65,95) 现在只剩右半条链。
# 链路实际占 x −17~30.5 / y 70~95.3,取 0.5 mm/格 一行 96 字,够宽。
x0, x1, y0, y1 = -18.0, 32.0, 69.0, 96.0
sym = [(VIN_RAW, 'R'), (VIN_FUSED, 'F'), (VIN_SW, 'S')]
grids = [(s, Grid(net, L_TOP,
[r for r in REGIONS if r['layer'] == L_TOP and r['net'] == net],
@@ -1029,7 +1116,7 @@ def ascii_map_top(pads, obs=()):
x = x0 + (i + .5) * g
ch = '.'
for s, gr in grids:
ii, jj = int(x / G), int(y / G)
ii, jj = gi(x), gj(y)
if 0 <= ii < gr.nx and 0 <= jj < gr.ny and gr.c[jj][ii]:
ch = s
break
@@ -1058,10 +1145,10 @@ def ascii_map(pads, obs=()):
for j in range(ny - 1, -1, -1):
row = ''
for i in range(nx):
x, y = (i + .5) * g, (j + .5) * g
x, y = BX0 + (i + .5) * g, BY0 + (j + .5) * g
ch = ' '
for net, gr in grids:
ii, jj = int(x / G), int(y / G)
ii, jj = gi(x), gj(y)
if 0 <= ii < gr.nx and 0 <= jj < gr.ny and gr.c[jj][ii]:
ch = sym[net]
break
@@ -1214,10 +1301,10 @@ def _cells_from_rings(rings):
xs_ = pts[0::2]
ys_ = pts[1::2]
n = len(xs_)
j0 = max(0, int(min(ys_) / G))
j1 = min(ny - 1, int(max(ys_) / G) + 1)
j0 = max(0, gj(min(ys_)))
j1 = min(ny - 1, gj(max(ys_)) + 1)
for j in range(j0, j1 + 1):
y = (j + 0.5) * G
y = gy(j)
cr = []
for k in range(n):
y1, y2 = ys_[k], ys_[(k + 1) % n]
@@ -1227,10 +1314,10 @@ def _cells_from_rings(rings):
cr.sort()
row = acc[j]
for a in range(0, len(cr) - 1, 2):
ia = max(0, int(cr[a] / G))
ib = min(nx - 1, int(cr[a + 1] / G))
ia = max(0, gi(cr[a]))
ib = min(nx - 1, gi(cr[a + 1]))
for i in range(ia, ib + 1):
xc = (i + 0.5) * G
xc = gx(i)
if cr[a] <= xc <= cr[a + 1]:
row[i] ^= 1
return [[bool(v) for v in r] for r in acc]
@@ -1317,16 +1404,22 @@ def verify(refresh=True):
g = G_[k]
live = set()
for x, y in anchors.get(k, ()):
i, j = int(x / G), int(y / G)
i, j = gi(x), gj(y)
if 0 <= i < g.nx and 0 <= j < g.ny and g.lab[j][i]:
live.add(g.lab[j][i])
dead = [(cid, n) for cid, n in g.sizes.items() if cid not in live]
big = [(c, n) for c, n in dead if n * G * G >= MIN_FLOAT_AREA]
dust = [(c, n) for c, n in dead if n * G * G < MIN_FLOAT_AREA]
lay = {1: 'Top', 2: 'Bottom', 15: 'Inner1', 16: 'Inner2'}[k[1]]
chk(not dead,
if dust:
print(u' (%s %s 另有 %d 粒 < %.1f mm² 的碎屑,按 MIN_FLOAT_AREA 放过:%s mm²)'
% (lay, k[0], len(dust), MIN_FLOAT_AREA,
' '.join('%.2f' % (n * G * G) for _c, n in sorted(dust, key=lambda t: -t[1])[:6])))
chk(not big,
u'%s %s 的 %d 块碎片各自都有本网焊盘/过孔落脚' % (lay, k[0], g.ncomp),
u'%s %s 有 %d 块**浮铜**(没有任何本网落脚点):%s mm²'
% (lay, k[0], len(dead),
' '.join('%.1f' % (n * G * G) for _c, n in sorted(dead, key=lambda t: -t[1])[:6])))
% (lay, k[0], len(big),
' '.join('%.1f' % (n * G * G) for _c, n in sorted(big, key=lambda t: -t[1])[:6])))
grid_checks(G_, pads, chk)
return bad
+622 -611
查看文件
文件差异内容过多而无法显示 加载差异
文件差异内容过多而无法显示 加载差异
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
+1 -1
查看文件
@@ -27,7 +27,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2'
PCB = '942ed1560d81c1e7'
MIL = 39.3700787401575
COPPER = (1, 15, 16, 2)
+94 -94
查看文件
@@ -1,94 +1,94 @@
# -*- coding: utf-8 -*-
u"""删掉指定矩形框里的**某个网络**的走线与过孔(板坐标或站内坐标)。
/c/Python314/python .claude/pcb2_del_box.py GND --site 54.9 36.8 56.5 37.5
/c/Python314/python .claude/pcb2_del_box.py GND --board 100 60 102 62
加 `--push` 才真删,不加只列出来。
## 为什么需要它
板上的铜是好几个脚本各画各的(`pcb2_dp_long` / `pcb2_fanout` / `pcb2_vo_vias` / `pcb2_route_sig`),
每个脚本的 `--push` 只删**自己上一轮**画的东西。挪了一个元件之后,
它那颗扇出孔就成了没人认领的孤儿 —— 谁的删除逻辑都不管,DRC 只报一条
「过孔没连上任何东西」,很容易被当成噪声放过去。
## 判据
- **完全落在框里**才删(导线两端都在框内、过孔中心在框内)。半进半出的一律不动,
免得把一条长线截断一半留在板上 —— 那比不删还糟。
- `--site` 是站内坐标,会自动展开到五个手机位(`pcb2_place.to_board`,4/5 站镜像)。
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
from pcb2_place import SITES, to_board
PCB = 'f126ffb29c92f9b2'
MIL = 39.3700787401575
def boxes_from_argv():
if '--site' in sys.argv:
i = sys.argv.index('--site')
u0, v0, u1, v1 = [float(v) for v in sys.argv[i + 1:i + 5]]
out = []
for n in SITES:
ax, ay, _ = to_board(n, u0, v0)
bx, by, _ = to_board(n, u1, v1)
out.append((min(ax, bx), min(ay, by), max(ax, bx), max(ay, by)))
return out
i = sys.argv.index('--board')
x0, y0, x1, y1 = [float(v) for v in sys.argv[i + 1:i + 5]]
return [(min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1))]
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1500));
const M = 39.3700787401575, q = v => Math.round(v / M * 1e4) / 1e4;
const NET = %s, B = %s, DO = %s;
const inB = (x, y) => B.some(b => x >= b[0] && x <= b[2] && y >= b[1] && y <= b[3]);
const lines = [], vias = [];
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
if ((l.net || '') !== NET) continue;
if (l.layer !== 1 && l.layer !== 2) continue;
if (inB(q(l.startX), q(l.startY)) && inB(q(l.endX), q(l.endY)))
lines.push([l.primitiveId, q(l.startX), q(l.startY), q(l.endX), q(l.endY), l.layer]);
}
for (const v of await eda.pcb_PrimitiveVia.getAll()) {
if ((v.net || '') !== NET) continue;
if (inB(q(v.x), q(v.y))) vias.push([v.primitiveId, q(v.x), q(v.y)]);
}
if (DO) {
if (lines.length) await eda.pcb_PrimitiveLine.delete(lines.map(l => l[0]));
if (vias.length) await eda.pcb_PrimitiveVia.delete(vias.map(v => v[0]));
await eda.pcb_Document.save();
}
return {lines: lines.map(l => l.slice(1)), vias: vias.map(v => v.slice(1)), did: DO};
"""
def main():
net = sys.argv[1]
boxes = boxes_from_argv()
do = '--push' in sys.argv
r = run(JS % (PCB, json.dumps(net), json.dumps(boxes), 'true' if do else 'false'),
timeout=600)
if not r.get('success'):
sys.exit(u'失败: ' + json.dumps(r, ensure_ascii=False)[:400])
x = r['result']
print(u'%s:%d 个框里有 %s 的导线 %d 条 / 过孔 %d 个'
% (u'已删' if do else u'(预览,加 --push 才删)', len(boxes), net,
len(x['lines']), len(x['vias'])))
for l in x['lines'][:20]:
print(u' 线 L%d (%.3f, %.3f) -> (%.3f, %.3f)' % (l[4], l[0], l[1], l[2], l[3]))
for v in x['vias'][:20]:
print(u' 孔 (%.3f, %.3f)' % (v[0], v[1]))
if __name__ == '__main__':
main()
# -*- coding: utf-8 -*-
u"""删掉指定矩形框里的**某个网络**的走线与过孔(板坐标或站内坐标)。
/c/Python314/python .claude/pcb2_del_box.py GND --site 54.9 36.8 56.5 37.5
/c/Python314/python .claude/pcb2_del_box.py GND --board 100 60 102 62
加 `--push` 才真删,不加只列出来。
## 为什么需要它
板上的铜是好几个脚本各画各的(`pcb2_dp_long` / `pcb2_fanout` / `pcb2_vo_vias` / `pcb2_route_sig`),
每个脚本的 `--push` 只删**自己上一轮**画的东西。挪了一个元件之后,
它那颗扇出孔就成了没人认领的孤儿 —— 谁的删除逻辑都不管,DRC 只报一条
「过孔没连上任何东西」,很容易被当成噪声放过去。
## 判据
- **完全落在框里**才删(导线两端都在框内、过孔中心在框内)。半进半出的一律不动,
免得把一条长线截断一半留在板上 —— 那比不删还糟。
- `--site` 是站内坐标,会自动展开到五个手机位(`pcb2_place.to_board`,4/5 站镜像)。
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
from pcb2_place import SITES, to_board
PCB = '942ed1560d81c1e7'
MIL = 39.3700787401575
def boxes_from_argv():
if '--site' in sys.argv:
i = sys.argv.index('--site')
u0, v0, u1, v1 = [float(v) for v in sys.argv[i + 1:i + 5]]
out = []
for n in SITES:
ax, ay, _ = to_board(n, u0, v0)
bx, by, _ = to_board(n, u1, v1)
out.append((min(ax, bx), min(ay, by), max(ax, bx), max(ay, by)))
return out
i = sys.argv.index('--board')
x0, y0, x1, y1 = [float(v) for v in sys.argv[i + 1:i + 5]]
return [(min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1))]
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1500));
const M = 39.3700787401575, q = v => Math.round(v / M * 1e4) / 1e4;
const NET = %s, B = %s, DO = %s;
const inB = (x, y) => B.some(b => x >= b[0] && x <= b[2] && y >= b[1] && y <= b[3]);
const lines = [], vias = [];
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
if ((l.net || '') !== NET) continue;
if (l.layer !== 1 && l.layer !== 2) continue;
if (inB(q(l.startX), q(l.startY)) && inB(q(l.endX), q(l.endY)))
lines.push([l.primitiveId, q(l.startX), q(l.startY), q(l.endX), q(l.endY), l.layer]);
}
for (const v of await eda.pcb_PrimitiveVia.getAll()) {
if ((v.net || '') !== NET) continue;
if (inB(q(v.x), q(v.y))) vias.push([v.primitiveId, q(v.x), q(v.y)]);
}
if (DO) {
if (lines.length) await eda.pcb_PrimitiveLine.delete(lines.map(l => l[0]));
if (vias.length) await eda.pcb_PrimitiveVia.delete(vias.map(v => v[0]));
await eda.pcb_Document.save();
}
return {lines: lines.map(l => l.slice(1)), vias: vias.map(v => v.slice(1)), did: DO};
"""
def main():
net = sys.argv[1]
boxes = boxes_from_argv()
do = '--push' in sys.argv
r = run(JS % (PCB, json.dumps(net), json.dumps(boxes), 'true' if do else 'false'),
timeout=600)
if not r.get('success'):
sys.exit(u'失败: ' + json.dumps(r, ensure_ascii=False)[:400])
x = r['result']
print(u'%s:%d 个框里有 %s 的导线 %d 条 / 过孔 %d 个'
% (u'已删' if do else u'(预览,加 --push 才删)', len(boxes), net,
len(x['lines']), len(x['vias'])))
for l in x['lines'][:20]:
print(u' 线 L%d (%.3f, %.3f) -> (%.3f, %.3f)' % (l[4], l[0], l[1], l[2], l[3]))
for v in x['vias'][:20]:
print(u' 孔 (%.3f, %.3f)' % (v[0], v[1]))
if __name__ == '__main__':
main()
+247 -245
查看文件
@@ -1,245 +1,247 @@
# -*- coding: utf-8 -*-
u"""六对**长差分**(USB1~5 的 U32↔U5n + UPSTREAM 的 U32↔J23)+ Hub 西缘被 D± 夹住的两条 +3V3 车道。
**v5 版(2026-09-06,载板全部底层、接口边朝板边)。** v3 版存档在 `obsolete/pcb2_dp_long_v3_2026-09-06.py`。
被 `pcb2_route_dp.py` import;校核 / 下发都用那个脚本,这里只放坐标。
## 出发点:U32 西缘的 12 条车道
U32(QFN-64,**底层**,中心 (100, 68))的 D± 全在**西缘 x 95.55**,0.5 脚距、南→北:
1DM 64.25 | 1DP 64.75 | 2DM 65.25 | 2DP 65.75 | pin5 +3V3 66.25 |
3DM 66.75 | 3DP 67.25 | 4DM 67.75 | 4DP 68.25 | pin10 +3V3 68.75 |
5DM 69.25 | 5DP 69.75 | pin13 GND 70.25 | 14~16 NC
目的地却是四面八方:1→西北远处 (U51 22/24, 87)、2→正北 (U52 94/96, 87)、3→东北 (U53 165/167, 87)、
4→西南 (U54 88.6/90.5, 48)、5→东南 (U55 160/162, 48)、UP→正东 (J23 212, 61~62)。
### 定下来的走法(理由都在代码注释里)
| 车道 | 层 | 走法 |
|---|---|---|
| pair 1 | 底 | 直着西行 y 64.25/64.75 到 x 22.34/22.82,北上进 U51 |
| pair 2 | 底 | 西行到 89.27/89.75 北上,y 80 处 45° 斜到 93.84/95.74 进 U52 |
| pin 5 | 底→顶 | 西行到 90.4,跟着 pair 3 北上到 70.5 打孔翻顶层,西去 (88.2, 70.5) 再下来进 C747 |
| pair 3 | 底 | 西行到 91.05/91.53 北上,y 76.50/76.98 东行,过了 pair 5 之后压到 75.40/75.88 从 MH 焊盘下面钻过去,x 165 北上 |
| pair 4 | **顶** | 西行到 x 92.2 打孔上顶层(DM 67.95 / DP 68.85),沿 x 91.5/91.98 南下到 y 52,斜到 (89.6/90.5, 50.3) 下来进 U54 |
| pin 10 | 底 | 西行到 92.95,跟着 pair 5 北上,斜到 (92.5, 70.0) 进 C748;(92.5, 70.0) 上有一个孔接顶层 +3V3 干线 |
| pair 5 | 底 | 西行到 93.47/93.95 北上,y 75.50/75.98 东行到 110.30/110.78 南下,**y 59.2~61.6 翻顶层跨过 UP**,y 52.5/52.98 东行,x 160.1/162 南下进 U55 |
| UP | 底 | 南缘 pin58/59 从 C743/C745 之间的竖井下到 60.12/60.60,一路东行,x 205 抬到 61.25/61.75 进 J23 B6/A7 |
| +3V3 干线 | 顶 | U31 pin2 旁 (90.3, 61.65) 打孔上顶层,北上到 (90.4, 70.5)(与 pin5 的孔汇合),东去 (92.5, 70.0) 下来进 C748 |
### 三条关键约束
1. **0.5 脚距的车道之间打不了孔**:孔要离两边车道各 0.645,所以只有当邻居车道都拐走之后才能打。
北边的先拐(pair 5 → pin 10 → pair 4 打孔 → pair 3 → pin 5 → pair 2),像剥洋葱。
pair 4 的两个孔卡在 DP3 的横车道(y 67.25,要求孔心 ≥ 67.895)和 pin10 北上车道(x 92.95,要求孔心 x ≤ 92.305)之间,
所以 DP4 得先在 x 93.1→92.5 抬到 68.85 再打孔,两孔上下错 0.9。
2. **顶层只走 Inner1 是 GND 的地方**:Hub 周围 x ≥ 87(VO4 岛东界已从 89 收到 87)、y < 75.5(VO2 的 Hub 北横带以南)是一整片 GND;
pair 4 全程、pair 5 跨 UP 的那 2.4 mm、+3V3 干线都在这片里。pair 5 的跨越点 x 110~111.5 也在 VO4/VO5 之间的 GND 窗口里。
3. **平行 45° 斜段要保持 0.48 间距**:两根线各自的拐点不能同 x/同 y,第一拐**内侧**那根要早拐 0.2
(0.48·(√2−1)),否则斜段上的垂直间距只剩 0.34。文件里所有成对斜段都按这个写。
### 每个站点最后一段
U5n 朝 Hub 那排的 DP/DM 相距 1.9 mm,车道并排 0.48 到最后 1.4 mm 再用 45° 分开,
分开的那根一律是**转弯时的外侧**,所以到站不用交叉 —— 五对都核过了
(转弯规则:向右拐时左边那根变成外侧,连拐两次回到原样)。
"""
import math
L_TOP, L_BOT = 1, 2
W = 0.28
def chamfer(pts, r=0.35):
u"""把折线的拐点切成 45°。换层点(相邻两点层不同)不切。
⚠ 点的 layer/width 语义是「从这一点到下一点」,所以在拐点 b 处插入的
b1 用**前一段**的层/宽(它承担斜切段),b2 用 b 自己的。
"""
if len(pts) < 3:
return list(pts)
out = [pts[0]]
for i in range(1, len(pts) - 1):
a, b, c = pts[i - 1], pts[i], pts[i + 1]
if a[2] != b[2] or b[2] != c[2]: # 拐点两侧不同层 -> 这是过孔点
out.append(b)
continue
ax, ay = b[0] - a[0], b[1] - a[1]
cx, cy = c[0] - b[0], c[1] - b[1]
la, lc = math.hypot(ax, ay), math.hypot(cx, cy)
if la < 1e-9 or lc < 1e-9:
out.append(b)
continue
# 共线就不用切;**已经是 45° 的拐(正 + 斜)也不切** —— 再切一刀会变成两个 22.5°,
# 而且切出来的那一小段会向内侧挪 0.1 mm,实测把 DP4 的抬升段切进了 pin10 车道的净距圈(0.125)。
dot = max(-1.0, min(1.0, (ax * cx + ay * cy) / (la * lc)))
if math.degrees(math.acos(dot)) <= 46.0:
out.append(b)
continue
d = min(r, la / 2.0, lc / 2.0)
out.append((round(b[0] - ax / la * d, 4), round(b[1] - ay / la * d, 4), a[2], a[3]))
out.append((round(b[0] + cx / lc * d, 4), round(b[1] + cy / lc * d, 4), b[2], b[3]))
out.append(pts[-1])
return out
def P(*pts):
u"""(x, y[, layer[, width]]) -> 规范化四元组;层/宽省略时沿用上一点。"""
out, lay, w = [], L_BOT, W
for t in pts:
x, y = t[0], t[1]
lay = t[2] if len(t) > 2 else lay
w = t[3] if len(t) > 3 else w
out.append((round(x, 4), round(y, 4), lay, w))
return out
PAD_X = 95.55 # U32 西缘焊盘中心(焊盘 x 95.125~95.975,hh 0.15)
# 0.28 的线直着从 0.5 脚距的焊盘出来,两边净距 0.21,不用让位。
# ══════════ pair 1 → U51(西北远处)══════════
# 直着西行:y 64.25/64.75 从 x 95.55 到 x 22~23 一路无障碍(U31 pin1 顶 63.35、J31 底排焊盘底 65.38、
# R721/R722 顶 61.77、U34 顶 ~62)。西行时 DM 在南 = 左侧,右拐北上后左侧变西 ⇒ DM 走西车道 22.34(正对 U51.6),
# DP 在 22.82,到 y 84.3 再 45° 分到 24.24 进 U51.4。
SITE1 = {
'USB1_DM': [(PAD_X, 64.25), (22.34, 64.25), (22.34, 86.475)],
'USB1_DP': [(PAD_X, 64.75), (22.82, 64.75), (22.82, 84.30), (24.24, 85.72), (24.24, 86.475)],
}
# ══════════ pair 2 → U52(正北)══════════
# 西行到 x 89.27/89.75 右拐北上(DM 西)。北上车道左边是 J31 最东一列焊盘(x ≤ 86.98),右边是 pin5 的北上车道 90.4。
# 一直到 y 80 才 45° 斜向东北 —— 必须过了 pair 3 / pair 5 东行车道(y ≤ 76.97)之后才能往东挪,
# 而且要绕开 site2 的定位孔 H1 (89.39, 85.36)。斜段直接斜到各自焊盘正下方:DM 到 93.84、DP 一口气到 95.74。
# 内侧(DP,东)早拐 0.2 保持斜段 0.48 间距。
SITE2 = {
'USB2_DM': [(PAD_X, 65.25), (89.27, 65.25), (89.27, 80.00), (93.84, 84.57), (93.84, 86.475)],
'USB2_DP': [(PAD_X, 65.75), (89.75, 65.75), (89.75, 79.80), (95.74, 85.79), (95.74, 86.475)],
}
# ══════════ pair 3 → U53(东北)══════════
# 西行到 91.05/91.53 右拐北上(DM 西),再右拐东行(西车道变北车道 ⇒ DM 76.98 / DP 76.50),
# 这一段在 pair 5 东行车道(75.50/75.98)的北边;pair 5 在 x 110.78 南下之后,pair 3 在 x 112 压到 75.40/75.88 ——
# 因为 MH (148.1, 79.5) 的 Ø6 焊盘底在 76.5,北车道上沿必须 ≤ 76.16。
# 到 x 165 左拐北上:北车道(DM)是内侧变西 ⇒ DM 165.34 正对 U53.6,DP 在 165.82 到 y 84.6 再分到 167.24。
# 下压的斜段:第一拐向右(东→东南),内侧是南车道 DP,早拐 0.2。
SITE3 = {
'USB3_DM': [(PAD_X, 66.75), (91.05, 66.75), (91.05, 76.98), (112.00, 76.98), (113.10, 75.88),
(165.34, 75.88), (165.34, 86.475)],
'USB3_DP': [(PAD_X, 67.25), (91.53, 67.25), (91.53, 76.50), (111.80, 76.50), (112.90, 75.40),
(165.82, 75.40), (165.82, 84.60), (167.24, 86.02), (167.24, 86.475)],
}
# ══════════ pair 4 → U54(西南)—— 全程顶层 ══════════
# 它北边是 pin10(要跟 pair 5 北上)、南边是 pair 3(北上)、pair 2、pair 1 全都横在西边 —— 底层无路,
# 一出焊盘就打孔上顶层。两个孔:DM4 (92.2, 67.95)(离 DP3 横车道 67.39 有 0.255)、DP4 (92.2, 68.85)(上下错 0.9),
# DP4 先在 x 93.1→92.5 斜抬到 68.85 再西行到孔,避开 DM4 的孔(孔顶 68.255 vs 车道底 68.11 才 0.055)。
# 顶层:DP 西车道 91.5 / DM 91.98 南下到 y 52,45° 斜到 (89.6, 50.3) / (90.5, 50.3) 打孔回底层,
# 进 U54.4 (88.6, 48.149) / U54.6 (90.5, 48.149)。DM 的孔正对焊盘、直下;DP 的孔再斜 1 mm 到 88.6。
# 顶层这一段底下 Inner1:x ≥ 87 全是 GND(VO4 岛东界 87、site4 岛 x ≤ 84.75、VO2 从 y 75.5 起)。
SITE4 = {
'USB4_DM': [(PAD_X, 67.75), (92.60, 67.75), (92.40, 67.95), (92.20, 67.95),
(92.20, 67.95, L_TOP), (91.98, 67.73), (91.98, 52.00), (90.50, 50.52), (90.50, 50.30),
(90.50, 50.30, L_BOT), (90.50, 48.685)],
'USB4_DP': [(PAD_X, 68.25), (93.10, 68.25), (92.50, 68.85), (92.20, 68.85),
(92.20, 68.85, L_TOP), (91.50, 68.85), (91.50, 52.50), (89.60, 50.60), (89.60, 50.30),
(89.60, 50.30, L_BOT), (88.60, 49.30), (88.60, 48.685)],
}
# ══════════ pair 5 → U55(东南)—— 绕 Hub 北边走 ══════════
# 最北的一对,出来就右拐北上(DM 西 93.47 / DP 东 93.95;DP 的东沿离 C751 焊盘 94.295 只剩 0.205,
# 再东一点 pin13 的 GND 就打不了孔了 —— 给它留的孔位是 (94.6, 71.4))。
# 北上到 75.50/75.98(C751 顶 74.974,DP 拐角的 45° 切角离它 0.24)右拐东行(DM 北),到 x 110.30/110.78 右拐南下(南车道 DP 是内侧变西 ⇒ DP 110.30)。
# 南下时 x 110.3 离 R711/R716/R720 的东沿 109.62 有 0.54、离 J64 焊盘 107.43 有 2.7。
# **y 59.2~61.6 翻到顶层跨过 UP 的两根横线**(60.12/60.60):DP 的孔在 110.30,DM 的孔错到 111.3(两孔 1.0)。
# 到 y 52.5/52.98 左拐东行(东车道 DM 是内侧变北 ⇒ DM 52.98),DP 在 x 160.10 南下进 U55.4,DM 到 162.00 进 U55.6。
SITE5 = {
'USB5_DM': [(PAD_X, 69.25), (93.47, 69.25), (93.47, 75.98), (110.78, 75.98),
(110.78, 62.60), (111.30, 62.08), (111.30, 61.60),
(111.30, 61.60, L_TOP), (111.30, 59.20),
(111.30, 59.20, L_BOT), (111.30, 58.72), (110.78, 58.20),
(110.78, 52.98), (162.00, 52.98), (162.00, 48.685)],
'USB5_DP': [(PAD_X, 69.75), (93.95, 69.75), (93.95, 75.50), (110.30, 75.50),
(110.30, 61.60),
(110.30, 61.60, L_TOP), (110.30, 59.20),
(110.30, 59.20, L_BOT), (110.30, 52.50), (160.10, 52.50), (160.10, 48.685)],
}
# ══════════ UPSTREAM → J23(U32 南缘 → 板子正东)══════════
# pin59 UP_DP (98.75, 63.55) / pin58 UP_DM (99.25, 63.55) 直着南下,从 C743(x ≤ 97.62)与 C745(x ≥ 99.78)之间的
# 竖井下到 y 60.12/60.60(那排 0402 的焊盘底 60.98),一路东行到 x 205 —— 中间 y 60~61 从 x 99 到 208 全空。
# J23(底层 rot 270):B6 UP_DP 61.25 / A7 UP_DM 61.75 / A6 UP_DP 62.25 / B7 UP_DM 62.75,B5 CC2 在 60.25、A8 空脚 60.75。
# 车道压在 60.17 会把 B5 的 CC2 走线堵死(R718 在 59.5),所以 x 205 就抬到 61.25/61.75 平着进 B6/A7。
# 抬升是左拐再右拐,第一拐内侧是北车道 DM ⇒ DM 早拐 0.2。
UP = {
'UP_DP': [(98.75, 63.55), (98.75, 60.12), (205.00, 60.12), (206.13, 61.25), (212.392, 61.25)],
'UP_DM': [(99.25, 63.55), (99.25, 60.60), (204.80, 60.60), (205.95, 61.75), (212.392, 61.75)],
}
# ══════════ J23 的 A6↔B6 / A7↔B7 短接(USB-C 正反插的一对重复脚)══════════
# 几何照抄 v3(那一版过了 DRC),只是 J23 翻到底层之后网络对调:61.25↔62.25 这一对现在是 UP_DP(B6/A6),
# 61.75↔62.75 是 UP_DM(A7/B7);出线在底层,跨越段翻到**顶层**。
# ⚠ 表贴焊盘只到 x=213.04,过孔要离它 0.505 ⇒ 最西只能放到 213.55;出线前 0.65 mm 必须平着走(阵列上方起坡会蹭隔壁脚)。
# 另一根的 61.75 横线离 (213.85, 62.25) 那个孔只有 0.165 —— 这是同一对的两根(UP_DP vs UP_DM),按对内 0.15 判。
J23_TIE = {
'UP_DP#2': [(212.392, 62.25, L_BOT, W), (213.85, 62.25), (213.85, 62.25, L_TOP),
(213.85, 60.70), (213.85, 60.70, L_BOT), (213.50, 61.25),
(213.20, 61.25), (212.392, 61.25)],
'UP_DM#2': [(212.392, 61.75, L_BOT, W), (213.20, 61.75), (213.50, 61.75),
(214.25, 61.30), (214.75, 61.30), (214.75, 61.30, L_TOP),
(214.75, 63.20), (214.75, 63.20, L_BOT), (214.25, 63.20),
(213.50, 62.75), (213.20, 62.75), (212.392, 62.75)],
}
# ══════════ Hub 西缘 pin5 / pin10 的 +3V3(被 D± 夹住,只能在这里一起画)══════════
# 后面的信号布线器(pcb2_route_sig.py)把这些线当**已有障碍**,别删。
# pin5 (66.25) 夹在 DP2 (65.75) 与 DM3 (66.75) 之间:西行到 90.4(pair 3 拐走之后)跟着北上,到 (90.4, 70.5) 打孔翻顶层
# —— 孔离 DP2 车道 89.75 / DM3 车道 91.05 各 0.65。顶层西去 (88.2, 70.5) 再下来进 C747(底层 rot 90,x 88.2,
# 在 J31 最东列 86.98 与 DM2 车道 89.27 之间)。(88.2, 70.5) 的孔离 VO2 岛东界 87.3 有 0.9,反焊盘不碰岛。
# pin10 (68.75) 夹在 DP4 (68.25) 与 DM5 (69.25) 之间:西行到 92.95 跟着 pair 5 北上(离 DM5 车道 0.24、离 pair 4 的孔 0.305),
# y 69.55 起 45° 斜到 (92.5, 70.0),直进 C748(底层 rot 90,x 92.5,夹在 DP3 车道 91.53 与 DM5 车道 93.47 中间)。
# +3V3 干线:U31 pin2(+3V3 输出,(90.47, 60.5) 焊盘 y ≤ 61.05)旁 (90.3, 61.65) 打孔上顶层,沿 x 90.3 北上
# (pair 4 的顶层车道在 91.5 以东),到 (90.4, 70.5) 与 pin5 的孔汇合,再东去 (92.5, 70.0) 打孔下来进 C748。
# 顶层这三段底下 Inner1 都是 GND(x 87.3~100.5、y 65.15~75.5 之间没有任何岛)。
# 电容的哪个焊盘是 +3V3 由 build() 从障碍表里查,不写死。
V_P5, V_P5W = (90.40, 70.50), (88.20, 70.50)
V_P10 = (92.50, 70.00)
V_TRUNK = (90.30, 61.65)
def _pad(pads, ref, net):
for p in pads:
if p['ref'] == ref and p['net'] == net:
return (p['x'], p['y'])
raise SystemExit(u'障碍表里找不到 %s 的 %s 焊盘(先 --dump)' % (ref, net))
def power_lanes(pads):
c747 = _pad(pads, 'C747', '+3V3_HUB')
c748 = _pad(pads, 'C748', '+3V3_HUB')
for p in pads: # 干线起点必须真的落在 U31 pin2(+3V3 输出)里
if p['ref'] == 'U31' and p['num'] == '2':
assert abs(p['x'] - 90.47) < 0.05 and abs(p['y'] - 60.5) < 0.05, u'U31 pin2 挪了,改 V_TRUNK'
return {
'+3V3_HUB#p5': [(PAD_X, 66.25), (90.40, 66.25), (V_P5[0], V_P5[1]),
(V_P5[0], V_P5[1], L_TOP), (V_P5W[0], V_P5W[1]),
(V_P5W[0], V_P5W[1], L_BOT), (c747[0], c747[1])],
'+3V3_HUB#p10': [(PAD_X, 68.75), (92.95, 68.75), (92.95, 69.55), (V_P10[0], V_P10[1]),
(c748[0], c748[1])],
'+3V3_HUB#trunk': [(90.30, 61.00), (V_TRUNK[0], V_TRUNK[1]),
(V_TRUNK[0], V_TRUNK[1], L_TOP), (90.30, 70.40), (V_P5[0], V_P5[1]),
(V_P10[0], V_P10[1]), (V_P10[0], V_P10[1], L_BOT), (c748[0], c748[1])],
}
def build(pads=None):
out = {}
for grp in (SITE1, SITE2, SITE3, SITE4, SITE5, UP, J23_TIE):
for net, pts in grp.items():
out[net] = chamfer(P(*pts))
if pads is not None:
for net, pts in power_lanes(pads).items():
out[net] = chamfer(P(*pts))
return out
# -*- coding: utf-8 -*-
u"""六对**长差分**(USB1~5 的 U32↔U5n + UPSTREAM 的 U32↔J23)+ Hub 西缘被 D± 夹住的两条 +3V3 车道。
**v5 版(2026-09-06,载板全部底层、接口边朝板边)。** v3 版存档在 `obsolete/pcb2_dp_long_v3_2026-09-06.py`。
被 `pcb2_route_dp.py` import;校核 / 下发都用那个脚本,这里只放坐标。
## 出发点:U32 西缘的 12 条车道
U32(QFN-64,**底层**,中心 (100, 68))的 D± 全在**西缘 x 95.55**,0.5 脚距、南→北:
1DM 64.25 | 1DP 64.75 | 2DM 65.25 | 2DP 65.75 | pin5 +3V3 66.25 |
3DM 66.75 | 3DP 67.25 | 4DM 67.75 | 4DP 68.25 | pin10 +3V3 68.75 |
5DM 69.25 | 5DP 69.75 | pin13 GND 70.25 | 14~16 NC
目的地却是四面八方:1→西北远处 (U51 22/24, 87)、2→正北 (U52 94/96, 87)、3→东北 (U53 165/167, 87)、
4→西南 (U54 88.6/90.5, 48)、5→东南 (U55 160/162, 48)、UP→正东 (J23 212, 61~62)。
### 定下来的走法(理由都在代码注释里)
| 车道 | 层 | 走法 |
|---|---|---|
| pair 1 | 底 | 直着西行 y 64.25/64.75 到 x 22.34/22.82,北上进 U51 |
| pair 2 | 底 | 西行到 89.27/89.75 北上,y 80 处 45° 斜到 93.84/95.74 进 U52 |
| pin 5 | 底→顶 | 西行到 90.4,跟着 pair 3 北上到 70.5 打孔翻顶层,西去 (88.2, 70.5) 再下来进 C747 |
| pair 3 | 底 | 西行到 91.05/91.53 北上,y 76.50/76.98 东行,过了 pair 5 之后压到 75.40/75.88 从 MH 焊盘下面钻过去,x 165 北上 |
| pair 4 | **顶** | 西行到 x 92.2 打孔上顶层(DM 67.95 / DP 68.85),沿 x 91.5/91.98 南下到 y 52,斜到 (89.6/90.5, 50.3) 下来进 U54 |
| pin 10 | 底 | 西行到 92.95,跟着 pair 5 北上,斜到 (92.5, 70.0) 进 C748;(92.5, 70.0) 上有一个孔接顶层 +3V3 干线 |
| pair 5 | 底 | 西行到 93.47/93.95 北上,y 75.50/75.98 东行到 110.30/110.78 南下,**y 59.2~61.6 翻顶层跨过 UP**,y 52.5/52.98 东行,x 160.1/162 南下进 U55 |
| UP | 底 | 南缘 pin58/59 从 C743/C745 之间的竖井下到 60.12/60.60,一路东行,x 205 抬到 61.25/61.75 进 J23 B6/A7 |
| +3V3 干线 | 顶 | U31 pin2 旁 (90.3, 61.65) 打孔上顶层,北上到 (90.4, 70.5)(与 pin5 的孔汇合),东去 (92.5, 70.0) 下来进 C748 |
### 三条关键约束
1. **0.5 脚距的车道之间打不了孔**:孔要离两边车道各 0.645,所以只有当邻居车道都拐走之后才能打。
北边的先拐(pair 5 → pin 10 → pair 4 打孔 → pair 3 → pin 5 → pair 2),像剥洋葱。
pair 4 的两个孔卡在 DP3 的横车道(y 67.25,要求孔心 ≥ 67.895)和 pin10 北上车道(x 92.95,要求孔心 x ≤ 92.305)之间,
所以 DP4 得先在 x 93.1→92.5 抬到 68.85 再打孔,两孔上下错 0.9。
2. **顶层只走 Inner1 是 GND 的地方**:Hub 周围 x ≥ 87(VO4 岛东界已从 89 收到 87)、y < 75.5(VO2 的 Hub 北横带以南)是一整片 GND;
pair 4 全程、pair 5 跨 UP 的那 2.4 mm、+3V3 干线都在这片里。pair 5 的跨越点 x 110~111.5 也在 VO4/VO5 之间的 GND 窗口里。
3. **平行 45° 斜段要保持 0.48 间距**:两根线各自的拐点不能同 x/同 y,第一拐**内侧**那根要早拐 0.2
(0.48·(√2−1)),否则斜段上的垂直间距只剩 0.34。文件里所有成对斜段都按这个写。
### 每个站点最后一段
U5n 朝 Hub 那排的 DP/DM 相距 1.9 mm,车道并排 0.48 到最后 1.4 mm 再用 45° 分开,
分开的那根一律是**转弯时的外侧**,所以到站不用交叉 —— 五对都核过了
(转弯规则:向右拐时左边那根变成外侧,连拐两次回到原样)。
"""
import math
import sys as _s, os as _o; _s.path.insert(0, _o.path.dirname(_o.path.abspath(__file__)))
KSEP = chr(0) # 路径 key 的分隔符,与 pcb2_route_dp.KSEP 同值(不能 import:route_dp --dump 会反过来 import 本模块,循环)
L_TOP, L_BOT = 1, 2
W = 0.28
def chamfer(pts, r=0.35):
u"""把折线的拐点切成 45°。换层点(相邻两点层不同)不切。
⚠ 点的 layer/width 语义是「从这一点到下一点」,所以在拐点 b 处插入的
b1 用**前一段**的层/宽(它承担斜切段),b2 用 b 自己的。
"""
if len(pts) < 3:
return list(pts)
out = [pts[0]]
for i in range(1, len(pts) - 1):
a, b, c = pts[i - 1], pts[i], pts[i + 1]
if a[2] != b[2] or b[2] != c[2]: # 拐点两侧不同层 -> 这是过孔点
out.append(b)
continue
ax, ay = b[0] - a[0], b[1] - a[1]
cx, cy = c[0] - b[0], c[1] - b[1]
la, lc = math.hypot(ax, ay), math.hypot(cx, cy)
if la < 1e-9 or lc < 1e-9:
out.append(b)
continue
# 共线就不用切;**已经是 45° 的拐(正 + 斜)也不切** —— 再切一刀会变成两个 22.5°,
# 而且切出来的那一小段会向内侧挪 0.1 mm,实测把 DP4 的抬升段切进了 pin10 车道的净距圈(0.125)。
dot = max(-1.0, min(1.0, (ax * cx + ay * cy) / (la * lc)))
if math.degrees(math.acos(dot)) <= 46.0:
out.append(b)
continue
d = min(r, la / 2.0, lc / 2.0)
out.append((round(b[0] - ax / la * d, 4), round(b[1] - ay / la * d, 4), a[2], a[3]))
out.append((round(b[0] + cx / lc * d, 4), round(b[1] + cy / lc * d, 4), b[2], b[3]))
out.append(pts[-1])
return out
def P(*pts):
u"""(x, y[, layer[, width]]) -> 规范化四元组;层/宽省略时沿用上一点。"""
out, lay, w = [], L_BOT, W
for t in pts:
x, y = t[0], t[1]
lay = t[2] if len(t) > 2 else lay
w = t[3] if len(t) > 3 else w
out.append((round(x, 4), round(y, 4), lay, w))
return out
PAD_X = 95.55 # U32 西缘焊盘中心(焊盘 x 95.125~95.975,hh 0.15)
# 0.28 的线直着从 0.5 脚距的焊盘出来,两边净距 0.21,不用让位。
# ══════════ pair 1 → U51(西北远处)══════════
# 直着西行:y 64.25/64.75 从 x 95.55 到 x 22~23 一路无障碍(U31 pin1 顶 63.35、J31 底排焊盘底 65.38、
# R721/R722 顶 61.77、U34 顶 ~62)。西行时 DM 在南 = 左侧,右拐北上后左侧变西 ⇒ DM 走西车道 22.34(正对 U51.6),
# DP 在 22.82,到 y 84.3 再 45° 分到 24.24 进 U51.4。
SITE1 = {
'USB1_DM': [(PAD_X, 64.25), (22.34, 64.25), (22.34, 86.475)],
'USB1_DP': [(PAD_X, 64.75), (22.82, 64.75), (22.82, 84.30), (24.24, 85.72), (24.24, 86.475)],
}
# ══════════ pair 2 → U52(正北)══════════
# 西行到 x 89.27/89.75 右拐北上(DM 西)。北上车道左边是 J31 最东一列焊盘(x ≤ 86.98),右边是 pin5 的北上车道 90.4。
# 一直到 y 80 才 45° 斜向东北 —— 必须过了 pair 3 / pair 5 东行车道(y ≤ 76.97)之后才能往东挪,
# 而且要绕开 site2 的定位孔 H1 (89.39, 85.36)。斜段直接斜到各自焊盘正下方:DM 到 93.84、DP 一口气到 95.74。
# 内侧(DP,东)早拐 0.2 保持斜段 0.48 间距。
SITE2 = {
'USB2_DM': [(PAD_X, 65.25), (89.27, 65.25), (89.27, 80.00), (93.84, 84.57), (93.84, 86.475)],
'USB2_DP': [(PAD_X, 65.75), (89.75, 65.75), (89.75, 79.80), (95.74, 85.79), (95.74, 86.475)],
}
# ══════════ pair 3 → U53(东北)══════════
# 西行到 91.05/91.53 右拐北上(DM 西),再右拐东行(西车道变北车道 ⇒ DM 76.98 / DP 76.50),
# 这一段在 pair 5 东行车道(75.50/75.98)的北边;pair 5 在 x 110.78 南下之后,pair 3 在 x 112 压到 75.40/75.88 ——
# 因为 MH (148.1, 79.5) 的 Ø6 焊盘底在 76.5,北车道上沿必须 ≤ 76.16。
# 到 x 165 左拐北上:北车道(DM)是内侧变西 ⇒ DM 165.34 正对 U53.6,DP 在 165.82 到 y 84.6 再分到 167.24。
# 下压的斜段:第一拐向右(东→东南),内侧是南车道 DP,早拐 0.2。
SITE3 = {
'USB3_DM': [(PAD_X, 66.75), (91.05, 66.75), (91.05, 76.98), (112.00, 76.98), (113.10, 75.88),
(165.34, 75.88), (165.34, 86.475)],
'USB3_DP': [(PAD_X, 67.25), (91.53, 67.25), (91.53, 76.50), (111.80, 76.50), (112.90, 75.40),
(165.82, 75.40), (165.82, 84.60), (167.24, 86.02), (167.24, 86.475)],
}
# ══════════ pair 4 → U54(西南)—— 全程顶层 ══════════
# 它北边是 pin10(要跟 pair 5 北上)、南边是 pair 3(北上)、pair 2、pair 1 全都横在西边 —— 底层无路,
# 一出焊盘就打孔上顶层。两个孔:DM4 (92.2, 67.95)(离 DP3 横车道 67.39 有 0.255)、DP4 (92.2, 68.85)(上下错 0.9),
# DP4 先在 x 93.1→92.5 斜抬到 68.85 再西行到孔,避开 DM4 的孔(孔顶 68.255 vs 车道底 68.11 才 0.055)。
# 顶层:DP 西车道 91.5 / DM 91.98 南下到 y 52,45° 斜到 (89.6, 50.3) / (90.5, 50.3) 打孔回底层,
# 进 U54.4 (88.6, 48.149) / U54.6 (90.5, 48.149)。DM 的孔正对焊盘、直下;DP 的孔再斜 1 mm 到 88.6。
# 顶层这一段底下 Inner1:x ≥ 87 全是 GND(VO4 岛东界 87、site4 岛 x ≤ 84.75、VO2 从 y 75.5 起)。
SITE4 = {
'USB4_DM': [(PAD_X, 67.75), (92.60, 67.75), (92.40, 67.95), (92.20, 67.95),
(92.20, 67.95, L_TOP), (91.98, 67.73), (91.98, 52.00), (90.50, 50.52), (90.50, 50.30),
(90.50, 50.30, L_BOT), (90.50, 48.685)],
'USB4_DP': [(PAD_X, 68.25), (93.10, 68.25), (92.50, 68.85), (92.20, 68.85),
(92.20, 68.85, L_TOP), (91.50, 68.85), (91.50, 52.50), (89.60, 50.60), (89.60, 50.30),
(89.60, 50.30, L_BOT), (88.60, 49.30), (88.60, 48.685)],
}
# ══════════ pair 5 → U55(东南)—— 绕 Hub 北边走 ══════════
# 最北的一对,出来就右拐北上(DM 西 93.47 / DP 东 93.95;DP 的东沿离 C751 焊盘 94.295 只剩 0.205,
# 再东一点 pin13 的 GND 就打不了孔了 —— 给它留的孔位是 (94.6, 71.4))。
# 北上到 75.50/75.98(C751 顶 74.974,DP 拐角的 45° 切角离它 0.24)右拐东行(DM 北),到 x 110.30/110.78 右拐南下(南车道 DP 是内侧变西 ⇒ DP 110.30)。
# 南下时 x 110.3 离 R711/R716/R720 的东沿 109.62 有 0.54、离 J64 焊盘 107.43 有 2.7。
# **y 59.2~61.6 翻到顶层跨过 UP 的两根横线**(60.12/60.60):DP 的孔在 110.30,DM 的孔错到 111.3(两孔 1.0)。
# 到 y 52.5/52.98 左拐东行(东车道 DM 是内侧变北 ⇒ DM 52.98),DP 在 x 160.10 南下进 U55.4,DM 到 162.00 进 U55.6。
SITE5 = {
'USB5_DM': [(PAD_X, 69.25), (93.47, 69.25), (93.47, 75.98), (110.78, 75.98),
(110.78, 62.60), (111.30, 62.08), (111.30, 61.60),
(111.30, 61.60, L_TOP), (111.30, 59.20),
(111.30, 59.20, L_BOT), (111.30, 58.72), (110.78, 58.20),
(110.78, 52.98), (162.00, 52.98), (162.00, 48.685)],
'USB5_DP': [(PAD_X, 69.75), (93.95, 69.75), (93.95, 75.50), (110.30, 75.50),
(110.30, 61.60),
(110.30, 61.60, L_TOP), (110.30, 59.20),
(110.30, 59.20, L_BOT), (110.30, 52.50), (160.10, 52.50), (160.10, 48.685)],
}
# ══════════ UPSTREAM → J23(U32 南缘 → 板子正东)══════════
# pin59 UP_DP (98.75, 63.55) / pin58 UP_DM (99.25, 63.55) 直着南下,从 C743(x ≤ 97.62)与 C745(x ≥ 99.78)之间的
# 竖井下到 y 60.12/60.60(那排 0402 的焊盘底 60.98),一路东行到 x 205 —— 中间 y 60~61 从 x 99 到 208 全空。
# J23(底层 rot 270):B6 UP_DP 61.25 / A7 UP_DM 61.75 / A6 UP_DP 62.25 / B7 UP_DM 62.75,B5 CC2 在 60.25、A8 空脚 60.75。
# 车道压在 60.17 会把 B5 的 CC2 走线堵死(R718 在 59.5),所以 x 205 就抬到 61.25/61.75 平着进 B6/A7。
# 抬升是左拐再右拐,第一拐内侧是北车道 DM ⇒ DM 早拐 0.2。
UP = {
'UP_DP': [(98.75, 63.55), (98.75, 60.12), (205.00, 60.12), (206.13, 61.25), (212.392, 61.25)],
'UP_DM': [(99.25, 63.55), (99.25, 60.60), (204.80, 60.60), (205.95, 61.75), (212.392, 61.75)],
}
# ══════════ J23 的 A6↔B6 / A7↔B7 短接(USB-C 正反插的一对重复脚)══════════
# 几何照抄 v3(那一版过了 DRC),只是 J23 翻到底层之后网络对调:61.25↔62.25 这一对现在是 UP_DP(B6/A6),
# 61.75↔62.75 是 UP_DM(A7/B7);出线在底层,跨越段翻到**顶层**。
# ⚠ 表贴焊盘只到 x=213.04,过孔要离它 0.505 ⇒ 最西只能放到 213.55;出线前 0.65 mm 必须平着走(阵列上方起坡会蹭隔壁脚)。
# 另一根的 61.75 横线离 (213.85, 62.25) 那个孔只有 0.165 —— 这是同一对的两根(UP_DP vs UP_DM),按对内 0.15 判。
J23_TIE = {
'UP_DP' + KSEP + '2': [(212.392, 62.25, L_BOT, W), (213.85, 62.25), (213.85, 62.25, L_TOP),
(213.85, 60.70), (213.85, 60.70, L_BOT), (213.50, 61.25),
(213.20, 61.25), (212.392, 61.25)],
'UP_DM' + KSEP + '2': [(212.392, 61.75, L_BOT, W), (213.20, 61.75), (213.50, 61.75),
(214.25, 61.30), (214.75, 61.30), (214.75, 61.30, L_TOP),
(214.75, 63.20), (214.75, 63.20, L_BOT), (214.25, 63.20),
(213.50, 62.75), (213.20, 62.75), (212.392, 62.75)],
}
# ══════════ Hub 西缘 pin5 / pin10 的 +3V3(被 D± 夹住,只能在这里一起画)══════════
# 后面的信号布线器(pcb2_route_sig.py)把这些线当**已有障碍**,别删。
# pin5 (66.25) 夹在 DP2 (65.75) 与 DM3 (66.75) 之间:西行到 90.4(pair 3 拐走之后)跟着北上,到 (90.4, 70.5) 打孔翻顶层
# —— 孔离 DP2 车道 89.75 / DM3 车道 91.05 各 0.65。顶层西去 (88.2, 70.5) 再下来进 C747(底层 rot 90,x 88.2,
# 在 J31 最东列 86.98 与 DM2 车道 89.27 之间)。(88.2, 70.5) 的孔离 VO2 岛东界 87.3 有 0.9,反焊盘不碰岛。
# pin10 (68.75) 夹在 DP4 (68.25) 与 DM5 (69.25) 之间:西行到 92.95 跟着 pair 5 北上(离 DM5 车道 0.24、离 pair 4 的孔 0.305),
# y 69.55 起 45° 斜到 (92.5, 70.0),直进 C748(底层 rot 90,x 92.5,夹在 DP3 车道 91.53 与 DM5 车道 93.47 中间)。
# +3V3 干线:U31 pin2(+3V3 输出,(90.47, 60.5) 焊盘 y ≤ 61.05)旁 (90.3, 61.65) 打孔上顶层,沿 x 90.3 北上
# (pair 4 的顶层车道在 91.5 以东),到 (90.4, 70.5) 与 pin5 的孔汇合,再东去 (92.5, 70.0) 打孔下来进 C748。
# 顶层这三段底下 Inner1 都是 GND(x 87.3~100.5、y 65.15~75.5 之间没有任何岛)。
# 电容的哪个焊盘是 +3V3 由 build() 从障碍表里查,不写死。
V_P5, V_P5W = (90.40, 70.50), (88.20, 70.50)
V_P10 = (92.50, 70.00)
V_TRUNK = (90.30, 61.65)
def _pad(pads, ref, net):
for p in pads:
if p['ref'] == ref and p['net'] == net:
return (p['x'], p['y'])
raise SystemExit(u'障碍表里找不到 %s 的 %s 焊盘(先 --dump)' % (ref, net))
def power_lanes(pads):
c747 = _pad(pads, 'C747', '+3V3_HUB')
c748 = _pad(pads, 'C748', '+3V3_HUB')
for p in pads: # 干线起点必须真的落在 U31 pin2(+3V3 输出)里
if p['ref'] == 'U31' and p['num'] == '2':
assert abs(p['x'] - 90.47) < 0.05 and abs(p['y'] - 60.5) < 0.05, u'U31 pin2 挪了,改 V_TRUNK'
return {
'+3V3_HUB' + KSEP + 'p5': [(PAD_X, 66.25), (90.40, 66.25), (V_P5[0], V_P5[1]),
(V_P5[0], V_P5[1], L_TOP), (V_P5W[0], V_P5W[1]),
(V_P5W[0], V_P5W[1], L_BOT), (c747[0], c747[1])],
'+3V3_HUB' + KSEP + 'p10': [(PAD_X, 68.75), (92.95, 68.75), (92.95, 69.55), (V_P10[0], V_P10[1]),
(c748[0], c748[1])],
'+3V3_HUB' + KSEP + 'trunk': [(90.30, 61.00), (V_TRUNK[0], V_TRUNK[1]),
(V_TRUNK[0], V_TRUNK[1], L_TOP), (90.30, 70.40), (V_P5[0], V_P5[1]),
(V_P10[0], V_P10[1]), (V_P10[0], V_P10[1], L_BOT), (c748[0], c748[1])],
}
def build(pads=None):
out = {}
for grp in (SITE1, SITE2, SITE3, SITE4, SITE5, UP, J23_TIE):
for net, pts in grp.items():
out[net] = chamfer(P(*pts))
if pads is not None:
for net, pts in power_lanes(pads).items():
out[net] = chamfer(P(*pts))
return out
+3 -3
查看文件
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
u"""载板 PCB2-CARRIER 的扇出 —— 复用下板 `pcb_fanout.py` 的规划器,只换掉板子相关的常量。
/c/Python314/python .claude/dump_pads.py f126ffb29c92f9b2 pcb2-copper.json # 1. 抓障碍表
/c/Python314/python .claude/dump_pads.py 942ed1560d81c1e7 pcb2-copper.json # 1. 抓障碍表
/c/Python314/python .claude/pcb2_fanout.py # 2. 只规划
/c/Python314/python .claude/pcb2_fanout.py --push # 3. 下发
@@ -47,7 +47,7 @@ F = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(F)
# ── 换掉板子相关的常量 ────────────────────────────────────────────────
F.PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
F.PCB = '942ed1560d81c1e7' # PCB2-CARRIER
F.SRC = os.path.join(HERE, 'pcb2-copper.json')
F.BOARD = (0.0, 0.0, 220.0, 135.0)
F.R_CORNER = 5.0
@@ -168,7 +168,7 @@ def clean2():
def main():
if not os.path.exists(F.SRC):
sys.exit(u'先抓障碍表:/c/Python314/python .claude/dump_pads.py '
u'f126ffb29c92f9b2 pcb2-copper.json')
u'942ed1560d81c1e7 pcb2-copper.json')
print(u'载板扇出:板框 %.0f × %.0f,元件面 %s,GND -> layer %d'
% (F.BOARD[2], F.BOARD[3], F.FANOUT_LAYERS, F.TARGET['GND']))
if SKIP_HDR:
+274 -255
查看文件
@@ -1,255 +1,274 @@
# -*- coding: utf-8 -*-
u"""补上 §25.8 遗留的**悬空 GND 焊盘** —— 自己找出来,自己打孔。
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
/c/Python314/python .claude/pcb2_gnd_patch.py # 2. 找悬空 + 选孔位 + 净距校核
/c/Python314/python .claude/pcb2_gnd_patch.py --check # 只问「现在板上还有没有悬空的」
/c/Python314/python .claude/pcb2_gnd_patch.py --push # 3. 下发(**只加不删**)
/c/Python314/python .claude/pcb2_pour.py --push # 4. 必须重铺
## 「悬空」是自己算的,不是抄 DRC
把 GND 网络的**焊盘 / 过孔 / 导线**当图,两两相触连边(判据同 `pcb2_net_conn.py`),
一个连通域里只要有**一个过孔或一个通孔焊盘**,它就落到 Inner1/Inner2 的地平面上 = 连通。
**一个都没有的连通域就是悬空的** —— 那是纯顶层/底层的一小块孤铜。
⇒ 这个判据是幂等的:补完孔再跑一次,找到 0 个。
## 和 `pcb2_fanout.py` 的分工
`pcb_fanout` 是**批量**扇出(210 个孔),它对每个焊盘只试固定的几个方位,
挤不下就跳过 —— §25.8 里剩下的这几个就是被它跳过的。
这里换成和 `pcb2_vo_vias.py` 同一套搜索:8 个方向 × 距离 × 线宽阶梯,逐个试到过为止。
## 下发**不能**用 `pcb2_route_dp.push()`
那个函数会先把 net 上**所有**线和过孔删掉再重画 —— 对 GND 就是把 210 个扇出孔
连同 BM24 的两条梳子全部抹掉。这里自己写一段只动**本脚本自己那几个对象**的 JS。
## ⚠️ 幂等靠 `_pcb2_gnd_patch.json` 这份「我上次画了啥」的清单
GND 上有两百多个别人的过孔,没法按网络整网重画,所以本脚本把自己下发的
过孔/导线坐标记在 `_pcb2_gnd_patch.json` 里,下次 `--push` 先按坐标删掉它们再重画。
**不记账的代价实测过一次**:改完选点逻辑直接再 `--push`,板上就多出一对
旧坐标的过孔(425 而不是 423),而且新画的短线被 EasyEDA 并进了旧的粗线里 ——
DRC 照样报错,看上去像"改了没生效"。
"""
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb2_route_dp as R
from pcb2_vo_vias import holes, local, place
NET = 'GND'
STATE = os.path.join(HERE, '_pcb2_gnd_patch.json')
W_STEPS = [0.40, 0.30, 0.25, 0.20] # GND 没有专门的线宽规则,扇出历来用 0.30
EPS = 1e-6
def clusters(pads, vias, lines):
u"""GND 的连通域;返回**悬空的**那些(域里一个过孔/通孔焊盘都没有)。"""
items = []
for p in pads:
if p['net'] == NET:
items.append(('pad', set((1, 2)) if p['lay'] == 12 else set((p['lay'],)), p))
for v in vias:
if v['net'] == NET:
items.append(('via', set((1, 2)), v))
for l in lines:
if l['net'] == NET:
items.append(('line', set((l['lay'],)), l))
n = len(items)
par = list(range(n))
def find(i):
while par[i] != i:
par[i] = par[par[i]]
i = par[i]
return i
def touch(a, b):
(ka, la, oa), (kb, lb, ob) = a, b
if not (la & lb):
return False
if ka == 'line' and kb == 'line':
return R.seg_seg((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
(ob['x0'], ob['y0']), (ob['x1'], ob['y1'])) <= (oa['w'] + ob['w']) / 2 + EPS
if ka == 'line' and kb == 'pad':
return R.seg_rect((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['w'] / 2 + EPS
if ka == 'line' and kb == 'via':
return R.seg_pt(oa['x0'], oa['y0'], oa['x1'], oa['y1'],
ob['x'], ob['y']) <= oa['w'] / 2 + ob['r'] + EPS
if ka == 'via' and kb == 'via':
return math.hypot(oa['x'] - ob['x'], oa['y'] - ob['y']) <= oa['r'] + ob['r'] + EPS
if ka == 'via' and kb == 'pad':
return R.seg_rect((oa['x'], oa['y']), (oa['x'], oa['y']),
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['r'] + EPS
if ka == 'pad' and kb == 'pad':
return False
return touch(b, a)
for i in range(n):
for j in range(i + 1, n):
if touch(items[i], items[j]):
a, b = find(i), find(j)
if a != b:
par[a] = b
comp = {}
for i in range(n):
comp.setdefault(find(i), []).append(i)
out = []
for _k, idx in comp.items():
if any(items[i][0] == 'via' or (items[i][0] == 'pad' and items[i][2]['lay'] == 12)
for i in idx):
continue
out.append([items[i] for i in idx])
return out
JS_ADD = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1800));
const SEGS = %s, VIAS = %s, MIL = %f, OLD = %s;
// 先按坐标删掉**上一次本脚本画的**那几个(清单在 _pcb2_gnd_patch.json)
const hit = (x, y) => OLD.some(t => Math.hypot(x/MIL - t[0], y/MIL - t[1]) < 0.02);
let dv = 0, dl = 0;
for (const v of await eda.pcb_PrimitiveVia.getAll())
if (v.getState_Net() === 'GND' && hit(v.getState_X(), v.getState_Y())) {
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++;
}
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Net() === 'GND'
&& (hit(l.getState_StartX(), l.getState_StartY()) || hit(l.getState_EndX(), l.getState_EndY()))) {
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;
}
let nl = 0, nv = 0;
for (const [net, lay, x0, y0, x1, y1, w] of SEGS) {
await eda.pcb_PrimitiveLine.create(net, lay, x0*MIL, y0*MIL, x1*MIL, y1*MIL, w*MIL); nl++;
}
for (const [net, x, y] of VIAS) {
await eda.pcb_PrimitiveVia.create(net, x*MIL, y*MIL, %f*MIL, %f*MIL); nv++;
}
await eda.pcb_Document.save();
await new Promise(r=>setTimeout(r,1500));
return {lines: nl, vias: nv, delVia: dv, delLine: dl,
totVia: (await eda.pcb_PrimitiveVia.getAll()).length};
"""
def load_state():
u"""上一轮本脚本下发的坐标(过孔中心 + 每段线的两端)。"""
try:
return json.load(open(STATE, encoding='utf-8'))
except Exception:
return []
def drop_mine(pads, vias, lines):
u"""算之前先把**自己上一轮的产物**从障碍表里摘掉 —— 否则悬空焊盘已经不悬空了,
脚本会以为没活干(而实际上那几个对象马上就要被删掉重画)。"""
old = load_state()
def mine(x, y):
return any(math.hypot(x - a, y - b) < 0.02 for a, b in old)
vias = [v for v in vias if not mine(v['x'], v['y'])]
lines = [l for l in lines
if not (mine(l['x0'], l['y0']) or mine(l['x1'], l['y1'])) or l['net'] != NET]
return pads, vias, lines
def push_add(routes):
u"""**只 create、不 delete** —— GND 上有 210 个扇出孔和 BM24 的梳子,一删就完。"""
segs, vias = [], []
for key, path in routes.items():
net = R.netof(key)
s, v = R.expand(path)
for x0, y0, x1, y1, lay, w in s:
segs.append([net, lay, x0, y0, x1, y1, w])
for x, y in v:
vias.append([net, x, y])
old = load_state()
r = R.run(JS_ADD % (R.PCB, json.dumps(segs), json.dumps(vias), R.MIL,
json.dumps(old), R.VIA_H, R.VIA_D), timeout=600)
if not r.get('success'):
sys.exit(u'下发失败: ' + json.dumps(r, ensure_ascii=False)[:500])
d = r['result']
pts = [[v[1], v[2]] for v in vias]
pts += [[s_[2], s_[3]] for s_ in segs] + [[s_[4], s_[5]] for s_ in segs]
json.dump(pts, open(STATE, 'w', encoding='utf-8'))
print(u'下发完成:先删掉自己上一轮的 %d 孔 / %d 线,再画 %d 线 / %d 孔,板上过孔共 %d 个'
% (d['delVia'], d['delLine'], d['lines'], d['vias'], d['totVia']))
def main():
if '--check' in sys.argv:
# ⚠️ 不摘自己上一轮的产物 —— 这一支回答的是「板子现在还有没有悬空 GND」,
# 默认那一支回答的是「要画成什么样」(所以必须先摘掉自己画的再算)。
left = clusters(*R.geom())
print(u'板上还悬空的 GND 连通域 %d 个' % len(left))
for cl in left:
print(u' %s' % ' + '.join('%s.%s' % (o['ref'], o['num'])
for k, _l, o in cl if k == 'pad'))
sys.exit(1 if left else 0)
pads, vias, lines = drop_mine(*R.geom())
floats = clusters(pads, vias, lines)
print(u'悬空的 GND 连通域 %d 个:' % len(floats))
if not floats:
print(u' 没有 —— 不用补。')
return
hl = holes(pads)
routes, mine, mvias, fail = {}, [], [], 0
for k, cl in enumerate(floats):
cands = [o for kind, _l, o in cl if kind == 'pad']
desc = ' + '.join('%s.%s' % (o['ref'], o['num']) for o in cands)
best = None
for pad in cands: # 域里哪个焊盘都行,取第一个打得下孔的
lp, lv, ll, lh = local(pads, vias, lines, pad['x'], pad['y'], hl)
lay = R.L_TOP if pad['lay'] == 1 else R.L_BOT
got = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias)
if got:
best = (pad, lay) + got
break
if not best:
print(u' [!!] %-28s 找不到能落孔的位置' % desc)
fail += 1
continue
pad, lay, w, segs, (vx, vy) = best
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
pts = [(segs[0][1], segs[0][2], lay, w)] + [(x1, y1, lay, w) for _l, _a, _b, x1, y1, _w in segs]
routes[u'%s#patch%d' % (NET, k)] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
for l_, x0, y0, x1, y1, w_ in segs:
mine.append((NET, l_, x0, y0, x1, y1, w_))
mvias.append((NET, vx, vy))
print(u' %-28s 从 %s.%s 出,过孔 (%8.3f,%8.3f) %d 段 线宽 %.2f'
% (desc, pad['ref'], pad['num'], vx, vy, len(segs), w))
segs = sum(len(R.expand(p)[0]) for p in routes.values())
nv = sum(len(R.expand(p)[1]) for p in routes.values())
print(u'')
print(u'%d/%d 个域搞定:%d 段导线 / %d 个过孔' % (len(routes), len(floats), segs, nv))
if nv != len(routes):
sys.exit(u'!! 过孔数和域数对不上 —— expand() 又把末端过孔吞了')
n_sharp = len(R.sharp_corners(routes))
print(u'直角拐点 %d(拐弯只许 45°,应当是 0)' % n_sharp)
print(u'')
print(u'净距校核(一般 %.2f mm):' % R.CLR)
bad = R.check(routes, pads, vias, lines)
if not bad:
print(u' 全部通过 ✔')
if '--push' in sys.argv:
if bad or fail or n_sharp:
sys.exit(u'\n!! 有未解决的问题,不下发。')
push_add(routes)
print(u'下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
if __name__ == '__main__':
main()
# -*- coding: utf-8 -*-
u"""补上 §25.8 遗留的**悬空 GND 焊盘** —— 自己找出来,自己打孔。
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
/c/Python314/python .claude/pcb2_gnd_patch.py # 2. 找悬空 + 选孔位 + 净距校核
/c/Python314/python .claude/pcb2_gnd_patch.py --check # 只问「现在板上还有没有悬空的」
/c/Python314/python .claude/pcb2_gnd_patch.py --push # 3. 下发(**只加不删**)
/c/Python314/python .claude/pcb2_pour.py --push # 4. 必须重铺
## 「悬空」是自己算的,不是抄 DRC
把 GND 网络的**焊盘 / 过孔 / 导线**当图,两两相触连边(判据同 `pcb2_net_conn.py`),
一个连通域里只要有**一个过孔或一个通孔焊盘**,它就落到 Inner1/Inner2 的地平面上 = 连通。
**一个都没有的连通域就是悬空的** —— 那是纯顶层/底层的一小块孤铜。
⇒ 这个判据是幂等的:补完孔再跑一次,找到 0 个。
## 和 `pcb2_fanout.py` 的分工
`pcb_fanout` 是**批量**扇出(210 个孔),它对每个焊盘只试固定的几个方位,
挤不下就跳过 —— §25.8 里剩下的这几个就是被它跳过的。
这里换成和 `pcb2_vo_vias.py` 同一套搜索:8 个方向 × 距离 × 线宽阶梯,逐个试到过为止。
## 下发**不能**用 `pcb2_route_dp.push()`
那个函数会先把 net 上**所有**线和过孔删掉再重画 —— 对 GND 就是把 210 个扇出孔
连同 BM24 的两条梳子全部抹掉。这里自己写一段只动**本脚本自己那几个对象**的 JS。
## ⚠️ 幂等靠 `_pcb2_gnd_patch.json` 这份「我上次画了啥」的清单
GND 上有两百多个别人的过孔,没法按网络整网重画,所以本脚本把自己下发的
过孔/导线坐标记在 `_pcb2_gnd_patch.json` 里,下次 `--push` 先按坐标删掉它们再重画。
**不记账的代价实测过一次**:改完选点逻辑直接再 `--push`,板上就多出一对
旧坐标的过孔(425 而不是 423),而且新画的短线被 EasyEDA 并进了旧的粗线里 ——
DRC 照样报错,看上去像"改了没生效"。
"""
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb2_route_dp as R
from pcb2_vo_vias import holes, local, place
NET = 'GND'
STATE = os.path.join(HERE, '_pcb2_gnd_patch.json')
W_STEPS = [0.40, 0.30, 0.25, 0.20] # GND 没有专门的线宽规则,扇出历来用 0.30
EPS = 1e-6
def clusters(pads, vias, lines):
u"""GND 的连通域;返回**悬空的**那些(域里一个过孔/通孔焊盘都没有)。"""
items = []
for p in pads:
if p['net'] == NET:
items.append(('pad', set((1, 2)) if p['lay'] == 12 else set((p['lay'],)), p))
for v in vias:
if v['net'] == NET:
items.append(('via', set((1, 2)), v))
for l in lines:
if l['net'] == NET:
items.append(('line', set((l['lay'],)), l))
n = len(items)
par = list(range(n))
def find(i):
while par[i] != i:
par[i] = par[par[i]]
i = par[i]
return i
def touch(a, b):
(ka, la, oa), (kb, lb, ob) = a, b
if not (la & lb):
return False
if ka == 'line' and kb == 'line':
return R.seg_seg((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
(ob['x0'], ob['y0']), (ob['x1'], ob['y1'])) <= (oa['w'] + ob['w']) / 2 + EPS
if ka == 'line' and kb == 'pad':
return R.seg_rect((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['w'] / 2 + EPS
if ka == 'line' and kb == 'via':
return R.seg_pt(oa['x0'], oa['y0'], oa['x1'], oa['y1'],
ob['x'], ob['y']) <= oa['w'] / 2 + ob['r'] + EPS
if ka == 'via' and kb == 'via':
return math.hypot(oa['x'] - ob['x'], oa['y'] - ob['y']) <= oa['r'] + ob['r'] + EPS
if ka == 'via' and kb == 'pad':
return R.seg_rect((oa['x'], oa['y']), (oa['x'], oa['y']),
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['r'] + EPS
if ka == 'pad' and kb == 'pad':
return False
return touch(b, a)
# 2026-09-07:包围盒预筛。原来纯两两比对,第三十轮 GND 件数上去之后跑了 20 多分钟没出来。
# 每件算一个外扩 EPS 的包围盒,先按 x 排序,只比 x 区间相交的对。
def _bb(it):
k, _, o = it
if k == 'line':
r = o['w'] / 2.0
return (min(o['x0'], o['x1']) - r, min(o['y0'], o['y1']) - r,
max(o['x0'], o['x1']) + r, max(o['y0'], o['y1']) + r)
if k == 'via':
return (o['x'] - o['r'], o['y'] - o['r'], o['x'] + o['r'], o['y'] + o['r'])
return (o['x'] - o['hw'], o['y'] - o['hh'], o['x'] + o['hw'], o['y'] + o['hh'])
bbs = [_bb(it) for it in items]
order = sorted(range(n), key=lambda i: bbs[i][0])
for oi, i in enumerate(order):
bi = bbs[i]
for j in order[oi + 1:]:
bj = bbs[j]
if bj[0] > bi[2] + EPS:
break
if bj[1] > bi[3] + EPS or bj[3] < bi[1] - EPS:
continue
if touch(items[i], items[j]):
a, b = find(i), find(j)
if a != b:
par[a] = b
comp = {}
for i in range(n):
comp.setdefault(find(i), []).append(i)
out = []
for _k, idx in comp.items():
if any(items[i][0] == 'via' or (items[i][0] == 'pad' and items[i][2]['lay'] == 12)
for i in idx):
continue
out.append([items[i] for i in idx])
return out
JS_ADD = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1800));
const SEGS = %s, VIAS = %s, MIL = %f, OLD = %s;
// 先按坐标删掉**上一次本脚本画的**那几个(清单在 _pcb2_gnd_patch.json)
const hit = (x, y) => OLD.some(t => Math.hypot(x/MIL - t[0], y/MIL - t[1]) < 0.02);
let dv = 0, dl = 0;
for (const v of await eda.pcb_PrimitiveVia.getAll())
if (v.getState_Net() === 'GND' && hit(v.getState_X(), v.getState_Y())) {
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++;
}
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Net() === 'GND'
&& (hit(l.getState_StartX(), l.getState_StartY()) || hit(l.getState_EndX(), l.getState_EndY()))) {
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;
}
let nl = 0, nv = 0;
for (const [net, lay, x0, y0, x1, y1, w] of SEGS) {
await eda.pcb_PrimitiveLine.create(net, lay, x0*MIL, y0*MIL, x1*MIL, y1*MIL, w*MIL); nl++;
}
for (const [net, x, y] of VIAS) {
await eda.pcb_PrimitiveVia.create(net, x*MIL, y*MIL, %f*MIL, %f*MIL); nv++;
}
await eda.pcb_Document.save();
await new Promise(r=>setTimeout(r,1500));
return {lines: nl, vias: nv, delVia: dv, delLine: dl,
totVia: (await eda.pcb_PrimitiveVia.getAll()).length};
"""
def load_state():
u"""上一轮本脚本下发的坐标(过孔中心 + 每段线的两端)。"""
try:
return json.load(open(STATE, encoding='utf-8'))
except Exception:
return []
def drop_mine(pads, vias, lines):
u"""算之前先把**自己上一轮的产物**从障碍表里摘掉 —— 否则悬空焊盘已经不悬空了,
脚本会以为没活干(而实际上那几个对象马上就要被删掉重画)。"""
old = load_state()
def mine(x, y):
return any(math.hypot(x - a, y - b) < 0.02 for a, b in old)
vias = [v for v in vias if not mine(v['x'], v['y'])]
lines = [l for l in lines
if not (mine(l['x0'], l['y0']) or mine(l['x1'], l['y1'])) or l['net'] != NET]
return pads, vias, lines
def push_add(routes):
u"""**只 create、不 delete** —— GND 上有 210 个扇出孔和 BM24 的梳子,一删就完。"""
segs, vias = [], []
for key, path in routes.items():
net = R.netof(key)
s, v = R.expand(path)
for x0, y0, x1, y1, lay, w in s:
segs.append([net, lay, x0, y0, x1, y1, w])
for x, y in v:
vias.append([net, x, y])
old = load_state()
r = R.run(JS_ADD % (R.PCB, json.dumps(segs), json.dumps(vias), R.MIL,
json.dumps(old), R.VIA_H, R.VIA_D), timeout=600)
if not r.get('success'):
sys.exit(u'下发失败: ' + json.dumps(r, ensure_ascii=False)[:500])
d = r['result']
pts = [[v[1], v[2]] for v in vias]
pts += [[s_[2], s_[3]] for s_ in segs] + [[s_[4], s_[5]] for s_ in segs]
json.dump(pts, open(STATE, 'w', encoding='utf-8'))
print(u'下发完成:先删掉自己上一轮的 %d 孔 / %d 线,再画 %d 线 / %d 孔,板上过孔共 %d 个'
% (d['delVia'], d['delLine'], d['lines'], d['vias'], d['totVia']))
def main():
if '--check' in sys.argv:
# ⚠️ 不摘自己上一轮的产物 —— 这一支回答的是「板子现在还有没有悬空 GND」,
# 默认那一支回答的是「要画成什么样」(所以必须先摘掉自己画的再算)。
left = clusters(*R.geom())
print(u'板上还悬空的 GND 连通域 %d 个' % len(left))
for cl in left:
print(u' %s' % ' + '.join('%s.%s' % (o['ref'], o['num'])
for k, _l, o in cl if k == 'pad'))
sys.exit(1 if left else 0)
pads, vias, lines = drop_mine(*R.geom())
floats = clusters(pads, vias, lines)
print(u'悬空的 GND 连通域 %d 个:' % len(floats))
if not floats:
print(u' 没有 —— 不用补。')
return
hl = holes(pads)
routes, mine, mvias, fail = {}, [], [], 0
for k, cl in enumerate(floats):
cands = [o for kind, _l, o in cl if kind == 'pad']
desc = ' + '.join('%s.%s' % (o['ref'], o['num']) for o in cands)
best = None
for pad in cands: # 域里哪个焊盘都行,取第一个打得下孔的
lp, lv, ll, lh = local(pads, vias, lines, pad['x'], pad['y'], hl)
lay = R.L_TOP if pad['lay'] == 1 else R.L_BOT
got = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias)
if got:
best = (pad, lay) + got
break
if not best:
print(u' [!!] %-28s 找不到能落孔的位置' % desc)
fail += 1
continue
pad, lay, w, segs, (vx, vy) = best
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
pts = [(segs[0][1], segs[0][2], lay, w)] + [(x1, y1, lay, w) for _l, _a, _b, x1, y1, _w in segs]
routes[u'%s%spatch%d' % (NET, R.KSEP, k)] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
for l_, x0, y0, x1, y1, w_ in segs:
mine.append((NET, l_, x0, y0, x1, y1, w_))
mvias.append((NET, vx, vy))
print(u' %-28s 从 %s.%s 出,过孔 (%8.3f,%8.3f) %d 段 线宽 %.2f'
% (desc, pad['ref'], pad['num'], vx, vy, len(segs), w))
segs = sum(len(R.expand(p)[0]) for p in routes.values())
nv = sum(len(R.expand(p)[1]) for p in routes.values())
print(u'')
print(u'%d/%d 个域搞定:%d 段导线 / %d 个过孔' % (len(routes), len(floats), segs, nv))
if nv != len(routes):
sys.exit(u'!! 过孔数和域数对不上 —— expand() 又把末端过孔吞了')
n_sharp = len(R.sharp_corners(routes))
print(u'直角拐点 %d(拐弯只许 45°,应当是 0)' % n_sharp)
print(u'')
print(u'净距校核(一般 %.2f mm):' % R.CLR)
bad = R.check(routes, pads, vias, lines)
if not bad:
print(u' 全部通过 ✔')
if '--push' in sys.argv:
if bad or fail or n_sharp:
sys.exit(u'\n!! 有未解决的问题,不下发。')
push_add(routes)
print(u'下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
if __name__ == '__main__':
main()
+1 -1
查看文件
@@ -12,7 +12,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
PCB = '942ed1560d81c1e7' # PCB2-CARRIER
OUT = os.path.join(HERE, 'pcb2-parts.json')
JS = u"""
+162 -14
查看文件
@@ -79,28 +79,95 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
PCB = '942ed1560d81c1e7' # PCB2-CARRIER
MIL = 39.3700787401575
BW, BH = 220.0, 135.0 # 板框 mm
CORNER_R = 5.0
# 载板自己的 M3 安装孔(来历见 obsolete/pcb2_place_topside_2026-09-06.py 文件头,位置没动)。
MH = [(8.0, 8.0), (212.0, 8.0), (7.0, 60.0), (213.0, 45.0),
(76.6, 79.5), (148.1, 79.5)] # v3:中跨孔随上排下移 0.6(焊盘顶 82.5 < 83.0)
(76.6, 79.5), (148.1, 79.5), # v3:中跨孔随上排下移 0.6(焊盘顶 82.5 < 83.0)
# 2026-09-06 用户指令「两板尺寸取大对齐、四角定位孔对齐」:下板改成同样的 220×135、
# 四角孔与本板重合。底部两角本来就有 (8,8)/(212,8);顶部两角被上排主板(y ≤ 127.66)和
# 尾插槽(site1 x 8.09~27.09,见 SLOTS 注释)夹住,只剩板角 4×4 那一块:
# 孔心 (4,131)/(216,131),焊盘 Ø6 → 离板边 1.0、离主板边 0.34(主板架在 3 mm 立柱上,螺钉头 ≤ 2)、离槽 1.09。
(4.0, 131.0), (216.0, 131.0)]
MH_HOLE, MH_PAD = 3.2, 6.0
MH_KEEP = 3.5 # 底层元件离 M3 孔中心的最小距离(螺钉头 Ø6)
# ---- 小米8 主板(顶视,板左下角原点,mm)----
PW, PH = 67.09, 44.66
HOLES = {'H1': (54.2, 42.3), 'H2': (2.6, 38.2), 'H3': (61.9, 33.7), 'H4': (62.1, 29.2),
'H5': (2.6, 27.0), 'H6': (4.0, 10.8), 'H7': (42.6, 3.7)}
HOLE_D = 2.5 # 载板上的定位孔,NPTH
HOLE_KEEP = 2.8 # 底层元件离定位孔中心的最小距离(M2.5 尼龙螺钉头 Ø4.5 + 余量)
# 2026-09-06 实测替换(旧值的 7 孔是**从照片认的**,最大错 4.8 mm)。
# 新值来源:测量底板(四基准点 Φ6,圆心矩形 160x110)上描轮廓 + 每孔戳一点 -> 扫描 ->
# `pcb2_measure_read.py` 单应反算。**再拿真主板平板扫描件做了一次独立复核**:
# 用这 7 个孔去套扫描件上的孔,**7/7 命中、平均残差 0.38 mm**(`_fit.png`)。
# ⚠️ 编号整体重排过,别拿旧 H 号对新 H 号。
# ⚠️ 板长 67 = **裸板**。用户描轮廓时 SIM 卡托还插着,卡托在 u 大的那一端伸出约 3.3 mm,
# 所以描出来是 70.3。他早先给的 u 值是拿 66 / 70.3 混着换算的,才有那 3~4 mm 的怪差。
PW, PH = 67.0, 44.0
HOLES = {'H1': (1.4, 9.9), 'H2': (0.7, 27.3), 'H3': (1.7, 38.7), 'H4': (41.9, 1.9),
'H5': (57.4, 41.2), 'H6': (64.6, 28.1), 'H7': (64.8, 32.4)}
HOLE_D = 1.8 # 载板上的定位孔,NPTH。2026-09-06 用户卡尺:**主板孔 Φ2.0**
# (先前口头的 Φ1.5 作废)。Φ2.0 是手机自己的过孔 ⇒ 配 **M1.6 尼龙立柱**
# (M2 螺杆 Φ2.0 塞 Φ2.0 没余量,不选);载板这侧是螺钉过孔,取 M1.6 的通孔 Φ1.8。
# 原来整套按 Φ2.5 / M2.5 做的,是错的。
HOLE_KEEP = 2.2 # 底层元件离定位孔中心的最小距离(M1.6 尼龙螺钉头 Ø3.2 + 余量)
# 2026-09-07:**H5 不打**。J61~J65(EDL 座)离 P*H5 只有 0.92(要 2.2),二选一里「不打 H5」代价为零:
# 每台还有 6 颗螺钉,H5 在 u 大 / v 大那个角(离侧键触点最远的角),那里没有需要压紧的东西。
# 模型里 HOLES 保留 7 个(它是实测数据),只在下发孔 / 螺钉头避让时跳过这里列的。
SKIP_HOLES = {'H5'}
# 侧键触点(§22.3b,已折算成顶视投影)。顶针顺序 = c4_site.py 的 pogo[0..3]
POGO_XY = [(3.11, 6.91), (2.70, 4.60), (5.41, 6.52), (2.12, 2.46)]
# 侧键触点。旧值(§22.3b 那四个点)是从照片认的,**内部排布是错的**,见下。
#
# ---- 已经问清楚的(可信)----
# 触点是 **4 个 Φ1.35 的圆盘,排成菱形**:相邻 1.87 mm、对角 2.65 mm。
# 来源:中框(配合件)上那四个金色焊盘的平板扫描,边界干净,k-means 分开后直接量。
# ⚠️ **顶针本体直径必须 < 1.87 mm**,常规 Φ2 以上的排不下 —— 选型硬约束。
#
# ---- 位置:**中框螺柱配准**那一版(已被卡尺判定为正确)----
# 2026-09-06:用户卡尺实测 **H1 孔心到四触点菱形中心 = 5 mm**,判死了两条路之争:
# 中框配准版 (3.42, 5.07) -> 离 H1 5.24 mm ✔ 采信
# 红漆配准版 (0.81, 7.08) -> 离 H1 2.88 mm �’ 弃
# 红漆那条当时的论据是「同图内量到最近孔 2.7 mm,不经配准也不吃手性」,**论据是错的**:
# 那个「最近的孔」是环形滤波器几百个候选里的一个,多半是过孔/焊盘误报,不是真孔。
# ⚠️ 教训:环形滤波器在这块板上误报率极高,**任何"离最近的孔多少"都不能直接用**,
# 必须先确认那个候选真的是 7 孔之一。
# 旁证:中框版四个点全在板内(红漆版算出 GND 在板边外 0.5 mm,与用户「GND 离短板边最近」矛盾),
# 而且和旧 §22.3b 的簇心 (3.34, 5.12) 只差 0.1 mm —— 旧数据的**簇心本来就是对的,错的是内部排布**。
# 下面四个坐标直接取中框配准的实测值(不是理想菱形),残差 0.57 mm。
POGO_XY = [(4.72, 5.16), # PWR 电源 u+
(3.42, 6.37), # VUP 音量+ v+
(3.49, 3.74), # VDN 音量- v-
(2.05, 4.99)] # GND u-
POGO_TAG = ['J603 PWR', 'J601 VUP', 'J602 VDN', 'J604 GND']
# ---- 2026-09-06 用户改方案:**顶针取消,改成和中框一样的焊盘** ----
# 装配方式跟着改:不再靠顶针弹力,而是**用螺丝把主板压在载板上**,
# 由主板自己的侧键弹片直接压在载板的焊盘上 —— 完全复刻中框的接触方式。
# 好处:顶针那个「本体必须 < 1.87 mm」的硬卡点直接消失(现有 POGO-SMD1P_D1.9 的 Ø1.9
# 在真实 1.78 mm 最小间距下是负净距,正是那 20 条重叠的来源)。
#
# 焊盘定径:四点实测最小间距 **1.78 mm**(不是理想菱形的 1.87)⇒
# 净距 0.20 -> 最大 Ø1.58 · 净距 0.25 -> Ø1.53 · 净距 0.30 -> Ø1.48
# 中框原盘 Ø1.35。取 **Ø1.5**:净距 0.28(好做),容许位置误差 0.50 mm。
POGO_PAD_D = 1.5
# ⚠️ **位置不确定度 ±0.6 mm 已经吃掉全部余量**(Ø1.5 只容许 0.50)。
# 打样前必须把触点位置收到 ±0.3:办法是「**一次装夹**」——
# 主板放测量底板上,先戳 7 个孔、再把弹片涂红盖印,孔和触点落进同一套基准圆,一次量完。
# ⚠️ **立柱高度 3.0 -> 1.0 mm**:2026-09-06 用户实测中框「螺柱支撑面 到 金焊盘面 = 1 mm」,
# 这就是载板面到主板贴合面该有的间隙。原来是 3.0 mm 立柱 + 顶针压缩 0.5,整套作废。
# ✅ **不用挖空**:2026-09-06 用户实测「贴合面上元件全都低于 1 mm」(屏蔽罩、SIM 卡座都算过),
# 所以载板**保持平板**,1.0 mm 间隙直接成立。曾担心的「挖空会和底层 BAND 那组元件打架」不存在了。
# ✅ 排线反而更宽裕(绕边下落 5 -> 3 mm):
# 尾插 2.9 + 3 + 2.9 + 1.6 + 34.1 ≈ 44.5 ≤ 50(原 46.5)
# 电池 4.6 + 3 + ... ≈ 29.6 ≤ 35(原 31.6)
# ⇒ §25.16 那串长度核算只会更松,不用改路径。
# ⚠️ **1 mm 立柱不好买**:M1.6 尼龙立柱通常从 3 mm 起。可行做法是
# 「螺钉从上穿过手机孔 + 1 mm 尼龙垫片 + 穿过载板 Φ1.8 孔 + 底面尼龙螺母」。垫片可叠。
# ⚠️ 顶角那两个 M3 孔(4,131)/(216,131) 的旧注释写着「主板架在 3 mm 立柱上,螺钉头 ≤ 2」——
# 现在只剩 1 mm,那两个孔真要补的话螺钉头必须 ≤ 1 或者挪开。(它们本来就没打到板上。)
POGO_STANDOFF_H = 1.0
# ---- 五个手机位:本体左下角 + 旋转(0 = 接口边朝下,180 = 接口边朝上;to_board 里 rot 180 是绕本体中心转,位置矩形不变)----
# v3(2026-09-06 用户拍板):穿线槽挪到**主板投影外侧、靠板边**那一条,排线(按 6 cm 算)
# 跨过主板正面到远端边、下穿、再折回底面。上排下移 / 下排上移各 2.5 mm,让板边那条留出
@@ -132,7 +199,9 @@ SLOTS = {
# 2026-09-06 v4c(用户拍板):尾插槽挪到**和电池槽同一条线**(接口边外侧 v −4.2~−1.2),排线翻过接口边直接下穿,
# 不再从主板反面 3.0 mm 的缝里钻。背面到 J7n 变长 5.6,J7n 从 v 37 收到 **v 35**:
# 2.86 + 绕边 5 + 到槽心 2.7 + 穿板 1.6 + 背面 37.7 ≈ 49.9 ≤ 50 ✔(实长 55)。
'tail': (45.0, 65.0, -4.2, -1.2), # 尾插排线:接口边外侧,与电池槽同线,正对 J1401 (u 47.5~59.7)
# 2026-09-06 顶角 M3 孔 (4,131):site1 rot 180 下槽的 x = 72.09 − u,u=65 → x 7.09 会压到 Ø6 焊盘(边 7.0)。
# 东端 65 → 64(x 8.09,离焊盘 1.09)。排线 18 宽盖 u 44.6~62.6,64 端还余 1.4。
'tail': (45.0, 64.0, -4.2, -1.2), # 尾插排线:接口边外侧,与电池槽同线,正对 J1401 (u 47.5~59.7)
}
# 排线在背面占的车道(本体坐标)。**车道里不放任何元件**(排线要平贴着走到插头 / 焊盘,0402 也别放):
# 尾插排线 u 44.6~62.6,v 4.4 ~ J7n 北沿(EDL 组 / VBUS 组因此整体西移到 u ≤ 44);
@@ -195,6 +264,19 @@ BAND = {
'r76': (52.233, 38.0, 0), # R76n 56k CC1→VBUS,pin1 (51.800) = J7n.22 正北
'r77': (54.8, 38.0, 0), # R77n 100k,pin1 (54.367) 收 THERM
'j6': (62.5, 41.0, 0), # J6n EDL 1.25mm 2P,J7n 东北、H1 keep-out 东侧;EDL 飞线随尾插排线穿槽走背面 ~40 mm
# ── 按键 MOS 三列(2026-09-06 随 74HC595 从下板搬来,§26.23):顶针(顶层,u 2.1~5.4 / v 2.5~7)背面那片空地。
# 列距 3.5;SOT-23 框 ±1.5×±1.27,0402 框 ±0.72×±0.27(竖放 ±0.27×±0.72)。
# 东沿 15.5+1.5 = 17.0 < 电池排线车道 18.8;离 H6 (4,10.8) 最近的是 R%21 (8.5,10):4.6 > 2.8;
# 漏极→顶针焊盘要换层(顶针是顶层单面焊盘),过孔由布线器自己打。
'q3': (8.5, 4.0, 0), # Q%03 PWR
'q4': (12.0, 4.0, 0), # Q%04 VUP
'q5': (15.5, 4.0, 0), # Q%05 VDN
'r20': (8.5, 7.5, 90), # R%20 1k 栅阻
'r22': (12.0, 7.5, 90),
'r24': (15.5, 7.5, 90),
'r21': (8.5, 10.0, 90), # R%21 100k 栅极下拉
'r23': (12.0, 10.0, 90),
'r25': (15.5, 10.0, 90),
}
# SMD 罐体 keep-out(相对元件原点):10.3 见方 + 单边 0.25
CAN_BODY = (-5.4, -5.4, 5.4, 5.4)
@@ -211,6 +293,9 @@ def desig(n):
'j7': 'J7%d' % n, 'u5': 'U5%d' % n, 'r76': 'R76%d' % n, 'r77': 'R77%d' % n,
'u2': 'U%d' % (23 + n), 'c70': 'C70%d' % n, 'c71': 'C71%d' % n,
'c72': 'C72%d' % n, 'r70': 'R70%d' % n,
'q3': 'Q%d03' % n, 'q4': 'Q%d04' % n, 'q5': 'Q%d05' % n,
'r20': 'R%d20' % n, 'r21': 'R%d21' % n, 'r22': 'R%d22' % n,
'r23': 'R%d23' % n, 'r24': 'R%d24' % n, 'r25': 'R%d25' % n,
}
@@ -246,7 +331,23 @@ GLOBAL = {
# v3 布线:U31 组挪到 J31 东端**南**侧(走廊北带 y 70~80 留给差分对上顶层北上)
'U31': (87.5, 60.5, 0), # 底边 57.65 让开 CH4 电池槽(y ≤ 56.36 + 0.5);USB1 车道改走 y 64.0/64.46
'C736': (80.5, 58.5, 0), # +5V 输入 0805(U31 西侧)
'C737': (93.0, 54.0, 0), # +3V3 输出 1206,Hub 晶振窝西南、CH4 电池槽东侧
# ── 2026-09-07:U32 的 PRT_SWP1~5 五个 strap 脚的 100k 下拉(R900~R904,见 §21.6a)──
# BOOST/GANG_EN/SWP6-7 那五颗用户拍板不装(采到 1 也无功能后果)。
# ⚠️ 第一版把十颗全摆在 U32 北边 y=79 那一排(那一片唯一的整块空地),
# `pcb2_route_sig` **10 条只布通 3 条** —— 南边(49/50/51)、西边(15/17)、东南(47/48)
# 那七个脚要绕过整颗 QFN 和一圈去耦,绕不出去。改成**每颗贴着自己的引脚**。
# 坐标是 `pcb2_strap_place.py` 按布线器的障碍表搜出来的(以引脚为心一圈圈往外找,
# 判据:两个焊盘离异网铜 >= 0.20 且不落进任何已有元件的本体投影)。改摆位请重跑它。
'R904': (103.458, 73.632, 0), # U32.31 PRT_SWP5,离脚 1.2 mm
'R903': (105.632, 71.958, 90), # U32.33 PRT_SWP4,离脚 1.2 mm
'R902': (106.635, 62.145, 0), # U32.47 PRT_SWP3,离脚 3.4 mm
'R901': (103.021, 59.414, 0), # U32.49 PRT_SWP2,离脚 4.2 mm
'R900': (102.750, 58.550, 0), # U32.51 PRT_SWP1,离脚 5.0 mm
# 2026-09-07:C737 由 1206 陶瓷换成 KEMET T491A226K010AT 固态钽(AMS1117 手册 §Stability
# 要求的 22uF 固态钽)。钽是极性件且 1 脚 = 正极,而板上 +3V3_HUB 的铜在东、GND 在西,
# 所以必须 rot 180;再东移 0.17 mm,让 1 脚(+3V3_HUB)西沿离那颗 GND 过孔 0.265 mm。
# 改这两个数之前先读 §26.25:0.17 是算出来的净距,不是随手摆的。
'C737': (93.17, 54.0, 180), # +3V3 输出,CASE-A_3216 固态钽
# LM75BD 测温 + 100nF(SCL/SDA 在 J31.21/23 = x 63.3/65.8,北排;走线从排母针缝里穿到南边)
# v2 第二版:挪到 J31 **南**边(走廊北带 y 70~76 要留给差分高速路)。CH4 行 B 只剩 U54(48.3)/R764/R774,这里是空的。
# v3 布线:J31 **南**边(USB1 那对在底层 y 62.9/63.6 从排母南边一路西行,U34 顶边 62.2)
@@ -255,6 +356,30 @@ GLOBAL = {
# 上游 VBUS 分压 -> UP_VBUS_DET -> J31 pin36 (81.049, 65.100),跟着高阻端走
'R721': (80.0, 61.5, 180), # pin1 = VBUS_UP(v3:让开 U31 与 USB1 车道)
'R722': (78.0, 61.5, 180), # pin1 = UP_VBUS_DET, pin2 = GND
# ── 三片 74HC595(2026-09-06 从下板搬来,§26.23)。SOIC-16 框 9.5 × 7.44(rot 90 后 7.44 宽 × 9.5 高)。
# 走廊中段 x 113~132 里能用的其实只有 **y 60.8~75.26 那条带**:UP_DP/UP_DM(底层 y 60.1/60.6)从 x 99 一路横穿到 205,
# 下面 y 52.5/53.0 是 USB5,上面 y 75.4/77.0 是 USB3(第一版只查了端点落在框里的线,把 UP 这对漏了,
# U21 rot 0 摆在 y 58.5 直接压在 UP 上;再上一版 rot 90 面对面缝 1.56,16 个脚的出线口互相堵死)。
# U21 (117,66.5) / U22 (128,66.5) rot 90:x 113.28~120.72 / 124.28~131.72(缝 3.56,出线段各 1.3 还剩 1 mm),
# y 61.75~71.25:离 UP_DM 线边 60.74 有 1.0,离 USB3_DP 线边 75.26 有 4.0;离 USB5 竖段 111.44 有 1.84、离 J32 焊盘边 133.0 有 1.28。
# U33 单独放 **J32 东侧**(x 183~208 / y 52~86 只有 UP 那对在 y 60.1/60.6 横穿):(193.5,68.5) rot 90 → y 63.75~73.25,离 UP_DM 3.0。
# 四根控制线从 J32 北排 3~6 行(x 138.95~146.57)进来,正好夹在两处中间。
# 2026-09-07 第三十轮:rot 90 的两片各 8 个西排脚全从 x 113~114 / 124 那两条竖缝出线,8 个网络布不通
# (EDL_EN2/3/5、K5_VDN 等,§26.23 落地末尾)。改成 **rot 0 / 180:引脚排朝南北**,16 根脚沿 9 mm 宽的边出线:
# 北带 y 70.23~75.26(5 mm,C604/R609/C605 在 y 73)、南带 y 60.74~62.77(2 mm,够打一排换层孔到顶层)。
# 底层 rot 0:脚 1~8(K/EDL 输出)在**北**排、9~16(控制/+3V3)在南排;rot 180 反过来。
# U21 rot 0:K1 去 site1(西北)、EDL_EN2/3 去 site2/3(北),输出朝北顺路;控制脚南排沿 y≈63 东行去 J32。
# U22 rot 180:K4/K5 去 site4/5(南),输出朝南,从南带换层到顶层跨过 UP 差分;控制脚北排。
# U21 (116.5,66.5):焊盘 x 111.56~121.44,离 USB5 竖段线边 111.4 有 0.5(≥0.2);
# U22 (127.5,66.5):焊盘 x 122.56~132.44,离 J32.1 盘边 ~133.1 有 0.6;两片焊盘之间 1.1。
'U21': (116.5, 66.5, 0),
'U22': (127.5, 66.5, 180),
'U33': (193.5, 68.5, 90),
# 去耦 / OE# 上拉都放两片北边那条带(y 72.73~73.27,离芯片上沿 71.25 有 1.5、离 USB3_DP 有 2.0)
'C604': (117.0, 73.0, 0),
'C605': (128.0, 73.0, 0),
'R609': (122.5, 73.0, 0),
'C739': (199.5, 68.5, 90),
}
HDR = {'J31': (62.0, 67.5, 0), 'J32': (158.0, 67.5, 0)}
HUB_CENTER = (100.0, 68.0) # v1 是 (110, 67):西移 10 让开 CH2 行 A / CH5 行 B,北移 1 让 C741 离开 CH4 电池槽
@@ -416,9 +541,20 @@ def check(pos):
errs.append(u'越框: %s @ (%.1f, %.1f)' % (d, pos[d][0], pos[d][1]))
# 同层重叠
# 侧键触点盘(J41~J60)是 Ø1.5 的圆盘、盘心距最小 1.78 ⇒ 净距 0.28。
# 包围盒判会把菱形里相邻的两个盘报成重叠(dx 1.30 < 1.50),所以这一组按**圆**判:
# 盘心距 >= POGO_PAD_D + 0.20(DRC 净距)。
pogo = set()
for n in SITES:
pogo |= set(desig(n)['pogo'])
for i in range(len(items)):
for j in range(i + 1, len(items)):
a, b_ = items[i], items[j]
if a in pogo and b_ in pogo:
dd = math.hypot(pos[a][0] - pos[b_][0], pos[a][1] - pos[b_][1])
if dd < POGO_PAD_D + 0.20:
errs.append(u'触点盘太近: %s x %s 盘心距 %.2f(要 >= %.2f)' % (a, b_, dd, POGO_PAD_D + 0.20))
continue
shared = set(layers(a, pos[a][3])) & set(layers(b_, pos[b_][3]))
for L in sorted(shared):
if _overlap(abs_box(a, L), abs_box(b_, L)):
@@ -474,6 +610,8 @@ def check(pos):
b = abs_box(d, 2)
for n in SITES:
for hn, (u_, v_) in HOLES.items():
if hn in SKIP_HOLES:
continue
hx, hy, _ = to_board(n, u_, v_)
dd = _dist_box_pt(b, hx, hy)
if dd < HOLE_KEEP:
@@ -641,11 +779,13 @@ return k;
def mech_items():
u"""35 个手机定位孔(NPTH)+ 5 个手机位的丝印外框与标签。"""
u"""手机定位孔(NPTH,7 孔模型减去 SKIP_HOLES ⇒ 30 个)+ 5 个手机位的丝印外框与标签。"""
holes, lines, texts = [], [], []
for n in SITES:
x0, y0, rho = SITES[n]
for hn, (u, v) in HOLES.items():
if hn in SKIP_HOLES: # 2026-09-07:H5 不打(push_mech 会先删掉板上全部 P*H* 再建)
continue
x, y, _ = to_board(n, u, v)
holes.append(['P%d%s' % (n, hn), round(x * MIL, 2), round(y * MIL, 2)])
c = [(x0, y0), (x0 + PW, y0), (x0 + PW, y0 + PH), (x0, y0 + PH)]
@@ -791,6 +931,7 @@ return out;
HEIGHT = {
'J31': 8.5, 'J32': 8.5, # 直插排母本体(C5307344)
'J23': 3.2, 'U31': 1.8, 'U32': 0.9, 'Y1': 1.2, 'U34': 1.6,
'C737': 1.8, # KEMET A 壳 EIA-3216-18,最大高 1.8(不是 1206 的 1.7)
}
@@ -850,8 +991,15 @@ if __name__ == '__main__':
print(' ', e)
if len(errs) > 40:
print(u' ...还有 %d 条' % (len(errs) - 40))
sys.exit(1)
print(u'本地检查通过:无重叠、无越框、没人压槽、顶层只有顶针、底层件避开了螺钉头')
# --force:只下发 --mech / --outline(孔、槽、丝印)时,元件级的报错不该挡路 ——
# 那些是「元件 vs 孔」「元件 vs 元件」,和要写的机械图元本身无关。
# ⚠️ 但 --push(重摆元件)绝不允许 force,摆错了会连着布线一起烂。
if '--force' in sys.argv and '--push' not in sys.argv:
print(u'--force:以上按警告处理,继续下发机械图元(元件没动)')
else:
sys.exit(1)
else:
print(u'本地检查通过:无重叠、无越框、没人压槽、顶层只有顶针、底层件避开了螺钉头')
if '--layers' in sys.argv:
push_layers()
if '--clean-copper' in sys.argv:
+4 -2
查看文件
@@ -108,7 +108,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
PCB = '942ed1560d81c1e7' # PCB2-CARRIER
MIL = 39.3700787401575
L_TOP, L_PWR, L_GND, L_BOT = 1, 15, 16, 2 # ⚠ 载板:Inner1=电源 / Inner2=地
@@ -441,7 +441,9 @@ def main():
r = call(JS_REBUILD)
if r.get('err'):
sys.exit(u'!! 打不开铺铜管理器: ' + r['err'])
for i in range(60):
# 2026-09-07:正常一次填充 7 块 < 30 s;卡在 0/7 超过 ~90 s 就是客户端卡住了(§9k),
# 再等到 4 分钟也不会好,早点退出去重载。
for i in range(22):
time.sleep(4)
n = call(JS_POLL, timeout=40)['poured']
print(u' 填充 %d / %d' % (n, len(ps)))
+76 -37
查看文件
@@ -1,37 +1,76 @@
# -*- coding: utf-8 -*-
u"""重载 EasyEDA 客户端页面(skill §9k/§9s):关对话框 → 删铺铜边框 → 存盘 → reload → 等 windowId 变掉。"""
import json, os, sys, time, urllib.request
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
from eda import run, port
JS = u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()==='取消' && b.offsetParent!==null) { nat(b); await sleep(800); }
await eda.dmt_EditorControl.openDocument('f126ffb29c92f9b2');
await sleep(1200);
let n = 0;
for (const o of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(o.primitiveId); n++; }
await eda.pcb_Document.save();
await sleep(1500);
window.onbeforeunload = null;
setTimeout(() => location.reload(true), 300);
return {deletedPours: n};
"""
def wid():
try:
with urllib.request.urlopen('http://127.0.0.1:%d/health' % port(), timeout=3) as r:
return json.loads(r.read()).get('activeWindowId')
except Exception:
return None
w0 = wid()
r = run(JS, timeout=60)
print(u'重载前 windowId %s,删铺铜边框 %s' % (w0, (r.get('result') or {}).get('deletedPours')))
for i in range(30):
time.sleep(5)
w = wid()
if w and w != w0:
print(u'已重载,新 windowId %s(%d s)' % (w, (i + 1) * 5)); time.sleep(10); sys.exit(0)
sys.exit(u'!! 150 s 内 windowId 没变,重载没成功')
# -*- coding: utf-8 -*-
u"""重载 EasyEDA 客户端页面(skill §9k/§9s):关对话框 → 删铺铜边框 → 存盘 → reload → 等 windowId 变掉。"""
import json, os, sys, time, urllib.request
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
from eda import run, port
JS = u"""
const sleep = ms => new Promise(r => setTimeout(r, ms));
const nat = b => { const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o)); };
for (const b of document.querySelectorAll('button'))
if ((b.innerText||'').trim()==='取消' && b.offsetParent!==null) { nat(b); await sleep(800); }
await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');
await sleep(1200);
let n = 0;
for (const o of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(o.primitiveId); n++; }
await eda.pcb_Document.save();
await sleep(1500);
window.onbeforeunload = null;
setTimeout(() => location.reload(true), 300);
return {deletedPours: n};
"""
def wid():
try:
with urllib.request.urlopen('http://127.0.0.1:%d/health' % port(), timeout=3) as r:
return json.loads(r.read()).get('activeWindowId')
except Exception:
return None
w0 = wid()
r = run(JS, timeout=60)
print(u'重载前 windowId %s,删铺铜边框 %s' % (w0, (r.get('result') or {}).get('deletedPours')))
COUNT_JS = u"""
await eda.dmt_EditorControl.openDocument('942ed1560d81c1e7');
await new Promise(r => setTimeout(r, 9000));
let nc=0, nl=0, nv=0, npad=0;
for (const c of await eda.pcb_PrimitiveComponent.getAll()) nc++;
for (const l of await eda.pcb_PrimitiveLine.getAll()) nl++;
for (const v of await eda.pcb_PrimitiveVia.getAll()) nv++;
for (const p of await eda.pcb_PrimitivePad.getAll()) npad++;
return {doc: (await eda.dmt_Pcb.getCurrentPcbInfo() || {}).name, nc, nl, nv, npad};
"""
def assert_alive():
u"""⚠⚠ 重载之后**必须**先数一遍图元,确认桥接和文档真的接上了再往下走。
2026-09-07 就是漏了这一步把载板弄丢的:重载回来后铜层读回全 0
(skill §9ae 那个陈旧上下文),而紧接着跑的 `pcb2_pour.py --push`
又在那个状态下调了一次 `save()` —— 把“空”写回了文档,
301 件的下板没事、223 件的载板只剩下丝印和板框,只能走工程历史恢复。
“重载成功”的判据不是 windowId 变了,是**图元数不为零**。
不过关、绝不允许后面任何一步 save()。
"""
r = run(COUNT_JS, timeout=120)
if not r.get('success'):
sys.exit(u'!! 重载后数图元失败,**不要再跑任何会 save() 的脚本**: %s'
% json.dumps(r, ensure_ascii=False)[:400])
d = r['result']
print(u'重载后回查:%s 元件 %s / 导线 %s / 过孔 %s / 焊盘 %s'
% (d.get('doc'), d.get('nc'), d.get('nl'), d.get('nv'), d.get('npad')))
if not d.get('nc') or not d.get('nl') or not d.get('npad'):
sys.exit(u'!! 铜层读回 0 —— 桥接与文档失同步(skill §9ae)。\n'
u' **立刻停手,不要 save(),不要跑 pcb2_pour.py** —— 那一步会把空写回去。\n'
u' 先 API Gateway → 重新连接,不行就重启客户端,再跑一次本脚本确认数字回来了。')
for i in range(30):
time.sleep(5)
w = wid()
if w and w != w0:
print(u'已重载,新 windowId %s(%d s)' % (w, (i + 1) * 5))
time.sleep(10)
assert_alive()
sys.exit(0)
sys.exit(u'!! 150 s 内 windowId 没变,重载没成功')
+1 -1
查看文件
@@ -40,7 +40,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2'
PCB = '942ed1560d81c1e7'
SNAP = os.path.join(HERE, '_pcb2_snap_prerouting.json')
# 11 对差分 = 22 个网络(和 pcb2_rules.PAIRS 同源,这里独立列一遍并断言一致)
+14 -2
查看文件
@@ -46,7 +46,7 @@ from eda import run
from pcb2_clear import half, deg
from pcb2_place import SITES, to_board
PCB = 'f126ffb29c92f9b2'
PCB = '942ed1560d81c1e7'
MIL = 39.3700787401575
GEOM = os.path.join(HERE, '_pcb2_route_geom.json')
@@ -73,10 +73,15 @@ PAIR_OF['UP_DP'] = 'UP_DM'
PAIR_OF['UP_DM'] = 'UP_DP'
KSEP = chr(0)
def netof(key):
u"""路径的 key -> 网络名。`NET#2` 这样的后缀表示**同一个网络的第二条支路**
(J23 的 A6↔B6 / A7↔B7 短接线就是这么加的),下发时按 `NET` 建线。"""
return key.split('#')[0]
# ⚠️ 分隔符不能用 '#':载板上有 `KEY_OE#`(2026-09-06 595 上载板之后),按 '#' 拆会把它拆成
# `KEY_OE`,校核时和自己的焊盘当成异网(下板 §26.19 踩过同一条)。用 chr(0)。
return key.split(KSEP)[0]
# ── 站内坐标(u, v):J7n ↔ U5n 的短差分(v5:全部**底层**,接口边朝板边)────────
@@ -165,6 +170,13 @@ def geom():
lines = [{'id': i, 'net': n or '', 'lay': l, 'x0': a / MIL, 'y0': b / MIL,
'x1': c / MIL, 'y1': e / MIL, 'w': w / MIL}
for i, n, l, a, b, c, e, w in d['lines']]
# 2026-09-07:封装自带的非金属化孔(J23 里两个 Ø0.60,`pcb2_npth.py --write` 产出)当成无网络的通孔盘,
# 两个布线器就都会躲开(原来只有 pcb2_vo_vias 读它,`$16N224` 的过孔直接打在孔上,DRC「挖槽区域到过孔 0mil」)。
npth = os.path.join(HERE, '_pcb2_npth.json')
if os.path.exists(npth):
for cx, cy, r in json.load(open(npth, encoding='utf-8')):
pads.append({'ref': 'NPTH', 'num': '', 'net': '', 'lay': 12,
'x': cx, 'y': cy, 'hw': r, 'hh': r})
return pads, vias, lines
+2009 -1577
查看文件
文件差异内容过多而无法显示 加载差异
+16 -1
查看文件
@@ -11,6 +11,21 @@ u"""载板 PCB2-CARRIER 设计规则 + D± 差分对(2026-09-05)。
· 写进去的 mm 会被换算成 mil 再换回来,`0.2` 回读是 `0.19999959999999997`,
任何几何比较都留 1e-4 容差
## ⚠⚠ 必须在「从原理图导入变更」**之后**跑(2026-09-08 实测)
网络指派(差分对 / 线宽 / 铺铜连接方式)是**按网络名**挂上去的,
而网络是元件导入时才在 PCB 上出现的。在空板上跑本脚本:
差分对 0 / 11 对
线宽指派 0 / 12 个网络
铺铜全连接指派 0 / 5 个网络
—— **而“规则配置已下发”那几行照常打印成功**,看上去像做完了。
导入元件后重跑一次就是 11/11、12/12、5/5。
⇒ 重建板子的顺序是:**切 4 层 → 导入元件 → 下规则 → 摆位**。
判据就是上面那三个分数,不是“没报错”。
## 和下板不同的三处
1. **差分对 11 对,不是 7 对。** 载板上每一路 D± 被 ESD 器件 `U5n` 切成两段:
@@ -51,7 +66,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
PCB = '942ed1560d81c1e7' # PCB2-CARRIER
# ── 线宽规则(mm)──────────────────────────────────────────────────────
TRACK_RULES = {
+52 -52
查看文件
@@ -1,52 +1,52 @@
# -*- coding: utf-8 -*-
u"""把载板 PCB2 当前的全部铜/元件状态落成一个 JSON(可回滚的安全网)。
/c/Python314/python .claude/pcb2_snapshot.py <out.json>
抓:元件 (位号,x,y,rot,layer)、导线、过孔、铺铜(含多边形)、自由焊盘、layer 11 多段线、填充。
单位 mil(EasyEDA 原生),不换算。
"""
import io, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2'
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1500));
const out = {comps:[], lines:[], vias:[], pours:[], fills:[], pads:[], polys:[], arcs:[]};
for (const c of await eda.pcb_PrimitiveComponent.getAll())
out.comps.push([c.getState_Designator(), c.getState_X(), c.getState_Y(), c.getState_Rotation(), c.getState_Layer()]);
for (const l of await eda.pcb_PrimitiveLine.getAll())
out.lines.push([l.layer, l.startX, l.startY, l.endX, l.endY, l.lineWidth, l.net]);
for (const a of await eda.pcb_PrimitiveArc.getAll())
out.arcs.push([a.layer, a.net, JSON.stringify(a).slice(0, 400)]);
for (const v of await eda.pcb_PrimitiveVia.getAll())
out.vias.push([v.x, v.y, v.holeDiameter, v.diameter, v.net]);
for (const p of await eda.pcb_PrimitivePour.getAll()) {
let poly = null; try { poly = p.complexPolygon; } catch(e) {}
out.pours.push({layer: p.layer, net: p.net, name: p.pourName, prio: p.pourPriority, fill: p.pourFillMethod,
poly: poly ? JSON.parse(JSON.stringify(poly)) : null});
}
for (const f of await eda.pcb_PrimitiveFill.getAll())
out.fills.push({layer: f.layer, net: f.net, raw: JSON.parse(JSON.stringify(f)).complexPolygon || null});
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const pid = p.getState_PrimitiveId();
out.pads.push([pid, p.getState_PadNumber(), p.getState_Layer(), p.getState_X(), p.getState_Y(), p.getState_Net()]);
}
for (const p of await eda.pcb_PrimitivePolyline.getAll())
out.polys.push({layer: p.layer, raw: JSON.parse(JSON.stringify(p))});
return out;
""" % PCB
if __name__ == '__main__':
r = run(JS, timeout=300)
if not r.get('success'):
print(json.dumps(r, ensure_ascii=False)[:800]); sys.exit(1)
res = r['result']
out = os.path.join(HERE, sys.argv[1] if len(sys.argv) > 1 else '_pcb2_snap.json')
io.open(out, 'w', encoding='utf-8').write(json.dumps(res))
print(u'元件 %d / 导线 %d / 弧 %d / 过孔 %d / 铺铜 %d / 填充 %d / 焊盘 %d / 多段线 %d -> %s' % (
len(res['comps']), len(res['lines']), len(res['arcs']), len(res['vias']), len(res['pours']),
len(res['fills']), len(res['pads']), len(res['polys']), out))
# -*- coding: utf-8 -*-
u"""把载板 PCB2 当前的全部铜/元件状态落成一个 JSON(可回滚的安全网)。
/c/Python314/python .claude/pcb2_snapshot.py <out.json>
抓:元件 (位号,x,y,rot,layer)、导线、过孔、铺铜(含多边形)、自由焊盘、layer 11 多段线、填充。
单位 mil(EasyEDA 原生),不换算。
"""
import io, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '942ed1560d81c1e7'
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1500));
const out = {comps:[], lines:[], vias:[], pours:[], fills:[], pads:[], polys:[], arcs:[]};
for (const c of await eda.pcb_PrimitiveComponent.getAll())
out.comps.push([c.getState_Designator(), c.getState_X(), c.getState_Y(), c.getState_Rotation(), c.getState_Layer()]);
for (const l of await eda.pcb_PrimitiveLine.getAll())
out.lines.push([l.layer, l.startX, l.startY, l.endX, l.endY, l.lineWidth, l.net]);
for (const a of await eda.pcb_PrimitiveArc.getAll())
out.arcs.push([a.layer, a.net, JSON.stringify(a).slice(0, 400)]);
for (const v of await eda.pcb_PrimitiveVia.getAll())
out.vias.push([v.x, v.y, v.holeDiameter, v.diameter, v.net]);
for (const p of await eda.pcb_PrimitivePour.getAll()) {
let poly = null; try { poly = p.complexPolygon; } catch(e) {}
out.pours.push({layer: p.layer, net: p.net, name: p.pourName, prio: p.pourPriority, fill: p.pourFillMethod,
poly: poly ? JSON.parse(JSON.stringify(poly)) : null});
}
for (const f of await eda.pcb_PrimitiveFill.getAll())
out.fills.push({layer: f.layer, net: f.net, raw: JSON.parse(JSON.stringify(f)).complexPolygon || null});
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const pid = p.getState_PrimitiveId();
out.pads.push([pid, p.getState_PadNumber(), p.getState_Layer(), p.getState_X(), p.getState_Y(), p.getState_Net()]);
}
for (const p of await eda.pcb_PrimitivePolyline.getAll())
out.polys.push({layer: p.layer, raw: JSON.parse(JSON.stringify(p))});
return out;
""" % PCB
if __name__ == '__main__':
r = run(JS, timeout=300)
if not r.get('success'):
print(json.dumps(r, ensure_ascii=False)[:800]); sys.exit(1)
res = r['result']
out = os.path.join(HERE, sys.argv[1] if len(sys.argv) > 1 else '_pcb2_snap.json')
io.open(out, 'w', encoding='utf-8').write(json.dumps(res))
print(u'元件 %d / 导线 %d / 弧 %d / 过孔 %d / 铺铜 %d / 填充 %d / 焊盘 %d / 多段线 %d -> %s' % (
len(res['comps']), len(res['lines']), len(res['arcs']), len(res['vias']), len(res['pours']),
len(res['fills']), len(res['pads']), len(res['polys']), out))
+435 -434
查看文件
@@ -1,434 +1,435 @@
# -*- coding: utf-8 -*-
u"""五路 `VOn_P` 的**贴片电容接到 Inner1 电源岛** —— 每个焊盘旁边打一个过孔。
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
/c/Python314/python .claude/pcb2_vo_vias.py # 2. 只算 + 本地净距校核
/c/Python314/python .claude/pcb2_vo_vias.py --push # 3. 下发
/c/Python314/python .claude/pcb2_pour.py --push # 4. **必须重铺**(新过孔要反焊盘)
## 为什么要有这一步
`VO1_P`~`VO5_P` 每个网络 10 个焊盘:**5 个通孔**(`C8n1.1` Ø10 电解正极、`TP8n1`
测试点、排母 3 针)本身就落在 Inner1 的岛上,不用管;剩下 **5 个贴片焊盘**
(`C8n2`/`C8n3` 100µF 1206、`C8n4`/`C8n5` 22µF 1206、`C8n6` 100nF 0402)
只有顶层铜,**和岛之间没有任何通路** —— 这就是 §25.14 里连接性 51 条中的 31 条。
所以这一步和布线不是一回事:不需要找路径,只需要**在焊盘边上打一个过孔**,
用一小段线接过去,垂直下到 Inner1 的岛上。25 个焊盘 = 25 个过孔。
## 选点判据(不是"看着差不多")
| | |
|---|---|
| 方向 | 只取 8 个方向(0/45/90…),**不许出现斜的怪角度**(全板 45° 化,§25.14)|
| 起步距离 | 过孔铜边到本焊盘矩形 **≥ 0.15 mm**(同网,不是 DRC 约束,是不想做 via-in-pad)|
| 净距 | 过孔和那一小段线对**异网**焊盘/过孔/导线 ≥ 0.20 —— 直接借 `pcb2_route_dp.check()` |
| 岛内余量 | 过孔中心离岛多边形边界 ≥ 0.61/2 + 0.30 = **0.605 mm**,免得铺出来只搭上一点点 |
| 线宽 | `PWR4A` 最小 0.30;从 1.00 起往下退让,取第一个过的 |
⚠️ 过孔打在**路径末点**上 —— `expand()` 会静默丢掉末端过孔(§25.14 那条最阴的 bug)。
这里靠把末点写两遍绕开(`_path()` 里 `(vx,vy,L_BOT)` 后面再来一份)。
"""
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb2_route_dp as R
from pcb2_pour import ISLANDS, island_poly
W_STEPS = [1.00, 0.80, 0.60, 0.50, 0.40, 0.30] # PWR4A 的 strokeWidthMin = 0.30
PAD_GAP = 0.15 # 过孔铜边到本焊盘的余量(同网,防 via-in-pad)
EDGE_IN = R.VIA_D / 2.0 + 0.30 # 过孔中心到岛边界的余量
D_MAX = 3.5
D_STEP = 0.05
DRILL = 0.33 # 钻孔间距(规则 otherClearance / copperThickness1oz 都要求 >= 11.8mil
# = 0.2997 mm),留 30 µm 余量。再大就过不了 J23 的屏蔽脚,见 place() 的注释
DIRS = [(math.cos(math.radians(a)), math.sin(math.radians(a))) for a in range(0, 360, 45)]
# ── 哪些位号要打孔、各打几个 ─────────────────────────────────────────
# 2026-09-06 补两类(上一版只有 25 个贴片电容):
#
# `C8n1.1` —— 换成 Rubycon 6.3TPV2200M10X10.5 之后**是贴片**(V-chip),
# 原来当它是直插电解、以为正极自己就落在岛上,其实一点铜都不通。
# `J8n.1` —— 电池排线改成 4 焊盘直焊(§13.5 末尾)之后,P+ 也是贴片。
# `TP8n1` 那个通孔测试点已经取消 ⇒ **每路 VOn_P 现在只剩排母那 3 针是通孔**。
#
# 个数按设计文档 §10.3「0.3 mm 孔单孔 1~1.5 A,功率网络每个焊盘打 3 个」:
# · `J8n.1` 是这一路 **4 A 的唯一去路**(岛 → 排线 → 手机),必须 3 个;
# · `C8n1.1` 是 2200 µF 的主储能,纹波电流也走它,给 3 个;
# · `C8n2`~`C8n6` 只承担高频纹波,各 1 个 —— 全按 3 个来会往五块岛里
# 再钻 50 个反焊盘,最窄断面(下限 7.0 mm)扛不住。这是本项目对 §10.3 的
# 一处**有意偏离**,见 PROGRESS。
N_VIA = {}
REFS = []
for _m in range(5):
REFS.append('C8%d1' % _m)
N_VIA['C8%d1' % _m] = 3
REFS += ['C8%d%d' % (_m, _k) for _k in range(2, 7)]
REFS.append('J8%d' % (_m + 1))
N_VIA['J8%d' % (_m + 1)] = 3
CAPS = REFS
def poly_of(net):
for _name, n, main, neck in ISLANDS:
if n == net:
return island_poly(main, neck)
raise KeyError(net)
def inside(poly, x, y, margin):
u"""点在多边形内,且离每条边 >= margin。岛都是轴对齐的凸/凹矩形拼接,用射线法 + 边距。"""
c = False
n = len(poly)
for i in range(n):
ax, ay = poly[i]
bx, by = poly[(i + 1) % n]
if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay):
c = not c
if not c:
return False
for i in range(n):
ax, ay = poly[i]
bx, by = poly[(i + 1) % n]
if R.seg_pt(ax, ay, bx, by, x, y) < margin:
return False
return True
def pad_dist(px, py, hw, hh, x, y):
u"""点到轴对齐焊盘矩形的距离(0 = 在里面)。"""
return math.hypot(max(abs(x - px) - hw, 0.0), max(abs(y - py) - hh, 0.0))
def _path(px, py, vx, vy, w):
u"""末点上的过孔要把点写两遍,否则 expand() 会把它丢掉(§25.14)。"""
return [(px, py, R.L_TOP, w), (vx, vy, R.L_TOP, w),
(vx, vy, R.L_BOT, w), (vx, vy, R.L_BOT, w)]
NEAR = 8.0 # 只拿焊盘 8 mm 以内的东西当障碍(全表 816+393+1373,全查太慢)
def holes(pads):
u"""每个**带钻孔的焊盘**的孔 -> 一段胶囊 (a, b, r)(圆孔就是 a == b)。
**两类孔的待遇不一样**(2026-09-06 用 DRC 一条条试出来的):
| | 线能不能压上去 | 过孔能不能靠近 |
|---|---|---|
| 焊盘自己的孔(`SLOT` / `ROUND`,金属化)| **能** —— 它就是焊盘的一部分 | 不能,钻孔间距 |
| 封装自带的非金属化孔(`npth=True`)| **不能**,DRC 报「挖槽区域 到 导线」| 不能 |
一开始把两类混在一起判,结果是「谁都连不上 J23 的屏蔽脚」——
槽孔焊盘的**任何一点**离自己的槽都不足 0.80 mm,按那个判据连焊盘都碰不得。
槽的长轴按焊盘的长轴定(`_pcb2_route_geom.json` 里的 hw/hh 已经是转过的),
`_pcb2_pads_full.json` 只存了孔的两个尺寸、没存角度,所以只能这么反推。
"""
out = []
# ⚠️ 封装自带的非金属化孔(J23 那两个 Ø0.60)**API 一个都读不出来**,
# 只能从内层铺铜的反焊盘反推 —— `pcb2_npth.py --write` 生成这份清单。
# 没有它的话,「从 A1B12 往东拉线打孔」这条最自然的走法会被 DRC
# 以「挖槽区域 到 导线 距离 0mil」打回来,而净距校核一条都不报。
try:
for cx, cy, r in json.load(open(os.path.join(HERE, '_pcb2_npth.json'), encoding='utf-8')):
out.append({'net': None, 'npth': True, 'a': (cx, cy), 'b': (cx, cy), 'r': r})
except Exception:
print(u' [!] 没有 _pcb2_npth.json —— 先跑 pcb2_npth.py --write,否则封装自带的孔看不见')
full = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
bykey = {}
for ref, num, _net, _lay, _x, _y, _shape, hole in full:
if hole:
bykey[(ref, num)] = hole
for p in pads:
h = bykey.get((p['ref'], p['num']))
if not h:
continue
if h[0] == 'ROUND':
r = float(h[1]) / R.MIL / 2.0
out.append({'net': p['net'], 'npth': False,
'a': (p['x'], p['y']), 'b': (p['x'], p['y']), 'r': r})
continue
w, L = sorted((float(h[1]) / R.MIL, float(h[2]) / R.MIL)) # 槽宽 / 槽长
r, ext = w / 2.0, (L - w) / 2.0
if p['hw'] >= p['hh']: # 焊盘长轴在 x 上
out.append({'net': p['net'], 'npth': False,
'a': (p['x'] - ext, p['y']), 'b': (p['x'] + ext, p['y']), 'r': r})
else:
out.append({'net': p['net'], 'npth': False,
'a': (p['x'], p['y'] - ext), 'b': (p['x'], p['y'] + ext), 'r': r})
return out
def local(pads, vias, lines, px, py, hl=()):
return ([p for p in pads if abs(p['x'] - px) < NEAR and abs(p['y'] - py) < NEAR],
[v for v in vias if abs(v['x'] - px) < NEAR and abs(v['y'] - py) < NEAR],
[l for l in lines
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) < NEAR],
[h for h in hl if abs(h['a'][0] - px) < NEAR and abs(h['a'][1] - py) < NEAR])
def fits(net, segs, via, lp, lv, ll, lh, mine, mvias):
u"""几段线 + 一个过孔对**异网**的净距都 >= CLR?
`segs` = [(lay, x0, y0, x1, y1, w), ...](本次要画的)
`via` = (x, y) 或 None
`mine` = 本轮已定的线 [(net, lay, x0, y0, x1, y1, w), ...]
`mvias` = 本轮已定的孔 [(net, x, y), ...]
`lh` = 附近的钻孔胶囊(`holes()` 的输出);**不分网络**,钻孔和铜是两条规则
⚠️ 线的层必须逐段拿,**不能写死顶层** —— 底层的 0402
第一版就是这么错的,本地判完全过、到 `check()` 才炸。
"""
Rv = R.VIA_D / 2.0
def seg_bad(lay, x0, y0, x1, y1, w):
h, a, b = w / 2.0, (x0, y0), (x1, y1)
for p in lp:
if p['lay'] != 12 and p['lay'] != lay:
continue
if p['net'] != net:
if R.seg_rect(a, b, p['x'], p['y'], p['hw'], p['hh']) - h < R.CLR - 1e-4:
return True
for v in lv:
if v['net'] == net:
continue
if R.seg_pt(x0, y0, x1, y1, v['x'], v['y']) - v['r'] - h < R.CLR - 1e-4:
return True
for l in ll:
if l['net'] == net or l['lay'] != lay:
continue
if R.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) - h - l['w'] / 2.0 < R.CLR - 1e-4:
return True
for n2, l2, ax, ay, bx, by, w2 in mine:
if n2 == net or l2 != lay:
continue
if R.seg_seg(a, b, (ax, ay), (bx, by)) - h - w2 / 2.0 < R.CLR - 1e-4:
return True
for n2, vx, vy in mvias:
if n2 == net:
continue
if R.seg_pt(x0, y0, x1, y1, vx, vy) - Rv - h < R.CLR - 1e-4:
return True
for q in lh: # 线只躲**非金属化**孔;焊盘自己的孔是焊盘的一部分
if q['npth'] and R.seg_seg(a, b, q['a'], q['b']) - q['r'] - h < DRILL - 1e-4:
return True
return False
for s in segs:
if seg_bad(*s):
return False
if via is None:
return True
vx, vy = via
for p in lp: # 过孔穿所有层,两个信号层的焊盘都要让
if p['net'] != net:
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < R.CLR - 1e-4:
return False
elif p['lay'] == 12:
# ⚠️ **同网也要躲开通孔焊盘** —— 铜可以重叠,钻孔不行。
# 第一版跳过了同网,于是给 J23 的两个 GND 屏蔽脚打的孔正落在 J23_1/J23_4
# 的**焊盘里**:净距校核全过,DRC 却报「钻孔 到 钻孔 距离 0mil」。
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < DRILL - 1e-4:
return False
for v in lv:
if v['net'] != net:
if math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < R.CLR - 1e-4:
return False
elif math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < DRILL - 1e-4:
return False # 同网过孔之间也是钻孔间距说了算
for l in ll:
if l['net'] == net:
continue
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], vx, vy) - Rv - l['w'] / 2.0 < R.CLR - 1e-4:
return False
for n2, l2, ax, ay, bx, by, w2 in mine:
if n2 == net:
continue
if R.seg_pt(ax, ay, bx, by, vx, vy) - Rv - w2 / 2.0 < R.CLR - 1e-4:
return False
for n2, ox, oy in mvias:
# ⚠️ 本轮新打的同网过孔也要按钻孔间距判 —— 漏了这一条,U34 相邻三个 GND 脚
# 的过孔排成一列、中心只隔 0.23 mm(三个钻孔互相打穿)。
need = R.CLR if n2 != net else DRILL
if math.hypot(vx - ox, vy - oy) - 2 * Rv < need - 1e-4:
return False
for q in lh:
if R.seg_pt(q['a'][0], q['a'][1], q['b'][0], q['b'][1], vx, vy) - q['r'] - Rv < DRILL - 1e-4:
return False
return True
def place(pad, lay, w_steps, lp, lv, ll, lh, mine, mvias, inside_ok=None):
u"""给一个焊盘找一个过孔位。
先试**一段直线**(只取 8 个方向,不会出现斜的怪角度);
不行再试**两段 45°**(第二段相对第一段只允许 ±45°,不允许直角)。
返回 (w, segs, (vx, vy)) 或 None。`inside_ok(x, y)` 可选,用来卡在铜岛内。
⚠️ 线**默认从焊盘中心起步**(最稳的连接),但焊盘中间可能有钻孔
(J23 的 A1B12/B1A12 就是 `ROUND 0.36`),压上去 DRC 报「挖槽区域 到 导线」。
所以起点会沿出线方向**往外退**,一直退到焊盘边缘内 `INSET` 为止,取第一个过的。
"""
px, py, hw, hh = pad['x'], pad['y'], pad['hw'], pad['hh']
RMIN = R.VIA_D / 2.0 + PAD_GAP
# 退到离焊盘边 0.02 为止:J23 的 A1B12 是 1.30×0.60 的焊盘、正中一个 Ø0.36 的
# **非金属化**定位孔,孔边到焊盘边只有 0.47 mm,要塞下 0.2997 的净距 + 半线宽,
# 起点必须一路退到边上(0.20 的线,实际余量 0.63−0.18−0.10−0.2997 = 50 µm)。
INSET = 0.02
def starts(dx, dy):
u"""沿 (dx, dy) 从焊盘中心往外的候选起点:中心 -> 焊盘边缘内 0.05。"""
t_edge = 1e9
if abs(dx) > 1e-9:
t_edge = min(t_edge, hw / abs(dx))
if abs(dy) > 1e-9:
t_edge = min(t_edge, hh / abs(dy))
t_max = max(0.0, t_edge - INSET)
out, t = [], 0.0
while t <= t_max + 1e-9:
out.append((round(px + t * dx, 4), round(py + t * dy, 4)))
t += 0.05
if out[-1] != (round(px + t_max * dx, 4), round(py + t_max * dy, 4)):
out.append((round(px + t_max * dx, 4), round(py + t_max * dy, 4)))
return out
def try_via(vx, vy, w, segs):
if pad_dist(px, py, hw, hh, vx, vy) < RMIN:
return None
if inside_ok and not inside_ok(vx, vy):
return None
if not fits(pad['net'], segs, (vx, vy), lp, lv, ll, lh, mine, mvias):
return None
return (w, segs, (vx, vy))
for w in w_steps:
d = 0.0
while d <= D_MAX + 1e-9:
d += D_STEP
for dx, dy in DIRS:
vx, vy = round(px + d * dx, 4), round(py + d * dy, 4)
for sx, sy in starts(dx, dy):
if math.hypot(vx - sx, vy - sy) < 1e-6:
continue
got = try_via(vx, vy, w, [(lay, sx, sy, vx, vy, w)])
if got:
return got
# 一段直线不行:拐一个 45°
for w in w_steps:
for i, (dx, dy) in enumerate(DIRS):
d1 = 0.0
while d1 <= 2.5 + 1e-9:
d1 += 0.10
mx, my = round(px + d1 * dx, 4), round(py + d1 * dy, 4)
for j in (i - 1, i + 1):
ex, ey = DIRS[j % len(DIRS)]
d2 = 0.0
while d2 <= D_MAX + 1e-9:
d2 += D_STEP
vx, vy = round(mx + d2 * ex, 4), round(my + d2 * ey, 4)
for sx, sy in starts(dx, dy):
if math.hypot(mx - sx, my - sy) < 1e-6:
continue
got = try_via(vx, vy, w, [(lay, sx, sy, mx, my, w),
(lay, mx, my, vx, vy, w)])
if got:
return got
return None
def solve(pads, vias, lines):
u"""逐个贴片焊盘挑过孔位置。返回 {key: path}。"""
byref = {}
for p in pads:
byref.setdefault(p['ref'], []).append(p)
hl = holes(pads)
routes, report, mine, mvias = {}, [], [], []
for ref in CAPS:
got = [p for p in byref.get(ref, []) if p['net'].startswith('VO') and p['net'].endswith('_P')]
if len(got) != 1:
sys.exit(u'%s 上找不到唯一的 VOn_P 焊盘(找到 %d 个)' % (ref, len(got)))
pad = got[0]
net, px, py = pad['net'], pad['x'], pad['y']
poly = poly_of(net)
lp, lv, ll, lh = local(pads, vias, lines, px, py, hl)
# 一个焊盘上要几个孔(`N_VIA`)。**逐个放、每放一个就进 mine/mvias** ——
# 后一个孔会自动躲开前一个(同网也按钻孔间距 0.33 判,见 fits())。
want = N_VIA.get(ref, 1)
for k in range(want):
# push()/check() 靠 netof(key) 取网络名,所以 '#' 前面必须是网络名
key = u'%s#%s' % (net, ref) if want == 1 else u'%s#%s-%d' % (net, ref, k + 1)
# ⚠️ 出线画在**焊盘自己那一层**,不是写死顶层。
# v1 时代全板器件在顶层,写死 L_TOP 没暴露问题;v2 之后
# VOn_P 的 35 个焊盘**全在底层**,照旧写 L_TOP 会画出 35 条
# 悬在顶层、一头不搭焊盘的线 —— 而且 fits() 拿 lay 去筛障碍,
# 连净距校核都是照着空荡荡的顶层算的,一条都不会报。
lay = pad['lay']
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
got2 = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias,
inside_ok=lambda x, y: inside(poly, x, y, EDGE_IN))
if not got2:
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net, None))
continue
w, segs, (vx, vy) = got2
pts = ([(segs[0][1], segs[0][2], lay, w)]
+ [(sx1, sy1, lay, w) for _l, _x0, _y0, sx1, sy1, _w in segs])
routes[key] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
for l_, x0, y0, x1, y1, w_ in segs:
mine.append((net, l_, x0, y0, x1, y1, w_))
mvias.append((net, vx, vy))
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net,
(w, math.hypot(vx - px, vy - py), vx, vy, len(segs))))
return routes, report
def main():
pads, vias, lines = R.geom()
ours = set('VO%d_P' % n for n in range(1, 6))
vias = [v for v in vias if v['net'] not in ours]
lines = [l for l in lines if l['net'] not in ours]
routes, report = solve(pads, vias, lines)
fail = 0
for ref, net, r in report:
if not r:
print(u' [!!] %-6s %-7s 找不到能落孔的位置' % (ref, net))
fail += 1
else:
w, d, vx, vy, ns = r
print(u' %-6s %-7s 过孔 (%8.3f,%8.3f) 离焊盘中心 %.2f 线宽 %.2f %d 段'
% (ref, net, vx, vy, d, w, ns))
segs = sum(len(R.expand(p)[0]) for p in routes.values())
nv = sum(len(R.expand(p)[1]) for p in routes.values())
print(u'')
want = sum(N_VIA.get(r, 1) for r in CAPS)
print(u'%d/%d 个过孔搞定(%d 个焊盘):%d 段导线 / %d 个过孔'
% (len(routes), want, len(CAPS), segs, nv))
if nv != len(routes):
sys.exit(u'!! 过孔数和焊盘数对不上 —— expand() 又把末端过孔吞了')
n_sharp = len(R.sharp_corners(routes))
print(u'直角拐点 %d(每条只有一段直线,应当是 0)' % n_sharp)
print(u'')
print(u'净距校核(一般 %.2f mm):' % R.CLR)
bad = R.check(routes, pads, vias, lines)
if not bad:
print(u' 全部通过 ✔')
if '--push' in sys.argv:
if bad or fail or n_sharp:
sys.exit(u'\n!! 有未解决的问题,不下发。')
print(u'')
R.push(routes)
print(u'⚠️ 下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
if __name__ == '__main__':
main()
# -*- coding: utf-8 -*-
u"""五路 `VOn_P` 的**贴片电容接到 Inner1 电源岛** —— 每个焊盘旁边打一个过孔。
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
/c/Python314/python .claude/pcb2_vo_vias.py # 2. 只算 + 本地净距校核
/c/Python314/python .claude/pcb2_vo_vias.py --push # 3. 下发
/c/Python314/python .claude/pcb2_pour.py --push # 4. **必须重铺**(新过孔要反焊盘)
## 为什么要有这一步
`VO1_P`~`VO5_P` 每个网络 10 个焊盘:**5 个通孔**(`C8n1.1` Ø10 电解正极、`TP8n1`
测试点、排母 3 针)本身就落在 Inner1 的岛上,不用管;剩下 **5 个贴片焊盘**
(`C8n2`/`C8n3` 100µF 1206、`C8n4`/`C8n5` 22µF 1206、`C8n6` 100nF 0402)
只有顶层铜,**和岛之间没有任何通路** —— 这就是 §25.14 里连接性 51 条中的 31 条。
所以这一步和布线不是一回事:不需要找路径,只需要**在焊盘边上打一个过孔**,
用一小段线接过去,垂直下到 Inner1 的岛上。25 个焊盘 = 25 个过孔。
## 选点判据(不是"看着差不多")
| | |
|---|---|
| 方向 | 只取 8 个方向(0/45/90…),**不许出现斜的怪角度**(全板 45° 化,§25.14)|
| 起步距离 | 过孔铜边到本焊盘矩形 **≥ 0.15 mm**(同网,不是 DRC 约束,是不想做 via-in-pad)|
| 净距 | 过孔和那一小段线对**异网**焊盘/过孔/导线 ≥ 0.20 —— 直接借 `pcb2_route_dp.check()` |
| 岛内余量 | 过孔中心离岛多边形边界 ≥ 0.61/2 + 0.30 = **0.605 mm**,免得铺出来只搭上一点点 |
| 线宽 | `PWR4A` 最小 0.30;从 1.00 起往下退让,取第一个过的 |
⚠️ 过孔打在**路径末点**上 —— `expand()` 会静默丢掉末端过孔(§25.14 那条最阴的 bug)。
这里靠把末点写两遍绕开(`_path()` 里 `(vx,vy,L_BOT)` 后面再来一份)。
"""
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb2_route_dp as R
from pcb2_pour import ISLANDS, island_poly
W_STEPS = [1.00, 0.80, 0.60, 0.50, 0.40, 0.30] # PWR4A 的 strokeWidthMin = 0.30
PAD_GAP = 0.15 # 过孔铜边到本焊盘的余量(同网,防 via-in-pad)
EDGE_IN = R.VIA_D / 2.0 + 0.30 # 过孔中心到岛边界的余量
D_MAX = 3.5
D_STEP = 0.05
DRILL = 0.33 # 钻孔间距(规则 otherClearance / copperThickness1oz 都要求 >= 11.8mil
# = 0.2997 mm),留 30 µm 余量。再大就过不了 J23 的屏蔽脚,见 place() 的注释
DIRS = [(math.cos(math.radians(a)), math.sin(math.radians(a))) for a in range(0, 360, 45)]
# ── 哪些位号要打孔、各打几个 ─────────────────────────────────────────
# 2026-09-06 补两类(上一版只有 25 个贴片电容):
#
# `C8n1.1` —— 换成 Rubycon 6.3TPV2200M10X10.5 之后**是贴片**(V-chip),
# 原来当它是直插电解、以为正极自己就落在岛上,其实一点铜都不通。
# `J8n.1` —— 电池排线改成 4 焊盘直焊(§13.5 末尾)之后,P+ 也是贴片。
# `TP8n1` 那个通孔测试点已经取消 ⇒ **每路 VOn_P 现在只剩排母那 3 针是通孔**。
#
# 个数按设计文档 §10.3「0.3 mm 孔单孔 1~1.5 A,功率网络每个焊盘打 3 个」:
# · `J8n.1` 是这一路 **4 A 的唯一去路**(岛 → 排线 → 手机),必须 3 个;
# · `C8n1.1` 是 2200 µF 的主储能,纹波电流也走它,给 3 个;
# · `C8n2`~`C8n6` 只承担高频纹波,各 1 个 —— 全按 3 个来会往五块岛里
# 再钻 50 个反焊盘,最窄断面(下限 7.0 mm)扛不住。这是本项目对 §10.3 的
# 一处**有意偏离**,见 PROGRESS。
N_VIA = {}
REFS = []
for _m in range(5):
REFS.append('C8%d1' % _m)
N_VIA['C8%d1' % _m] = 3
REFS += ['C8%d%d' % (_m, _k) for _k in range(2, 7)]
REFS.append('J8%d' % (_m + 1))
N_VIA['J8%d' % (_m + 1)] = 3
CAPS = REFS
def poly_of(net):
for _name, n, main, neck in ISLANDS:
if n == net:
return island_poly(main, neck)
raise KeyError(net)
def inside(poly, x, y, margin):
u"""点在多边形内,且离每条边 >= margin。岛都是轴对齐的凸/凹矩形拼接,用射线法 + 边距。"""
c = False
n = len(poly)
for i in range(n):
ax, ay = poly[i]
bx, by = poly[(i + 1) % n]
if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay):
c = not c
if not c:
return False
for i in range(n):
ax, ay = poly[i]
bx, by = poly[(i + 1) % n]
if R.seg_pt(ax, ay, bx, by, x, y) < margin:
return False
return True
def pad_dist(px, py, hw, hh, x, y):
u"""点到轴对齐焊盘矩形的距离(0 = 在里面)。"""
return math.hypot(max(abs(x - px) - hw, 0.0), max(abs(y - py) - hh, 0.0))
def _path(px, py, vx, vy, w):
u"""末点上的过孔要把点写两遍,否则 expand() 会把它丢掉(§25.14)。"""
return [(px, py, R.L_TOP, w), (vx, vy, R.L_TOP, w),
(vx, vy, R.L_BOT, w), (vx, vy, R.L_BOT, w)]
NEAR = 8.0 # 只拿焊盘 8 mm 以内的东西当障碍(全表 816+393+1373,全查太慢)
def holes(pads):
u"""每个**带钻孔的焊盘**的孔 -> 一段胶囊 (a, b, r)(圆孔就是 a == b)。
**两类孔的待遇不一样**(2026-09-06 用 DRC 一条条试出来的):
| | 线能不能压上去 | 过孔能不能靠近 |
|---|---|---|
| 焊盘自己的孔(`SLOT` / `ROUND`,金属化)| **能** —— 它就是焊盘的一部分 | 不能,钻孔间距 |
| 封装自带的非金属化孔(`npth=True`)| **不能**,DRC 报「挖槽区域 到 导线」| 不能 |
一开始把两类混在一起判,结果是「谁都连不上 J23 的屏蔽脚」——
槽孔焊盘的**任何一点**离自己的槽都不足 0.80 mm,按那个判据连焊盘都碰不得。
槽的长轴按焊盘的长轴定(`_pcb2_route_geom.json` 里的 hw/hh 已经是转过的),
`_pcb2_pads_full.json` 只存了孔的两个尺寸、没存角度,所以只能这么反推。
"""
out = []
# ⚠️ 封装自带的非金属化孔(J23 那两个 Ø0.60)**API 一个都读不出来**,
# 只能从内层铺铜的反焊盘反推 —— `pcb2_npth.py --write` 生成这份清单。
# 没有它的话,「从 A1B12 往东拉线打孔」这条最自然的走法会被 DRC
# 以「挖槽区域 到 导线 距离 0mil」打回来,而净距校核一条都不报。
try:
for cx, cy, r in json.load(open(os.path.join(HERE, '_pcb2_npth.json'), encoding='utf-8')):
out.append({'net': None, 'npth': True, 'a': (cx, cy), 'b': (cx, cy), 'r': r})
except Exception:
print(u' [!] 没有 _pcb2_npth.json —— 先跑 pcb2_npth.py --write,否则封装自带的孔看不见')
full = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
bykey = {}
for ref, num, _net, _lay, _x, _y, _shape, hole in full:
if hole:
bykey[(ref, num)] = hole
for p in pads:
h = bykey.get((p['ref'], p['num']))
if not h:
continue
if h[0] == 'ROUND':
r = float(h[1]) / R.MIL / 2.0
out.append({'net': p['net'], 'npth': False,
'a': (p['x'], p['y']), 'b': (p['x'], p['y']), 'r': r})
continue
w, L = sorted((float(h[1]) / R.MIL, float(h[2]) / R.MIL)) # 槽宽 / 槽长
r, ext = w / 2.0, (L - w) / 2.0
if p['hw'] >= p['hh']: # 焊盘长轴在 x 上
out.append({'net': p['net'], 'npth': False,
'a': (p['x'] - ext, p['y']), 'b': (p['x'] + ext, p['y']), 'r': r})
else:
out.append({'net': p['net'], 'npth': False,
'a': (p['x'], p['y'] - ext), 'b': (p['x'], p['y'] + ext), 'r': r})
return out
def local(pads, vias, lines, px, py, hl=()):
return ([p for p in pads if abs(p['x'] - px) < NEAR and abs(p['y'] - py) < NEAR],
[v for v in vias if abs(v['x'] - px) < NEAR and abs(v['y'] - py) < NEAR],
[l for l in lines
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], px, py) < NEAR],
[h for h in hl if abs(h['a'][0] - px) < NEAR and abs(h['a'][1] - py) < NEAR])
def fits(net, segs, via, lp, lv, ll, lh, mine, mvias):
u"""几段线 + 一个过孔对**异网**的净距都 >= CLR?
`segs` = [(lay, x0, y0, x1, y1, w), ...](本次要画的)
`via` = (x, y) 或 None
`mine` = 本轮已定的线 [(net, lay, x0, y0, x1, y1, w), ...]
`mvias` = 本轮已定的孔 [(net, x, y), ...]
`lh` = 附近的钻孔胶囊(`holes()` 的输出);**不分网络**,钻孔和铜是两条规则
⚠️ 线的层必须逐段拿,**不能写死顶层** —— 底层的 0402
第一版就是这么错的,本地判完全过、到 `check()` 才炸。
"""
Rv = R.VIA_D / 2.0
def seg_bad(lay, x0, y0, x1, y1, w):
h, a, b = w / 2.0, (x0, y0), (x1, y1)
for p in lp:
if p['lay'] != 12 and p['lay'] != lay:
continue
if p['net'] != net:
if R.seg_rect(a, b, p['x'], p['y'], p['hw'], p['hh']) - h < R.CLR - 1e-4:
return True
for v in lv:
if v['net'] == net:
continue
if R.seg_pt(x0, y0, x1, y1, v['x'], v['y']) - v['r'] - h < R.CLR - 1e-4:
return True
for l in ll:
if l['net'] == net or l['lay'] != lay:
continue
if R.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) - h - l['w'] / 2.0 < R.CLR - 1e-4:
return True
for n2, l2, ax, ay, bx, by, w2 in mine:
if n2 == net or l2 != lay:
continue
if R.seg_seg(a, b, (ax, ay), (bx, by)) - h - w2 / 2.0 < R.CLR - 1e-4:
return True
for n2, vx, vy in mvias:
if n2 == net:
continue
if R.seg_pt(x0, y0, x1, y1, vx, vy) - Rv - h < R.CLR - 1e-4:
return True
for q in lh: # 线只躲**非金属化**孔;焊盘自己的孔是焊盘的一部分
if q['npth'] and R.seg_seg(a, b, q['a'], q['b']) - q['r'] - h < DRILL - 1e-4:
return True
return False
for s in segs:
if seg_bad(*s):
return False
if via is None:
return True
vx, vy = via
for p in lp: # 过孔穿所有层,两个信号层的焊盘都要让
if p['net'] != net:
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < R.CLR - 1e-4:
return False
elif p['lay'] == 12:
# ⚠️ **同网也要躲开通孔焊盘** —— 铜可以重叠,钻孔不行。
# 第一版跳过了同网,于是给 J23 的两个 GND 屏蔽脚打的孔正落在 J23_1/J23_4
# 的**焊盘里**:净距校核全过,DRC 却报「钻孔 到 钻孔 距离 0mil」。
if R.seg_rect((vx, vy), (vx, vy), p['x'], p['y'], p['hw'], p['hh']) - Rv < DRILL - 1e-4:
return False
for v in lv:
if v['net'] != net:
if math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < R.CLR - 1e-4:
return False
elif math.hypot(vx - v['x'], vy - v['y']) - v['r'] - Rv < DRILL - 1e-4:
return False # 同网过孔之间也是钻孔间距说了算
for l in ll:
if l['net'] == net:
continue
if R.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], vx, vy) - Rv - l['w'] / 2.0 < R.CLR - 1e-4:
return False
for n2, l2, ax, ay, bx, by, w2 in mine:
if n2 == net:
continue
if R.seg_pt(ax, ay, bx, by, vx, vy) - Rv - w2 / 2.0 < R.CLR - 1e-4:
return False
for n2, ox, oy in mvias:
# ⚠️ 本轮新打的同网过孔也要按钻孔间距判 —— 漏了这一条,U34 相邻三个 GND 脚
# 的过孔排成一列、中心只隔 0.23 mm(三个钻孔互相打穿)。
need = R.CLR if n2 != net else DRILL
if math.hypot(vx - ox, vy - oy) - 2 * Rv < need - 1e-4:
return False
for q in lh:
if R.seg_pt(q['a'][0], q['a'][1], q['b'][0], q['b'][1], vx, vy) - q['r'] - Rv < DRILL - 1e-4:
return False
return True
def place(pad, lay, w_steps, lp, lv, ll, lh, mine, mvias, inside_ok=None):
u"""给一个焊盘找一个过孔位。
先试**一段直线**(只取 8 个方向,不会出现斜的怪角度);
不行再试**两段 45°**(第二段相对第一段只允许 ±45°,不允许直角)。
返回 (w, segs, (vx, vy)) 或 None。`inside_ok(x, y)` 可选,用来卡在铜岛内。
⚠️ 线**默认从焊盘中心起步**(最稳的连接),但焊盘中间可能有钻孔
(J23 的 A1B12/B1A12 就是 `ROUND 0.36`),压上去 DRC 报「挖槽区域 到 导线」。
所以起点会沿出线方向**往外退**,一直退到焊盘边缘内 `INSET` 为止,取第一个过的。
"""
px, py, hw, hh = pad['x'], pad['y'], pad['hw'], pad['hh']
RMIN = R.VIA_D / 2.0 + PAD_GAP
# 退到离焊盘边 0.02 为止:J23 的 A1B12 是 1.30×0.60 的焊盘、正中一个 Ø0.36 的
# **非金属化**定位孔,孔边到焊盘边只有 0.47 mm,要塞下 0.2997 的净距 + 半线宽,
# 起点必须一路退到边上(0.20 的线,实际余量 0.63−0.18−0.10−0.2997 = 50 µm)。
INSET = 0.02
def starts(dx, dy):
u"""沿 (dx, dy) 从焊盘中心往外的候选起点:中心 -> 焊盘边缘内 0.05。"""
t_edge = 1e9
if abs(dx) > 1e-9:
t_edge = min(t_edge, hw / abs(dx))
if abs(dy) > 1e-9:
t_edge = min(t_edge, hh / abs(dy))
t_max = max(0.0, t_edge - INSET)
out, t = [], 0.0
while t <= t_max + 1e-9:
out.append((round(px + t * dx, 4), round(py + t * dy, 4)))
t += 0.05
if out[-1] != (round(px + t_max * dx, 4), round(py + t_max * dy, 4)):
out.append((round(px + t_max * dx, 4), round(py + t_max * dy, 4)))
return out
def try_via(vx, vy, w, segs):
if pad_dist(px, py, hw, hh, vx, vy) < RMIN:
return None
if inside_ok and not inside_ok(vx, vy):
return None
if not fits(pad['net'], segs, (vx, vy), lp, lv, ll, lh, mine, mvias):
return None
return (w, segs, (vx, vy))
for w in w_steps:
d = 0.0
while d <= D_MAX + 1e-9:
d += D_STEP
for dx, dy in DIRS:
vx, vy = round(px + d * dx, 4), round(py + d * dy, 4)
for sx, sy in starts(dx, dy):
if math.hypot(vx - sx, vy - sy) < 1e-6:
continue
got = try_via(vx, vy, w, [(lay, sx, sy, vx, vy, w)])
if got:
return got
# 一段直线不行:拐一个 45°
for w in w_steps:
for i, (dx, dy) in enumerate(DIRS):
d1 = 0.0
while d1 <= 2.5 + 1e-9:
d1 += 0.10
mx, my = round(px + d1 * dx, 4), round(py + d1 * dy, 4)
for j in (i - 1, i + 1):
ex, ey = DIRS[j % len(DIRS)]
d2 = 0.0
while d2 <= D_MAX + 1e-9:
d2 += D_STEP
vx, vy = round(mx + d2 * ex, 4), round(my + d2 * ey, 4)
for sx, sy in starts(dx, dy):
if math.hypot(mx - sx, my - sy) < 1e-6:
continue
got = try_via(vx, vy, w, [(lay, sx, sy, mx, my, w),
(lay, mx, my, vx, vy, w)])
if got:
return got
return None
def solve(pads, vias, lines):
u"""逐个贴片焊盘挑过孔位置。返回 {key: path}。"""
byref = {}
for p in pads:
byref.setdefault(p['ref'], []).append(p)
hl = holes(pads)
routes, report, mine, mvias = {}, [], [], []
for ref in CAPS:
got = [p for p in byref.get(ref, []) if p['net'].startswith('VO') and p['net'].endswith('_P')]
if len(got) != 1:
sys.exit(u'%s 上找不到唯一的 VOn_P 焊盘(找到 %d 个)' % (ref, len(got)))
pad = got[0]
net, px, py = pad['net'], pad['x'], pad['y']
poly = poly_of(net)
lp, lv, ll, lh = local(pads, vias, lines, px, py, hl)
# 一个焊盘上要几个孔(`N_VIA`)。**逐个放、每放一个就进 mine/mvias** ——
# 后一个孔会自动躲开前一个(同网也按钻孔间距 0.33 判,见 fits())。
want = N_VIA.get(ref, 1)
for k in range(want):
# push()/check() 靠 netof(key) 取网络名,所以 '#' 前面必须是网络名
# 2026-09-07:分隔符改用 R.KSEP(chr(0)),'#' 会和 KEY_OE# 撞、也让 netof() 拆不开(第三十轮实测 13 条假「过孔-焊盘」)
key = u'%s%s%s' % (net, R.KSEP, ref) if want == 1 else u'%s%s%s-%d' % (net, R.KSEP, ref, k + 1)
# ⚠️ 出线画在**焊盘自己那一层**,不是写死顶层。
# v1 时代全板器件在顶层,写死 L_TOP 没暴露问题;v2 之后
# VOn_P 的 35 个焊盘**全在底层**,照旧写 L_TOP 会画出 35 条
# 悬在顶层、一头不搭焊盘的线 —— 而且 fits() 拿 lay 去筛障碍,
# 连净距校核都是照着空荡荡的顶层算的,一条都不会报。
lay = pad['lay']
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
got2 = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias,
inside_ok=lambda x, y: inside(poly, x, y, EDGE_IN))
if not got2:
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net, None))
continue
w, segs, (vx, vy) = got2
pts = ([(segs[0][1], segs[0][2], lay, w)]
+ [(sx1, sy1, lay, w) for _l, _x0, _y0, sx1, sy1, _w in segs])
routes[key] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
for l_, x0, y0, x1, y1, w_ in segs:
mine.append((net, l_, x0, y0, x1, y1, w_))
mvias.append((net, vx, vy))
report.append((ref if k == 0 else '%s#%d' % (ref, k + 1), net,
(w, math.hypot(vx - px, vy - py), vx, vy, len(segs))))
return routes, report
def main():
pads, vias, lines = R.geom()
ours = set('VO%d_P' % n for n in range(1, 6))
vias = [v for v in vias if v['net'] not in ours]
lines = [l for l in lines if l['net'] not in ours]
routes, report = solve(pads, vias, lines)
fail = 0
for ref, net, r in report:
if not r:
print(u' [!!] %-6s %-7s 找不到能落孔的位置' % (ref, net))
fail += 1
else:
w, d, vx, vy, ns = r
print(u' %-6s %-7s 过孔 (%8.3f,%8.3f) 离焊盘中心 %.2f 线宽 %.2f %d 段'
% (ref, net, vx, vy, d, w, ns))
segs = sum(len(R.expand(p)[0]) for p in routes.values())
nv = sum(len(R.expand(p)[1]) for p in routes.values())
print(u'')
want = sum(N_VIA.get(r, 1) for r in CAPS)
print(u'%d/%d 个过孔搞定(%d 个焊盘):%d 段导线 / %d 个过孔'
% (len(routes), want, len(CAPS), segs, nv))
if nv != len(routes):
sys.exit(u'!! 过孔数和焊盘数对不上 —— expand() 又把末端过孔吞了')
n_sharp = len(R.sharp_corners(routes))
print(u'直角拐点 %d(每条只有一段直线,应当是 0)' % n_sharp)
print(u'')
print(u'净距校核(一般 %.2f mm):' % R.CLR)
bad = R.check(routes, pads, vias, lines)
if not bad:
print(u' 全部通过 ✔')
if '--push' in sys.argv:
if bad or fail or n_sharp:
sys.exit(u'\n!! 有未解决的问题,不下发。')
print(u'')
R.push(routes)
print(u'⚠️ 下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
if __name__ == '__main__':
main()
+14 -9
查看文件
@@ -32,10 +32,14 @@ HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
PCB = '7e9415a16a909c51'
MIL = 39.3701 # 1 mm = 39.3701 mil
BW, BH = 160.0, 125.0 # 板框 mm
BW, BH = 160.0, 125.0 # 元件区 mm(原点左下角,坐标系不变)
# 2026-09-06 用户指令:两板尺寸取大对齐 —— 下板板框放大到载板的 220×135,原点不动,
# 板框左下角在 (−30,−5)(下板坐标 = 载板坐标 − (30,5),见 pcb1_env.py)。
OX, OY, OW, OH = -30.0, -5.0, 220.0, 135.0
CORNER_R = 5.0 # 四角圆角半径 mm
# 定位孔:M3 金属化,孔 Φ3.2 / 焊盘 Φ6.0 / 禁布 Φ6.5,孔心与圆角同心
MH = [(5.0, 5.0), (BW - 5.0, 5.0), (5.0, BH - 5.0), (BW - 5.0, BH - 5.0)]
# 定位孔与载板 pcb2_place.MH 对齐(减 (30,5)):四角 + 两侧中点;事实来源是 pcb1_place.MH,这里同表
MH = [(-22.0, 3.0), (182.0, 3.0), (-26.0, 126.0), (186.0, 126.0), (-23.0, 55.0), (183.0, 40.0)]
MH_HOLE, MH_PAD, MH_KEEPOUT = 3.2, 6.0, 6.5
# ── 封装占位尺寸(mm,含焊盘;未旋转时 x 向宽 w、y 向高 h)──────────────
@@ -469,14 +473,15 @@ def outline_poly():
要求闭合,不闭合就建不出来)。ARC 的角度**正值 = 逆时针**(本工程 Y 轴朝上)。
"""
R = CORNER_R
X0, Y0, X1, Y1 = OX, OY, OX + OW, OY + OH
pts = []
L = lambda x, y: pts.append(('L', x, y))
A = lambda x, y: pts.append(('ARC', 90.0, x, y))
start = (R, 0.0)
L(BW - R, 0.0); A(BW, R)
L(BW, BH - R); A(BW - R, BH)
L(R, BH); A(0.0, BH - R)
L(0.0, R); A(R, 0.0) # 回到起点
start = (X0 + R, Y0)
L(X1 - R, Y0); A(X1, Y0 + R)
L(X1, Y1 - R); A(X1 - R, Y1)
L(X0 + R, Y1); A(X0, Y1 - R)
L(X0, Y0 + R); A(X0 + R, Y0) # 回到起点
src, mode = [start[0] * MIL, start[1] * MIL], None
for p in pts:
@@ -549,8 +554,8 @@ return {del, made: r ? r.primitiveId : null, left};
if n['left'] != 1:
print(u' !! layer 11 上有 %d 条多段线,应为 1 —— 板框没建成或有残留' % n['left'])
sys.exit(1)
print(u'板框: 1 条闭合多段线(4 直边 + 4 圆角 R%.0f),%.0f x %.0f mm;'
u'顺带删掉 %d 个旧图元' % (CORNER_R, BW, BH, n['del']))
print(u'板框: 1 条闭合多段线(4 直边 + 4 圆角 R%.0f),%.0f x %.0f mm,左下角 (%.0f,%.0f);'
u'顺带删掉 %d 个旧图元' % (CORNER_R, OW, OH, OX, OY, n['del']))
n = call("""
const MH = %s;
+7 -1
查看文件
@@ -43,7 +43,13 @@ from eda import run
TOL = 0.6
_CACHE = json.load(io.open(os.path.join(HERE, 'eda-parts.json'), encoding='utf-8'))
# 默认扫下板;`--board Board2` 扫载板(两块板的页表都在 eda-parts.json 里)
if '--board' in sys.argv:
# `--r2 BLADE` / `--r2 BASE` 扫 R2 刀片式工程(页表在 .claude/r2/r2-uuids.json)——
# ⚠ 它只按 uuid 打开文档,不切工程:跑之前先让客户端停在 PhoneRail-R2 上。
if '--r2' in sys.argv:
_b = sys.argv[sys.argv.index('--r2') + 1]
PAGES = json.load(io.open(os.path.join(HERE, 'r2', 'r2-uuids.json'),
encoding='utf-8'))['boards'][_b]['pages']
elif '--board' in sys.argv:
_b = sys.argv[sys.argv.index('--board') + 1]
PAGES = _CACHE['schematic_boards'][_b]['pages']
else:
+225
查看文件
@@ -0,0 +1,225 @@
# R2 底板(BASE)剩余四页的**跨页契约** — 2026-09-08
`A1-INPUT` / `A2-AUX` 已画完。本文件钉死 `A3-CTRL` / `A4-HUB` / `A5-SLOTS` / `A6-FAN`
四页之间的**网络名、方向、器件归属、位号区段**。四页由四个子代理并行设计,
**只有主代理能下发到 EasyEDA**(全机只有一个 EDA 客户端,`openDocument` 是全局状态,
两个人同时推必然把图元放到别人的页上)。
⛔ **子代理绝对不许**:跑 `.claude/eda.py`、`dump_page.py`、`xpage.py`、`shot.py`、
`verify_sch.py`、任何带 `--push` 的脚本,或任何会连 `127.0.0.1:4962x` 的东西。
你的交付物是**一个能本地自检通过的 Python 脚本**,主代理负责下发与几何验证。
---
## 0. 已经存在的东西(不要重画,也不要改名)
| 页 | 内容 | 对外端口 / 旗 |
|---|---|---|
| `A1-INPUT` | J1 DC-005 → F1 → Q1 反接 → C2~C6 → Rs6 → 12V 母线;U13 INA226(0x43);U29 TL431 泄放 | `+12V`(旗) `GND`(旗) `+3V3`(旗) `SDA` `SCL` `INA_ALERT`(端口) |
| `A2-AUX` | U19 12V→3V3(0.5A);U30 12V→5V(2A) | `+12V` `+3V3` `GND`(旗) `+5V`(端口,OUT) |
`xpage` 现在的 4 个单实例 `+5V` / `SDA` / `SCL` / `INA_ALERT` 就是等你们补对端的。
---
## 1. 网络名总表(**唯一事实来源**)
### 1.1 电源
| 网络 | 图元类型 | 说明 |
|---|---|---|
| `+12V` | **Power 旗** | 全板 12V 母线 |
| `+3V3` | **Power 旗** | U19 出来的控制轨(0.5A),给 ESP32 / mux / DAC / 刀片逻辑 |
| `+5V` | **端口** | U30 出来的 5V(2A)。A2 已发 OUT,A4/A5 用 **IN** |
| `+3V3_HUB` | **Power 旗**(仅 A4 页内) | U31 AMS1117 给 U32 专用,**不许**和 `+3V3` 并 |
| `FAN_12V_SW` | **Power 旗**(仅 A6 页内) | Q7 高边开关之后的风扇 12V |
| `GND` | **Ground 旗** | — |
### 1.2 跨页信号(发出方 → 接收方)
| 网络 | 发出 | 接收 | 说明 |
|---|---|---|---|
| `SDA` / `SCL` | A1(U13) | **A3**(上拉 + ESP32 + U35 上游 + U6/U7 DAC) | I²C **主干**,不经 mux |
| `INA_ALERT` | A1(U13) | **A3**(ESP32 GPIO) | 开漏,上拉放在 A3 |
| `SDA1`~`SDA5` / `SCL1`~`SCL5` | **A3**(U35 通道 0~4) | **A5**(槽 1~5) | mux 分支,**上拉在刀片上**(B2-CTRL 的 R40/R41),底板这一侧不加 |
| `VDAC1`~`VDAC5` | **A3**(U6/U7) | **A5** | 模拟量,跨金手指进刀片 |
| `EFUSE_EN1`~`EFUSE_EN5` | **A3**(ESP32 直驱) | **A5** | 不经 IO 扩展 |
| `PRSNT1#`~`PRSNT5#` | **A5**(槽 **B31** 一针) | **A3**(ESP32 输入 + 上拉) | ⚠️ **2026-09-08 修正**:底板是**主板侧**,`A1` 与 `B31` 必须是**两个节点** —— `A1 → GND`、`B31 → PRSNT{n}#`。短接 A1↔B31 的是**刀片**(`b4_edge.py`,PCIe add-in card 的标准做法),插上刀片才把 B31 拉到地。若底板也照字面「镜像」把两针并成一个网,回路里一个对地通路都没有 ⇒ **插不插都读「未插入」,而 DRC/ERC/几何验证/跨页对账四道全过**。见 `a5_slots.py` 表头的对照表 |
| `INT#` | **A5**(五槽 B11 并联) | **A3**(ESP32 + **唯一一颗 10k 上拉到 +3V3**) | 开漏 wire-OR,五槽共一个网络 |
| `HUB_RST` | **A3**(ESP32) | **A4**(U32) | ⚠ 见 §3 的保命约束 |
| `UP_VBUS_DET` | **A4** | **A3**(ESP32) | 电脑在不在 |
| `USB1_DP/DM` ~ `USB5_DP/DM` | **A4**(U32 下行口 1~5) | **A5**(槽 B14/B15) | |
| `USB6_DP` / `USB6_DM` | **A4**(U32 下行口 6) | **A3**(ESP32 原生 USB,IO19=D−/IO20=D+) | D13 |
| `FAN_EN` | **A3**(ESP32) | **A6** | 高边总开关,复位态必须**关电** |
| `FAN_PWM` | **A3**(ESP32) | **A6** | 两只风扇共用(A6 里经反相成 `FAN_PWM_L`) |
| `FAN_TACH_IN` / `FAN_TACH_OUT` | **A6** | **A3**(ESP32) | 两路独立测速 |
| `NTC1` / `NTC2` | **A6**(RT601/RT602 分压节点) | **A3**(ESP32 **ADC1** 脚) | ⚠ 必须 ADC1(IO1~IO10),ADC2 与 WiFi 冲突 |
### 1.3 页内网络(不跨页,别在别的页上用这些名字)
- A3:`MUX_RST#`(U35 的 RESET#,10k 上拉 + 一个 ESP32 GPIO)、`EN` / `IO0` / `TXD0` / `RXD0`(J33)
- A4:`UP_DP` `UP_DM` `VBUS_UP` `VBUS_DET` `XTAL1` `XTAL2` `RBIAS` `VDD18` `VDD18PLL`
`CFGSEL0` `CFGSEL1` `NONREM0` `NONREM1`(全部沿用 R1 `C2-HUB` 的名字)
- A6:`FAN_PWM_L` `FAN_IN_TACH_RAW` `FAN_OUT_TACH_RAW`
- A5:无(全是 §1.2 的网络 + 电源 + 地)
### 1.4 **明确不存在**的网络(R1 有、R2 没有,写了就是错)
`BUCK_PG`(A2 的 U19 没引 PG)、`IOEXP_INT#`(改叫 `INT#`)、
`EFUSE_PG*` / `EFUSE_FLT*`(eFuse 在刀片上,状态走刀片自己的 PCA9555)、
`VBUS_EN*`(在刀片 B2-CTRL 上)、`KEY_*`(74HC595 已作废,D9)、
`USB_DP` / `USB_DM` 不带编号的形式(那是刀片页内的名字)。
---
## 2. 位号区段(防撞)
| 页 | 允许使用 |
|---|---|
| A1(已画) | `J1` `F1` `Q1` `Q3` `DZ1` `D1` `R1` `R4` `R5` `R6` `R9A~R9E` `C1~C6` `C11` `Rs6` `U13` `U29` |
| A2(已画) | `U19` `U30` `L2` `L3` `R2` `R3` `R706` `R707` `C7~C10` `C731~C735` |
| **A3-CTRL** | `U20` `U35` `U6` `U7` `J33`,新增无源件用 `R601~R650` / `C601~C650` |
| **A4-HUB** | `U31` `U32` `Y1` `J23`,无源件 `R708~R730` / `C737` `C740~C759` |
| **A5-SLOTS** / **A5B-SLOTS** | 插槽 `J34~J38`(槽 1~5)、D± 退路座 `J39~J43`,无源件 `R760~R790` / `C761~C790` |
| **A6-FAN** | `Q6~Q10` `D2` `DZ3` `J26` `J27` `RT601` `RT602`,电容 `C736` `C738` `C739`,电阻 `R733` `R734` `R746~R757` + 新增 `R660~R699` |
> ⚠️ **2026-09-08 修订**:原表里 A4 的 `C740~C760` 与 A5 的 `C760~C790` 在 `C760` 上撞车(A6 代理发现),
> 已改成 `C740~C759` / `C761~C790`。A6 原来只点名了 `C738`,现补 `C736` / `C739`(全表仅剩的两个无主号)。
> `A5B-SLOTS` 是 2026-09-08 新开的第七页(`e493f8bce3a51aae`),与 A5 共用同一段号。
---
## 3. 三条会烧板 / 会自断后路的硬约束
1. **`HUB_RST` 必须外部上拉到 `+3V3_HUB`(10k),且 ESP32 复位期间 Hub 保持工作。**
ESP32 的控制台就挂在这颗 Hub 后面(D13),误复位 = 自断控制台 + 下次烧不进去。
固件侧只在启动自检脉冲一次 —— 硬件侧要保证「不驱动 = 不复位」。
2. **`FAN_EN` 复位态必须是关电。** Q7 是 P-MOS 高边,栅极靠 Q6 拉低才导通;
Q6 栅极要有 100k 下拉,ESP32 高阻时风扇不转(R1 P6 已经是这个拓扑,照抄)。
3. **`+3V3_HUB` 与 `+3V3` 是两条轨**(U32 满载 ≈420 mA,挂不进 U19 那条 0.5A 轨)。
两者只共地。A4 页上不许出现把它们并起来的导线或同名旗。
---
## 4. 器件与引脚几何(全部实测,直接用,不要自己猜)
`.claude/eda-parts.json` 是器件表;下面是**这四页要用到的新器件**的引脚相对坐标(rot 0,相对元件中心):
```
TCA9548APWR (C130026) 左列 x=-50,右列 x=+50
1 A0 (-50, 55) 13 SD4 (50,-55)
2 A1 (-50, 45) 14 SC4 (50,-45)
3 RESET#(-50, 35) 15 SD5 (50,-35)
4 SD0 (-50, 25) 16 SC5 (50,-25)
5 SC0 (-50, 15) 17 SD6 (50,-15)
6 SD1 (-50, 5) 18 SC6 (50, -5)
7 SC1 (-50, -5) 19 SD7 (50, 5)
8 SD2 (-50,-15) 20 SC7 (50, 15)
9 SC2 (-50,-25) 21 A2 (50, 25)
10 SD3 (-50,-35) 22 SCL (50, 35)
11 SC3 (-50,-45) 23 SDA (50, 45)
12 GND (-50,-55) 24 VCC (50, 55)
NCP18XH103F03RB (C13564) 1 (-20,0) 2 (+20,0) 无极性
2.54mm 1*5P 直排针 (C50951) 1(-20,20) 2(-20,10) 3(-20,0) 4(-20,-10) 5(-20,-20) 全部朝左
AMS1117-3.3 (C6186) 1 GND(-45,10) 2 VOUT(-45,0) 3 VIN(-45,-10) 4 VOUT(+45,0)
PCIE-064-02-F-D-TH (C4597619) A1~A32: x=-35, y=155-10*(n-1)
B1~B32: x=+35, y=155-10*(n-1)
```
其余器件(ESP32-S3-WROOM-1 / DAC63204 / USB2517I / Y1 / AO3400A / AO3401A /
SS34 / BZT52C8V2 / TYPE-C-31-M-12 / 2510-4A / A1251WV-S-4P / 各种 0402)的引脚坐标
**从 R1 的实测 dump 里直接读**,见 §5。
---
## 5. R1 参照版图(已 dump 成 JSON,直接读,**不要连 EDA**)
| 文件 | 是哪一页 | 给谁用 |
|---|---|---|
| `.claude/r2/_p3_live.json` | R1 `P3-CONTROL`(U20 ESP32 + J22 + RT601/602) | **A3**(ESP32 那一块整块搬)、**A6**(NTC 分压那一小块搬走) |
| `.claude/r2/_p3b_live2.json` | R1 `P3B-EXPAND`(U6/U7 DAC + U23 PCA9555) | **A3**(两颗 DAC 整块搬,U23 不要) |
| `.claude/r2/_c2hub_live.json` | R1 `C2-HUB`(U32 + Y1 + J23 + 一堆去耦) | **A4**(整页搬,加第 6 个下行口) |
| `.claude/r2/_p6_live.json` | R1 `P6-FAN-EDL`(风扇部分 + EDL 部分) | **A6**(**只要风扇部分**,EDL 已挪去刀片 B3-PHONE) |
| `.claude/r2/b4_edge.py` | R2 刀片 `B4-EDGE`(金手指 64 针分配) | **A5**(针位表照抄并做**逐脚对账**) |
JSON 结构见 `.claude/r2/dump_page.py` 的文档串:
`parts` = `[位号, 器件名, 子部件, x, y, rot, mirror, uuid, libraryUuid, [[脚号,脚名,x,y],...]]`,
`wires` = **端点对**(不是折线),`flags` = `[种类, 网名, x, y, rot]`,
`ports` = `[dir, 网名, x, y, rot]`,`texts` = `[x, y, 内容, rot, 字号]`。
⚠️ **搬版图时引脚坐标是绝对坐标**:算出 `(引脚x - 元件x, 引脚y - 元件y)` 得到相对量,
再加上你的新元件中心。**不要相信任何"方向表"**(skill §6c:同一个 `rot` 在不同型号上语义相反)。
---
## 6. 每页交付物
一个 `.claude/r2/aN_xxx.py`,**照抄 `a1_input.py` / `a2_aux.py` 的骨架**:
```python
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from schcheck import Sheet, by_rel, two_pin, pins_of, _on # 按需
PAGE, NAME = '<页uuid>', '<页名>'
def placement(): ... # [[位号, eda-parts的键, x, y, rot], ...]
def expect(): ... # {位号: {脚号: (绝对x, 绝对y)}} 放件后逐脚回读比对用
def build(): ... # 返回填好 W/F/PT/TX 的 Sheet
def main():
s = build(); s.assert_grid()
errs = s.netcheck(PINS, NC, ALLOW_SPLIT)
s.report(len(placement()), errs)
if '--push' in sys.argv:
s.clear(); s.place(placement(), expect()); s.push_geometry()
```
**页 uuid**(`.claude/r2/r2-uuids.json` 的 `boards.BASE.pages`):
| 页 | uuid |
|---|---|
| A3-CTRL | `38327f806bc13133` |
| A4-HUB | `09d583ff95cd35d9` |
| A5-SLOTS | `2c73c5bf77f34053` |
| A6-FAN | `68c9ffaa578572d3` |
跑 `/c/Python314/python .claude/r2/aN_xxx.py`(**不带 `--push`**)必须打印出:
```
元件 N 导线 N 旗 N 端口 N 文字 N
交叉点(不相连,供人工确认): N
本地自检通过(短路 0 / 漏连 0 / 悬空 0 / 旗端口全在端点)
```
---
## 7. 画图硬规矩(全部来自 `.claude/skills/easyeda-sch-geometry/SKILL.md`,**先读它**)
1. **Y 轴向上**(§1)。
2. 所有坐标必须是 **5 的倍数**(§3、§8a-2b)—— 否则旗/端口静默不连。
3. 导线只能**横平竖直**,用 `s.w(net, p1, p2, p3...)` 逐段声明。
4. 旗 / 端口的锚点必须落在**某段导线的端点**上,不能压在中段(§8a-2),
也不能和元件引脚**同坐标**(§8a:那会变成零长度导线,验证过得去、重画就断)。
5. 同一个锚点上如果有两段**共线**的导线,合并后就成了中段 —— `netcheck` 会报。
6. 页面范围 `x 20~1145` / `y 10~812`;**标题栏**在 `x>455 且 y<192`,文字不许伸进去(§8c)。
7. 文字宽度经验值:`右边界 ≈ x + 字号 × (中文字数 + 0.5×其余字符数)`。
8. **叠字是这个项目的常见事故**:端口的符号+标签往锚点**左边**伸出去约 70 个单位,
`flag_clash` 那种「横向 <20」的判据够不着。排版时给端口左边留 ≥80 的空白。
9. 电源/地旗的 `rot`:`0` = 符号朝上(往上挂),`180` = 朝下。极性件别信方向表,按**脚号**断言。
10. `ALLOW_SPLIT` 只放行「靠同名旗/端口成网、导线上天然分块」的网络(`GND` `+12V` `+3V3` `+5V` 这类)。
信号网络出现「被拆成 N 块」就是真漏连,不许往 `ALLOW_SPLIT` 里加来消错。
---
## 8. 汇报格式
做完后回报(**不要贴整段脚本**):
1. 脚本路径 + dry-run 的那三行统计输出(原样贴)。
2. 本页画了什么(一句话一块)。
3. **你发出/接收的端口清单**(网名 + dir),主代理要拿它对账。
4. 你做的**设计决定**与依据(尤其是 GPIO 分配、strap 电平、上下拉取值)。
5. 你发现的**风险 / 与方案文档不一致的地方**(B3=+12V 那种错就是这么揪出来的)。
6. 交叉点清单(如果有),说明为什么可接受。
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 564 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 205 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 524 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 457 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 509 KiB

+38
查看文件
@@ -0,0 +1,38 @@
# M6 手工补线作业单(按板上实际铜算,5 个网)
布线方向约定:底层(器件面)纵向、顶层横向;信号线宽 0.15,过孔 0.6/0.3。
⚠ 这些脚的出口都被**自家邻脚的引出脚 / 过孔**封死(如 `U2.15` 被 `EFUSE_EN` 的孔 (89.5,31) 卡到只剩 0.04 mm),脚本布线器四种办法(换顺序 / PathFinder / 只补线 / 过孔扇出)都到不了。交互式布线时先挪开挡路的那根引出脚或过孔再拉线。
## VDAC(2 只脚)— R8.2 一根铜都没有
- `J1.A16` (49.50, 3.45) 底层
- `R8.2` (93.50, 49.85) 底层
现有铜 7.8 mm
## $1N147(3 只脚)— U2.15 一根铜都没有
- `R9.2` (77.20, 27.35) 底层
- `R10.1` (79.33, 27.35) 底层
- `U2.15` (89.75, 30.02) 底层
现有铜 2.1 mm
## $1N156(2 只脚)— R11.1 U2.17 一根铜都没有
- `R11.1` (82.33, 27.35) 底层
- `U2.17` (91.00, 29.27) 底层
现有铜 0.0 mm
## EFUSE_PG(3 只脚)— U2.2 一根铜都没有
- `R15.1` (79.33, 31.35) 底层
- `U2.2` (89.75, 27.02) 底层
- `U4.16` (79.32, 20.12) 底层
现有铜 52.7 mm
## $1N165(8 只脚)— R16.1 一根铜都没有
- `R16.1` (79.33, 29.35) 底层
- `Rs1.2` (87.87, 23.04) 底层
- `U2.6` (87.75, 27.02) 底层
- `U2.5` (88.25, 27.02) 底层
- `U2.4` (88.75, 27.02) 底层
- `U2.8` (87.00, 28.27) 底层
- `U2.7` (87.00, 27.77) 底层
- `U3.10` (85.05, 19.10) 底层
现有铜 23.1 mm
+92
查看文件
@@ -0,0 +1,92 @@
# 手工布线作业单(13 条)
布线方向约定:**底层(器件面)纵向、顶层横向**;线宽 ≥ 规则下限(信号 0.15)。
## VBUS_EN(3 只脚)
- `R62.2` (59.20, 14.85) 底层
- `U4.8` (73.28, 19.47) 底层
- `U5.3` (42.45, 21.45) 底层
现有铜 0.00 mm;一根都没有,整条要画
## EFUSE_EN(4 只脚)
- `J1.A11` (42.50, 3.45) 底层
- `Q1.1` (78.55, 35.28) 底层
- `R14.1` (82.33, 31.35) 底层
- `U2.14` (89.25, 30.02) 底层
现有铜 88.19 mm;已有部分,缺的是把它们连成一片
## $1N164(3 只脚)
- `R16.2` (80.20, 29.35) 底层
- `R17.1` (82.33, 29.35) 底层
- `U2.3` (89.25, 27.02) 底层
现有铜 2.13 mm;已有部分,缺的是把它们连成一片
## USB_DPJ(2 只脚)
- `J4.6` (48.26, 19.94) 顶层
- `U7.3` (49.04, 17.75) 底层
现有铜 0.00 mm;一根都没有,整条要画
## $1N151(2 只脚)
- `R12.2` (77.20, 29.35) 底层
- `U2.19` (91.00, 28.27) 底层
现有铜 0.00 mm;一根都没有,整条要画
## SCL(4 只脚)
- `J1.B5` (36.50, 3.45) 顶层
- `R41.1` (77.83, 25.45) 底层
- `U3.5` (83.05, 14.87) 底层
- `U4.22` (79.32, 16.23) 底层
现有铜 245.61 mm;已有部分,缺的是把它们连成一片
## KEY_PWR(2 只脚)
- `R42.2` (2.30, 22.85) 底层
- `U4.4` (73.28, 16.88) 底层
现有铜 0.00 mm;一根都没有,整条要画
## INT#(2 只脚)
- `J1.B11` (42.50, 3.45) 顶层
- `U4.1` (73.28, 14.93) 底层
现有铜 7.75 mm;已有部分,缺的是把它们连成一片
## PRSNT#(2 只脚)
- `J1.A1` (32.50, 4.00) 底层
- `J1.B31` (64.50, 4.00) 顶层
现有铜 15.50 mm;已有部分,缺的是把它们连成一片
## +3V3(14 只脚)
- `C13.1` (86.38, 18.00) 底层
- `C40.1` (72.30, 25.45) 底层
- `C69.1` (67.30, 22.35) 底层
- `J1.A10` (41.50, 3.45) 底层
- `J1.A9` (40.50, 3.45) 底层
- `J1.B8` (39.50, 3.45) 顶层
- `R13.1` (76.33, 31.35) 底层
- `R15.2` (80.20, 31.35) 底层
- `R18.1` (80.83, 33.35) 底层
- `R40.2` (75.70, 25.45) 底层
- `R41.2` (78.70, 25.45) 底层
- `U3.6` (83.05, 19.10) 底层
- `U4.24` (79.32, 14.93) 底层
- `U6.5` (65.30, 21.30) 底层
现有铜 131.15 mm;已有部分,缺的是把它们连成一片
## VBUS(6 只脚)
- `C67.1` (44.80, 14.85) 底层
- `J4.51` (45.73, 21.00) 顶层
- `R64.2` (45.70, 16.85) 底层
- `U5.5` (45.15, 20.50) 底层
- `U5.6` (45.15, 19.55) 底层
- `U7.5` (48.09, 15.45) 底层
现有铜 21.65 mm;已有部分,缺的是把它们连成一片
## EFUSE_FLT(3 只脚)
- `R13.2` (77.20, 31.35) 底层
- `U2.20` (91.00, 27.77) 底层
- `U4.13` (79.32, 22.07) 底层
现有铜 57.31 mm;已有部分,缺的是把它们连成一片
## EFUSE_PG(3 只脚)
- `R15.1` (79.33, 31.35) 底层
- `U2.2` (89.75, 27.02) 底层
- `U4.16` (79.32, 20.12) 底层
现有铜 49.57 mm;已有部分,缺的是把它们连成一片
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 440 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 282 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 281 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 264 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 284 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 272 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 595 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 522 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 608 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 97 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 134 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 243 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 366 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 468 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 482 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 243 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 138 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 141 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 188 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 474 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 319 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 592 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 404 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 524 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 734 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 426 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 426 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 273 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 413 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 423 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 392 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 302 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 379 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 270 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 422 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 350 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 458 KiB

二进制
查看文件
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 372 KiB

某些文件未显示,因为此 diff 中更改的文件太多 显示更多