Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.
"""Schematic <-> PCB equivalence (``akcli verify sch.kicad_sch board.kicad_pcb``).
Compares the schematic's normalized netlist against the board's pad-level net
bindings (schema 1.2 deep PCB read). Net NAMES are not trusted — conversions
and auto-named nets differ legitimately — the comparison is the **partition**:
two pins joined on the schematic must be joined on the board and vice versa.
Finding codes:
* ``SCHPCB_MISSING_ON_PCB`` / ``SCHPCB_EXTRA_ON_PCB`` — refdes set drift;
* ``SCHPCB_FOOTPRINT_MISMATCH`` / ``SCHPCB_VALUE_MISMATCH`` — assignment drift;
* ``SCHPCB_PAD_MISSING`` — a schematic pin with no pad on the board footprint;
* ``SCHPCB_NET_SPLIT`` — one schematic net spans >1 board nets;
* ``SCHPCB_NET_MERGE`` — one board net shorts >1 schematic nets;
* ``SCHPCB_UNNETTED_PAD`` — a board pad carrying no net while its schematic
pin belongs to a multi-pin net (NOTE: single-pin nets are expected NC pads).
"""
from __future__ import annotations
from collections import defaultdict
from ..model import Pcb, Schematic
from ..report import Finding, Severity, anchor
__all__ = ["run"]
_LIMIT = 10 # max pins listed per net finding message
def _fmt_pins(pins) -> str:
ordered = sorted(f"{d}.{p}" for d, p in pins)
listed = ", ".join(ordered[:_LIMIT])
if len(ordered) > _LIMIT:
listed += f", … (+{len(ordered) - _LIMIT})"
return listed
def run(sch: Schematic, pcb: Pcb) -> list[Finding]:
findings: list[Finding] = []
out = findings.append
# '#'-prefixed designators (power ports, PWR_FLAG) are schematic-only
# pseudo-components; KiCad never forwards them to the board.
sch_by_ref = {c.designator: c for c in sch.components
if not c.designator.startswith("#")}
pcb_by_ref = {f.designator: f for f in pcb.footprints if f.designator}
# --- refdes presence ---------------------------------------------------- #
for ref in sorted(set(sch_by_ref) - set(pcb_by_ref)):
out(Finding("SCHPCB_MISSING_ON_PCB", Severity.ERROR,
f"{ref}: on the schematic but not on the board",
anchors=[anchor("component", ref)]))
for ref in sorted(set(pcb_by_ref) - set(sch_by_ref)):
out(Finding("SCHPCB_EXTRA_ON_PCB", Severity.WARNING,
f"{ref}: on the board but not on the schematic "
"(board-only mechanical parts should be documented)",
anchors=[anchor("component", ref)]))
# --- footprint / value assignment --------------------------------------- #
for ref in sorted(set(sch_by_ref) & set(pcb_by_ref)):
c, f = sch_by_ref[ref], pcb_by_ref[ref]
if c.footprint and f.footprint_name and c.footprint != f.footprint_name:
out(Finding("SCHPCB_FOOTPRINT_MISMATCH", Severity.ERROR,
f"{ref}: schematic assigns {c.footprint!r}, board has "
f"{f.footprint_name!r}",
anchors=[anchor("component", ref)]))
if (c.value or "") != (f.value or "") and c.value and f.value:
out(Finding("SCHPCB_VALUE_MISMATCH", Severity.WARNING,
f"{ref}: schematic value {c.value!r}, board value "
f"{f.value!r}",
anchors=[anchor("component", ref)]))
# --- pad-level net partition -------------------------------------------- #
pcb_net_of: dict[tuple[str, str], str | None] = {}
pcb_pads_of_ref: dict[str, set[str]] = defaultdict(set)
for pad in pcb.pads:
key = (pad.get("component") or "", pad.get("number") or "")
if not key[0] or not key[1]:
continue
# a pad number can repeat (thermal pads split into several pads);
# any netted instance wins over None
if key not in pcb_net_of or (pcb_net_of[key] is None and pad.get("net")):
pcb_net_of[key] = pad.get("net")
pcb_pads_of_ref[key[0]].add(key[1])
if not pcb.pads:
out(Finding("SCHPCB_NO_PAD_DATA", Severity.WARNING,
"board has no pad-level data — net equivalence not checked "
"(pre-1.2 export or empty board)"))
return findings
sch_net_of: dict[tuple[str, str], int] = {}
for i, net in enumerate(sch.nets):
for ref_pin in net.members:
sch_net_of[ref_pin] = i
common_refs = set(sch_by_ref) & set(pcb_by_ref)
# schematic pins whose component exists on the board but the pad doesn't
for c in sch.components:
if c.designator not in common_refs:
continue
have = pcb_pads_of_ref.get(c.designator, set())
for pin in c.pins:
if pin.number not in have:
out(Finding("SCHPCB_PAD_MISSING", Severity.ERROR,
f"{c.designator}.{pin.number}: schematic pin has no "
"pad on the board footprint",
anchors=[anchor("pin", f"{c.designator}.{pin.number}")]))
# net split: one schematic net -> multiple board nets
for net in sch.nets:
members = [m for m in net.members if m[0] in common_refs and m in pcb_net_of]
groups: dict[str, list] = defaultdict(list)
unnetted = []
for m in members:
pnet = pcb_net_of[m]
if pnet is None:
unnetted.append(m)
else:
groups[pnet].append(m)
label = net.name or f"<unnamed {net.stable_id}>"
if len(groups) > 1:
detail = "; ".join(
f"{pnet!r}: {_fmt_pins(pins)}" for pnet, pins in sorted(groups.items()))
out(Finding("SCHPCB_NET_SPLIT", Severity.ERROR,
f"schematic net {label} is split across "
f"{len(groups)} board nets — {detail}",
anchors=[anchor("net", label)]))
if unnetted and len(net.members) > 1:
out(Finding("SCHPCB_UNNETTED_PAD", Severity.WARNING,
f"schematic net {label}: board pads carry no net: "
f"{_fmt_pins(unnetted)}",
anchors=[anchor("net", label)]))
# net merge: one board net -> multiple schematic nets
board_groups: dict[str, set[int]] = defaultdict(set)
board_members: dict[str, list] = defaultdict(list)
for ref_pin, pnet in pcb_net_of.items():
if pnet is None or ref_pin not in sch_net_of:
continue
board_groups[pnet].add(sch_net_of[ref_pin])
board_members[pnet].append(ref_pin)
for pnet, snets in sorted(board_groups.items()):
if len(snets) > 1:
names = sorted((sch.nets[i].name or f"<unnamed {sch.nets[i].stable_id}>")
for i in snets)
out(Finding("SCHPCB_NET_MERGE", Severity.ERROR,
f"board net {pnet!r} shorts {len(snets)} schematic nets "
f"({', '.join(names[:6])}) — pads {_fmt_pins(board_members[pnet])}",
anchors=[anchor("net", pnet)]))
return findings