From d454cf75af9e010cecfd556fbf9eb292a3de8521 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Fri, 28 Aug 2026 21:00:04 +0200 Subject: [PATCH] Tie decoupling and I2C checks to pintable pin names, not mux tables. Ignore enable straps on a power rail, NC nets, and SPI aliases; treat FB as an inductor and RN as a resistor so pull-ups and beads classify correctly. Co-authored-by: Cursor --- backend/pinscopex/graph.py | 2 + backend/pinscopex/passive_rail_check.py | 76 ++++++++++++++++--------- frontend/src/lib/types.ts | 2 +- tests/test_passive_rail_check.py | 55 ++++++++++++++++++ 4 files changed, 106 insertions(+), 29 deletions(-) diff --git a/backend/pinscopex/graph.py b/backend/pinscopex/graph.py index 3f0da56..2579e32 100644 --- a/backend/pinscopex/graph.py +++ b/backend/pinscopex/graph.py @@ -31,8 +31,10 @@ from backend.pinscopex.resolve_passives import SkippedItem, resolve_bom, resolve _PREFIX_TYPE: dict[str, ComponentType] = { "R": ComponentType.RESISTOR, + "RN": ComponentType.RESISTOR, "C": ComponentType.CAPACITOR, "L": ComponentType.INDUCTOR, + "FB": ComponentType.INDUCTOR, "U": ComponentType.IC, "IC": ComponentType.IC, "J": ComponentType.CONNECTOR, diff --git a/backend/pinscopex/passive_rail_check.py b/backend/pinscopex/passive_rail_check.py index 3bbfc95..74f2ff4 100644 --- a/backend/pinscopex/passive_rail_check.py +++ b/backend/pinscopex/passive_rail_check.py @@ -1,7 +1,8 @@ """Deterministic supply decoupling and I2C/reset pull-up checks. -These only fire when the graph already shows a power pin, an I2C net, or a -reset pin — they do not guess capacitor values or datasheet µF minima. +These only fire when the graph already shows a pintable supply pin, an I2C +net/pin name, or a reset pin — they do not guess capacitor values, mux +alt-functions, or datasheet µF minima. """ from __future__ import annotations @@ -22,15 +23,21 @@ _SUPPLY_PIN_RE = re.compile( r"VIN|VBAT|VBUS|VCORE)(?:$|[_/\d])", re.IGNORECASE, ) +_RAIL_PIN_RE = re.compile(r"^(?:\+?\d+V\d*)$", re.IGNORECASE) _NOT_SUPPLY_RE = re.compile( r"\b(VSS|GND|VEE|VOUT|VREF|SW|LX|FB|BOOT|NC|VPP)\b", re.IGNORECASE, ) -_I2C_RE = re.compile(r"\b(SDA|SCL)(\d+)?\b", re.IGNORECASE) +_I2C_RE = re.compile(r"(?:^|[^A-Za-z0-9])(SDA|SCL)(\d+)?(?:$|[^A-Za-z0-9])", re.IGNORECASE) +_SPI_NAME_RE = re.compile(r"(?i)\b(MISO|MOSI|SCLK|SCK)\b") _RESET_RE = re.compile( r"\b(N?RST(?:N|B)?|NRST|RESET(?:_?N|_?B)?|NRESET|CHIP_PU)\b", re.IGNORECASE, ) +_NC_NET_RE = re.compile( + r"^(?:n/?c|n\.c\.|nc|unconnected|no[_-]?connect|not[_-]?connected)$", + re.IGNORECASE, +) def check_supply_decoupling( @@ -47,6 +54,8 @@ def check_supply_decoupling( for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])): if net_name in seen_nets: continue + if _is_nc_net(net_name): + continue if not _is_ic_supply_pin(graph, cons, pin_num, net_name): continue seen_nets.add(net_name) @@ -93,6 +102,8 @@ def check_i2c_pullups( for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])): if net_name in seen_nets: continue + if _is_nc_net(net_name): + continue if not _is_i2c_pin(graph, cons, pin_num, net_name): continue seen_nets.add(net_name) @@ -140,6 +151,8 @@ def check_reset_pullups( for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])): if net_name in seen_nets: continue + if _is_nc_net(net_name): + continue if not _is_reset_pin(graph, cons, pin_num, net_name): continue seen_nets.add(net_name) @@ -183,17 +196,27 @@ def _pin_label(cons: ComponentConstraints | None, pin_num: str, net_name: str) - return str(pin_num) -def _pin_blob( - cons: ComponentConstraints | None, pin_num: str, net_name: str, -) -> str: - parts = [net_name or ""] - if cons: - pin = cons.pin_by_number(pin_num) - if pin: - parts.append(pin.name or "") - if pin.functions: - parts.extend(pin.functions) - return " ".join(parts) +def _is_nc_net(name: str) -> bool: + return bool(_NC_NET_RE.match((name or "").strip())) + + +def _pin_name_tokens(cons: ComponentConstraints | None, pin_num: str) -> list[str]: + """Slash-separated pin *name* tokens only — not the mux alt-function table.""" + if not cons: + return [] + pin = cons.pin_by_number(pin_num) + if not pin or not pin.name: + return [] + return [t.strip() for t in re.split(r"[/,]", pin.name) if t.strip()] + + +def _looks_like_supply(text: str) -> bool: + t = (text or "").strip() + if not t: + return False + if _NOT_SUPPLY_RE.search(t) and not _SUPPLY_PIN_RE.search(t): + return False + return bool(_SUPPLY_PIN_RE.search(t) or _RAIL_PIN_RE.match(t)) def _is_ic_supply_pin( @@ -202,10 +225,11 @@ def _is_ic_supply_pin( pin_num: str, net_name: str, ) -> bool: - blob = _pin_blob(cons, pin_num, net_name) - if _NOT_SUPPLY_RE.search(blob) and not _SUPPLY_PIN_RE.search(blob): - return False - if _SUPPLY_PIN_RE.search(blob): + tokens = _pin_name_tokens(cons, pin_num) + if tokens: + return any(_looks_like_supply(t) for t in tokens) + # No pintable row: fall back to net name / POWER type. + if _looks_like_supply(net_name or ""): return True net = graph.nets.get(net_name) return bool(net and net.net_type == NetType.POWER) @@ -222,18 +246,12 @@ def _is_i2c_pin( r"(?i)\bSPI[_-]?(CLK|SCK|MOSI|MISO|CS|SS)\b", net, ): return False - primary = "" - if cons: - pin = cons.pin_by_number(pin_num) - if pin and pin.name: - primary = pin.name.split("/")[0].strip() - if re.search(r"(?i)\b(MISO|MOSI|SCLK|SCK)\b", primary): + tokens = _pin_name_tokens(cons, pin_num) + if any(_SPI_NAME_RE.search(t) for t in tokens): return False if _I2C_RE.search(net): return True - if _I2C_RE.search(primary): - return True - return False + return any(_I2C_RE.search(t) for t in tokens) def _is_reset_pin( @@ -242,7 +260,9 @@ def _is_reset_pin( pin_num: str, net_name: str, ) -> bool: - return bool(_RESET_RE.search(_pin_blob(cons, pin_num, net_name))) + if _RESET_RE.search(net_name or ""): + return True + return any(_RESET_RE.search(t) for t in _pin_name_tokens(cons, pin_num)) def _is_ground_net(graph: DesignGraph, name: str) -> bool: diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index c4df487..cd296eb 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -13,7 +13,7 @@ export interface Finding { status: FindingStatus; recommendation?: string; reference: string; - source?: string | null; // "pin_mux_check"/"led_current_check" = deterministic; null/"review" = LLM + source?: string | null; // pin_mux_check / led_current_check / supply_decoupling_check / i2c_pullup_check / reset_pullup_check = deterministic; null/"review" = LLM } export interface FindingComment { diff --git a/tests/test_passive_rail_check.py b/tests/test_passive_rail_check.py index 86afd8e..2f4fdc3 100644 --- a/tests/test_passive_rail_check.py +++ b/tests/test_passive_rail_check.py @@ -199,3 +199,58 @@ def test_reset_floating_is_warning(): assert len(findings) == 1 assert findings[0].source == "reset_pullup_check" assert findings[0].status == "WARNING" + + +def test_enable_strapped_to_rail_is_not_decoupling(): + cons = { + "UTEST": ComponentConstraints( + mpn="UTEST", + pintable=[ + Pin(number=1, name="EN"), + Pin(number=2, name="GND"), + ], + absolute_maximum_ratings=[], rules=[], + ) + } + g = _graph( + {"U1": _ic("U1", {"1": "3V3", "2": "GND"})}, + { + "3V3": (NetType.POWER, [("U1", "1")]), + "GND": (NetType.GROUND, [("U1", "2")]), + }, + ) + assert check_supply_decoupling(g, cons) == [] + + +def test_i2c_from_slash_alias_in_pin_name(): + cons = { + "UTEST": ComponentConstraints( + mpn="UTEST", + pintable=[Pin(number=12, name="GPIO12/I2C1_SDA")], + absolute_maximum_ratings=[], rules=[], + ) + } + g = _graph( + {"U1": _ic("U1", {"12": "NET-U1-12"})}, + {"NET-U1-12": (NetType.SIGNAL, [("U1", "12")])}, + ) + findings = check_i2c_pullups(g, cons) + assert len(findings) == 1 + assert findings[0].source == "i2c_pullup_check" + + +def test_nc_supply_net_is_skipped(): + g = _graph( + {"U1": _ic("U1", {"1": "NC"})}, + {"NC": (NetType.POWER, [("U1", "1")])}, + ) + assert check_supply_decoupling(g, _cmap_vdd()) == [] + + +def test_fb_and_rn_prefixes(): + from backend.pinscopex.graph import _classify_component + from backend.pinscopex.models import ComponentType + + assert _classify_component("FB1", "") == ComponentType.INDUCTOR + assert _classify_component("RN4", "") == ComponentType.RESISTOR + assert _classify_component("F1", "") == ComponentType.FUSE