sync: HP_BOWEI 2026-09-09
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""fix_pcb.py — 导入后 PCB 修复(阶段 1.5 的两个硬骨头)。必须用 KiCad 自带 python 跑。
|
||||
|
||||
A) BGA / 平面焊盘丢网络回填
|
||||
Altium 用负片内电层隐式连接电源地,KiCad 导入时这些焊盘拿不到网络,
|
||||
表现为成百上千个 "GND vs 空网络" 假短路。用阶段 0 导出的 Altium 网表真值
|
||||
(component,pin,net) 把空网络焊盘重新指派回去。
|
||||
|
||||
B) 覆铜 zone 的本地 clearance
|
||||
Altium 导入器把【每块 zone 的本地 Clearance 写成 KiCad 默认 0.5mm】
|
||||
((zone ... (connect_pads (clearance 0.5)))),而本地值会盖过网络类 clearance
|
||||
—— 这就是导入后异 net 避让间隔明显偏大的根因。只改 .kicad_pro 的网络类
|
||||
或板级规则没有用,必须逐块 zone 改。目标值取 Altium 的 InPolygon 规则
|
||||
(XMOD/GQ 两个工程实测都是 0.254mm,但别照抄,去基线 pcb_design_rules.json 里查)。
|
||||
热焊盘 thermal_gap 通常已经对上 Altium PolygonConnect AirGap(0.165mm),不要动。
|
||||
|
||||
本脚本改完不 refill。大板上 SWIG refill 容易超时甚至段错误,去 KiCad GUI 里按 B
|
||||
填充,或走 IPC 后端。
|
||||
|
||||
用法(KICAD_PYTHON 见 ~\\skills_env.md):
|
||||
# 先干跑看看要动多少
|
||||
D:\\KiCad\\10.0\\bin\\python.exe fix_pcb.py --board X.kicad_pcb --netlist-csv t.csv \\
|
||||
--zone-clearance-mm 0.254 --dry-run
|
||||
# 确认后真改(会先备份 .kicad_pcb 到 <board>.bak_fixpcb)
|
||||
D:\\KiCad\\10.0\\bin\\python.exe fix_pcb.py --board X.kicad_pcb --netlist-csv t.csv \\
|
||||
--zone-clearance-mm 0.254
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
COMP_KEYS = ("component", "designator", "ref", "reference")
|
||||
PIN_KEYS = ("pin", "pin_number", "pad", "pin designator")
|
||||
NET_KEYS = ("net", "netname", "net name")
|
||||
|
||||
|
||||
def pick(fieldnames, candidates) -> str | None:
|
||||
lowered = {(f or "").strip().lower(): f for f in fieldnames}
|
||||
for c in candidates:
|
||||
if c in lowered:
|
||||
return lowered[c]
|
||||
return None
|
||||
|
||||
|
||||
def load_truth(csv_path: Path) -> dict[tuple[str, str], str]:
|
||||
"""Altium 网表真值 (component, pin) -> net。
|
||||
|
||||
Altium 的 tabular 导出会把隐藏的电源/平面引脚重复列成 net='?' 的行,
|
||||
这些行没有信息量,直接丢掉。
|
||||
"""
|
||||
with csv_path.open(newline="", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
c_key = pick(reader.fieldnames or [], COMP_KEYS)
|
||||
p_key = pick(reader.fieldnames or [], PIN_KEYS)
|
||||
n_key = pick(reader.fieldnames or [], NET_KEYS)
|
||||
if not (c_key and p_key and n_key):
|
||||
raise SystemExit(
|
||||
f"[ERROR] 认不出 CSV 的列名:{reader.fieldnames}\n"
|
||||
f" 需要 component / pin / net 三列(大小写和别名可容忍)。")
|
||||
truth: dict[tuple[str, str], str] = {}
|
||||
dropped = 0
|
||||
for row in reader:
|
||||
comp = (row.get(c_key) or "").strip()
|
||||
pin = (row.get(p_key) or "").strip()
|
||||
net = (row.get(n_key) or "").strip()
|
||||
if not (comp and pin):
|
||||
continue
|
||||
if not net or net == "?":
|
||||
dropped += 1
|
||||
continue
|
||||
truth[(comp, pin)] = net
|
||||
print(f"[csv] 真值引脚 {len(truth)} 条(丢弃 net='?'/空 的重复行 {dropped} 条)")
|
||||
return truth
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Post-import KiCad PCB repair (nets + zone clearance)")
|
||||
ap.add_argument("--board", required=True, help=".kicad_pcb 路径")
|
||||
ap.add_argument("--netlist-csv", required=True,
|
||||
help="Altium 侧网表真值 CSV(阶段0 proj_export_netlist tabular)")
|
||||
ap.add_argument("--zone-clearance-mm", type=float, required=True,
|
||||
help="Altium InPolygon 规则的 polygon clearance,去基线 pcb_design_rules.json 查")
|
||||
ap.add_argument("--dry-run", action="store_true", help="只统计不落盘")
|
||||
ap.add_argument("--report", help="把统计写成 JSON")
|
||||
ap.add_argument("--skip-nets", action="store_true", help="只改 zone clearance,不动焊盘网络")
|
||||
ap.add_argument("--skip-zones", action="store_true", help="只回填焊盘网络,不动 zone clearance")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
import pcbnew
|
||||
except ImportError:
|
||||
print("[ERROR] 导不到 pcbnew —— 本脚本必须用 KiCad 自带的 python 跑,"
|
||||
"例如 D:\\KiCad\\10.0\\bin\\python.exe(见 ~\\skills_env.md 的 KICAD_PYTHON)。")
|
||||
return 1
|
||||
|
||||
board_path = Path(args.board)
|
||||
if not board_path.is_file():
|
||||
print(f"[ERROR] 板文件不存在:{board_path}")
|
||||
return 1
|
||||
|
||||
truth = load_truth(Path(args.netlist_csv)) if not args.skip_nets else {}
|
||||
board = pcbnew.LoadBoard(str(board_path))
|
||||
|
||||
stats = {"assigned": 0, "no_truth": 0, "no_ref": 0, "already": 0,
|
||||
"zones_fixed": 0, "zones_skipped": 0}
|
||||
missing_examples: list[str] = []
|
||||
|
||||
# ---- A) 焊盘网络回填 ----
|
||||
if not args.skip_nets:
|
||||
def get_net(name):
|
||||
ni = board.FindNet(name)
|
||||
if ni is None:
|
||||
ni = pcbnew.NETINFO_ITEM(board, name)
|
||||
board.Add(ni)
|
||||
return ni
|
||||
|
||||
for fp in board.GetFootprints():
|
||||
ref = fp.GetReference()
|
||||
for pad in fp.Pads():
|
||||
if pad.GetNetCode() != 0 and pad.GetNetname() != "":
|
||||
stats["already"] += 1
|
||||
continue
|
||||
if not ref:
|
||||
stats["no_ref"] += 1
|
||||
continue
|
||||
net = truth.get((ref, pad.GetNumber()))
|
||||
if not net:
|
||||
stats["no_truth"] += 1
|
||||
if len(missing_examples) < 20:
|
||||
missing_examples.append(f"{ref}.{pad.GetNumber()}")
|
||||
continue
|
||||
if not args.dry_run:
|
||||
pad.SetNet(get_net(net))
|
||||
stats["assigned"] += 1
|
||||
print(f"[nets] 待回填={stats['assigned']} 真值里查不到={stats['no_truth']} "
|
||||
f"无位号={stats['no_ref']} 本来就有网络={stats['already']}")
|
||||
if missing_examples:
|
||||
print(f"[nets] 查不到真值的空网络焊盘举例:{', '.join(missing_examples)}")
|
||||
print(" 多为机械/屏蔽/安装孔焊盘。确认属实的话,按需单独指派 GND。")
|
||||
|
||||
# ---- B) zone 本地 clearance ----
|
||||
if not args.skip_zones:
|
||||
clr = pcbnew.FromMM(args.zone_clearance_mm)
|
||||
for z in board.Zones():
|
||||
if z.GetIsRuleArea() or not z.IsOnCopperLayer():
|
||||
stats["zones_skipped"] += 1
|
||||
continue
|
||||
if not args.dry_run:
|
||||
z.SetLocalClearance(clr)
|
||||
stats["zones_fixed"] += 1
|
||||
print(f"[zones] 铺铜 zone 本地 clearance -> {args.zone_clearance_mm}mm "
|
||||
f"共 {stats['zones_fixed']} 块;跳过 {stats['zones_skipped']} 块(规则区/非铜层)")
|
||||
|
||||
if args.dry_run:
|
||||
print("[dry-run] 没有写盘。去掉 --dry-run 才会真正修改。")
|
||||
else:
|
||||
backup = board_path.with_suffix(board_path.suffix + ".bak_fixpcb")
|
||||
shutil.copy2(board_path, backup)
|
||||
board.Save(str(board_path))
|
||||
print(f"[save] 已写回 {board_path}(原文件备份为 {backup.name})")
|
||||
print("[next] 还要重填铺铜:KiCad GUI 里按 B,或走 IPC 后端。"
|
||||
"大板不要用 SWIG refill,会超时/段错误。")
|
||||
|
||||
if args.report:
|
||||
Path(args.report).write_text(
|
||||
json.dumps({**stats, "zone_clearance_mm": args.zone_clearance_mm,
|
||||
"dry_run": args.dry_run,
|
||||
"no_truth_examples": missing_examples},
|
||||
ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"[report] {args.report}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user