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
+331
View File
@@ -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:]))