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:
2026-09-10 22:07:14 +02:00
co-authored by Cursor
parent e4fd6c3849
commit 4fca789517
4 changed files with 486 additions and 31 deletions
+197
View File
@@ -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()) == []
+46
View File
@@ -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.