sync: HP_BOWEI 2026-10-15
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
"""原理图验收: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:]))
|
||||
Reference in New Issue
Block a user