Files
2026-10-15 01:59:40 +08:00

211 lines
8.6 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.
"""n101:从原理图网表确定性地建 PCB(封装 + 网络,封装按页排在"停车场"里),再逐焊盘验收。
**必须用 KiCad 自带的 python 跑**(要 pcbnew):
<KICAD_PYTHON> -m schgen.pcbimport <根图.kicad_sch> <输出.kicad_pcb> [--layers 2|4]
为什么不用 kicad MCP 的 sync_schematic_to_board(G-0039):它自报 success,实际
ipots 载板写出 338 个封装(应 327)、191 个网(应 426);DVBS2_dmod 上 26 个网(应 60),
还冒出一个叫 PWR_FLAG 的网、U4 一串焊盘没网——所有 "Net-(...)" 无名网全丢。
kicad-cli 没有"从原理图更新 PCB"子命令,GUI 的 F8 要人点,所以这里自己建。
pcbnew swig 的坑(G-0038):FootprintLoad() 便捷函数坏的,走 PCB_IO_KICAD_SEXPR;
对 BOARD Remove() 过 FOOTPRINT 后 swig 类型注册会坏,所以**每次都建一块新板**,
不做"载入旧板再删"。已有板子会被覆盖——布局开始之后就别再跑它了。
"""
import io
import os
import re
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
import pcbnew
from .env import kicad_cli, kicad_footprint_dir
GAP, SHEET_GAP, FIELD_W = 2.0, 12.0, 250.0
def mm(v):
return pcbnew.FromMM(float(v))
def netlist(sch):
out = os.path.join(tempfile.mkdtemp(prefix="kf_pcb_"), "n.xml")
subprocess.check_call([kicad_cli(), "sch", "export", "netlist", "--format", "kicadxml",
"-o", out, sch], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
r = ET.parse(out).getroot()
comps = []
for c in r.iter("comp"):
sh = c.find("sheetpath")
flds = {f.get("name"): (f.text or "") for f in c.iter("field")}
props = {pr.get("name") for pr in c.iter("property")}
comps.append(dict(ref=c.get("ref"), val=c.findtext("value") or "", fields=flds,
dnp="dnp" in props, no_bom="exclude_from_bom" in props,
fp=c.findtext("footprint") or "",
sheet=sh.get("names") if sh is not None else "",
path=(sh.get("tstamps") if sh is not None else "") + (c.findtext("tstamps") or "")))
nets = [(n.get("name"), [(x.get("ref"), x.get("pin")) for x in n.iter("node")])
for n in r.iter("net")]
return comps, nets
def fp_dirs(prj_dir):
d = {}
t = os.path.join(prj_dir, "fp-lib-table")
if os.path.exists(t):
for m in re.finditer(r'\(name "([^"]+)"\).*?\(uri "([^"]+)"\)', io.open(t, encoding="utf-8").read()):
d[m.group(1)] = m.group(2).replace("${KIPRJMOD}", prj_dir)
return d
_IO = pcbnew.PCB_IO_KICAD_SEXPR()
def load_fp(libdir, name):
fp = _IO.FootprintLoad(libdir, name)
if fp is not None and not hasattr(fp, "SetReference"):
fp = pcbnew.Cast_to_FOOTPRINT(fp)
return fp
def fp_box(fp):
"""占地与包围盒相对原点的偏移——原点不一定在中心(连接器常在 1 脚)。"""
fp.SetPosition(pcbnew.VECTOR2I(0, 0))
try:
bb = fp.GetBoundingBox(False, False)
except TypeError:
bb = fp.GetBoundingBox()
return (pcbnew.ToMM(bb.GetWidth()), pcbnew.ToMM(bb.GetHeight()),
pcbnew.ToMM(bb.GetX()), pcbnew.ToMM(bb.GetY()))
def note(board, s, x, y, size=2.0):
t = pcbnew.PCB_TEXT(board)
t.SetText(s)
t.SetPosition(pcbnew.VECTOR2I(mm(x), mm(y)))
t.SetLayer(pcbnew.Cmts_User)
t.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size)))
t.SetTextThickness(mm(size / 6.0))
t.SetHorizJustify(pcbnew.GR_TEXT_H_ALIGN_LEFT)
board.Add(t)
def build(sch, pcb, layers=2, x0=20.0, y0=20.0):
prj_dir = os.path.dirname(os.path.abspath(sch))
dirs = fp_dirs(prj_dir)
share = kicad_footprint_dir()
comps, nets = netlist(sch)
board = pcbnew.CreateEmptyBoard()
board.SetCopperLayerCount(layers)
placed, y = {}, y0
for sheet in sorted(set(c["sheet"] for c in comps)):
grp = sorted((c for c in comps if c["sheet"] == sheet), key=lambda c: (c["fp"], c["ref"]))
note(board, "%s (%d)" % (sheet, len(grp)), x0, y - 4)
x, rowh = x0, 0.0
for c in grp:
if not c["fp"]:
raise SystemExit("%s 没有封装" % c["ref"])
lib, name = c["fp"].split(":", 1)
fp = load_fp(dirs.get(lib) or os.path.join(share, lib + ".pretty"), name)
if fp is None:
raise SystemExit("载入不了封装 %s(%s)" % (c["fp"], c["ref"]))
fp.SetFPID(pcbnew.LIB_ID(lib, name)) # 板上必须是 "库:名",否则以后无法从库更新
fp.SetReference(c["ref"])
fp.SetValue(c["val"])
# 原理图上的附加字段(MPN 等)也写进封装,否则 DRC 原理图对照报 Missing symbol field
for k, v in c.get("fields", {}).items():
if k not in ("Reference", "Value", "Footprint", "Datasheet") and v:
fp.SetField(k, v)
fd = fp.GetField(k)
if fd is not None:
fd.SetVisible(False) # 新建字段默认可见,会被印成丝印
# 不贴(DNP)/不进 BOM 跟着原理图走,否则 DRC 报 footprint_symbol_mismatch
fp.SetDNP(c.get("dnp", False))
fp.SetExcludedFromBOM(c.get("no_bom", False))
if c["path"]:
try:
fp.SetPath(pcbnew.KIID_PATH(c["path"])) # F8 靠它认同一个器件
except Exception:
pass
board.Add(fp)
w, h, ox, oy = fp_box(fp)
if x > x0 and x + w > x0 + FIELD_W:
x, y, rowh = x0, y + rowh + GAP, 0.0
fp.SetPosition(pcbnew.VECTOR2I(mm(x - ox), mm(y - oy)))
placed[c["ref"]] = fp
x += w + GAP
rowh = max(rowh, h)
y += rowh + SHEET_GAP
miss = []
for name, nodes in nets:
ni = pcbnew.NETINFO_ITEM(board, name)
board.Add(ni)
for ref, pin in nodes:
fp = placed.get(ref)
pads = [p for p in fp.Pads() if p.GetNumber() == pin] if fp else []
if not pads: # 同号多焊盘(散热盘/安装脚)要全挂上
miss.append("%s.%s" % (ref, pin))
for pad in pads:
pad.SetNet(ni)
board.BuildListOfNets()
pcbnew.SaveBoard(pcb, board)
print("写出 %s:封装 %d,网 %d" % (pcb, len(placed), len(nets)))
if miss:
print("!! 连不上的节点 %d 个:%s" % (len(miss), " ".join(miss[:20])))
return comps, nets, miss
def verify(sch, pcb):
"""独立验收:重新载入板子,按网表逐焊盘比对(G-0039:别信导入工具的自报)。"""
comps, nets = netlist(sch)
b = pcbnew.LoadBoard(pcb)
fps = {f.GetReference(): f for f in b.GetFootprints()}
bad = []
want = {c["ref"] for c in comps}
if set(fps) != want:
bad.append("器件不一致:多 %s 少 %s" % (sorted(set(fps) - want), sorted(want - set(fps))))
for c in comps:
f = fps.get(c["ref"])
if f and f.GetFPIDAsString() != c["fp"]:
bad.append("%s 封装 %s != %s" % (c["ref"], f.GetFPIDAsString(), c["fp"]))
pinnet = {(r, p): n for n, nodes in nets for r, p in nodes}
npads = 0
for ref, f in fps.items():
for pad in f.Pads():
num = pad.GetNumber()
if not num:
continue # 无编号的机械孔
npads += 1
exp = pinnet.get((ref, num))
got = str(pad.GetNetname())
if exp is None and got:
bad.append("%s.%s 板上挂了 %s,网表里没有" % (ref, num, got))
elif exp is not None and got != exp:
bad.append("%s.%s 应为 %s,板上是 %r" % (ref, num, exp, got))
boardnets = {str(n) for n in b.GetNetsByName().keys() if str(n)} # swig 给的是 wxString
wantnets = {n for n, _ in nets}
if boardnets != wantnets:
bad.append("网名不一致:多 %s 少 %s" % (sorted(boardnets - wantnets)[:10],
sorted(wantnets - boardnets)[:10]))
print("验收:器件 %d/%d,网 %d/%d,有编号焊盘 %d,问题 %d"
% (len(fps), len(want), len(boardnets), len(wantnets), npads, len(bad)))
for x in bad[:30]:
print(" !! " + x)
return bad
def main(argv):
sch, pcb = argv[0], argv[1]
layers = int(argv[argv.index("--layers") + 1]) if "--layers" in argv else 2
_, _, miss = build(sch, pcb, layers)
bad = verify(sch, pcb)
return 1 if (miss or bad) else 0
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
sys.exit(main(sys.argv[1:]))