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.
"""MCU pin -> net mapping + optional cross-check (GENERIC) (SPEC §3.6).
This check is deliberately **generic**: it emits the MCU's ``pin -> net`` table
from the (authoritative) schematic and, when given an *external* expected
``pin -> signal`` table, cross-checks the two and flags divergence.
What this module does NOT do (by design): it never parses a DTS, a board
pinout, or any firmware artifact. Producing the ``expected`` dict from such
sources lives in ``adapters/`` (``dts.py`` / ``pinout_md.py``) so this engine
stays reusable across projects. The ``expected`` table is passed in already
shaped as ``{pin_key: signal_name}``.
Authority model (LOCKED by SPEC): the **schematic is authoritative**; the
``expected`` table is **advisory**. A divergence is therefore reported as a
WARNING whose message states the schematic value wins -- it never mutates the
schematic and never escalates above WARNING.
Pin-name ``Pn.mm`` parser: GPIO pin names such as ``"P0.25"`` (and the common
``"P0_25"`` variant, with an optional trailing ``/alt-function`` suffix) are
normalized to a canonical ``"P<port>.<pin>"`` key so an expected table keyed by
either pin *number* or pin *name* still matches.
"""
from __future__ import annotations
import re
from ..model import Component, Net, Schematic
from ..report import Finding, Severity
# ``P<port>.<pin>`` / ``P<port>_<pin>`` with an optional trailing alt-function
# (e.g. ``P0.25/AIN1``). Anchored at the start so only true GPIO names parse.
_PIN_NAME_RE = re.compile(r"^P(\d+)[._](\d+)(?:\b|$)", re.IGNORECASE)
def parse_pin_name(name: str | None) -> tuple[int, int] | None:
"""Parse a ``Pn.mm`` GPIO pin name into ``(port, pin)``.
Accepts ``P0.25``, ``P0_25`` and a trailing alt-function such as
``P0.25/AIN1``. Returns ``None`` for anything that is not a GPIO pin name.
"""
if not name:
return None
m = _PIN_NAME_RE.match(name.strip())
if not m:
return None
return int(m.group(1)), int(m.group(2))
def _canonical_pin_name(name: str | None) -> str | None:
"""Return the canonical ``P<port>.<pin>`` form, or ``None`` if not GPIO-shaped."""
parsed = parse_pin_name(name)
if parsed is None:
return None
return f"P{parsed[0]}.{parsed[1]}"
def _pin_keys(number: str | None, name: str | None) -> set[str]:
"""All (upper-cased) keys an expected-table entry may use to reference a pin."""
keys: set[str] = set()
if number:
keys.add(number.strip().upper())
if name:
keys.add(name.strip().upper())
canon = _canonical_pin_name(name)
if canon:
keys.add(canon.upper())
return keys
def _normalize_expected_key(key: object) -> set[str]:
"""Normalize an expected-table key into the comparable key set."""
k = str(key).strip()
out = {k.upper()}
canon = _canonical_pin_name(k)
if canon:
out.add(canon.upper())
return out
def _net_name_set(net: Net | None) -> set[str]:
"""Case-folded set of every name a net answers to (name + aliases + sources)."""
if net is None:
return set()
names = [net.name, *net.aliases, *net.source_names]
return {n.strip().upper() for n in names if n}
def _find_mcu(sch: Schematic, designator: str) -> Component | None:
target = designator.strip().upper()
for comp in sch.components:
if comp.designator and comp.designator.strip().upper() == target:
return comp
return None
def _pin_to_net_index(sch: Schematic) -> dict[tuple[str, str], Net]:
"""Map every ``(designator, pin_number)`` member to its owning net."""
index: dict[tuple[str, str], Net] = {}
for net in sch.nets:
for member in net.members:
index[member] = net
return index
def _pin_sort_key(number: str | None) -> tuple[int, object]:
"""Sort pins numerically when possible, else lexicographically (numbers first)."""
n = (number or "").strip()
if n.isdigit():
return (0, int(n))
return (1, n.upper())
def _ref(designator: str, number: str | None) -> str:
return f"{designator}.{number}" if number else designator
def run(sch: Schematic, cfg: object | None, expected: dict | None = None) -> list[Finding]:
"""Emit the MCU pin->net map and (optionally) cross-check ``expected``.
Args:
sch: the authoritative parsed schematic.
cfg: a ``config.Config`` (or anything exposing ``mcu_designator``); may
be ``None``.
expected: optional external ``{pin_key: signal_name}`` table. Keys may be
pin numbers (``"2"``) or pin names (``"P0.25"``/``"P0_25"``); values
are the expected net/signal names. The schematic wins on conflict.
Returns:
A list of ``report.Finding`` (the MCU map as INFO findings, plus any
cross-check WARNING/NOTE findings).
"""
findings: list[Finding] = []
mcu_designator = getattr(cfg, "mcu_designator", None)
if not mcu_designator:
findings.append(
Finding(
"PINMAP_NO_MCU",
Severity.WARNING,
"no MCU designator configured -- pass --mcu <REF> (e.g. "
"`akcli pinmap main.SchDoc --mcu U3`) or set "
"[project].mcu_designator in akcli.toml; "
"cannot build a pin map",
[],
)
)
return findings
mcu = _find_mcu(sch, mcu_designator)
if mcu is None:
findings.append(
Finding(
"PINMAP_MCU_NOT_FOUND",
Severity.WARNING,
f"configured MCU '{mcu_designator}' not found in schematic",
[mcu_designator],
)
)
return findings
if not mcu.pins:
findings.append(
Finding(
"PINMAP_NO_PINS",
Severity.NOTE,
f"MCU '{mcu.designator}' has no pins to map",
[mcu.designator],
)
)
return findings
index = _pin_to_net_index(sch)
pins = sorted(mcu.pins, key=lambda p: _pin_sort_key(p.number))
# --- (1) GENERIC output: emit the MCU pin -> net table -------------------
# Each entry records the pin, optional GPIO name, and its resolved net.
pin_records: list[tuple[object, Net | None]] = []
for pin in pins:
net = index.get((mcu.designator, pin.number))
pin_records.append((pin, net))
canon = _canonical_pin_name(pin.name)
name_part = ""
if pin.name:
name_part = f" ({pin.name})" if canon is None else f" ({canon})"
if net is None:
findings.append(
Finding(
"PINMAP_FLOATING",
Severity.NOTE,
f"{_ref(mcu.designator, pin.number)}{name_part} -> "
"(no net / single-pin)",
[_ref(mcu.designator, pin.number)],
)
)
else:
findings.append(
Finding(
"PINMAP",
Severity.INFO,
f"{_ref(mcu.designator, pin.number)}{name_part} -> {net.name}",
[_ref(mcu.designator, pin.number)],
)
)
# --- (2) optional cross-check against the external expected table --------
if expected:
# Build a lookup from every key a pin answers to -> (pin, net).
key_to_pin: dict[str, tuple[object, Net | None]] = {}
for pin, net in pin_records:
for key in _pin_keys(pin.number, pin.name):
# First writer wins; pins should not collide, but be defensive.
key_to_pin.setdefault(key, (pin, net))
matched_pins: set[str] = set()
for raw_key, signal in expected.items():
exp_keys = _normalize_expected_key(raw_key)
hit = next(
((key_to_pin[k]) for k in exp_keys if k in key_to_pin), None
)
if hit is None:
findings.append(
Finding(
# WARNING (not NOTE) so an expected table that doesn't line up
# with the schematic fails the exit code instead of green-lighting
# silently (e.g. GPIO-number keys vs symbol pin names).
"PINMAP_EXPECTED_PIN_MISSING",
Severity.WARNING,
f"expected pin '{raw_key}' (-> '{signal}') is not present "
f"on MCU '{mcu.designator}'",
[str(raw_key)],
)
)
continue
pin, net = hit
matched_pins.add(pin.number)
ref = _ref(mcu.designator, pin.number)
exp_signal = str(signal).strip()
actual_names = _net_name_set(net)
if not actual_names:
findings.append(
Finding(
"PINMAP_MISMATCH",
Severity.WARNING,
f"{ref}: expected signal '{exp_signal}' but schematic pin "
"is on no net (schematic is authoritative)",
[ref],
)
)
elif exp_signal.upper() in actual_names:
findings.append(
Finding(
"PINMAP_MATCH",
Severity.INFO,
f"{ref}: expected '{exp_signal}' matches schematic net "
f"'{net.name}'",
[ref],
)
)
else:
findings.append(
Finding(
"PINMAP_MISMATCH",
Severity.WARNING,
f"{ref}: expected signal '{exp_signal}' but schematic net "
f"is '{net.name}' (schematic is authoritative; expected "
"table is advisory)",
[ref],
)
)
# MCU pins with a net that the expected table never covered.
for pin, net in pin_records:
if net is None or pin.number in matched_pins:
continue
findings.append(
Finding(
"PINMAP_UNEXPECTED",
Severity.NOTE,
f"{_ref(mcu.designator, pin.number)} -> '{net.name}' is not "
"listed in the expected table",
[_ref(mcu.designator, pin.number)],
)
)
return findings