sync: HP_BOWEI 2026-09-09

This commit is contained in:
Myfro9
2026-09-09 09:51:06 +08:00
parent 01b7a00b12
commit 170f4eb53f
10 changed files with 1926 additions and 5 deletions
+183
View File
@@ -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())