Add protocol-certification M8 DDR instance grouping (2.70.0).

Controller-aware per-memory instances, DQ/DQS vs addr/cmd/ctrl/ck, HBM not classic DDR PCB DQ. No universal mm/ps. No M6 packs, no L3 solver.
This commit is contained in:
2026-09-22 12:46:01 +02:00
parent 24c4beecc9
commit 14272d7446
14 changed files with 1006 additions and 98 deletions
+170
View File
@@ -0,0 +1,170 @@
"""M8 DDR instance + L1 grouping: byte lanes, addr/cmd/ctrl/ck, controller-aware, HBM N/A."""
from __future__ import annotations
import json
from backend.periscopex.models import (
Component,
ComponentType,
DesignGraph,
Net,
NetType,
PinConnection,
)
from backend.periscopex.protocol_l0 import protocol_exam
from backend.periscopex.protocol_l1 import certify_l1
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 _ddr4_pins() -> dict[str, str]:
pins = {str(i): f"DDR4_DQ{i}" for i in range(8)}
pins.update({
"8": "DDR4_DQS0_P", "9": "DDR4_DQS0_N", "10": "DDR4_DM0",
"11": "DDR4_CK_P", "12": "DDR4_CK_N",
"13": "DDR4_A0", "14": "DDR4_RAS",
"15": "DDR4_CS", "16": "DDR4_CKE",
})
return pins
def _fpga_ddr_graph() -> DesignGraph:
pins = _ddr4_pins()
mem = _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")
fpga = _ic("U3", dict(pins), mpn="XC7A100T", value="Artix-7 DDR3/DDR4 controller")
nets = {n: _net(n, ("U5", p), ("U3", p)) for p, n in pins.items()}
return DesignGraph(components={"U5": mem, "U3": fpga}, nets=nets)
def _two_ddr_graph() -> DesignGraph:
a = _ddr4_pins()
b = {p: n.replace("DDR4_", "DDR4B_") for p, n in a.items()}
m1 = _ic("U5", a, mpn="MT41K256M16TW", value="DDR4")
m2 = _ic("U6", b, mpn="MT41K256M16TW", value="DDR4")
fpga = _ic(
"U3",
{**{f"a{p}": n for p, n in a.items()}, **{f"b{p}": n for p, n in b.items()}},
mpn="XC7A100T", value="FPGA memory controller",
)
nets = {n: _net(n, ("U5", p), ("U3", f"a{p}")) for p, n in a.items()}
nets.update({n: _net(n, ("U6", p), ("U3", f"b{p}")) for p, n in b.items()})
return DesignGraph(components={"U5": m1, "U6": m2, "U3": fpga}, nets=nets)
def _hbm_graph() -> DesignGraph:
pins = {str(i): f"HBM_DQ{i}" for i in range(8)}
pins["8"] = "HBM_DQS0_P"
return DesignGraph(
components={"U8": _ic("U8", pins, mpn="H26M64202AMR", value="HBM2e stack")},
nets={n: _net(n, ("U8", p)) for p, n in pins.items()},
)
def test_controller_aware_groups_split():
insts = recognize_physical_buses(_fpga_ddr_graph())
ddr = [i for i in insts if i.logical_protocol_id == "ddr4"]
assert len(ddr) == 1
one = ddr[0]
assert one.host_ref == "U3"
assert "U5" in one.peer_refs
kinds = {g.kind for g in one.groups}
assert "BYTE_LANE" in kinds
assert "CLOCK_GROUP" in kinds
assert "ADDRESS_GROUP" in kinds
assert "COMMAND_GROUP" in kinds
assert "CONTROL_GROUP" in kinds
data = {n for g in one.groups if g.kind == "BYTE_LANE" for n in g.nets}
addr = {n for g in one.groups if g.kind == "ADDRESS_GROUP" for n in g.nets}
cmd = {n for g in one.groups if g.kind == "COMMAND_GROUP" for n in g.nets}
ctrl = {n for g in one.groups if g.kind == "CONTROL_GROUP" for n in g.nets}
assert "DDR4_DQ0" in data and "DDR4_DQS0_P" in data
assert "DDR4_A0" in addr
assert "DDR4_RAS" in cmd
assert "DDR4_CS" in ctrl or "DDR4_CKE" in ctrl
assert not (data & addr)
assert not (data & cmd)
assert not (data & ctrl)
blob = json.dumps(one.model_dump())
assert "90" not in blob
assert "ohm" not in blob.lower()
assert "50ps" not in blob.lower()
def test_two_ddr4_are_two_instances():
insts = recognize_physical_buses(_two_ddr_graph())
ddr = [i for i in insts if i.logical_protocol_id == "ddr4"]
assert len(ddr) == 2
refs = {tuple(sorted(i.peer_refs)) for i in ddr}
assert ("U5",) in refs and ("U6",) in refs
def test_l1_grouping_pass_without_layout_skew_stays_dependent():
g = _fpga_ddr_graph()
insts = recognize_physical_buses(g)
by, findings = certify_l1(g, insts, layout=None)
rows = [r for i in insts if i.logical_protocol_id == "ddr4" for r in by[i.instance_id]]
mapping = [r for r in rows if r.check == "byte_lane_mapping"]
assert mapping and mapping[0].result == "PASS"
skew = [r for r in rows if r.check == "dq_to_dqs_skew"]
assert skew
assert skew[0].result in {"PHY_DEPENDENT", "CONTROLLER_DEPENDENT", "VENDOR_DEPENDENT", "UNKNOWN"}
assert skew[0].result != "PASS"
assert skew[0].limit_mm is None
blob = json.dumps([r.model_dump() for r in rows])
assert "90" not in blob
assert "universal" in blob.lower() or "controller" in blob.lower()
def test_hbm_not_classic_ddr_dq_rules():
insts = recognize_physical_buses(_hbm_graph())
hbm = [i for i in insts if "hbm" in i.logical_protocol_id]
ddr = [i for i in insts if i.logical_protocol_id.startswith("ddr")]
assert hbm
assert not ddr
one = hbm[0]
assert one.pcb_relevant == "NO"
assert one.nets == []
assert one.groups == []
assert one.physical_interface_id.endswith("-package")
sec, findings = protocol_exam(_hbm_graph())
assert sec.macrophase == "M8"
assert all(f.status != "ERROR" for f in findings)
rows = [
c for r in sec.recognized_instances if "hbm" in str(r.get("logical_protocol_id"))
for c in (r.get("l0_checks") or []) + (r.get("l1_checks") or [])
]
assert rows
assert all(c.get("result") == "NOT_APPLICABLE" for c in rows)
blob = json.dumps(one.model_dump())
assert "BYTE_LANE" not in blob
def test_incomplete_grouping_not_pass():
pins = {str(i): f"MEM_DAT{i}" for i in range(8)}
g = DesignGraph(
components={"U5": _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")},
nets={n: _net(n, ("U5", p)) for p, n in pins.items()},
)
insts = recognize_physical_buses(g)
ddr = [i for i in insts if "ddr" in i.logical_protocol_id]
assert ddr
assert not any(g.kind == "BYTE_LANE" for g in ddr[0].groups)
by, _ = certify_l1(g, insts, layout=None)
rows = [r for i in ddr for r in by[i.instance_id]]
mapping = [r for r in rows if r.check == "byte_lane_mapping"]
assert mapping
assert mapping[0].result != "PASS"
+2 -2
View File
@@ -130,8 +130,8 @@ def test_axi_internal_l0_not_applicable_not_fail():
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_report import PACK_MACROPHASE as L5_MACRO
assert sec.macrophase == L5_MACRO
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")
+1 -1
View File
@@ -166,7 +166,7 @@ def test_axi_internal_l1_not_applicable():
assert all(r.result != "FAIL" for r in rows)
sec, exam_findings = protocol_exam(_axi_graph(), layout=LayoutGraph())
assert sec.max_level_reached == "L2"
assert sec.macrophase == "M5"
assert sec.macrophase == "M8"
assert all(f.status != "ERROR" for f in exam_findings)
blob = json.dumps([r.model_dump() for r in rows])
assert "electrical certification" not in blob.lower() or "not electrical" in blob.lower()
+1 -1
View File
@@ -205,7 +205,7 @@ def test_axi_internal_l2_not_applicable():
assert all(r.result == "NOT_APPLICABLE" for r in rows)
sec, findings = protocol_exam(_axi_graph())
assert sec.max_level_reached == "L2"
assert sec.macrophase == "M5"
assert sec.macrophase == "M8"
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
+1 -1
View File
@@ -96,7 +96,7 @@ def test_result_rank_fail_before_warning():
def test_usb2_pair_l0_pass_l2_z_unknown_no_invented_ohm():
sec, _ = protocol_exam(_usb_connected())
assert sec.max_level_reached == "L2"
assert sec.macrophase == "M5"
assert sec.macrophase == "M8"
usb = next(
r for r in sec.recognized_instances
if "usb" in str(r.get("logical_protocol_id"))