Files
2026-09-09 09:51:06 +08:00

184 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())