Phase A: stub, return-split, and BOM termination on buses present on the graph; DDR/CPU/FPGA class certifiers only when that device exists. Skip without evidence. Via, pad, track, and zone stay distinct. Not DRC or FEM.
429 lines
15 KiB
Python
429 lines
15 KiB
Python
"""G1 SI vs simple_project — no invented millimetres or USBPHY boards.
|
||
|
||
Favor: real /USB.D+ and /USB.D- pair by suffix; eval has no PE-SI keys.
|
||
Against: no .kicad_pcb → no PE-SI-001; 3W is not invented.
|
||
I2C/GPIO/CC are not 50/90 Ω. ImpedenceFinder numbers vs layout_rules only.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from tests.paths import SIMPLE_PROJECT, TAXONOMY
|
||
|
||
from pathlib import Path
|
||
|
||
from backend.periscopex.eval_report import eval_simple_project
|
||
from backend.periscopex.finding_engine import complete_finding
|
||
from backend.periscopex.models import (
|
||
Component,
|
||
ComponentConstraints,
|
||
ComponentType,
|
||
DesignGraph,
|
||
LayoutGraph,
|
||
LayoutSegment,
|
||
LayoutVia,
|
||
Net,
|
||
NetType,
|
||
Pin,
|
||
PinConnection,
|
||
)
|
||
from backend.periscopex.si_check import bus_class, check_si, partner_net, skip_si_net
|
||
|
||
SIMPLE = SIMPLE_PROJECT
|
||
|
||
|
||
def _graph() -> DesignGraph:
|
||
return DesignGraph.model_validate_json(
|
||
(SIMPLE / "design_graph.json").read_text()
|
||
)
|
||
|
||
|
||
def test_simple_project_usb_dp_dm_are_a_named_pair():
|
||
g = _graph()
|
||
assert "/USB.D+" in g.nets
|
||
assert "/USB.D-" in g.nets
|
||
assert partner_net("/USB.D+") == "/USB.D-"
|
||
assert partner_net("/USB.D-") == "/USB.D+"
|
||
assert partner_net("/USBC.D+") == "/USBC.D-"
|
||
|
||
|
||
def test_simple_project_without_pcb_has_no_ps_si_001():
|
||
findings = check_si(_graph(), {}, None)
|
||
assert findings == []
|
||
assert all(f.rule_id != "PE-3W-001" for f in findings)
|
||
|
||
|
||
def test_simple_project_eval_has_no_si_keys():
|
||
scores = eval_simple_project(SIMPLE)
|
||
assert scores.finding_count == 8
|
||
assert scores.precision == 1.0
|
||
assert scores.recall == 1.0
|
||
assert not any(
|
||
k.startswith("PE-SI-") or k.startswith("PE-3W-")
|
||
for k in scores.extra_keys
|
||
)
|
||
|
||
|
||
def test_skip_i2c_gpio_cc_regn():
|
||
assert skip_si_net("I2C_SDA")
|
||
assert skip_si_net("GPIO9")
|
||
assert skip_si_net("USB_CC1")
|
||
assert skip_si_net("/power/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+") == "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("MIPI_D0_P") == "mipi"
|
||
assert bus_class("GTX_CLK") == "hf_clk"
|
||
assert bus_class("XTAL_IN") is None
|
||
assert bus_class("USB_CC1") is None
|
||
assert bus_class("I2C_SCL") is None
|
||
|
||
|
||
def _usb_graph() -> DesignGraph:
|
||
return DesignGraph(
|
||
components={
|
||
"U1": Component(
|
||
reference="U1", value="PHY", footprint="",
|
||
component_type=ComponentType.IC, mpn="PHY",
|
||
pins={"1": "USB_D+", "2": "USB_D-", "3": "USB_CC1", "4": "I2C_SDA"},
|
||
),
|
||
},
|
||
nets={
|
||
"USB_D+": Net(name="USB_D+", net_type=NetType.SIGNAL, pins=[
|
||
PinConnection(component_ref="U1", pin_number="1"),
|
||
]),
|
||
"USB_D-": Net(name="USB_D-", net_type=NetType.SIGNAL, pins=[
|
||
PinConnection(component_ref="U1", pin_number="2"),
|
||
]),
|
||
"USB_CC1": Net(name="USB_CC1", net_type=NetType.SIGNAL, pins=[]),
|
||
"I2C_SDA": Net(name="I2C_SDA", net_type=NetType.SIGNAL, pins=[]),
|
||
},
|
||
)
|
||
|
||
|
||
def _usb_layout() -> LayoutGraph:
|
||
return LayoutGraph(
|
||
segments=[
|
||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||
LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
||
LayoutSegment(start=(0, 5), end=(8, 5), width=0.2, layer="F.Cu", net="USB_CC1"),
|
||
LayoutSegment(start=(0, 8), end=(20, 8), width=0.2, layer="F.Cu", net="I2C_SDA"),
|
||
],
|
||
vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)],
|
||
)
|
||
|
||
|
||
def _if_rows(**kwargs):
|
||
base = {
|
||
"net_name": "USB_D+",
|
||
"length_mm": 10.0,
|
||
"partner_net_name": "USB_D-",
|
||
"is_differential": True,
|
||
"z0_avg_ohms": 88.0,
|
||
"z0_min_ohms": 46.0,
|
||
"z0_max_ohms": 92.0,
|
||
"topologies": ["MICROSTRIP"],
|
||
"flags": (),
|
||
}
|
||
base.update(kwargs)
|
||
dm = {
|
||
"net_name": "USB_D-",
|
||
"length_mm": 12.5,
|
||
"partner_net_name": "USB_D+",
|
||
"is_differential": True,
|
||
"z0_avg_ohms": 88.0,
|
||
"z0_min_ohms": 46.0,
|
||
"z0_max_ohms": 92.0,
|
||
"topologies": ["MICROSTRIP"],
|
||
"flags": (),
|
||
}
|
||
return [base, dm]
|
||
|
||
|
||
def test_emmaforo_usb_avg_in_window_min_out_is_margin():
|
||
"""Zavg 88 Ω in 81–99, min 46 out → MARGIN; CC is not a 90 Ω pair."""
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="D+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "impedance",
|
||
"net_class": "usb",
|
||
"zdiff_ohm": 90,
|
||
"tolerance_pct": 10,
|
||
"note": "USB DP/DM 90 Ω ±10%",
|
||
"source_page": 12,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||
zf = [f for f in findings if f.rule_id == "PE-SI-002"]
|
||
assert len(zf) == 1
|
||
assert zf[0].finding.startswith("MARGIN:")
|
||
complete_finding(zf[0])
|
||
assert zf[0].status == "WARNING"
|
||
assert zf[0].finding_class != "RULE"
|
||
assert not any("CC" in (f.net or "") for f in findings)
|
||
assert not any("I2C" in (f.net or "") for f in findings)
|
||
|
||
|
||
def test_usb_without_library_z_is_insufficient_not_90ohm_fail():
|
||
findings = check_si(_usb_graph(), {}, _usb_layout(), _if_rows())
|
||
ids = {f.rule_id for f in findings}
|
||
assert "PE-SI-010" in ids
|
||
assert "PE-SI-002" not in ids
|
||
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 "")
|
||
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 "")
|
||
|
||
|
||
def test_length_match_2_5mm_vs_1mm_is_fail():
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="D+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "length_match",
|
||
"net_class": "usb",
|
||
"max_distance_mm": 1.0,
|
||
"note": "intra-pair < 1 mm",
|
||
"source_page": 12,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||
sk = [f for f in findings if f.rule_id == "PE-SI-001"]
|
||
assert sk and sk[0].finding.startswith("FAIL:")
|
||
complete_finding(sk[0])
|
||
assert sk[0].status == "ERROR"
|
||
|
||
|
||
def test_i2c_not_checked_as_50_ohm():
|
||
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": "should not hit I2C",
|
||
"source_page": 1,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), [
|
||
*_if_rows(),
|
||
{"net_name": "I2C_SDA", "z0_avg_ohms": 120.0, "length_mm": 20.0},
|
||
])
|
||
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 MAC–PHY 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)
|