Files
periscope/tests/pcb/test_protocol_l0_m2.py
T
michele 0848eb8416 Add protocol-certification M4 L2 electrical certifier (2.68.0).
Z/termination/levels from datasheet, stackup, fab, or cited pack only. Never invented USB ohms. No L3/OpenEMS. L0/L1 are not electrical.
2026-09-22 12:30:13 +02:00

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 == "L2"
from backend.periscopex.protocol_l2 import PACK_MACROPHASE as L2_MACRO
assert sec.macrophase == L2_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)