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.
156 lines
4.9 KiB
Python
156 lines
4.9 KiB
Python
"""M2 L0 structural certifier: presence/connection; AXI N/A; RECOMMENDED not FAIL."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from backend.periscopex.models import (
|
|
Component,
|
|
ComponentType,
|
|
DesignGraph,
|
|
Net,
|
|
NetType,
|
|
PinConnection,
|
|
)
|
|
from backend.periscopex.protocol_l0 import (
|
|
_pack,
|
|
certify_l0,
|
|
protocol_exam,
|
|
)
|
|
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 _usb_dangling_minus() -> DesignGraph:
|
|
return DesignGraph(
|
|
components={
|
|
"U1": _ic("U1", {"1": "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-", ("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_one_ended() -> DesignGraph:
|
|
pins = {str(i): f"DDR4_DQ{i}" for i in range(8)}
|
|
pins.update({"8": "DDR4_DQS0_P", "9": "DDR4_DQS0_N"})
|
|
return DesignGraph(
|
|
components={"U5": _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")},
|
|
nets={n: _net(n, ("U5", p)) for p, n in pins.items()},
|
|
)
|
|
|
|
|
|
def _l0_for(graph: DesignGraph, logical_sub: str) -> list:
|
|
insts = recognize_physical_buses(graph)
|
|
by, _ = certify_l0(graph, insts)
|
|
rows = []
|
|
for inst in insts:
|
|
if logical_sub in inst.logical_protocol_id:
|
|
rows.extend(by.get(inst.instance_id, []))
|
|
return rows
|
|
|
|
|
|
def test_usb_connected_pair_l0_topology_pass():
|
|
rows = _l0_for(_usb_connected(), "usb2")
|
|
topo = [r for r in rows if r.check == "topology"]
|
|
assert topo
|
|
assert topo[0].result == "PASS"
|
|
assert topo[0].level == "L0"
|
|
blob = json.dumps(topo[0].model_dump())
|
|
assert "ohm" not in blob.lower()
|
|
assert "90" not in blob
|
|
|
|
|
|
def test_usb_unconnected_dminus_l0_fail_mandatory():
|
|
g = _usb_dangling_minus()
|
|
insts = recognize_physical_buses(g)
|
|
usb = [i for i in insts if "usb" in i.logical_protocol_id]
|
|
assert usb
|
|
by, findings = certify_l0(g, insts)
|
|
rows = [r for i in usb for r in by[i.instance_id]]
|
|
topo = [r for r in rows if r.check == "topology"]
|
|
assert topo
|
|
assert topo[0].result == "FAIL"
|
|
assert topo[0].mandatory == "MANDATORY"
|
|
assert any(f.status == "ERROR" and f.rule_id == "PE-PRT-L0-001" for f in findings)
|
|
|
|
|
|
def test_axi_internal_l0_not_applicable_not_fail():
|
|
rows = _l0_for(_axi_graph(), "axi4")
|
|
assert rows
|
|
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
|
assert all(r.result != "FAIL" for r in rows)
|
|
sec, findings = protocol_exam(_axi_graph())
|
|
assert sec.max_level_reached == "L3"
|
|
from backend.periscopex.protocol_report import PACK_MACROPHASE as L8_MACRO
|
|
assert sec.macrophase == L8_MACRO
|
|
assert all(f.status != "ERROR" for f in findings)
|
|
assert all("AXI_AWVALID" not in (f.net or "") for f in findings if f.rule_id == "PE-PRT-L0-001")
|
|
|
|
|
|
def test_ddr_unconnected_memory_fails_l0_connection():
|
|
rows = _l0_for(_ddr_one_ended(), "ddr4")
|
|
mapping = [r for r in rows if r.check == "byte_lane_mapping"]
|
|
assert mapping
|
|
assert mapping[0].result == "FAIL"
|
|
assert "unconnected" in mapping[0].notes
|
|
|
|
|
|
def test_recommended_never_parses_as_fail():
|
|
rec = _pack("return_path", "FAIL", "RECOMMENDED", notes="missing")
|
|
assert rec.result != "FAIL"
|
|
assert rec.status != "ERROR"
|
|
|
|
|
|
def test_l0_does_not_run_impedance():
|
|
rows = _l0_for(_usb_connected(), "usb2")
|
|
assert not any(r.check in {"differential_impedance", "intra_pair_skew", "length"} for r in rows)
|