diff --git a/periscope/src/backend/periscopex/protocol_catalog.py b/periscope/src/backend/periscopex/protocol_catalog.py index 627c62c..f83b177 100644 --- a/periscope/src/backend/periscopex/protocol_catalog.py +++ b/periscope/src/backend/periscopex/protocol_catalog.py @@ -377,6 +377,7 @@ DDR_CHECKS = [ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -574,7 +575,7 @@ def build_m0_catalog() -> dict[str, Any]: cls: SourceClass = "VENDOR" if source_type != "STANDARD" else "NORMATIVE" params = [ "impedance", "max_length", "dq_to_dqs_skew", "byte_to_byte_skew", - "topology", "termination", + "topology", "termination", "ck_to_command_skew", "ck_to_address_skew", ] return [ _c(pid, p, kind, source_type=source_type, source_class=cls) diff --git a/periscope/src/backend/periscopex/protocol_data/catalog.json b/periscope/src/backend/periscopex/protocol_data/catalog.json index 5dacb4b..255e6b5 100644 --- a/periscope/src/backend/periscopex/protocol_data/catalog.json +++ b/periscope/src/backend/periscopex/protocol_data/catalog.json @@ -1041,6 +1041,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1132,6 +1133,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr-family-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr-family-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "No universal mm/ps for DDR." @@ -1154,6 +1183,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1245,6 +1275,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr1-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr1-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "Controller/PHY dependent. Zero universal skew/length." @@ -1267,6 +1325,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1358,6 +1417,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr2-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr2-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "Controller/PHY dependent. Zero universal skew/length." @@ -1380,6 +1467,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1471,6 +1559,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr3-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr3-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "Controller/PHY dependent. Zero universal skew/length." @@ -1493,6 +1609,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1584,6 +1701,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr3l-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr3l-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "Controller/PHY dependent. Zero universal skew/length." @@ -1606,6 +1751,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1697,6 +1843,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr4-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr4-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "Controller/PHY dependent. Zero universal skew/length." @@ -1719,6 +1893,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1810,6 +1985,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "ddr5-sdram-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "ddr5-sdram-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "Controller/PHY dependent. Zero universal skew/length." @@ -1833,6 +2036,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -1924,6 +2128,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr-family-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr-family-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" @@ -1948,6 +2180,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -2039,6 +2272,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr2-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr2-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" @@ -2063,6 +2324,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -2154,6 +2416,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr3-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr3-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "CONTROLLER_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "CONTROLLER", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" @@ -2178,6 +2468,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -2269,6 +2560,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr4-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr4-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" @@ -2293,6 +2612,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -2384,6 +2704,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr4x-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr4x-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" @@ -2408,6 +2756,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -2499,6 +2848,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr5-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr5-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" @@ -2523,6 +2900,7 @@ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "topology", "impedance", "termination", @@ -2614,6 +2992,34 @@ "conditions": [], "value_origin": "SPEC", "typical": false + }, + { + "id": "lpddr5x-pcb-ck_to_command_skew", + "parameter": "ck_to_command_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false + }, + { + "id": "lpddr5x-pcb-ck_to_address_skew", + "parameter": "ck_to_address_skew", + "value_kind": "PHY_DEPENDENT", + "mandatory": "MANDATORY", + "source_type": "PHY", + "source_class": "VENDOR", + "value": null, + "unit": null, + "source": null, + "conditions": [], + "value_origin": "SPEC", + "typical": false } ], "notes": "" diff --git a/periscope/src/backend/periscopex/protocol_ddr.py b/periscope/src/backend/periscopex/protocol_ddr.py new file mode 100644 index 0000000..32e9e0a --- /dev/null +++ b/periscope/src/backend/periscopex/protocol_ddr.py @@ -0,0 +1,319 @@ +"""M8: DDR physical instance grouping. Controller-aware. No universal mm/ps/ohm. + +DQ/DQS byte lanes are separate from ADDRESS / COMMAND / CONTROL / CLOCK. +HBM is package/interposer — not classic DDR PCB DQ rules. +""" + +from __future__ import annotations + +import re + +from backend.periscopex.models import Component, ComponentType, DesignGraph +from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net +from backend.periscopex.protocol_recognize import ( + EvidenceKind, + PhysicalBusInstance, + SignalGroup, + _blob, + _EVIDENCE_SCORE, +) + +PACK_MACROPHASE = "M8" + +_DQ_RE = re.compile(r"(?:^|[_/.])DQ(\d+)(?:$|[_/.])", re.I) +_DQS_RE = re.compile(r"(?:^|[_/.])DQS(\d*)(?:[_#]?(P|N|PINS|PLUS|MINUS))?(?:$|[_/.])", re.I) +_DM_RE = re.compile(r"(?:^|[_/.])(?:DM|DBI)(\d+)(?:$|[_/.])", re.I) +_CK_RE = re.compile(r"(?:^|[_/.])(?:CK)(?:[_#]?(P|N))?(?:$|[_/.])", re.I) +_WCK_RE = re.compile(r"(?:^|[_/.])WCK", re.I) +_ADDR_RE = re.compile(r"(?:^|[_/.])(?:A|ADDR|BA|BG)(\d+)(?:$|[_/.])", re.I) +_CA_RE = re.compile(r"(?:^|[_/.])CA(\d+)(?:$|[_/.])", re.I) +_CMD_RE = re.compile(r"(?:^|[_/.])(?:RAS|CAS|WE|ACT)(?:[#_N]*)(?:$|[_/.])", re.I) +_CTRL_RE = re.compile( + r"(?:^|[_/.])(?:CS|CKE|ODT|RESET|ZQ)(?:[#_N]*)(?:$|[_/.])", re.I, +) +_HBM_RE = re.compile( + r"\bHBM(?:2E|3E|[23])?\b|H26M|MT54A|interposer", + re.I, +) +_DDR_GEN = [ + ("ddr5", re.compile(r"\bDDR5\b", re.I)), + ("ddr4", re.compile(r"\bDDR4\b|MT41|K4A|IS43TR", re.I)), + ("ddr3l", re.compile(r"\bDDR3L\b", re.I)), + ("ddr3", re.compile(r"\bDDR3\b|K4B", re.I)), + ("ddr2", re.compile(r"\bDDR2\b|MT47", re.I)), + ("ddr1", re.compile(r"\bDDR1\b", re.I)), +] +_LPDDR_GEN = [ + ("lpddr5x", re.compile(r"LPDDR5X", re.I)), + ("lpddr5", re.compile(r"LPDDR5", re.I)), + ("lpddr4x", re.compile(r"LPDDR4X", re.I)), + ("lpddr4", re.compile(r"LPDDR4", re.I)), + ("lpddr3", re.compile(r"LPDDR3", re.I)), + ("lpddr2", re.compile(r"LPDDR2", re.I)), + ("lpddr-family", re.compile(r"\bLPDDR\b", re.I)), +] +_DDR4_PART = re.compile(r"MT41K|K4A|IS43TR|W631|EDY4016", re.I) +_MEMORY_PART = re.compile(r"MT41|MT47|K4A|K4B|IS43TR|W631|EDY4016", re.I) +_CONTROLLER_HINT = re.compile( + r"FPGA|XC7|XCKU|Artix|Kintex|Zynq|STM32|i\.?MX|RK33|controller|MIG|\bSoC\b", + re.I, +) + + +def _leaf(name: str) -> str: + n = normalize_kicad_hierarchy_net(name) + return n.split("/")[-1] if n else "" + + +def is_hbm_blob(text: str) -> bool: + return bool(_HBM_RE.search(text or "")) + + +def ddr_gen_from_text(text: str) -> str | None: + if is_hbm_blob(text): + return None + for gen, cre in _LPDDR_GEN: + if cre.search(text): + return gen + if _DDR4_PART.search(text): + return "ddr4" + for gen, cre in _DDR_GEN: + if cre.search(text): + return gen + return None + + +def hbm_gen_from_text(text: str) -> str | None: + if not is_hbm_blob(text): + return None + t = re.sub(r"[\s_\-]", "", text.upper()) + for gen in ("HBM3E", "HBM2E", "HBM3", "HBM2", "HBM"): + if gen in t: + return gen.lower() + return "hbm-family" + + +def byte_lanes(net_names: list[str]) -> list[SignalGroup]: + dq: dict[int, str] = {} + dqs: dict[int, dict[str, str]] = {} + dm: dict[int, str] = {} + for n in net_names: + leaf = _leaf(n) + m = _DQ_RE.search(leaf) + if m and "DQS" not in leaf.upper(): + dq[int(m.group(1))] = n + continue + m = _DQS_RE.search(leaf) + if m: + idx = int(m.group(1) or "0") + pol = (m.group(2) or "P").upper()[:1] + dqs.setdefault(idx, {})[pol] = n + continue + m = _DM_RE.search(leaf) + if m: + dm[int(m.group(1))] = n + lanes: list[SignalGroup] = [] + lane_ids = set(i // 8 for i in dq) | set(dqs) | set(dm) + for lane in sorted(lane_ids): + roles: dict[str, str] = {} + nets: list[str] = [] + for bit in range(lane * 8, lane * 8 + 8): + if bit in dq: + roles[f"DQ{bit}"] = dq[bit] + nets.append(dq[bit]) + if lane in dqs: + for pol, net in dqs[lane].items(): + roles[f"DQS{lane}_{pol}"] = net + nets.append(net) + if lane in dm: + roles[f"DM{lane}"] = dm[lane] + nets.append(dm[lane]) + if nets: + lanes.append(SignalGroup( + kind="BYTE_LANE", id=f"BYTE_LANE_{lane}", nets=nets, roles=roles, + )) + return lanes + + +def memory_groups(net_names: list[str]) -> list[SignalGroup]: + """DQ/DQS lanes vs ADDRESS/COMMAND/CONTROL vs CLOCK. No invented numbers.""" + groups: list[SignalGroup] = list(byte_lanes(net_names)) + data_nets = {n for g in groups for n in g.nets} + + def _take(kind: str, gid: str, pred) -> None: + nets = [n for n in net_names if pred(_leaf(n)) and n not in data_nets] + if nets: + groups.append(SignalGroup(kind=kind, id=gid, nets=nets, roles={})) # type: ignore[arg-type] + + _take("CLOCK_GROUP", "CK", lambda lf: _CK_RE.search(lf) and not _WCK_RE.search(lf)) + _take("CLOCK_GROUP", "WCK", lambda lf: bool(_WCK_RE.search(lf))) + _take("ADDRESS_GROUP", "ADDRESS", lambda lf: bool(_ADDR_RE.search(lf)) and not _CA_RE.search(lf)) + _take("ADDRESS_GROUP", "CA", lambda lf: bool(_CA_RE.search(lf))) + _take("COMMAND_GROUP", "COMMAND", lambda lf: bool(_CMD_RE.search(lf))) + _take("CONTROL_GROUP", "CONTROL", lambda lf: bool(_CTRL_RE.search(lf))) + return groups + + +def data_vs_addr_overlap(groups: list[SignalGroup]) -> list[str]: + data = {n for g in groups if g.kind == "BYTE_LANE" for n in g.nets} + other = { + n for g in groups + if g.kind in {"ADDRESS_GROUP", "COMMAND_GROUP", "CONTROL_GROUP", "CLOCK_GROUP"} + for n in g.nets + } + return sorted(data & other) + + +def find_controller(graph: DesignGraph, memory: Component, nets: list[str]) -> Component | None: + netset = set(nets) + for comp in graph.components.values(): + if comp.reference == memory.reference: + continue + if comp.component_type != ComponentType.IC: + continue + blob = _blob(comp, graph) + if is_hbm_blob(blob): + continue + if ddr_gen_from_text(blob) and _MEMORY_PART.search(blob): + continue + pins = set(comp.pins.values()) + if pins & netset: + return comp + return None + + +def phys_id_for_gen(gen: str, ifaces: dict) -> str: + if gen.startswith("lpddr"): + pid = f"{gen}-pcb" + return pid if pid in ifaces else "lpddr-family-pcb" + if gen == "ddr-family": + return "ddr-family-sdram-pcb" + pid = f"{gen}-sdram-pcb" + return pid if pid in ifaces else "ddr-family-sdram-pcb" + + +def build_ddr_instance( + graph: DesignGraph, + memory: Component, + gen: str, + evidence: EvidenceKind, + ifaces: dict, +) -> PhysicalBusInstance: + iface_id = phys_id_for_gen(gen, ifaces) + iface = ifaces[iface_id] + nets = [n for n in memory.pins.values() if n] + groups = memory_groups(nets) + overlap = data_vs_addr_overlap(groups) + controller = find_controller(graph, memory, nets) + lanes = [g for g in groups if g.kind == "BYTE_LANE"] + incomplete = not lanes + mixed = bool(overlap) + status = "REVIEW" if incomplete or mixed or gen in {"ddr-family", "lpddr-family"} else "RECOGNIZED" + notes = ( + f"Controller-aware DDR instance. memory={memory.reference}" + f"{(' controller=' + controller.reference) if controller else ' (no controller IC on nets)'}." + " No universal mm/ps or Z." + ) + if incomplete: + notes += " Byte-lane grouping incomplete — REVIEW, not PASS." + if mixed: + notes += f" Address/command mixed into DQ: {overlap}. Not certified as DDR data." + status = "REVIEW" + host = controller.reference if controller else memory.reference + peers = [memory.reference] + if controller: + peers = [memory.reference] + group_nets = sorted({n for g in groups for n in g.nets}) + return PhysicalBusInstance( + instance_id=f"{iface_id}:{memory.reference}", + logical_protocol_id=gen, + physical_interface_id=iface_id, + pcb_relevant=iface.pcb_relevant, + confidence=_EVIDENCE_SCORE[evidence], + evidence_kind=evidence, + recognition_status=status, + nets=group_nets, + groups=groups, + host_ref=host, + peer_refs=peers, + ambiguous_logical_ids=["ddr-family"] if gen == "ddr-family" else [], + notes=notes, + ) + + +def recognize_hbm(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]: + out: list[PhysicalBusInstance] = [] + for comp in graph.components.values(): + blob = _blob(comp, graph) + gen = hbm_gen_from_text(blob) + if not gen: + continue + pid = f"{gen}-package" if f"{gen}-package" in ifaces else "hbm-family-package" + iface = ifaces[pid] + out.append(PhysicalBusInstance( + instance_id=f"{pid}:{comp.reference}", + logical_protocol_id=gen if gen in { + "hbm", "hbm2", "hbm2e", "hbm3", "hbm3e", "hbm-family", + } else "hbm-family", + physical_interface_id=pid, + pcb_relevant="NO", + confidence=_EVIDENCE_SCORE["part" if comp.mpn else "silicon"], + evidence_kind="part" if comp.mpn else "silicon", + recognition_status="NOT_APPLICABLE", + nets=[], + groups=[], + host_ref=comp.reference, + notes=( + "HBM is package/interposer. Classic DDR PCB DQ rules do not apply. " + "pcb_relevant=NO." + ), + )) + return out + + +def recognize_ddr_instances(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]: + memories: list[tuple[Component, str, EvidenceKind]] = [] + for comp in graph.components.values(): + blob = _blob(comp, graph) + if is_hbm_blob(blob): + continue + gen = ddr_gen_from_text(blob) + if not gen: + continue + if _CONTROLLER_HINT.search(blob) and not _MEMORY_PART.search(blob): + continue + kind: EvidenceKind = "part" if (comp.mpn or _DDR4_PART.search(blob)) else "silicon" + memories.append((comp, gen, kind)) + out = [build_ddr_instance(graph, m, gen, kind, ifaces) for m, gen, kind in memories] + if out: + return out + if any(is_hbm_blob(_blob(c, graph)) for c in graph.components.values()): + return [] + net_gen = None + for n in graph.nets: + g = ddr_gen_from_text(_leaf(n)) + if g: + net_gen = g + break + if not net_gen: + dq = [n for n in graph.nets if _DQ_RE.search(_leaf(n)) and "DQS" not in _leaf(n).upper()] + dqs = [n for n in graph.nets if "DQS" in _leaf(n).upper()] + if len(dq) >= 8 and dqs: + net_gen = "ddr-family" + else: + return [] + groups = memory_groups(list(graph.nets)) + pid = phys_id_for_gen(net_gen, ifaces) + iface = ifaces[pid] + return [PhysicalBusInstance( + instance_id=f"{pid}:nets", + logical_protocol_id=net_gen, + physical_interface_id=pid, + pcb_relevant=iface.pcb_relevant, + confidence=_EVIDENCE_SCORE["net_name"], + evidence_kind="net_name", + recognition_status="REVIEW", + nets=sorted({n for g in groups for n in g.nets}), + groups=groups, + notes="DDR nets without a memory MPN. Controller-aware grouping only. No universal mm/ps.", + )] diff --git a/periscope/src/backend/periscopex/protocol_l0.py b/periscope/src/backend/periscopex/protocol_l0.py index bbfb89b..3626c9d 100644 --- a/periscope/src/backend/periscopex/protocol_l0.py +++ b/periscope/src/backend/periscopex/protocol_l0.py @@ -94,6 +94,8 @@ def _constraint_for(iface: PhysicalInterface, check: str) -> ProtocolConstraint "topology": "topology", "byte_lane_mapping": "topology", "byte_lane_skew": "byte_to_byte_skew", + "ck_to_command_skew": "ck_to_command_skew", + "ck_to_address_skew": "ck_to_address_skew", "length": "max_length", "clock_length": "max_length", "data_length": "max_length", @@ -302,7 +304,7 @@ def protocol_exam( from backend.periscopex.protocol_l1 import certify_l1 from backend.periscopex.protocol_l2 import LEVEL as L2_LEVEL, certify_l2 from backend.periscopex.protocol_report import ( - PACK_MACROPHASE as L5_MACRO, + PACK_MACROPHASE as L8_MACRO, attach_instance_report, result_rank, ) @@ -313,7 +315,7 @@ def protocol_exam( instances = recognize_physical_buses(graph, cat) if not instances: sec = empty_protocol_section() - sec.macrophase = L5_MACRO + sec.macrophase = L8_MACRO return sec, [] l0, findings = certify_l0(graph, instances, cat) l1, l1_findings_out = certify_l1(graph, instances, layout, cat) @@ -343,7 +345,7 @@ def protocol_exam( dumps.sort(key=lambda r: result_rank(str(r.get("worst_result") or "UNKNOWN"))) sec = ProtocolCertificationSection( schema_version=SCHEMA_VERSION, - macrophase=L5_MACRO, + macrophase=L8_MACRO, recognized_instances=dumps, message="", max_level_reached=L2_LEVEL, diff --git a/periscope/src/backend/periscopex/protocol_l1.py b/periscope/src/backend/periscopex/protocol_l1.py index 3adab4c..1df30f4 100644 --- a/periscope/src/backend/periscopex/protocol_l1.py +++ b/periscope/src/backend/periscopex/protocol_l1.py @@ -39,6 +39,7 @@ L1_CHECKS = frozenset({ "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "clock_to_data_skew", "vias", "via_count", @@ -182,6 +183,57 @@ def _skip_grouping(check: str, mand: MandatoryClass, inst: PhysicalBusInstance) ) +def _grouping_recon( + inst: PhysicalBusInstance, + check: str, + mand: MandatoryClass, + kind: str, +) -> L1CheckResult: + lanes = [g for g in inst.groups if g.kind == "BYTE_LANE"] + addr_cmd = [ + g for g in inst.groups + if g.kind in {"ADDRESS_GROUP", "COMMAND_GROUP", "CONTROL_GROUP", "CLOCK_GROUP"} + ] + data_nets = {n for g in lanes for n in g.nets} + other_nets = {n for g in addr_cmd for n in g.nets} + mixed = sorted(data_nets & other_nets) + if mixed: + return _pack( + check, "UNKNOWN", mand, nets=mixed, value_kind=kind, + notes=( + f"Interfaccia {inst.physical_interface_id} non certificata a L1 " + f"per grouping misto DQ e ADDRESS/COMMAND ({mixed}). Not PASS. " + "No universal mm/ps." + ), + ) + if check == "cmd_dat_grouping": + if not lanes or not addr_cmd: + return _skip_grouping(check, mand, inst) + return _pack( + check, "PASS", mand, nets=_instance_nets(inst), value_kind=kind, + notes=( + "DQ/DQS vs ADDRESS/COMMAND/CONTROL/CLOCK reconstructed separately. " + "Controller-aware. L1 is not electrical. No universal mm/ps." + ), + ) + if not lanes: + return _skip_grouping(check, mand, inst) + has_dqs = any( + k.upper().startswith("DQS") for g in lanes for k in g.roles + ) + if check == "dqs_relationship_if_present" and not has_dqs: + return _skip_grouping(check, mand, inst) + if check == "byte_lane_mapping" and not has_dqs: + return _skip_grouping(check, mand, inst) + return _pack( + check, "PASS", mand, nets=_instance_nets(inst), value_kind=kind, + notes=( + "Byte-lane DQ/DQS grouping reconstructed. Not a global length-match PASS. " + "L1 is not electrical certification. No universal mm/ps." + ), + ) + + def certify_instance_l1( graph: DesignGraph, inst: PhysicalBusInstance, @@ -209,30 +261,40 @@ def _run_l1_check( mand = _mandatory(iface, check) cons = _constraint_for(iface, check) kind = cons.value_kind if cons else "UNKNOWN" - if layout is None: - return _skip_no_geometry(check, mand, inst) - - grouping_names = { + grouping_recon = { "byte_lane_mapping", "dq_group", "dqs_relationship_if_present", - "dq_to_dqs_skew", "byte_lane_skew", + "cmd_dat_grouping", + } + skew_checks = { + "dq_to_dqs_skew", "byte_lane_skew", "ck_to_command_skew", + "ck_to_address_skew", "clock_to_data_skew", "lane_relationships", + "intra_pair_skew", "pair_skew", } lanes = [g for g in inst.groups if g.kind == "BYTE_LANE"] - if check in grouping_names and not lanes: - return _skip_grouping(check, mand, inst) - if check == "cmd_dat_grouping": - cmd = [g for g in inst.groups if g.kind in {"COMMAND_GROUP", "ADDRESS_GROUP", "CLOCK_GROUP"}] - if not cmd: - return _skip_grouping(check, mand, inst) - return _pack( - check, "PASS", mand, nets=_instance_nets(inst), value_kind=kind, - notes="CMD/DAT grouping present. L1 is not electrical certification.", - ) - if check in {"byte_lane_mapping", "dq_group", "dqs_relationship_if_present"}: - return _pack( - check, "PASS", mand, nets=_instance_nets(inst), value_kind=kind, - notes="Geometric grouping present. L1 is not electrical certification.", - ) + if check in grouping_recon: + return _grouping_recon(inst, check, mand, kind) + + if check in skew_checks: + if check in { + "dq_to_dqs_skew", "byte_lane_skew", + } and not lanes: + return _skip_grouping(check, mand, inst) + if layout is None: + dep = kind if kind in { + "CONTROLLER_DEPENDENT", "PHY_DEPENDENT", "VENDOR_DEPENDENT", + "UNKNOWN", "MISSING_SOURCE", + } else "CONTROLLER_DEPENDENT" + return _pack( + check, dep, mand, nets=_instance_nets(inst), value_kind=kind, + notes=( + "Controller/PHY dependent skew — no universal mm/ps. " + "L1 grouping is not a length-match PASS. Not electrical." + ), + ) + + if layout is None: + return _skip_no_geometry(check, mand, inst) if check == "topology": nets = _instance_nets(inst) @@ -291,7 +353,7 @@ def _run_l1_check( if check == "byte_lane_skew": return _lane_spread(lanes, layout, cons, mand, kind, "byte_lane_skew") - if check in {"ck_to_command_skew", "clock_to_data_skew", "lane_relationships"}: + if check in {"ck_to_command_skew", "ck_to_address_skew", "clock_to_data_skew", "lane_relationships"}: measured = _group_spread(inst, layout) verdict = geometric_verdict(cons, measured) return _pack( diff --git a/periscope/src/backend/periscopex/protocol_recognize.py b/periscope/src/backend/periscopex/protocol_recognize.py index aede08b..ab4a9cc 100644 --- a/periscope/src/backend/periscopex/protocol_recognize.py +++ b/periscope/src/backend/periscopex/protocol_recognize.py @@ -267,6 +267,7 @@ def recognize_physical_buses( out: list[PhysicalBusInstance] = [] out.extend(_recognize_declared(graph, ifaces, logical)) out.extend(_recognize_usb(graph, ifaces)) + out.extend(_recognize_hbm(graph, ifaces)) out.extend(_recognize_ddr(graph, ifaces)) out.extend(_recognize_axi(graph, ifaces)) return _dedupe_instances(out) @@ -417,73 +418,13 @@ def _ddr_gen_from_text(text: str) -> str | None: def _recognize_ddr(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]: - memories: list[tuple[Component, str, EvidenceKind]] = [] - for comp in graph.components.values(): - blob = _blob(comp, graph) - gen = _ddr_gen_from_text(blob) - if not gen: - continue - kind: EvidenceKind = "part" if (comp.mpn or _DDR4_PART.search(blob)) else "silicon" - memories.append((comp, gen, kind)) - net_gen = None - for n in graph.nets: - g = _ddr_gen_from_text(_leaf(n)) - if g: - net_gen = g - break - if not memories and not net_gen: - dq = [n for n in graph.nets if _DQ_RE.search(_leaf(n)) and "DQS" not in _leaf(n).upper()] - dqs = [n for n in graph.nets if "DQS" in _leaf(n).upper()] - if len(dq) >= 8 and dqs: - net_gen = "ddr-family" - else: - return [] + from backend.periscopex.protocol_ddr import recognize_ddr_instances + return recognize_ddr_instances(graph, ifaces) - gen = memories[0][1] if memories else net_gen or "ddr-family" - phys = f"{gen}-sdram-pcb" if f"{gen}-sdram-pcb" in ifaces else "ddr-family-sdram-pcb" - if gen == "ddr-family": - phys = "ddr-family-sdram-pcb" - iface = ifaces[phys] - host = memories[0][0].reference if memories else "" - evidence: EvidenceKind = memories[0][2] if memories else "net_name" - all_nets = list(graph.nets) - if host: - host_nets = list(graph.components[host].pins.values()) - all_nets = host_nets or all_nets - lanes = _byte_lanes(all_nets) - groups = list(lanes) - ck = [n for n in all_nets if _CK_RE.search(_leaf(n))] - if ck: - groups.append(SignalGroup(kind="CLOCK_GROUP", id="CK", nets=ck, roles={})) - addr = [n for n in all_nets if _ADDR_RE.search(_leaf(n))] - if addr: - groups.append(SignalGroup(kind="ADDRESS_GROUP", id="ADDRESS", nets=addr, roles={})) - cmd = [n for n in all_nets if _CMD_RE.search(_leaf(n))] - if cmd: - groups.append(SignalGroup(kind="COMMAND_GROUP", id="COMMAND_CONTROL", nets=cmd, roles={})) - nets = sorted({n for g in groups for n in g.nets}) - incomplete = bool(memories) and not lanes - status: RecognitionStatus = "REVIEW" if incomplete or gen == "ddr-family" else "RECOGNIZED" - notes = "" - if incomplete: - notes = "DDR device present but byte-lane grouping incomplete — REVIEW, not FAIL." - if gen == "ddr-family": - notes = (notes + " " if notes else "") + "DDR generation not unique." - return [PhysicalBusInstance( - instance_id=f"{phys}:{host or 'nets'}", - logical_protocol_id=gen, - physical_interface_id=phys, - pcb_relevant=iface.pcb_relevant, - confidence=_EVIDENCE_SCORE[evidence], - evidence_kind=evidence, - recognition_status=status, - nets=nets, - groups=groups, - host_ref=host, - peer_refs=[c.reference for c, _, _ in memories], - ambiguous_logical_ids=["ddr-family"] if gen == "ddr-family" else [], - notes=notes, - )] + +def _recognize_hbm(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]: + from backend.periscopex.protocol_ddr import recognize_hbm + return recognize_hbm(graph, ifaces) def _recognize_axi(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]: diff --git a/periscope/src/backend/periscopex/protocol_report.py b/periscope/src/backend/periscopex/protocol_report.py index cb2ac5f..2d89a47 100644 --- a/periscope/src/backend/periscopex/protocol_report.py +++ b/periscope/src/backend/periscopex/protocol_report.py @@ -12,7 +12,7 @@ from backend.periscopex.protocol_catalog import PhysicalInterface, ProtocolConst from backend.periscopex.protocol_l0 import _constraint_for from backend.periscopex.protocol_recognize import PhysicalBusInstance -PACK_MACROPHASE = "M5" +PACK_MACROPHASE = "M8" _SKIP = frozenset({ "UNKNOWN", "MISSING_SOURCE", "VENDOR_DEPENDENT", diff --git a/periscope/src/frontend/content/changelog.md b/periscope/src/frontend/content/changelog.md index 078402e..ee6ab9c 100644 --- a/periscope/src/frontend/content/changelog.md +++ b/periscope/src/frontend/content/changelog.md @@ -2,6 +2,13 @@ What's new in Periscope. +## 2.70.0 — 2026-09-22 — Protocol certification M8 (DDR instance + L1 grouping) + +DDR **physical instances** are controller-aware: one instance per memory device, DQ/DQS **byte lanes** separate from ADDRESS / COMMAND / CONTROL / CLOCK. Skew/length stay **CONTROLLER_DEPENDENT / PHY_DEPENDENT / UNKNOWN** — no universal mm/ps/ohm. HBM is package/interposer (`pcb_relevant: NO`); classic DDR PCB DQ rules are not applied. M6 numeric packs and L3 solver are not in this release. + +- [New] `protocol_ddr.py`; L1 grouping reconstruction without PCB geometry. +- [New] pytest `tests/pcb/test_protocol_ddr_m8.py`. + ## 2.69.0 — 2026-09-22 — Protocol certification M5 (Protocolli report) Report section **Protocolli**: recognised instances, max level L0–L3 (L3 is a visible not-run skip, no solver), each check with measured / limit / margin / source / method, traceability chain net → group → physical interface → protocol → constraint → document → section. FAIL is listed first and styled as Error — not hidden under Warning (HubAudio). Visible skips stay on the page. No pack numbers, no invented Z, no OpenEMS. diff --git a/periscope/src/frontend/package.json b/periscope/src/frontend/package.json index 52e3021..a3662a1 100644 --- a/periscope/src/frontend/package.json +++ b/periscope/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "periscope-web", - "version": "2.69.0", + "version": "2.70.0", "private": true, "scripts": { "sync-version": "node scripts/sync-version.mjs", diff --git a/tests/pcb/test_protocol_ddr_m8.py b/tests/pcb/test_protocol_ddr_m8.py new file mode 100644 index 0000000..e8158df --- /dev/null +++ b/tests/pcb/test_protocol_ddr_m8.py @@ -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" diff --git a/tests/pcb/test_protocol_l0_m2.py b/tests/pcb/test_protocol_l0_m2.py index 1a97364..15c0245 100644 --- a/tests/pcb/test_protocol_l0_m2.py +++ b/tests/pcb/test_protocol_l0_m2.py @@ -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") diff --git a/tests/pcb/test_protocol_l1_m3.py b/tests/pcb/test_protocol_l1_m3.py index 747b7b6..76fd6d1 100644 --- a/tests/pcb/test_protocol_l1_m3.py +++ b/tests/pcb/test_protocol_l1_m3.py @@ -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() diff --git a/tests/pcb/test_protocol_l2_m4.py b/tests/pcb/test_protocol_l2_m4.py index 8ef0c7f..4209245 100644 --- a/tests/pcb/test_protocol_l2_m4.py +++ b/tests/pcb/test_protocol_l2_m4.py @@ -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 diff --git a/tests/pcb/test_protocol_report_m5.py b/tests/pcb/test_protocol_report_m5.py index 9df31d8..7d8376d 100644 --- a/tests/pcb/test_protocol_report_m5.py +++ b/tests/pcb/test_protocol_report_m5.py @@ -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"))