Gate SI layout_rules to the quoted bus, not EN RC onto USB.

PE-SI-009 no longer treats ESP32 CHIP_PU/EN RC as a USB series R.
USB without a library Z number stays PE-SI-010 measurement-only.
Pintable skill 1.13.0 requires net_class on SI kinds (usb2/usb3/MDI/RGMII/DDR3).
This commit is contained in:
2026-09-20 13:13:19 +02:00
parent a63e5bd7ab
commit 312057b6d3
10 changed files with 546 additions and 82 deletions
+28 -3
View File
@@ -48,10 +48,35 @@ def test_si_refresh_when_old_extract_has_only_decoupling():
)
assert not needs_layout_rules_refresh(
{
"model_version": "1.9.0",
"layout_rules": [{"kind": "decoupling_proximity", "max_distance_mm": None}],
"model_version": "1.12.0",
"layout_rules": [{"kind": "series_resistor", "value_ohms": 10000}],
},
min_scan_version="1.10.0",
min_scan_version="1.12.0",
)
assert needs_layout_rules_refresh(
{
"model_version": "1.12.0",
"layout_rules": [{
"kind": "series_resistor",
"value_ohms": 10000,
"note": "EN RC 10 kΩ / 1 µF",
}],
},
min_scan_version="1.13.0",
)
assert not needs_layout_rules_refresh(
{
"model_version": "1.12.0",
"layout_rules": [{"kind": "impedance", "net_class": "usb2", "zdiff_ohm": 90}],
},
min_scan_version="1.13.0",
)
assert not needs_layout_rules_refresh(
{
"model_version": "1.12.0",
"layout_rules": [],
},
min_scan_version="1.13.0",
)
+208 -2
View File
@@ -69,7 +69,13 @@ def test_skip_i2c_gpio_cc_regn():
assert skip_si_net("ESP32_EN")
assert not skip_si_net("USB_D+")
assert not skip_si_net("USB_DP")
assert bus_class("USB_D+") == "usb"
assert bus_class("USB_D+") == "usb2"
assert bus_class("USB_SSTX_P") == "usb3"
assert bus_class("ETH_TX+") == "eth_mdi"
assert bus_class("RGMII_TXD0") == "rgmii"
assert bus_class("SGMII_TX_P") == "sgmii"
assert bus_class("DDR3_DQ0") == "ddr3_dq"
assert bus_class("DDR3_DQS0_P") == "ddr3_dqs"
assert bus_class("USB_CC1") is None
assert bus_class("I2C_SCL") is None
@@ -170,7 +176,10 @@ def test_usb_without_library_z_is_insufficient_not_90ohm_fail():
f = next(x for x in findings if x.rule_id == "PE-SI-010")
assert f.evidence_status == "INSUFFICIENT"
assert f.status == "INFO"
assert "90" not in (f.requirement or "") or "folklore" in (f.inference or "").lower() or True
assert "90" not in (f.requirement or "")
blob = f"{f.finding} {f.requirement} {f.inference}"
assert "FAIL" not in f.finding
assert "90 Ω" not in blob or "folklore" in (f.inference or "").lower()
assert "CC" not in (f.net or "")
@@ -215,3 +224,200 @@ def test_i2c_not_checked_as_50_ohm():
])
assert all("I2C" not in (f.net or "") for f in findings)
assert all("SDA" not in f.finding for f in findings)
def test_en_rc_series_resistor_does_not_paint_usb():
"""PE-SI-009 must not use ESP32 EN RC 10 kΩ / 1 µF as a USB series R."""
cons = ComponentConstraints(
mpn="ESP32",
pintable=[Pin(number="1", name="D+"), Pin(number="3", name="EN")],
absolute_maximum_ratings=[],
rules=[],
layout_rules=[{
"kind": "series_resistor",
"pin": "EN",
"value_ohms": 10000,
"note": "EN RC 10 kΩ / 1 µF",
"source_page": 28,
}],
)
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
si009 = [f for f in findings if f.rule_id == "PE-SI-009"]
assert si009 == []
zfail = [f for f in findings if f.rule_id == "PE-SI-002"]
assert zfail == []
si010 = [f for f in findings if f.rule_id == "PE-SI-010"]
assert si010
assert si010[0].evidence_status == "INSUFFICIENT"
assert "88" in (si010[0].finding or "")
assert "90" not in (si010[0].requirement or "")
def _phy_graph() -> DesignGraph:
pins = {
"1": "ETH_TX+", "2": "ETH_TX-",
"3": "RGMII_TXD0", "4": "RGMII_TXD1",
"5": "USB_SSTX_P", "6": "USB_SSTX_N",
"7": "DDR3_DQ0", "8": "DDR3_DQ1",
"9": "DDR3_DQS0_P", "10": "DDR3_DQS0_N",
"11": "USB_D+", "12": "USB_D-",
}
nets = {
n: Net(name=n, net_type=NetType.SIGNAL, pins=[
PinConnection(component_ref="U1", pin_number=p),
])
for p, n in pins.items()
}
return DesignGraph(
components={
"U1": Component(
reference="U1", value="PHY", footprint="",
component_type=ComponentType.IC, mpn="PHY",
pins=pins,
),
},
nets=nets,
)
def _phy_layout() -> LayoutGraph:
segs = [
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="ETH_TX+"),
LayoutSegment(start=(0, 0.4), end=(10.2, 0.4), width=0.2, layer="F.Cu", net="ETH_TX-"),
LayoutSegment(start=(0, 2), end=(8, 2), width=0.15, layer="F.Cu", net="RGMII_TXD0"),
LayoutSegment(start=(0, 2.4), end=(8.4, 2.4), width=0.15, layer="F.Cu", net="RGMII_TXD1"),
LayoutSegment(start=(0, 4), end=(20, 4), width=0.12, layer="F.Cu", net="USB_SSTX_P"),
LayoutSegment(start=(0, 4.3), end=(20.5, 4.3), width=0.12, layer="F.Cu", net="USB_SSTX_N"),
LayoutSegment(start=(0, 6), end=(15, 6), width=0.1, layer="F.Cu", net="DDR3_DQ0"),
LayoutSegment(start=(0, 6.2), end=(15.1, 6.2), width=0.1, layer="F.Cu", net="DDR3_DQ1"),
LayoutSegment(start=(0, 7), end=(12, 7), width=0.1, layer="F.Cu", net="DDR3_DQS0_P"),
LayoutSegment(start=(0, 7.2), end=(12.3, 7.2), width=0.1, layer="F.Cu", net="DDR3_DQS0_N"),
LayoutSegment(start=(0, 9), end=(10, 9), width=0.2, layer="F.Cu", net="USB_D+"),
LayoutSegment(start=(0, 9.4), end=(12.5, 9.4), width=0.2, layer="F.Cu", net="USB_D-"),
]
return LayoutGraph(segments=segs, vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)])
def _z(name, partner, avg, length, **kw):
row = {
"net_name": name,
"partner_net_name": partner,
"is_differential": True,
"z0_avg_ohms": avg,
"z0_min_ohms": avg - 2,
"z0_max_ohms": avg + 2,
"length_mm": length,
"topologies": ["MICROSTRIP"],
"flags": (),
}
row.update(kw)
return row
def test_coverage_mdi_rgmii_ddr3_usb3_gated_not_cross_mapped():
cons = ComponentConstraints(
mpn="PHY",
pintable=[Pin(number="1", name="TX+")],
absolute_maximum_ratings=[],
rules=[],
layout_rules=[
{
"kind": "impedance",
"net_class": "eth_mdi",
"zdiff_ohm": 100,
"tolerance_pct": 10,
"note": "MDI 100 Ω to RJ45",
"source_page": 40,
},
{
"kind": "max_length",
"net_class": "rgmii",
"max_distance_mm": 20,
"note": "RGMII MACPHY trace < 20 mm",
"source_page": 41,
},
{
"kind": "length_match",
"net_class": "ddr3",
"max_distance_mm": 1.0,
"note": "DDR3 DQS intra-pair",
"source_page": 42,
},
{
"kind": "impedance",
"net_class": "usb3",
"zdiff_ohm": 90,
"tolerance_pct": 10,
"note": "USB-C SuperSpeed 90 Ω",
"source_page": 43,
},
{
"kind": "series_resistor",
"net_class": "rgmii",
"value_ohms": 22,
"note": "RGMII series 22 Ω",
"source_page": 41,
},
],
)
rows = [
_z("ETH_TX+", "ETH_TX-", 95.0, 10.0),
_z("ETH_TX-", "ETH_TX+", 95.0, 10.2),
_z("USB_SSTX_P", "USB_SSTX_N", 88.0, 20.0),
_z("USB_SSTX_N", "USB_SSTX_P", 88.0, 20.5),
_z("DDR3_DQS0_P", "DDR3_DQS0_N", 50.0, 12.0),
_z("DDR3_DQS0_N", "DDR3_DQS0_P", 50.0, 12.3),
_z("USB_D+", "USB_D-", 88.0, 10.0),
_z("USB_D-", "USB_D+", 88.0, 12.5),
{"net_name": "RGMII_TXD0", "z0_avg_ohms": 48.0, "length_mm": 8.0, "topologies": ["MICROSTRIP"]},
{"net_name": "RGMII_TXD1", "z0_avg_ohms": 48.0, "length_mm": 8.4, "topologies": ["MICROSTRIP"]},
{"net_name": "DDR3_DQ0", "z0_avg_ohms": 50.0, "length_mm": 15.0, "topologies": ["MICROSTRIP"]},
]
findings = check_si(_phy_graph(), {"PHY": cons}, _phy_layout(), rows)
by = {}
for f in findings:
by.setdefault(f.rule_id, []).append(f)
mdi_z = [f for f in by.get("PE-SI-002", []) if f.net and "ETH" in f.net]
assert mdi_z and mdi_z[0].finding.startswith("PASS:")
usb3_z = [f for f in by.get("PE-SI-002", []) if f.net and "SSTX" in f.net]
assert usb3_z and usb3_z[0].finding.startswith("PASS:")
usb2_z = [f for f in by.get("PE-SI-002", []) if f.net and "USB_D" in (f.net or "")]
assert usb2_z == []
rgmii_len = [f for f in by.get("PE-SI-003", []) if f.net and "RGMII" in f.net]
assert rgmii_len and rgmii_len[0].finding.startswith("PASS:")
assert not any(f.net and "ETH_TX" in f.net for f in by.get("PE-SI-003", []))
dqs = [f for f in by.get("PE-SI-001", []) if f.net and "DQS" in f.net]
assert dqs and dqs[0].finding.startswith("PASS:")
si009 = by.get("PE-SI-009", [])
assert si009
assert all(f.net and "RGMII" in f.net for f in si009)
assert not any(f.net and "USB" in f.net for f in si009)
si010 = by.get("PE-SI-010", [])
nets010 = {f.net for f in si010}
assert any(n and "USB_D" in n for n in nets010)
assert not any(n and "ETH_TX" in n for n in nets010)
assert not any(n and "SSTX" in n for n in nets010)
def test_empty_net_class_impedance_does_not_paint_usb():
cons = ComponentConstraints(
mpn="PHY",
pintable=[Pin(number="1", name="D+")],
absolute_maximum_ratings=[],
rules=[],
layout_rules=[{
"kind": "impedance",
"z0_ohm": 50,
"tolerance_pct": 10,
"note": "generic 50 Ω with no bus",
"source_page": 1,
}],
)
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
assert all(f.rule_id != "PE-SI-002" for f in findings)
assert any(f.rule_id == "PE-SI-010" for f in findings)