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:
@@ -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,
|
||||||
|
|||||||
@@ -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]:
|
||||||
pin = cons.pin_by_number(pin_num)
|
"""Slash-separated pin *name* tokens only — not the mux alt-function table."""
|
||||||
if pin:
|
if not cons:
|
||||||
parts.append(pin.name or "")
|
return []
|
||||||
if pin.functions:
|
pin = cons.pin_by_number(pin_num)
|
||||||
parts.extend(pin.functions)
|
if not pin or not pin.name:
|
||||||
return " ".join(parts)
|
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(
|
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:
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user