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
@@ -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 同上"
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
[{"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]}]
|
||||
@@ -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'])))
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = 载板面到主板贴合面该有的间隙"
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 没变,重载没成功')
|
||||
|
||||
@@ -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 同源,这里独立列一遍并断言一致)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |