Files
periscope/backend/pinscopex/pin_mux_check.py
T
Siddharth Kothari 6672d2be57 Pinscope open-source core
Agentic schematic validation: datasheet extraction via Claude Console
Skills, netlist/BOM design graph, per-IC direct datasheet review with
page citations, capacitor derating, Next.js report UI.

Extracted from the Pinscope cloud codebase. Auth and billing live in the
private gateway repo behind stable seams (billing_hook.py, adapter files
listed in CLAUDE.md).
2026-07-16 21:29:45 -07:00

173 lines
6.4 KiB
Python

"""Deterministic pin-mux feasibility check.
For each IC pin whose net name asserts a peripheral function (e.g. a net named
``MCU-UART5-TX`` asserts ``UART5_TX``), verify that the pin can actually be
configured for that function per the datasheet alternate-function table. A pin
that exposes peripheral P but *not* the asserted signal S (e.g. PD2 exposes
UART5 only as ``UART5_RX``) cannot be muxed to S — a hard, context-free defect.
This is a FEASIBILITY check, never a DIRECTION check. It makes no claim about
whether a TX should connect to a peer's RX (direct-UART crossover) or TX
(transceiver/isolator straight-through) — that is context-dependent and left to
the agentic reviewer. To stay sound it SKIPS any net that also lands on another
IC exposing the same peripheral (an inter-device link, where the net name's
perspective is ambiguous).
"""
from __future__ import annotations
from backend.pinscopex.models import (
ComponentConstraints,
ComponentType,
DesignGraph,
Finding,
)
from backend.pinscopex.pin_function_tokens import (
complement,
normalize_functions,
parse_net_token,
signals_for_peripheral,
)
from backend.pinscopex.validate import _match_constraints
def check_pin_mux_feasibility(
graph: DesignGraph,
constraints_map: dict[str, ComponentConstraints],
) -> list[Finding]:
"""Flag IC pins assigned a peripheral function their silicon can't route."""
findings: list[Finding] = []
for ref, comp in sorted(graph.components.items()):
if comp.component_type != ComponentType.IC:
continue
cons = _match_constraints(comp.mpn or comp.value, constraints_map)
if not cons:
continue
for pin_num, net_name in comp.pins.items():
token = parse_net_token(net_name)
if token is None:
continue
peripheral, signal = token
pin = cons.pin_by_number(pin_num)
if pin is None or not pin.functions:
continue
exposed = signals_for_peripheral(
normalize_functions(pin.functions), peripheral
)
if not exposed:
continue # pin doesn't expose this peripheral at all — not our case
if signal in exposed:
continue # feasible; any direction question is the reviewer's call
# Pin exposes the peripheral but NOT the asserted signal -> infeasible.
# Gate: skip if another IC pin on this net also exposes the peripheral
# (inter-device same-peripheral link — could be a legitimate crossover
# or transceiver straight-through; leave it to the agentic reviewer).
if _peer_exposes_peripheral(
graph, constraints_map, net_name, ref, peripheral
):
continue
findings.append(
_feasibility_finding(
ref, comp.mpn or "", pin_num, pin.name,
net_name, peripheral, signal, exposed, pin.functions,
)
)
return findings
def _peer_exposes_peripheral(
graph: DesignGraph,
constraints_map: dict[str, ComponentConstraints],
net_name: str,
self_ref: str,
peripheral: str,
) -> bool:
"""True if any *other* IC pin on this net exposes the given peripheral."""
net = graph.nets.get(net_name)
if not net:
return False
for pc in net.pins:
if pc.component_ref == self_ref:
continue
other = graph.components.get(pc.component_ref)
if not other or other.component_type != ComponentType.IC:
continue
ocons = _match_constraints(other.mpn or other.value, constraints_map)
if not ocons:
continue
opin = ocons.pin_by_number(pc.pin_number)
if opin is None or not opin.functions:
continue
if signals_for_peripheral(normalize_functions(opin.functions), peripheral):
return True
return False
def _feasibility_finding(
ref: str,
mpn: str,
pin_num: str,
pin_name: str,
net_name: str,
peripheral: str,
signal: str,
exposed: set[str],
functions: list[str],
) -> Finding:
# Full alternate-function list, verbatim from the datasheet and in datasheet
# order — NOT our canonicalized tokens. Printing the raw strings keeps the
# finding self-auditing: a reader (or a future us) can spot a naming synonym
# we haven't taught the tokenizer yet (this is how the SPI PICO/POCI==MOSI/MISO
# false positive slipped through — the finding only showed the derived subset).
functions_str = ", ".join(functions) if functions else "(none listed)"
comp_sig = complement(signal)
is_swap = bool(comp_sig and comp_sig in exposed)
swap_hint = ""
rec = (
f"Move '{net_name}' to a pin whose alternate functions include "
f"{peripheral}_{signal}."
)
if is_swap:
swap_hint = (
f" This pin's {peripheral} role is {peripheral}_{comp_sig} — the "
f"complement of {peripheral}_{signal} — so the {signal}/{comp_sig} "
f"nets are most likely swapped."
)
rec = (
f"Move '{net_name}' to a {peripheral}_{signal}-capable pin, or swap "
f"it with the paired {peripheral}_{comp_sig} net if that resolves both."
)
return Finding(
designator=ref,
mpn=mpn,
aspect="pin_mux",
source="pin_mux_check",
source_page=None,
status="ERROR",
finding=(
f"Net '{net_name}' assigns {ref} pin {pin_num} ({pin_name}) the "
f"{peripheral}_{signal} function, but this pin cannot be muxed as "
f"{peripheral}_{signal}."
),
why=(
f"The intended function {peripheral}_{signal} was inferred from the "
f"net name '{net_name}'. Per the datasheet alternate-function table, "
f"pin {pin_num} ({pin_name}) can be muxed as: {functions_str}. "
f"{peripheral}_{signal} is not in that list, so the silicon cannot "
f"route it here regardless of downstream wiring." + swap_hint +
f" If '{net_name}' is not actually configured for {peripheral} in "
f"firmware (e.g. bit-banged GPIO, or a label carried over from the "
f"connected part), disregard this finding."
),
recommendation=rec,
reference=f"{mpn or ref} alternate-function table",
)