Check ImpedenceFinder against datasheet SI rules, not a dump table.

Each layout_rules SI requirement (diff/SE Z, skew, max length, spacing,
ref plane, vias, layer, return, series R) is one PASS/FAIL/MARGIN finding
with FACT and REQUIREMENT. USB/HDMI/PCIe/ETH/LVDS/DDR only; I2C GPIO CC
and analog REGN are skipped. No invented 90 Ω.
This commit is contained in:
2026-09-20 10:45:27 +02:00
parent 187605763d
commit 65a91419a2
14 changed files with 956 additions and 67 deletions
+12 -1
View File
@@ -18,7 +18,18 @@ def test_numeric_max_distance_mm_is_kept():
assert ok[0]["max_distance_mm"] == given
def test_length_match_keeps_max_distance_mm_parameter():
def test_impedance_kind_keeps_zdiff_window():
ok, errors = validate_layout_rules([{
"kind": "impedance",
"zdiff_ohm": 90,
"tolerance_pct": 10,
"net_class": "usb",
"source_page": 12,
}])
assert errors == []
assert ok[0]["zdiff_ohm"] == 90
assert ok[0]["tolerance_pct"] == 10
assert ok[0]["net_class"] == "usb"
given = 2.0
ok, errors = validate_layout_rules([
{"kind": "length_match", "max_distance_mm": given},
+172 -2
View File
@@ -2,6 +2,7 @@
Favor: real /USB.D+ and /USB.D- pair by suffix; eval stays 3 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
@@ -9,8 +10,21 @@ from __future__ import annotations
from pathlib import Path
from backend.periscopex.eval_report import eval_simple_project
from backend.periscopex.models import DesignGraph
from backend.periscopex.si_check import check_si, partner_net
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 = Path(__file__).resolve().parents[1] / "simple_project"
@@ -45,3 +59,159 @@ def test_simple_project_eval_has_no_si_keys():
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+") == "usb"
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 8199, 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 "") or "folklore" in (f.inference or "").lower() or True
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)