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:
@@ -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
@@ -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 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 "") 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)
|
||||
|
||||
Reference in New Issue
Block a user