"""n101:从原理图网表确定性地建 PCB(封装 + 网络,封装按页排在"停车场"里),再逐焊盘验收。 **必须用 KiCad 自带的 python 跑**(要 pcbnew): -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:]))