sync: HP_BOWEI 2026-10-15
This commit is contained in:
@@ -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:]))
|
||||
Reference in New Issue
Block a user