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.
"""Differential-pair and bus continuity checks (ROADMAP v0.9).
``run(sch, cfg) -> list[Finding]`` over the built net model — pure NAME-level
continuity (the review EMC layer owns geometric skew):
* **Broken differential pair** (``PAIR_INCOMPLETE``, WARNING): a net named
like one side of a pair (``FOO_P``, ``USB_D+``, ``CAN_H``) whose partner
net does not exist. **Asymmetric by design**: only the *positive/high* side
alone fires — a lone ``_N``/``_L``/``-`` name is the active-low signal
convention (``RESET_N``, ``CS_N``), not a broken pair.
* **Pair pin-count mismatch** (``PAIR_PIN_MISMATCH``, NOTE): both sides
exist but carry different pin counts — legal (test points, terminations)
but worth a look.
* **Bus index gap** (``BUS_GAP``): a numbered net family (``D0..D7``,
``ADDR3``) with holes between its lowest and highest index. A family not
starting at 0 is fine (``A8..A15`` on a memory bus); only *internal* gaps
are flagged — NOTE for small families, WARNING once the family has >= 4
members. Digit-only or single-member families never fire.
``[check]`` config: ``pairs = false`` removes the family from the default
`check` run, ``pair_suffixes = [["_P","_N"], ...]`` replaces the suffix table
(first entry = the side that fires alone), ``bus_min_family`` (default 2)
raises the family-size threshold. Findings are waivable like any other via
``[[waiver]]``.
"""
from __future__ import annotations
import re
from ..config import Config
from ..model import Net, Schematic
from ..report import Finding, Severity
__all__ = ["run"]
PAIR_INCOMPLETE = "PAIR_INCOMPLETE"
PAIR_PIN_MISMATCH = "PAIR_PIN_MISMATCH"
BUS_GAP = "BUS_GAP"
# (positive/high suffix, negative/low suffix) — the FIRST side alone fires;
# the second alone is presumed active-low naming and stays silent.
_DEFAULT_PAIR_SUFFIXES: tuple[tuple[str, str], ...] = (
("_P", "_N"),
("_DP", "_DN"),
("_H", "_L"),
("+", "-"),
)
# numbered family: prefix must end in a letter or underscore so pure numerals
# ("3V3" -> prefix "3V"? no: "3V" ends in a letter, allowed but needs >= 2
# same-prefix members to matter) and the index is the trailing decimal run.
_FAMILY_RE = re.compile(r"^(.*[A-Za-z_])(\d+)$")
_BUS_WARN_FAMILY = 4 # families this large get WARNING gaps, smaller get NOTE
def _pair_suffixes(cfg: Config | None) -> tuple[tuple[str, str], ...]:
raw = (cfg.check if cfg else {}).get("pair_suffixes")
if raw:
return tuple((str(a), str(b)) for a, b in raw)
return _DEFAULT_PAIR_SUFFIXES
def _split_suffix(name: str, suffix: str) -> str | None:
"""The base of ``name`` for a case-insensitive ``suffix`` match, or None."""
if len(name) <= len(suffix):
return None
if not name.upper().endswith(suffix.upper()):
return None
return name[: -len(suffix)]
def run(sch: Schematic, cfg: Config | None = None) -> list[Finding]:
findings: list[Finding] = []
named: dict[str, Net] = {}
for net in sch.nets:
for name in [net.name, *net.aliases]:
if name:
named.setdefault(name.upper(), net)
def _lookup(name: str) -> Net | None:
return named.get(name.upper())
# --- differential pairs -------------------------------------------------
seen_bases: set[tuple[str, str]] = set()
for net in sch.nets:
if not net.name:
continue
for pos_sfx, neg_sfx in _pair_suffixes(cfg):
base = _split_suffix(net.name, pos_sfx)
if base is None:
continue
key = (base.upper(), pos_sfx.upper())
if key in seen_bases:
continue
seen_bases.add(key)
partner_name = base + neg_sfx
partner = _lookup(partner_name)
if partner is None:
findings.append(
Finding(
PAIR_INCOMPLETE,
Severity.WARNING,
f"net {net.name!r} looks like one side of a "
f"differential pair but {partner_name!r} does not "
"exist — broken pair, or rename if single-ended",
refs=[net.name],
)
)
elif partner is not net and len(partner.members) != len(net.members):
findings.append(
Finding(
PAIR_PIN_MISMATCH,
Severity.NOTE,
f"pair {net.name!r}/{partner_name!r} pin counts differ "
f"({len(net.members)} vs {len(partner.members)}) — "
"check terminations/test points are intentional",
refs=[net.name, partner_name],
)
)
break # first matching suffix owns the name
# --- bus index gaps ------------------------------------------------------
min_family = int((cfg.check if cfg else {}).get("bus_min_family", 2) or 2)
families: dict[str, dict[int, str]] = {}
for net in sch.nets:
if not net.name:
continue
m = _FAMILY_RE.match(net.name)
if not m:
continue
prefix, idx = m.group(1), int(m.group(2))
families.setdefault(prefix.upper(), {})[idx] = net.name
for prefix_key in sorted(families):
members = families[prefix_key]
if len(members) < max(2, min_family):
continue
lo, hi = min(members), max(members)
missing = [i for i in range(lo, hi + 1) if i not in members]
if not missing:
continue
prefix = re.sub(r"\d+$", "", members[lo]) # display prefix, original case
sev = Severity.WARNING if len(members) >= _BUS_WARN_FAMILY else Severity.NOTE
shown = ", ".join(f"{prefix}{i}" for i in missing[:8])
if len(missing) > 8:
shown += f", … ({len(missing)} total)"
findings.append(
Finding(
BUS_GAP,
sev,
f"bus family {prefix}{lo}..{prefix}{hi} has "
f"{len(missing)} missing index(es): {shown} — gap in a "
"numbered net family (intentional? waive it)",
refs=sorted(members.values()),
)
)
return findings