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

332 lines
13 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.
"""原理图验收:ERC + 网表器件数 + 子电路画法检查。
python -m schgen.check <根图.kicad_sch> [--expect-refs 位号清单文件或逗号串]
四件事:
1. kicad-cli ERC(错误/警告条数与分类);
2. 网表里的器件数与网络数——判断一页有没有真被 KiCad 读进去**看这个**,
不要看 ERC 的 0(G-0018:一页解析失败时 ERC 照样 0 条);位号重名会被静默合并(G-0028),
给了 --expect-refs 就逐个比对缺了哪些;
3. 子电路画法(G-0035/G-0037):
A 级:两脚件两端都只挂标签、连通域里没有别的元件引脚——看不出它串在哪/退耦谁;
B 级:连通关系(可穿过其它无源件)到不了任何芯片/连接器引脚,且离最近的芯片/连接器
引脚也超过 NEAR mm——说明它没画在所服务的子电路里。
连通性含丁字口(端点落在另一条线中段,G-0036)。引脚坐标取自图纸自带的 lib_symbols,
不依赖库表。
C 级:三条以上线端(或线端+引脚)汇于一点却没画接点——电气连通但图面看不出(G-0036 的反面)。
4. 封装:每个器件的封装文件存在,符号引脚号与焊盘号一一对应。
"""
import io
import math
import os
import re
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from .env import kicad_cli
from .kicad_lib import balanced, rot_pt
TOL = 0.02
NEAR = 55.0
TWO_PIN_PREFIX = ("R", "C", "L", "FB", "JP", "D")
def sexp(text):
toks = re.findall(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()]+', text)
stack, cur = [], []
for tk in toks:
if tk == '(':
stack.append(cur); cur = []
elif tk == ')':
done = cur; cur = stack.pop(); cur.append(done)
else:
cur.append(tk[1:-1].replace('\\"', '"') if tk.startswith('"') else tk)
return cur
def find(node, name):
return [c for c in node if isinstance(c, list) and c and c[0] == name]
def at(node):
a = find(node, "at")
if not a:
return None
v = a[0]
return (float(v[1]), float(v[2]), float(v[3]) if len(v) > 3 else 0.0)
def embedded_pins(text):
"""{lib_id: {unit: {pin: (dx, dy)}}},取自 .kicad_sch 的 lib_symbols。"""
i = text.find("(lib_symbols")
if i < 0:
return {}
ls = balanced(text, i)
res = {}
for m in re.finditer(r'\n\s*\(symbol "([^"]+:[^"]+)"', ls):
blk = balanced(ls, m.start() + ls[m.start():].find("("))
lid = m.group(1)
units = {}
for sm in re.finditer(r'\(symbol "[^"]*_(\d+)_\d+"', blk):
u = int(sm.group(1))
sub = balanced(blk, sm.start())
for pm in re.finditer(r'\(pin\s+\S+\s+\S+\s*\(at\s+([-\d.]+)\s+([-\d.]+)', sub):
seg = sub[pm.end():pm.end() + 400]
nm = re.search(r'\(number "([^"]*)"', seg)
if nm:
units.setdefault(u, {})[nm.group(1)] = (float(pm.group(1)), float(pm.group(2)))
res[lid] = units
return res
class DS:
def __init__(self):
self.p = {}
def find(self, k):
self.p.setdefault(k, k)
while self.p[k] != k:
self.p[k] = self.p[self.p[k]]
k = self.p[k]
return k
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra != rb:
self.p[ra] = rb
def key(x, y):
return (round(x / TOL), round(y / TOL))
def on_seg(px, py, x1, y1, x2, y2):
if abs((x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)) > 0.05:
return False
return (min(x1, x2) - TOL <= px <= max(x1, x2) + TOL
and min(y1, y2) - TOL <= py <= max(y1, y2) + TOL)
def subckt(path):
text = io.open(path, encoding="utf-8").read()
lib = embedded_pins(text)
root = sexp(text)[0]
wires = []
for w in find(root, "wire"):
xy = [(float(p[1]), float(p[2])) for p in find(find(w, "pts")[0], "xy")]
if len(xy) == 2:
wires.append((xy[0][0], xy[0][1], xy[1][0], xy[1][1]))
labels = []
for kind in ("global_label", "hierarchical_label", "label"):
for lb in find(root, kind):
p = at(lb)
if p:
labels.append((p[0], p[1], lb[1]))
comps = []
for s in find(root, "symbol"):
lid = find(s, "lib_id")
pos = at(s)
if not lid or pos is None:
continue
ref = ""
for pr in find(s, "property"):
if len(pr) > 2 and pr[1] == "Reference":
ref = pr[2]
un = find(s, "unit")
u = int(un[0][1]) if un else 1
units = lib.get(lid[0][1], {})
pins = dict(units.get(0, {}))
pins.update(units.get(u, {}))
ab = {}
for n, (dx, dy) in pins.items():
rx, ry = rot_pt(dx, dy, pos[2])
ab[n] = (pos[0] + rx, pos[1] - ry)
comps.append(dict(ref=ref, pins=ab))
ds = DS()
nodes = [(px, py, "pin", "%s.%s" % (c["ref"], n)) for c in comps for n, (px, py) in c["pins"].items()]
nodes += [(x, y, "label", nm) for x, y, nm in labels]
for (x1, y1, x2, y2) in wires:
ds.union(key(x1, y1), key(x2, y2))
for w1 in wires:
for w in wires:
if w is w1:
continue
for (ex, ey) in ((w1[0], w1[1]), (w1[2], w1[3])):
if on_seg(ex, ey, *w):
ds.union(key(ex, ey), key(w[0], w[1]))
for (px, py, _, _) in nodes:
for w in wires:
if on_seg(px, py, *w):
ds.union(key(px, py), key(w[0], w[1]))
groups = {}
for (px, py, kind, nm) in nodes:
groups.setdefault(ds.find(key(px, py)), []).append((kind, nm))
def is_passive(c):
r = c["ref"]
return r and not r.startswith("#") and r.startswith(TWO_PIN_PREFIX) and len(c["pins"]) == 2
bad_a = []
for c in comps:
if not is_passive(c):
continue
ends = []
for n, (px, py) in c["pins"].items():
g = groups.get(ds.find(key(px, py)), [])
others = [m for m in g if m[0] == "pin" and not m[1].startswith(c["ref"] + ".")
and not m[1].startswith("#")]
ends.append((len(others), any(m[0] == "label" for m in g)))
if all(n == 0 and lb for n, lb in ends):
bad_a.append(c["ref"])
anchors, apts = set(), []
for c in comps:
if c["ref"] and not c["ref"].startswith("#") and not is_passive(c):
for n, pt in c["pins"].items():
anchors.add("%s.%s" % (c["ref"], n)); apts.append(pt)
du = DS()
passives = [c for c in comps if is_passive(c)]
for c in passives:
rs = [ds.find(key(px, py)) for px, py in c["pins"].values()]
du.union(rs[0], rs[1])
live = {du.find(r) for r, mem in groups.items() if any(k == "pin" and nm in anchors for k, nm in mem)}
bad_b = []
for c in passives:
ok = any(du.find(ds.find(key(px, py))) in live for px, py in c["pins"].values())
if not ok:
ok = any(math.hypot(px - ax, py - ay) <= NEAR
for px, py in c["pins"].values() for ax, ay in apts)
if not ok:
bad_b.append(c["ref"])
# C 级:三条及以上线端(或线端+引脚)汇在一点却没有接点。KiCad 电气上算连通,
# 但图面上看不出是连上的(DVBS2_dmod 第 3 轮 d20 抓到过两处)
jset = {key(float(j[1][1]), float(j[1][2])) for j in
[(x, find(x, "at")[0]) for x in find(root, "junction")]}
ends = {}
for (x1, y1, x2, y2) in wires:
for p in ((x1, y1), (x2, y2)):
ends.setdefault(key(*p), []).append(p)
pinpts = {key(px, py) for c in comps for (px, py) in c["pins"].values()}
bad_c = []
for k, pts in ends.items():
n = len(pts) + (1 if k in pinpts else 0)
mid = sum(1 for w in wires if on_seg(pts[0][0], pts[0][1], *w)
and key(w[0], w[1]) != k and key(w[2], w[3]) != k)
if n + 2 * mid >= 3 and k not in jset:
bad_c.append("(%.2f,%.2f)" % pts[0])
return len(comps), bad_a, bad_b, bad_c
def erc(root):
out = os.path.join(tempfile.mkdtemp(prefix="kf_erc_"), "erc.rpt")
subprocess.run([kicad_cli(), "sch", "erc", "--severity-error", "--severity-warning",
"-o", out, root], capture_output=True)
t = io.open(out, encoding="utf-8", errors="replace").read() if os.path.exists(out) else ""
kinds = {}
for m in re.finditer(r'\[(\w+)\]:\s*(.*)', t):
kinds.setdefault(m.group(1), []).append(m.group(2))
return t, kinds
def netlist(root):
out = os.path.join(tempfile.mkdtemp(prefix="kf_net_"), "n.xml")
subprocess.run([kicad_cli(), "sch", "export", "netlist", "--format", "kicadxml", "-o", out, root],
capture_output=True)
r = ET.parse(out).getroot()
comps = {c.get("ref"): (c.findtext("value"), c.findtext("footprint")) for c in r.iter("comp")}
nets = {}
for n in r.iter("net"):
nets[n.get("name")] = [(x.get("ref"), x.get("pin")) for x in n.iter("node")]
return comps, nets, out
def _fp_dirs(prj_dir):
"""库名 -> .pretty 目录:工程 fp-lib-table 优先,其余按 KiCad 自带库目录找。"""
from .env import kicad_footprint_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, kicad_footprint_dir()
def fpcheck(net_xml, prj_dir):
"""每个器件:封装文件存在;符号引脚号 ⊆ 焊盘号;有编号的焊盘都有对应引脚(否则它挂不上网)。
返回问题清单。"""
r = ET.parse(net_xml).getroot()
pins = {}
for lp in r.iter("libpart"):
k = (lp.get("lib"), lp.get("part"))
pins[k] = {p.get("num") for p in lp.iter("pin")}
dirs, share = _fp_dirs(prj_dir)
bad = []
for c in r.iter("comp"):
ref, fp = c.get("ref"), c.findtext("footprint") or ""
if ref.startswith("#"):
continue
if not fp:
bad.append("%s 没有封装" % ref)
continue
lib, name = fp.split(":", 1)
base = dirs.get(lib) or os.path.join(share, lib + ".pretty")
f = os.path.join(base, name + ".kicad_mod")
if not os.path.exists(f):
bad.append("%s 的封装 %s 找不到(%s)" % (ref, fp, f))
continue
pads = set(re.findall(r'\(pad\s+"([^"]+)"', io.open(f, encoding="utf-8").read()))
ls = c.find("libsource")
sp = pins.get((ls.get("lib"), ls.get("part")), set()) if ls is not None else set()
miss = sorted(sp - pads)
extra = sorted(pads - sp)
if miss:
bad.append("%s(%s) 符号引脚在封装里没有:%s" % (ref, name, ",".join(miss)))
if extra and sp:
bad.append("%s(%s) 焊盘没有对应符号引脚:%s" % (ref, name, ",".join(extra)))
return bad
def main(argv):
root = argv[0]
expect = None
if "--expect-refs" in argv:
v = argv[argv.index("--expect-refs") + 1]
expect = set((io.open(v).read() if os.path.exists(v) else v).replace("\n", ",").split(","))
expect.discard("")
sys.stdout.reconfigure(encoding="utf-8")
rc = 0
rpt, kinds = erc(root)
print("ERC:", ", ".join("%s x%d" % (k, len(v)) for k, v in sorted(kinds.items())) or "0 条")
comps, nets, npath = netlist(root)
real = {k: v for k, v in comps.items() if not k.startswith("#")}
print("网表: 器件 %d 个,网络 %d 条 (%s)" % (len(real), len(nets), npath))
fb = fpcheck(npath, os.path.dirname(os.path.abspath(root)))
print("封装: %s" % ("全部 %d 个器件有封装且引脚/焊盘一一对应" % len(real) if not fb
else "%d 处问题" % len(fb)))
for b in fb:
print(" !! " + b)
if fb:
rc = 1
if expect is not None:
miss = sorted(expect - set(real))
if miss:
rc = 1
print(" !! 网表里缺: %s" % " ".join(miss))
d = os.path.dirname(os.path.abspath(root))
files = sorted(set(re.findall(r'\(property "Sheetfile" "([^"]+)"', io.open(root, encoding="utf-8").read())))
for f in files:
n, a, b, cj = subckt(os.path.join(d, f))
flag = "OK " if not a and not b and not cj else "!! "
if a or b or cj:
rc = 1
print("%s%-28s 元件 %3d A:两端只挂标签 %d B:没进子电路 %d C:丁字口缺接点 %d %s"
% (flag, f, n, len(a), len(b), len(cj), " ".join(sorted(set(a) | set(b)) + cj)))
return rc
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))