sync: HP_BOWEI 2026-10-15

This commit is contained in:
Myfro9
2026-10-15 01:59:40 +08:00
parent 771acd4171
commit a0a3445bfd
21 changed files with 3042 additions and 16 deletions
+210
View File
@@ -0,0 +1,210 @@
"""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:]))