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).
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
"""Peripheral-function tokens parsed from net names and pin alternate-function
|
|
strings.
|
|
|
|
A *token* is a ``(peripheral, signal)`` pair, e.g. ``("UART5", "TX")`` or
|
|
``("I2C1", "SDA")``. Both the schematic net name (user-authored, e.g.
|
|
``"MCU-UART5-TX"``) and the datasheet-extracted pin functions (e.g.
|
|
``"UART5_RX"``, ``"SPI3_MOSI/I2S3_SDO"``) are reduced to the same canonical
|
|
token space so they can be compared.
|
|
|
|
Used by:
|
|
* ``pin_mux_check`` — the deterministic pin-mux feasibility check
|
|
* ``validate.build_component_context`` — to render alt-functions only on
|
|
peripheral-named-net pins (token-conscious context rendering)
|
|
|
|
Design goal is *high precision, low recall*: only emit a token when both the
|
|
bus family and the signal are unambiguous, so the feasibility check never
|
|
false-positives on opaque nets or vocabulary mismatches (CS vs NSS, TXD vs TX).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
# Bus families whose pin assignment is muxed and whose naming is stable enough
|
|
# to validate. Longer families that contain a shorter one as a substring
|
|
# (FDCAN/CAN, OCTOSPI/QSPI, USART/UART) are listed first; the patterns are
|
|
# anchored, so a token like "OCTOSPI1" never matches the bare "SPI" family.
|
|
_FAMILIES = (
|
|
"LPUART", "USART", "UART", "I2C", "OCTOSPI", "QSPI", "SPI",
|
|
"FDCAN", "CAN", "SDMMC", "SDIO", "I2S", "SAI", "USB",
|
|
)
|
|
_FAMILY_ALT = "|".join(_FAMILIES)
|
|
|
|
# A single net-name token that is exactly a bus family + optional instance number.
|
|
_PERIPHERAL_RE = re.compile(rf"^({_FAMILY_ALT})(\d*)$")
|
|
# A pin alternate-function string: <family><instance>_<signal...>.
|
|
_FUNCTION_RE = re.compile(rf"^({_FAMILY_ALT})(\d*)_(.+)$")
|
|
|
|
# Canonical signal names we compare on — restricted to signals with stable
|
|
# naming across user net labels and datasheet function strings. SPI's
|
|
# controller/peripheral names (PICO/POCI/COPI/CIPO) are NOT canonical — they are
|
|
# synonyms of MOSI/MISO (same physical line, renamed) and collapse below.
|
|
_SIGNALS = {
|
|
"TX", "RX", "SDA", "SCL", "MOSI", "MISO",
|
|
"SCK", "NSS", "DP", "DM",
|
|
}
|
|
|
|
# Synonyms collapsed to a canonical signal before comparison.
|
|
_SIGNAL_SYNONYMS = {
|
|
"TXD": "TX", "RXD": "RX",
|
|
"SCLK": "SCK", "CLK": "SCK",
|
|
"SS": "NSS", "CS": "NSS", "NCS": "NSS", "STE": "NSS",
|
|
"DPLUS": "DP", "DMINUS": "DM",
|
|
# SPI controller/peripheral nomenclature — the same physical lines as
|
|
# master/slave MOSI/MISO, just renamed (TI/NXP/ST modern parts). A net
|
|
# labelled SPI0_MOSI landing on a pin whose datasheet function is SPI0_PICO
|
|
# is feasible, not a defect. (SDO/SDI deliberately omitted — their meaning
|
|
# flips with controller-vs-peripheral perspective, so they aren't safe to
|
|
# equate here.)
|
|
"PICO": "MOSI", "COPI": "MOSI",
|
|
"POCI": "MISO", "CIPO": "MISO",
|
|
}
|
|
|
|
# Directional complements — the signal that *should* be present if the asserted
|
|
# one isn't. Used to phrase a feasibility finding as a likely swap. Keyed on
|
|
# canonical signals only (PICO/POCI collapse to MOSI/MISO before this is read).
|
|
_COMPLEMENT = {
|
|
"TX": "RX", "RX": "TX",
|
|
"SDA": "SCL", "SCL": "SDA",
|
|
"MOSI": "MISO", "MISO": "MOSI",
|
|
"DP": "DM", "DM": "DP",
|
|
}
|
|
|
|
# Chip-select alternates often carry an instance suffix (SPI0_CS0..CS3, STE0..);
|
|
# strip the trailing index so every variant canonicalises to the bare CS token.
|
|
_CHIP_SELECT_INDEXED_RE = re.compile(r"^(N?CS|SS|STE)\d+$")
|
|
|
|
|
|
def _canon_signal(tok: str) -> str | None:
|
|
"""Canonicalise a raw signal token, or return None if it isn't a known signal."""
|
|
t = tok.upper()
|
|
m = _CHIP_SELECT_INDEXED_RE.match(t)
|
|
if m:
|
|
t = m.group(1)
|
|
t = _SIGNAL_SYNONYMS.get(t, t)
|
|
return t if t in _SIGNALS else None
|
|
|
|
|
|
def _tokens(name: str) -> list[str]:
|
|
"""Split a net name into delimiter-separated tokens (uppercased)."""
|
|
s = name.upper().lstrip("/")
|
|
# Map the only signals that embed a delimiter char before splitting.
|
|
s = s.replace("D+", "DP").replace("D-", "DM")
|
|
s = re.sub(r"[._/]", "-", s)
|
|
return [p for p in s.split("-") if p]
|
|
|
|
|
|
def parse_net_token(net_name: str) -> tuple[str, str] | None:
|
|
"""Extract a ``(peripheral, canonical_signal)`` token from a net name, or None.
|
|
|
|
Emits only when a bus-family token is immediately followed by a known
|
|
signal, e.g. ``"MCU-UART5-TX" -> ("UART5", "TX")``,
|
|
``"I2C1-SDA-3V3" -> ("I2C1", "SDA")``. Opaque nets (``"NetC7_1"``,
|
|
``"MCU-RESET"``) return None.
|
|
"""
|
|
parts = _tokens(net_name)
|
|
for i in range(len(parts) - 1):
|
|
m = _PERIPHERAL_RE.match(parts[i])
|
|
if not m:
|
|
continue
|
|
sig = _canon_signal(parts[i + 1])
|
|
if sig is None:
|
|
continue
|
|
return (m.group(1) + m.group(2), sig)
|
|
return None
|
|
|
|
|
|
def normalize_functions(functions: list[str] | None) -> set[tuple[str, str]]:
|
|
"""Reduce a pin's alternate-function strings to canonical
|
|
``(peripheral, signal)`` tokens. Splits slash-joined alternates
|
|
(``"SPI3_MOSI/I2S3_SDO"`` -> two tokens)."""
|
|
out: set[tuple[str, str]] = set()
|
|
for f in functions or []:
|
|
for alt in f.upper().replace("D+", "DP").replace("D-", "DM").split("/"):
|
|
m = _FUNCTION_RE.match(alt.strip())
|
|
if not m:
|
|
continue
|
|
sig = _canon_signal(m.group(3))
|
|
if sig is None:
|
|
continue
|
|
out.add((m.group(1) + m.group(2), sig))
|
|
return out
|
|
|
|
|
|
def signals_for_peripheral(funcs: set[tuple[str, str]], peripheral: str) -> set[str]:
|
|
"""All canonical signals a function set exposes for one peripheral instance."""
|
|
return {s for (p, s) in funcs if p == peripheral}
|
|
|
|
|
|
def complement(signal: str) -> str | None:
|
|
"""The directional complement of a signal (TX<->RX, SDA<->SCL, ...), or None."""
|
|
return _COMPLEMENT.get(signal)
|