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 <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 21:00:04 +02:00
co-authored by Cursor
parent edbb47a08b
commit d454cf75af
4 changed files with 106 additions and 29 deletions
+2
View File
@@ -31,8 +31,10 @@ from backend.pinscopex.resolve_passives import SkippedItem, resolve_bom, resolve
_PREFIX_TYPE: dict[str, ComponentType] = { _PREFIX_TYPE: dict[str, ComponentType] = {
"R": ComponentType.RESISTOR, "R": ComponentType.RESISTOR,
"RN": ComponentType.RESISTOR,
"C": ComponentType.CAPACITOR, "C": ComponentType.CAPACITOR,
"L": ComponentType.INDUCTOR, "L": ComponentType.INDUCTOR,
"FB": ComponentType.INDUCTOR,
"U": ComponentType.IC, "U": ComponentType.IC,
"IC": ComponentType.IC, "IC": ComponentType.IC,
"J": ComponentType.CONNECTOR, "J": ComponentType.CONNECTOR,
+47 -27
View File
@@ -1,7 +1,8 @@
"""Deterministic supply decoupling and I2C/reset pull-up checks. """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 These only fire when the graph already shows a pintable supply pin, an I2C
reset pin — they do not guess capacitor values or datasheet µF minima. net/pin name, or a reset pin — they do not guess capacitor values, mux
alt-functions, or datasheet µF minima.
""" """
from __future__ import annotations from __future__ import annotations
@@ -22,15 +23,21 @@ _SUPPLY_PIN_RE = re.compile(
r"VIN|VBAT|VBUS|VCORE)(?:$|[_/\d])", r"VIN|VBAT|VBUS|VCORE)(?:$|[_/\d])",
re.IGNORECASE, re.IGNORECASE,
) )
_RAIL_PIN_RE = re.compile(r"^(?:\+?\d+V\d*)$", re.IGNORECASE)
_NOT_SUPPLY_RE = re.compile( _NOT_SUPPLY_RE = re.compile(
r"\b(VSS|GND|VEE|VOUT|VREF|SW|LX|FB|BOOT|NC|VPP)\b", r"\b(VSS|GND|VEE|VOUT|VREF|SW|LX|FB|BOOT|NC|VPP)\b",
re.IGNORECASE, 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( _RESET_RE = re.compile(
r"\b(N?RST(?:N|B)?|NRST|RESET(?:_?N|_?B)?|NRESET|CHIP_PU)\b", r"\b(N?RST(?:N|B)?|NRST|RESET(?:_?N|_?B)?|NRESET|CHIP_PU)\b",
re.IGNORECASE, re.IGNORECASE,
) )
_NC_NET_RE = re.compile(
r"^(?:n/?c|n\.c\.|nc|unconnected|no[_-]?connect|not[_-]?connected)$",
re.IGNORECASE,
)
def check_supply_decoupling( 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])): for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])):
if net_name in seen_nets: if net_name in seen_nets:
continue continue
if _is_nc_net(net_name):
continue
if not _is_ic_supply_pin(graph, cons, pin_num, net_name): if not _is_ic_supply_pin(graph, cons, pin_num, net_name):
continue continue
seen_nets.add(net_name) 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])): for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])):
if net_name in seen_nets: if net_name in seen_nets:
continue continue
if _is_nc_net(net_name):
continue
if not _is_i2c_pin(graph, cons, pin_num, net_name): if not _is_i2c_pin(graph, cons, pin_num, net_name):
continue continue
seen_nets.add(net_name) 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])): for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])):
if net_name in seen_nets: if net_name in seen_nets:
continue continue
if _is_nc_net(net_name):
continue
if not _is_reset_pin(graph, cons, pin_num, net_name): if not _is_reset_pin(graph, cons, pin_num, net_name):
continue continue
seen_nets.add(net_name) 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) return str(pin_num)
def _pin_blob( def _is_nc_net(name: str) -> bool:
cons: ComponentConstraints | None, pin_num: str, net_name: str, return bool(_NC_NET_RE.match((name or "").strip()))
) -> str:
parts = [net_name or ""]
if cons: 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) pin = cons.pin_by_number(pin_num)
if pin: if not pin or not pin.name:
parts.append(pin.name or "") return []
if pin.functions: return [t.strip() for t in re.split(r"[/,]", pin.name) if t.strip()]
parts.extend(pin.functions)
return " ".join(parts)
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( def _is_ic_supply_pin(
@@ -202,10 +225,11 @@ def _is_ic_supply_pin(
pin_num: str, pin_num: str,
net_name: str, net_name: str,
) -> bool: ) -> bool:
blob = _pin_blob(cons, pin_num, net_name) tokens = _pin_name_tokens(cons, pin_num)
if _NOT_SUPPLY_RE.search(blob) and not _SUPPLY_PIN_RE.search(blob): if tokens:
return False return any(_looks_like_supply(t) for t in tokens)
if _SUPPLY_PIN_RE.search(blob): # No pintable row: fall back to net name / POWER type.
if _looks_like_supply(net_name or ""):
return True return True
net = graph.nets.get(net_name) net = graph.nets.get(net_name)
return bool(net and net.net_type == NetType.POWER) 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, r"(?i)\bSPI[_-]?(CLK|SCK|MOSI|MISO|CS|SS)\b", net,
): ):
return False return False
primary = "" tokens = _pin_name_tokens(cons, pin_num)
if cons: if any(_SPI_NAME_RE.search(t) for t in tokens):
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):
return False return False
if _I2C_RE.search(net): if _I2C_RE.search(net):
return True return True
if _I2C_RE.search(primary): return any(_I2C_RE.search(t) for t in tokens)
return True
return False
def _is_reset_pin( def _is_reset_pin(
@@ -242,7 +260,9 @@ def _is_reset_pin(
pin_num: str, pin_num: str,
net_name: str, net_name: str,
) -> bool: ) -> 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: def _is_ground_net(graph: DesignGraph, name: str) -> bool:
+1 -1
View File
@@ -13,7 +13,7 @@ export interface Finding {
status: FindingStatus; status: FindingStatus;
recommendation?: string; recommendation?: string;
reference: 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 { export interface FindingComment {
+55
View File
@@ -199,3 +199,58 @@ def test_reset_floating_is_warning():
assert len(findings) == 1 assert len(findings) == 1
assert findings[0].source == "reset_pullup_check" assert findings[0].source == "reset_pullup_check"
assert findings[0].status == "WARNING" 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