Size I2C pull-ups, flag NRST pull-downs, and distinguish LDO Cout from MCU decoupling.
Keep these as WARNING hints with wide bounds so a 4.7 kΩ or 100 nF VDD cap is not treated as a datasheet µF error. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,13 +10,18 @@ from __future__ import annotations
|
||||
import re
|
||||
|
||||
from backend.pinscopex.models import (
|
||||
CapacitorSpecs,
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Finding,
|
||||
NetType,
|
||||
ResistorSpecs,
|
||||
)
|
||||
from backend.pinscopex.validate import _match_constraints
|
||||
from backend.pinscopex.led_current_check import _parse_resistance
|
||||
from backend.pinscopex.resolve_passives import _parse_spice_value
|
||||
|
||||
_SUPPLY_PIN_RE = re.compile(
|
||||
r"(?:^|[_/])(VDD|VCC|VDDA|VDDD|VDDIO|DVDD|AVDD|IOVDD|VDD33|VDD18|"
|
||||
@@ -38,13 +43,27 @@ _NC_NET_RE = re.compile(
|
||||
r"^(?:n/?c|n\.c\.|nc|unconnected|no[_-]?connect|not[_-]?connected)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_OUT_PIN_RE = re.compile(
|
||||
r"(?:^|[_/])(VOUT|V_OUT|VO|VREG|SWOUT)(?:$|[_/\d])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_ACTIVE_LOW_RESET_RE = re.compile(
|
||||
r"(?:N/?RST|NRST|NRESET|RESET[_-]?N|RSTN)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# NXP UM10204-style Rp window, widened so 2.2k–10k never false-positives.
|
||||
_RP_MIN_OHM = 1_000.0
|
||||
_RP_MAX_OHM = 22_000.0
|
||||
_VDD_MIN_FARADS = 50e-9
|
||||
_VOUT_MIN_FARADS = 0.47e-6
|
||||
|
||||
|
||||
def check_supply_decoupling(
|
||||
graph: DesignGraph,
|
||||
constraints_map: dict[str, ComponentConstraints],
|
||||
) -> list[Finding]:
|
||||
"""WARNING when an IC supply net has no capacitor to ground."""
|
||||
"""WARNING when an IC supply/VOUT net has no capacitor to ground, or
|
||||
only farads well below a typical Cin/Cout when every cap is valued."""
|
||||
findings: list[Finding] = []
|
||||
seen_nets: set[str] = set()
|
||||
for ref, comp in sorted(graph.components.items()):
|
||||
@@ -56,38 +75,77 @@ def check_supply_decoupling(
|
||||
continue
|
||||
if _is_nc_net(net_name):
|
||||
continue
|
||||
if not _is_ic_supply_pin(graph, cons, pin_num, net_name):
|
||||
role = None
|
||||
if _is_ic_supply_pin(graph, cons, pin_num, net_name):
|
||||
role = "supply"
|
||||
elif _is_regulator_output_pin(cons, pin_num):
|
||||
role = "output"
|
||||
if role is None:
|
||||
continue
|
||||
seen_nets.add(net_name)
|
||||
if _capacitor_to_ground(graph, net_name):
|
||||
continue
|
||||
pin_label = _pin_label(cons, pin_num, net_name)
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="decoupling",
|
||||
source="supply_decoupling_check",
|
||||
source_page=None,
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} supply net '{net_name}' ({pin_label}) has no "
|
||||
f"capacitor to ground."
|
||||
),
|
||||
why=(
|
||||
f"Pin {pin_label} sits on '{net_name}' and that net has no "
|
||||
f"capacitor whose other end is ground. Local decoupling "
|
||||
f"may be missing (or only present on a different island "
|
||||
f"behind a ferrite)."
|
||||
),
|
||||
recommendation=(
|
||||
f"Add a decoupling capacitor from '{net_name}' to ground "
|
||||
f"near {ref}."
|
||||
),
|
||||
reference="netlist topology",
|
||||
net=net_name,
|
||||
pins=[f"{ref}.{pin_num}"],
|
||||
rule_id="PS-DEC-001",
|
||||
))
|
||||
if not _capacitor_to_ground(graph, net_name):
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="decoupling",
|
||||
source="supply_decoupling_check",
|
||||
source_page=None,
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} supply net '{net_name}' ({pin_label}) has no "
|
||||
f"capacitor to ground."
|
||||
if role == "supply"
|
||||
else (
|
||||
f"{ref} regulator output '{net_name}' ({pin_label}) "
|
||||
f"has no Cout capacitor to ground."
|
||||
)
|
||||
),
|
||||
why=(
|
||||
f"Pin {pin_label} sits on '{net_name}' and that net has no "
|
||||
f"capacitor whose other end is ground. Local decoupling "
|
||||
f"may be missing (or only present on a different island "
|
||||
f"behind a ferrite)."
|
||||
),
|
||||
recommendation=(
|
||||
f"Add a decoupling capacitor from '{net_name}' to ground "
|
||||
f"near {ref}."
|
||||
),
|
||||
reference="netlist topology",
|
||||
net=net_name,
|
||||
pins=[f"{ref}.{pin_num}"],
|
||||
rule_id="PS-DEC-001",
|
||||
))
|
||||
continue
|
||||
min_f = _VOUT_MIN_FARADS if role == "output" else _VDD_MIN_FARADS
|
||||
max_c = _max_known_cap_farads(graph, net_name)
|
||||
if max_c is not None and max_c < min_f:
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="decoupling",
|
||||
source="supply_decoupling_check",
|
||||
source_page=None,
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} net '{net_name}' ({pin_label}) only has "
|
||||
f"{max_c * 1e6:.3g} µF to ground; typical "
|
||||
f"{'Cout' if role == 'output' else 'decoupling'} is larger."
|
||||
),
|
||||
why=(
|
||||
"Cap values are known on this net and the largest is "
|
||||
"below a wide typical minimum. This is not a datasheet "
|
||||
"µF requirement — treat as a sizing hint."
|
||||
),
|
||||
recommendation=(
|
||||
f"Add bulk capacitance on '{net_name}' (often ≥1 µF on "
|
||||
f"LDO VOUT, ≥100 nF on MCU VDD) if the datasheet agrees."
|
||||
),
|
||||
reference="netlist topology",
|
||||
net=net_name,
|
||||
pins=[f"{ref}.{pin_num}"],
|
||||
rule_id="PS-DEC-002",
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
@@ -114,6 +172,38 @@ def check_i2c_pullups(
|
||||
if net and net.net_type in (NetType.POWER, NetType.GROUND):
|
||||
continue
|
||||
if _resistor_to_power(graph, net_name):
|
||||
ohms = _parallel_pullup_ohms(graph, net_name)
|
||||
if ohms is not None and (
|
||||
ohms < _RP_MIN_OHM or ohms > _RP_MAX_OHM
|
||||
):
|
||||
pin_label = _pin_label(cons, pin_num, net_name)
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="i2c_pullup",
|
||||
source="i2c_pullup_check",
|
||||
source_page=None,
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"I2C net '{net_name}' ({ref} {pin_label}) pull-up "
|
||||
f"is {ohms:.3g} Ω (wide NXP-style band "
|
||||
f"{_RP_MIN_OHM:.0f}–{_RP_MAX_OHM:.0f} Ω)."
|
||||
),
|
||||
why=(
|
||||
"UM10204 Rp depends on Vdd, Iol and bus capacitance. "
|
||||
"This bound is wide on purpose; 2.2–10 kΩ at 3.3 V "
|
||||
"is typical. Unknown resistor values are not sized."
|
||||
),
|
||||
recommendation=(
|
||||
f"Use a pull-up on '{net_name}' inside "
|
||||
f"{_RP_MIN_OHM:.0f}–{_RP_MAX_OHM:.0f} Ω unless the "
|
||||
f"bus capacitance/Iol calculation says otherwise."
|
||||
),
|
||||
reference="NXP UM10204 (wide bound)",
|
||||
net=net_name,
|
||||
pins=[f"{ref}.{pin_num}"],
|
||||
rule_id="PS-I2C-002",
|
||||
))
|
||||
continue
|
||||
pin_label = _pin_label(cons, pin_num, net_name)
|
||||
findings.append(Finding(
|
||||
@@ -167,9 +257,38 @@ def check_reset_pullups(
|
||||
continue
|
||||
if _other_ic_on_net(graph, net_name, ref):
|
||||
continue
|
||||
pin_label = _pin_label(cons, pin_num, net_name)
|
||||
if _is_active_low_reset(cons, pin_num, net_name) and _resistor_to_ground(
|
||||
graph, net_name
|
||||
):
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="reset_pullup",
|
||||
source="reset_pullup_check",
|
||||
source_page=None,
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} active-low reset '{net_name}' ({pin_label}) "
|
||||
f"has a pull-down to ground."
|
||||
),
|
||||
why=(
|
||||
"An active-low NRST/RESET_N pin held down by a resistor "
|
||||
"will sit in reset unless a stronger pull-up wins. "
|
||||
"Datasheets that omit an internal pull-up expect a pull-up, "
|
||||
"not a pull-down."
|
||||
),
|
||||
recommendation=(
|
||||
f"Remove the pull-down on '{net_name}' or replace it "
|
||||
f"with a pull-up to the I/O rail."
|
||||
),
|
||||
reference="netlist topology",
|
||||
net=net_name,
|
||||
pins=[f"{ref}.{pin_num}"],
|
||||
rule_id="PS-RST-002",
|
||||
))
|
||||
if _resistor_to_power(graph, net_name):
|
||||
continue
|
||||
pin_label = _pin_label(cons, pin_num, net_name)
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
@@ -311,6 +430,90 @@ def _resistor_to_power(graph: DesignGraph, net_name: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _resistor_to_ground(graph: DesignGraph, net_name: str) -> bool:
|
||||
for ref in graph.components_on_net(net_name):
|
||||
comp = graph.components[ref]
|
||||
if comp.component_type != ComponentType.RESISTOR:
|
||||
continue
|
||||
others = {n for n in comp.pins.values() if n != net_name}
|
||||
if any(_is_ground_net(graph, n) for n in others):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _resistor_ohms(comp: Component) -> float | None:
|
||||
specs = comp.specs
|
||||
if isinstance(specs, ResistorSpecs) and specs.value_ohms > 0:
|
||||
return float(specs.value_ohms)
|
||||
return _parse_resistance(comp.value)
|
||||
|
||||
|
||||
def _parallel_pullup_ohms(graph: DesignGraph, net_name: str) -> float | None:
|
||||
acc = 0.0
|
||||
known = 0
|
||||
for ref in graph.components_on_net(net_name):
|
||||
comp = graph.components[ref]
|
||||
if comp.component_type != ComponentType.RESISTOR:
|
||||
continue
|
||||
others = {n for n in comp.pins.values() if n != net_name}
|
||||
if not any(_is_power_net(graph, n) for n in others):
|
||||
continue
|
||||
ohms = _resistor_ohms(comp)
|
||||
if ohms is None or ohms <= 0:
|
||||
return None
|
||||
acc += 1.0 / ohms
|
||||
known += 1
|
||||
if not known or acc <= 0:
|
||||
return None
|
||||
return 1.0 / acc
|
||||
|
||||
|
||||
def _cap_farads(comp: Component) -> float | None:
|
||||
specs = comp.specs
|
||||
if isinstance(specs, CapacitorSpecs) and specs.value_farads > 0:
|
||||
return float(specs.value_farads)
|
||||
raw = (comp.value or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
v = _parse_spice_value(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
return v if v > 0 else None
|
||||
|
||||
|
||||
def _max_known_cap_farads(graph: DesignGraph, power_net: str) -> float | None:
|
||||
known: list[float] = []
|
||||
any_unknown = False
|
||||
for ref in graph.capacitors_on_net(power_net):
|
||||
cap = graph.components[ref]
|
||||
others = {n for n in cap.pins.values() if n != power_net}
|
||||
if not any(_is_ground_net(graph, n) for n in others):
|
||||
continue
|
||||
farads = _cap_farads(cap)
|
||||
if farads is None:
|
||||
any_unknown = True
|
||||
continue
|
||||
known.append(farads)
|
||||
if any_unknown or not known:
|
||||
return None
|
||||
return max(known)
|
||||
|
||||
|
||||
def _is_regulator_output_pin(
|
||||
cons: ComponentConstraints | None, pin_num: str,
|
||||
) -> bool:
|
||||
return any(_OUT_PIN_RE.search(t) for t in _pin_name_tokens(cons, pin_num))
|
||||
|
||||
|
||||
def _is_active_low_reset(
|
||||
cons: ComponentConstraints | None, pin_num: str, net_name: str,
|
||||
) -> bool:
|
||||
if _ACTIVE_LOW_RESET_RE.search(net_name or ""):
|
||||
return True
|
||||
return any(_ACTIVE_LOW_RESET_RE.search(t) for t in _pin_name_tokens(cons, pin_num))
|
||||
|
||||
|
||||
def _other_ic_on_net(graph: DesignGraph, net_name: str, self_ref: str) -> bool:
|
||||
for ref in graph.components_on_net(net_name):
|
||||
if ref == self_ref:
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
What's new in Pinscope.
|
||||
|
||||
## 2.12.0 — 2026-09-10 — Pull-up sizing and LDO Cout
|
||||
|
||||
Deterministic schema checks now size I2C pull-ups, flag NRST pull-downs, and look at LDO VOUT capacitance — still WARNING, never a invented datasheet µF ERROR.
|
||||
|
||||
- [New] I2C pull-up value vs a wide NXP UM10204 band (`PS-I2C-002`). 4.7 kΩ is in-band; missing values are not sized.
|
||||
- [New] Active-low reset with a resistor to ground is `PS-RST-002`.
|
||||
- [New] Regulator VOUT needs Cout (`PS-DEC-001`); 100 nF-only on VOUT is `PS-DEC-002`. MCU VDD 100 nF is not flagged.
|
||||
- [Improved] Pin-mux UART0 on the `simple_project` MSPM0 nets is covered in tests (SPI PICO/POCI already was).
|
||||
|
||||
## 2.11.0 — 2026-09-10 — DeepSeek V4.1 and re-analyze
|
||||
|
||||
Pinscope now defaults to DeepSeek-V4.1-Flash (`deepseek-flash`) for every LLM stage, shows API cost in dollars, and lets you replace the BOM and netlist on an existing project without deleting it.
|
||||
|
||||
@@ -256,3 +256,200 @@ def test_fb_and_rn_prefixes():
|
||||
assert _classify_component("FB1", "") == ComponentType.INDUCTOR
|
||||
assert _classify_component("RN4", "") == ComponentType.RESISTOR
|
||||
assert _classify_component("F1", "") == ComponentType.FUSE
|
||||
|
||||
|
||||
def _res(ref, pins, value="4.7k", ohms=None):
|
||||
specs = None
|
||||
if ohms is not None:
|
||||
from backend.pinscopex.models import ResistorSpecs
|
||||
specs = ResistorSpecs(value_ohms=ohms, value_formatted=f"{ohms}")
|
||||
return Component(
|
||||
reference=ref, value=value, footprint="",
|
||||
component_type=ComponentType.RESISTOR, mpn=ref, pins=pins, specs=specs,
|
||||
)
|
||||
|
||||
|
||||
def _cap(ref, pins, value="100n", farads=None):
|
||||
specs = None
|
||||
if farads is not None:
|
||||
from backend.pinscopex.models import CapacitorSpecs
|
||||
specs = CapacitorSpecs(value_farads=farads, value_formatted=value)
|
||||
return Component(
|
||||
reference=ref, value=value, footprint="",
|
||||
component_type=ComponentType.CAPACITOR, mpn=ref, pins=pins, specs=specs,
|
||||
)
|
||||
|
||||
|
||||
def _cmap_i2c():
|
||||
return {
|
||||
"UTEST": ComponentConstraints(
|
||||
mpn="UTEST",
|
||||
pintable=[Pin(number=8, name="SDA")],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def test_i2c_4k7_pullup_is_in_nxp_wide_band():
|
||||
r = _res("R1", {"1": "I2C_SDA", "2": "3V3"}, value="4.7k")
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"8": "I2C_SDA"}), "R1": r},
|
||||
{
|
||||
"I2C_SDA": (NetType.SIGNAL, [("U1", "8"), ("R1", "1")]),
|
||||
"3V3": (NetType.POWER, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
assert check_i2c_pullups(g, _cmap_i2c()) == []
|
||||
|
||||
|
||||
def test_i2c_100ohm_pullup_is_too_stiff():
|
||||
r = _res("R1", {"1": "I2C_SDA", "2": "3V3"}, ohms=100)
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"8": "I2C_SDA"}), "R1": r},
|
||||
{
|
||||
"I2C_SDA": (NetType.SIGNAL, [("U1", "8"), ("R1", "1")]),
|
||||
"3V3": (NetType.POWER, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_i2c_pullups(g, _cmap_i2c())
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-I2C-002"
|
||||
assert findings[0].status == "WARNING"
|
||||
|
||||
|
||||
def test_i2c_100k_pullup_is_too_weak():
|
||||
r = _res("R1", {"1": "I2C_SDA", "2": "3V3"}, ohms=100_000)
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"8": "I2C_SDA"}), "R1": r},
|
||||
{
|
||||
"I2C_SDA": (NetType.SIGNAL, [("U1", "8"), ("R1", "1")]),
|
||||
"3V3": (NetType.POWER, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_i2c_pullups(g, _cmap_i2c())
|
||||
assert [f.rule_id for f in findings] == ["PS-I2C-002"]
|
||||
|
||||
|
||||
def test_i2c_pullup_without_value_is_not_sized():
|
||||
r = _res("R1", {"1": "I2C_SDA", "2": "3V3"}, value="")
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"8": "I2C_SDA"}), "R1": r},
|
||||
{
|
||||
"I2C_SDA": (NetType.SIGNAL, [("U1", "8"), ("R1", "1")]),
|
||||
"3V3": (NetType.POWER, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
assert check_i2c_pullups(g, _cmap_i2c()) == []
|
||||
|
||||
|
||||
def test_nrst_pulldown_is_warning():
|
||||
cons = {
|
||||
"UTEST": ComponentConstraints(
|
||||
mpn="UTEST",
|
||||
pintable=[Pin(number=4, name="NRST")],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
r = _res("R1", {"1": "/NRST", "2": "GND"}, value="10k")
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"4": "/NRST"}), "R1": r},
|
||||
{
|
||||
"/NRST": (NetType.SIGNAL, [("U1", "4"), ("R1", "1")]),
|
||||
"GND": (NetType.GROUND, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_reset_pullups(g, cons)
|
||||
assert any(f.rule_id == "PS-RST-002" for f in findings)
|
||||
|
||||
|
||||
def test_nrst_pullup_is_not_pulldown():
|
||||
cons = {
|
||||
"UTEST": ComponentConstraints(
|
||||
mpn="UTEST",
|
||||
pintable=[Pin(number=4, name="NRST")],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
r = _res("R8", {"1": "+3V3", "2": "/NRST"}, value="5k1")
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"4": "/NRST"}), "R8": r},
|
||||
{
|
||||
"/NRST": (NetType.SIGNAL, [("U1", "4"), ("R8", "2")]),
|
||||
"+3V3": (NetType.POWER, [("R8", "1")]),
|
||||
},
|
||||
)
|
||||
assert check_reset_pullups(g, cons) == []
|
||||
|
||||
|
||||
def test_ldo_vout_needs_cout():
|
||||
cons = {
|
||||
"LDOX": ComponentConstraints(
|
||||
mpn="LDOX",
|
||||
pintable=[
|
||||
Pin(number=1, name="VIN"),
|
||||
Pin(number=2, name="VOUT"),
|
||||
Pin(number=3, name="GND"),
|
||||
],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
cin = _cap("C1", {"1": "VIN", "2": "GND"}, value="1u")
|
||||
g = _graph(
|
||||
{
|
||||
"U1": Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="LDOX",
|
||||
pins={"1": "VIN", "2": "VOUT", "3": "GND"},
|
||||
),
|
||||
"C1": cin,
|
||||
},
|
||||
{
|
||||
"VIN": (NetType.POWER, [("U1", "1"), ("C1", "1")]),
|
||||
"VOUT": (NetType.POWER, [("U1", "2")]),
|
||||
"GND": (NetType.GROUND, [("U1", "3"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_supply_decoupling(g, cons)
|
||||
assert any(f.net == "VOUT" and f.rule_id == "PS-DEC-001" for f in findings)
|
||||
assert not any(f.net == "VIN" for f in findings)
|
||||
|
||||
|
||||
def test_ldo_vout_100n_only_is_value_warning():
|
||||
cons = {
|
||||
"LDOX": ComponentConstraints(
|
||||
mpn="LDOX",
|
||||
pintable=[Pin(number=2, name="VOUT")],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
cout = _cap("C2", {"1": "VOUT", "2": "GND"}, farads=100e-9)
|
||||
g = _graph(
|
||||
{
|
||||
"U1": Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="LDOX",
|
||||
pins={"2": "VOUT"},
|
||||
),
|
||||
"C2": cout,
|
||||
},
|
||||
{
|
||||
"VOUT": (NetType.POWER, [("U1", "2"), ("C2", "1")]),
|
||||
"GND": (NetType.GROUND, [("C2", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_supply_decoupling(g, cons)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-DEC-002"
|
||||
assert findings[0].status == "WARNING"
|
||||
|
||||
|
||||
def test_vdd_100n_is_not_a_value_warning():
|
||||
cap = _cap("C1", {"1": "3V3", "2": "GND"}, farads=100e-9)
|
||||
g = _graph(
|
||||
{"U1": _ic("U1", {"1": "3V3", "2": "GND"}), "C1": cap},
|
||||
{
|
||||
"3V3": (NetType.POWER, [("U1", "1"), ("C1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
assert check_supply_decoupling(g, _cmap_vdd()) == []
|
||||
|
||||
@@ -178,6 +178,52 @@ def test_spi_controller_peripheral_names_are_synonyms():
|
||||
assert normalize_functions(["SPI0_STE0"]) == {("SPI0", "NSS")}
|
||||
|
||||
|
||||
def test_simple_project_uart0_nets_are_feasible_on_mspm0_pins():
|
||||
from pathlib import Path
|
||||
|
||||
from backend.pinscopex.models import DesignGraph
|
||||
|
||||
graph = DesignGraph.model_validate_json(
|
||||
(Path(__file__).resolve().parents[1] / "simple_project" / "design_graph.json").read_text()
|
||||
)
|
||||
cmap = {
|
||||
"MSPM0G3507SPTR": _constraints(
|
||||
"MSPM0G3507SPTR",
|
||||
[
|
||||
Pin(number=1, name="PA11", functions=["UART0_TX", "SPI1_CS1"]),
|
||||
Pin(number=2, name="PA12", functions=["UART0_RX", "SPI1_CS0"]),
|
||||
],
|
||||
)
|
||||
}
|
||||
findings = check_pin_mux_feasibility(graph, cmap)
|
||||
uart = [f for f in findings if f.net and "UART0" in f.net]
|
||||
assert uart == []
|
||||
|
||||
|
||||
def test_simple_project_uart0_swapped_on_mspm0_is_error():
|
||||
from pathlib import Path
|
||||
|
||||
from backend.pinscopex.models import DesignGraph
|
||||
|
||||
graph = DesignGraph.model_validate_json(
|
||||
(Path(__file__).resolve().parents[1] / "simple_project" / "design_graph.json").read_text()
|
||||
)
|
||||
cmap = {
|
||||
"MSPM0G3507SPTR": _constraints(
|
||||
"MSPM0G3507SPTR",
|
||||
[
|
||||
Pin(number=1, name="PA11", functions=["UART0_RX"]),
|
||||
Pin(number=2, name="PA12", functions=["UART0_TX"]),
|
||||
],
|
||||
)
|
||||
}
|
||||
findings = check_pin_mux_feasibility(graph, cmap)
|
||||
nets = {f.net for f in findings}
|
||||
assert "/UART0.TX" in nets
|
||||
assert "/UART0.RX" in nets
|
||||
assert all(f.status == "ERROR" for f in findings if f.net and "UART0" in f.net)
|
||||
|
||||
|
||||
def test_spi_genuine_infeasibility_still_fires_with_modern_names():
|
||||
# Net asserts SPI0_MOSI on a pin that exposes SPI0 only as POCI (==MISO) —
|
||||
# genuinely infeasible even after synonym collapse -> ERROR.
|
||||
|
||||
Reference in New Issue
Block a user