L3 CHANNEL structure for timing budget, insertion loss, return loss, crosstalk, and complete channel. Numeric PASS/FAIL only from existing channel FACT; otherwise a visible skip. No OpenEMS, no invented S-parameters, no M6 packs.
262 lines
8.6 KiB
Python
262 lines
8.6 KiB
Python
"""M4 L2 ELECTRICAL certifier: Z from cite/datasheet/stackup only; never 90 Ω folklore."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from backend.periscopex.models import (
|
|
Component,
|
|
ComponentConstraints,
|
|
ComponentType,
|
|
DesignGraph,
|
|
LayoutGraph,
|
|
Net,
|
|
NetType,
|
|
PinConnection,
|
|
)
|
|
from backend.periscopex.protocol_catalog import (
|
|
ConstraintSource,
|
|
PhysicalInterface,
|
|
ProtocolConstraint,
|
|
)
|
|
from backend.periscopex.protocol_l0 import protocol_exam
|
|
from backend.periscopex.protocol_l2 import (
|
|
_pack,
|
|
certify_instance_l2,
|
|
certify_l2,
|
|
electrical_verdict,
|
|
intersect_windows,
|
|
)
|
|
from backend.periscopex.protocol_recognize import recognize_physical_buses
|
|
|
|
|
|
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "", value: str = "") -> Component:
|
|
return Component(
|
|
reference=ref, value=value or mpn, footprint="",
|
|
component_type=ComponentType.IC, mpn=mpn or None, pins=pins,
|
|
)
|
|
|
|
|
|
def _net(name: str, *pairs: tuple[str, str]) -> Net:
|
|
return Net(
|
|
name=name, net_type=NetType.SIGNAL,
|
|
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
|
|
)
|
|
|
|
|
|
def _usb_connected() -> DesignGraph:
|
|
return DesignGraph(
|
|
components={
|
|
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"),
|
|
"J2": Component(
|
|
reference="J2", value="USB2_TypeA",
|
|
footprint="USB_A",
|
|
component_type=ComponentType.CONNECTOR,
|
|
pins={"2": "USB_D+", "3": "USB_D-"},
|
|
),
|
|
},
|
|
nets={
|
|
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "2")),
|
|
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "3")),
|
|
},
|
|
)
|
|
|
|
|
|
def _axi_graph() -> DesignGraph:
|
|
return DesignGraph(
|
|
components={
|
|
"U3": _ic("U3", {"1": "AXI_AWVALID"}, mpn="XC7A100T", value="AXI4 interconnect"),
|
|
},
|
|
nets={"AXI_AWVALID": _net("AXI_AWVALID", ("U3", "1"))},
|
|
)
|
|
|
|
|
|
def _ddr_graph() -> DesignGraph:
|
|
pins = {str(i): f"DDR4_DQ{i}" for i in range(8)}
|
|
pins.update({"8": "DDR4_DQS0_P", "9": "DDR4_DQS0_N"})
|
|
u_mem = _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")
|
|
u_cpu = _ic("U4", {str(i): pins[str(i)] for i in pins}, mpn="STM32", value="MCU")
|
|
nets = {n: _net(n, ("U5", p), ("U4", p)) for p, n in pins.items()}
|
|
return DesignGraph(components={"U5": u_mem, "U4": u_cpu}, nets=nets)
|
|
|
|
|
|
def _cons(mpn: str, rules: list[dict]) -> ComponentConstraints:
|
|
return ComponentConstraints(
|
|
mpn=mpn, pintable=[], absolute_maximum_ratings=[], rules=[],
|
|
layout_rules=rules,
|
|
)
|
|
|
|
|
|
def _l2_for(graph: DesignGraph, logical_sub: str, **kwargs):
|
|
insts = recognize_physical_buses(graph)
|
|
by, findings = certify_l2(graph, insts, **kwargs)
|
|
rows = []
|
|
for inst in insts:
|
|
if logical_sub in inst.logical_protocol_id:
|
|
rows.extend(by.get(inst.instance_id, []))
|
|
return rows, findings, insts
|
|
|
|
|
|
def _cite() -> ConstraintSource:
|
|
return ConstraintSource(document="USB 2.0 spec", organization="USB-IF", section="7.1.1")
|
|
|
|
|
|
def test_usb_z_missing_source_never_invents_90():
|
|
zrep = {"nets": [
|
|
{"net_name": "USB_D+", "z0_avg_ohms": 45.0},
|
|
{"net_name": "USB_D-", "z0_avg_ohms": 46.0},
|
|
]}
|
|
rows, findings, _ = _l2_for(_usb_connected(), "usb2", impedance_nets=zrep)
|
|
z = [r for r in rows if r.check == "differential_impedance"]
|
|
assert z
|
|
assert z[0].result in {"MISSING_SOURCE", "UNKNOWN"}
|
|
assert z[0].result != "PASS"
|
|
blob = json.dumps(z[0].model_dump())
|
|
assert "90" not in blob
|
|
assert any(f.rule_id == "PE-PRT-L2-001" for f in findings)
|
|
assert any("non certificata a L2" in (f.finding or "") for f in findings)
|
|
|
|
|
|
def test_usb_z_skip_when_no_stackup_measurement():
|
|
rows, findings, _ = _l2_for(_usb_connected(), "usb2")
|
|
z = [r for r in rows if r.check == "differential_impedance"]
|
|
assert z
|
|
assert z[0].result in {"MISSING_SOURCE", "UNKNOWN"}
|
|
assert z[0].measured_ohm is None
|
|
assert any("mancanza di Z" in (f.finding or "") for f in findings)
|
|
|
|
|
|
def test_cited_pack_z_pass_and_fail():
|
|
graph = _usb_connected()
|
|
insts = recognize_physical_buses(graph)
|
|
usb = [i for i in insts if "usb" in i.logical_protocol_id][0]
|
|
iface = PhysicalInterface(
|
|
id="usb2-hs-dpair",
|
|
logical_protocol_id="usb2-hs",
|
|
bus_type="SERIAL",
|
|
physical_layer="SERIAL_DIFFERENTIAL",
|
|
pcb_relevant="YES",
|
|
required_checks=["differential_impedance"],
|
|
constraints=[ProtocolConstraint(
|
|
id="usb-z",
|
|
parameter="differential_impedance",
|
|
value_kind="NUMERIC",
|
|
mandatory="MANDATORY",
|
|
source_type="STANDARD",
|
|
source_class="NORMATIVE",
|
|
value=90.0,
|
|
unit="ohm",
|
|
source=_cite(),
|
|
limit_kind="STANDARD",
|
|
)],
|
|
)
|
|
zrep = {"nets": [{"net_name": "USB_D+", "zdiff_ohm": 90.0}]}
|
|
ok = certify_instance_l2(graph, usb, iface, impedance_nets=zrep)
|
|
assert ok[0].result == "PASS"
|
|
assert ok[0].measured_ohm == 90.0
|
|
bad = certify_instance_l2(
|
|
graph, usb, iface,
|
|
impedance_nets={"nets": [{"net_name": "USB_D+", "zdiff_ohm": 40.0}]},
|
|
)
|
|
assert bad[0].result == "FAIL"
|
|
|
|
|
|
def test_silicon_cross_phy_subset():
|
|
assert intersect_windows([(80.0, 100.0), (85.0, 95.0)]) == (85.0, 95.0)
|
|
graph = _usb_connected()
|
|
cmap = {
|
|
"CH340E": _cons("CH340E", [{
|
|
"kind": "impedance",
|
|
"z_min_ohm": 80,
|
|
"z_max_ohm": 100,
|
|
"note": "CH340E USB",
|
|
"source_page": 12,
|
|
}]),
|
|
}
|
|
# second PHY on the same nets
|
|
extra = _ic("U9", {"1": "USB_D+", "2": "USB_D-"}, mpn="USBPHY1")
|
|
graph.components["U9"] = extra
|
|
cmap["USBPHY1"] = _cons("USBPHY1", [{
|
|
"kind": "zdiff",
|
|
"z_min_ohm": 85,
|
|
"z_max_ohm": 95,
|
|
"note": "PHY subset",
|
|
"source_page": 3,
|
|
}])
|
|
zrep = {"nets": [{"net_name": "USB_D+", "zdiff_ohm": 90.0}]}
|
|
rows, _, _ = _l2_for(graph, "usb2", constraints_map=cmap, impedance_nets=zrep)
|
|
z = [r for r in rows if r.check == "differential_impedance"]
|
|
assert z
|
|
assert z[0].result == "PASS"
|
|
assert z[0].limit_ohm_min == 85.0
|
|
assert z[0].limit_ohm_max == 95.0
|
|
zrep_lo = {"nets": [{"net_name": "USB_D+", "zdiff_ohm": 70.0}]}
|
|
rows_lo, findings, _ = _l2_for(
|
|
graph, "usb2", constraints_map=cmap, impedance_nets=zrep_lo,
|
|
)
|
|
zlo = [r for r in rows_lo if r.check == "differential_impedance"]
|
|
assert zlo[0].result == "FAIL"
|
|
assert any(f.rule_id == "PE-PRT-L2-002" for f in findings)
|
|
|
|
|
|
def test_axi_internal_l2_not_applicable():
|
|
rows, _, _ = _l2_for(_axi_graph(), "axi4")
|
|
assert rows
|
|
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
|
sec, findings = protocol_exam(_axi_graph())
|
|
assert sec.max_level_reached == "L3"
|
|
assert sec.macrophase == "M9"
|
|
assert all(f.status != "ERROR" for f in findings)
|
|
blob = json.dumps([r.model_dump() for r in rows])
|
|
assert "L0/L1 are not electrical" in blob
|
|
|
|
|
|
def test_ddr_impedance_phy_dependent_not_invented_ohm():
|
|
rows, findings, _ = _l2_for(_ddr_graph(), "ddr4")
|
|
z = [r for r in rows if r.check == "impedance"]
|
|
assert z
|
|
assert z[0].result in {"PHY_DEPENDENT", "UNKNOWN", "VENDOR_DEPENDENT"}
|
|
assert z[0].result != "PASS"
|
|
blob = json.dumps(z[0].model_dump())
|
|
assert "40" not in blob
|
|
assert "90" not in blob
|
|
assert any(f.rule_id == "PE-PRT-L2-001" for f in findings)
|
|
|
|
|
|
def test_length_is_not_delay_timing_unknown():
|
|
cons = ProtocolConstraint(
|
|
id="t",
|
|
parameter="timing",
|
|
value_kind="NUMERIC",
|
|
mandatory="MANDATORY",
|
|
source_type="STANDARD",
|
|
source_class="NORMATIVE",
|
|
value=100.0,
|
|
unit="ps",
|
|
source=_cite(),
|
|
)
|
|
assert electrical_verdict(cons, 12.0, None) == "UNKNOWN"
|
|
|
|
|
|
def test_recommended_never_fail_l2():
|
|
rec = _pack("return_path", "FAIL", "RECOMMENDED", notes="weak")
|
|
assert rec.result != "FAIL"
|
|
assert rec.status != "ERROR"
|
|
|
|
|
|
def test_l2_does_not_run_l3_or_geometry():
|
|
rows, _, _ = _l2_for(_usb_connected(), "usb2")
|
|
assert not any(r.check in {"insertion_loss", "return_loss", "length", "intra_pair_skew"} for r in rows)
|
|
blob = json.dumps([r.model_dump() for r in rows])
|
|
assert "openems" not in blob.lower()
|
|
|
|
|
|
def test_protocol_exam_attaches_l2_checks():
|
|
sec, _ = protocol_exam(_usb_connected())
|
|
assert sec.max_level_reached == "L3"
|
|
row = sec.recognized_instances[0]
|
|
assert row.get("l2_checks")
|
|
assert all(c.get("level") == "L2" for c in row["l2_checks"])
|
|
assert row.get("l1_checks") is not None
|
|
assert row.get("l0_checks") is not None
|