Add protocol-certification M1 instance recognition (2.65.0).
physical_bus_instance from declaration, silicon/part, pin/net names, and pairs. USB and DDR fixtures emit groups; AXI4 internal attaches no PCB nets. Ambiguity is REVIEW, not FAIL. No L0–L3 certifier and no invented Z.
This commit is contained in:
@@ -0,0 +1,564 @@
|
|||||||
|
"""M1: physical_bus_instance recognition. No L0–L3 certifier. No invented Z.
|
||||||
|
|
||||||
|
Priority: declared > silicon/BOM > pinout > net/pin name > pair geometry.
|
||||||
|
Ambiguity is REVIEW, never FAIL. AXI4 internal does not attach PCB nets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from backend.periscopex.models import Component, DesignGraph
|
||||||
|
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
||||||
|
from backend.periscopex.protocol_catalog import (
|
||||||
|
EMPTY_PROTOCOL_MESSAGE,
|
||||||
|
SCHEMA_VERSION,
|
||||||
|
ProtocolCatalog,
|
||||||
|
ProtocolCertificationSection,
|
||||||
|
empty_protocol_section,
|
||||||
|
load_catalog,
|
||||||
|
)
|
||||||
|
|
||||||
|
PACK_MACROPHASE = "M1"
|
||||||
|
|
||||||
|
EvidenceKind = Literal[
|
||||||
|
"declared",
|
||||||
|
"silicon",
|
||||||
|
"part",
|
||||||
|
"pinout",
|
||||||
|
"net_name",
|
||||||
|
"pair_geometry",
|
||||||
|
]
|
||||||
|
GroupKind = Literal[
|
||||||
|
"DIFFERENTIAL_PAIR",
|
||||||
|
"BYTE_LANE",
|
||||||
|
"DATA_LANE",
|
||||||
|
"CLOCK_GROUP",
|
||||||
|
"ADDRESS_GROUP",
|
||||||
|
"COMMAND_GROUP",
|
||||||
|
"CONTROL_GROUP",
|
||||||
|
"STROBE_GROUP",
|
||||||
|
"CHANNEL",
|
||||||
|
"SUBCHANNEL",
|
||||||
|
]
|
||||||
|
RecognitionStatus = Literal["RECOGNIZED", "REVIEW", "NOT_APPLICABLE"]
|
||||||
|
|
||||||
|
_EVIDENCE_SCORE: dict[str, float] = {
|
||||||
|
"declared": 1.0,
|
||||||
|
"silicon": 0.9,
|
||||||
|
"part": 0.85,
|
||||||
|
"pinout": 0.7,
|
||||||
|
"net_name": 0.55,
|
||||||
|
"pair_geometry": 0.4,
|
||||||
|
}
|
||||||
|
|
||||||
|
_USB_C_RE = re.compile(r"USB[\s_\-]*C\b|TYPE[\s_\-]*C|USB4085", re.I)
|
||||||
|
_USB2_RE = re.compile(r"USB[\s_\-]*2|16P", re.I)
|
||||||
|
_USB3_RE = re.compile(r"USB[\s_\-]*3|SUPER\s*SPEED|\bSS\b", re.I)
|
||||||
|
_USB_HS_RE = re.compile(r"HIGH\s*SPEED|\b480\b|\bHS\b", re.I)
|
||||||
|
_USB_LSFS_RE = re.compile(r"LOW\s*SPEED|FULL\s*SPEED|\b12\s*MB|\b1\.5\s*MB", 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|MT41|K4B|IS43TR", re.I)),
|
||||||
|
("ddr2", re.compile(r"\bDDR2\b|MT47", re.I)),
|
||||||
|
("ddr1", re.compile(r"\bDDR1\b|\bDDR\b", re.I)),
|
||||||
|
]
|
||||||
|
_DDR4_PART = re.compile(r"MT41K|K4A|IS43TR|W631|EDY4016", re.I)
|
||||||
|
_AXI_RE = re.compile(r"\bAXI4(?:-LITE|-STREAM)?\b|\bAXI\b", re.I)
|
||||||
|
_AXI_C2C_RE = re.compile(r"CHIP[\s\-]*TO[\s\-]*CHIP|C2C|AXI.*PHY", re.I)
|
||||||
|
_DECLARE_RE = re.compile(
|
||||||
|
r"^(?:protocol|logical_protocol|physical_interface|bus)$", re.I,
|
||||||
|
)
|
||||||
|
_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|CLK)(?:[_#]?(P|N))?(?:$|[_/.])", re.I)
|
||||||
|
_ADDR_RE = re.compile(r"(?:^|[_/.])(?:A|ADDR|BA|BG)(\d+)(?:$|[_/.])", re.I)
|
||||||
|
_CMD_RE = re.compile(
|
||||||
|
r"(?:^|[_/.])(?:RAS|CAS|WE|ACT|CS|CKE|ODT|RESET|ZQ)(?:[#_N]*)(?:$|[_/.])",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
_USB_DP_RE = re.compile(
|
||||||
|
r"(?:^|[_/.])(?:USB[_]?D\+|USB[_]?DP|D\+)$", re.I,
|
||||||
|
)
|
||||||
|
_USB_DN_RE = re.compile(
|
||||||
|
r"(?:^|[_/.])(?:USB[_]?D\-|USB[_]?DM|D\-)$", re.I,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalGroup(BaseModel):
|
||||||
|
kind: GroupKind
|
||||||
|
id: str
|
||||||
|
nets: list[str] = Field(default_factory=list)
|
||||||
|
roles: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class PhysicalBusInstance(BaseModel):
|
||||||
|
instance_id: str
|
||||||
|
logical_protocol_id: str
|
||||||
|
physical_interface_id: str
|
||||||
|
pcb_relevant: Literal["YES", "NO", "CONDITIONAL"]
|
||||||
|
confidence: float
|
||||||
|
evidence_kind: EvidenceKind
|
||||||
|
recognition_status: RecognitionStatus
|
||||||
|
nets: list[str] = Field(default_factory=list)
|
||||||
|
groups: list[SignalGroup] = Field(default_factory=list)
|
||||||
|
host_ref: str = ""
|
||||||
|
peer_refs: list[str] = Field(default_factory=list)
|
||||||
|
ambiguous_logical_ids: list[str] = Field(default_factory=list)
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _leaf(name: str) -> str:
|
||||||
|
n = normalize_kicad_hierarchy_net(name)
|
||||||
|
return n.split("/")[-1] if n else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _blob(comp: Component, graph: DesignGraph) -> str:
|
||||||
|
extra = []
|
||||||
|
for bag in (
|
||||||
|
(graph.bom_fields or {}).get(comp.reference) or {},
|
||||||
|
(graph.schematic_fields or {}).get(comp.reference) or {},
|
||||||
|
):
|
||||||
|
extra.extend(str(v) for v in bag.values() if v)
|
||||||
|
specs = ""
|
||||||
|
if comp.specs is not None and getattr(comp.specs, "values", None):
|
||||||
|
specs = " ".join(str(v) for v in comp.specs.values.values() if v)
|
||||||
|
return " ".join(
|
||||||
|
str(x or "")
|
||||||
|
for x in (
|
||||||
|
comp.mpn, comp.value, comp.footprint, comp.component_subtype, specs, *extra,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _declared_ids(graph: DesignGraph) -> list[tuple[str, str, str]]:
|
||||||
|
"""(ref, logical_or_physical_id, raw)."""
|
||||||
|
hits: list[tuple[str, str, str]] = []
|
||||||
|
bags: list[tuple[str, dict]] = []
|
||||||
|
for ref, row in (graph.bom_fields or {}).items():
|
||||||
|
bags.append((ref, row or {}))
|
||||||
|
for ref, row in (graph.schematic_fields or {}).items():
|
||||||
|
bags.append((ref, row or {}))
|
||||||
|
for ref, comp in graph.components.items():
|
||||||
|
if comp.specs is not None and getattr(comp.specs, "values", None):
|
||||||
|
bags.append((ref, dict(comp.specs.values)))
|
||||||
|
for ref, row in bags:
|
||||||
|
for key, val in row.items():
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
if not _DECLARE_RE.match(str(key)):
|
||||||
|
continue
|
||||||
|
hits.append((ref, str(val).strip().lower(), str(val)))
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def _pn_pairs(net_names: list[str]) -> list[tuple[str, str]]:
|
||||||
|
by_leaf = {_leaf(n): n for n in net_names if n}
|
||||||
|
pairs: list[tuple[str, str]] = []
|
||||||
|
used: set[str] = set()
|
||||||
|
for leaf, full in by_leaf.items():
|
||||||
|
if full in used:
|
||||||
|
continue
|
||||||
|
mate_leaf = None
|
||||||
|
if leaf.endswith("+"):
|
||||||
|
mate_leaf = leaf[:-1] + "-"
|
||||||
|
elif leaf.endswith("_P") or leaf.endswith("_p"):
|
||||||
|
mate_leaf = leaf[:-2] + "_N"
|
||||||
|
elif leaf.upper().endswith("DP"):
|
||||||
|
stem = leaf[:-2]
|
||||||
|
mate_leaf = stem + ("DM" if leaf[-2].isupper() else "dm")
|
||||||
|
if mate_leaf and mate_leaf in by_leaf:
|
||||||
|
other = by_leaf[mate_leaf]
|
||||||
|
pairs.append((full, other))
|
||||||
|
used.add(full)
|
||||||
|
used.add(other)
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
def _usb_pairs(net_names: list[str]) -> list[tuple[str, str]]:
|
||||||
|
plus = [n for n in net_names if _USB_DP_RE.search(_leaf(n))]
|
||||||
|
minus = [n for n in net_names if _USB_DN_RE.search(_leaf(n))]
|
||||||
|
found: list[tuple[str, str]] = []
|
||||||
|
used: set[str] = set()
|
||||||
|
generic = _pn_pairs(net_names)
|
||||||
|
for a, b in generic:
|
||||||
|
la, lb = _leaf(a).upper(), _leaf(b).upper()
|
||||||
|
if "USB" in la or "USB" in lb or la.startswith("D") or lb.startswith("D"):
|
||||||
|
found.append((a, b))
|
||||||
|
used.add(a)
|
||||||
|
used.add(b)
|
||||||
|
for p in plus:
|
||||||
|
if p in used:
|
||||||
|
continue
|
||||||
|
pl = _leaf(p)
|
||||||
|
for m in minus:
|
||||||
|
if m in used:
|
||||||
|
continue
|
||||||
|
if pl.replace("+", "-").replace("DP", "DM").replace("_P", "_N") == _leaf(m):
|
||||||
|
found.append((p, m))
|
||||||
|
used.add(p)
|
||||||
|
used.add(m)
|
||||||
|
break
|
||||||
|
if not found and len(plus) == 1 and len(minus) == 1:
|
||||||
|
found.append((plus[0], minus[0]))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
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 _iface_map(catalog: ProtocolCatalog) -> dict[str, Any]:
|
||||||
|
return {p.id: p for p in catalog.physical_interfaces}
|
||||||
|
|
||||||
|
|
||||||
|
def _logical_ids(catalog: ProtocolCatalog) -> set[str]:
|
||||||
|
return {p.id for p in catalog.logical_protocols}
|
||||||
|
|
||||||
|
|
||||||
|
def recognize_physical_buses(
|
||||||
|
graph: DesignGraph,
|
||||||
|
catalog: ProtocolCatalog | None = None,
|
||||||
|
) -> list[PhysicalBusInstance]:
|
||||||
|
cat = catalog or load_catalog()
|
||||||
|
ifaces = _iface_map(cat)
|
||||||
|
logical = _logical_ids(cat)
|
||||||
|
out: list[PhysicalBusInstance] = []
|
||||||
|
out.extend(_recognize_declared(graph, ifaces, logical))
|
||||||
|
out.extend(_recognize_usb(graph, ifaces))
|
||||||
|
out.extend(_recognize_ddr(graph, ifaces))
|
||||||
|
out.extend(_recognize_axi(graph, ifaces))
|
||||||
|
return _dedupe_instances(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _recognize_declared(
|
||||||
|
graph: DesignGraph,
|
||||||
|
ifaces: dict,
|
||||||
|
logical: set[str],
|
||||||
|
) -> list[PhysicalBusInstance]:
|
||||||
|
out: list[PhysicalBusInstance] = []
|
||||||
|
for ref, ident, raw in _declared_ids(graph):
|
||||||
|
ident = ident.replace("_", "-")
|
||||||
|
phys_id = None
|
||||||
|
log_id = None
|
||||||
|
if ident in ifaces:
|
||||||
|
phys_id = ident
|
||||||
|
log_id = ifaces[ident].logical_protocol_id
|
||||||
|
elif ident in logical:
|
||||||
|
log_id = ident
|
||||||
|
for pid, iface in ifaces.items():
|
||||||
|
if iface.logical_protocol_id == ident and iface.pcb_relevant == "NO":
|
||||||
|
phys_id = pid
|
||||||
|
break
|
||||||
|
if phys_id is None:
|
||||||
|
for pid, iface in ifaces.items():
|
||||||
|
if iface.logical_protocol_id == ident:
|
||||||
|
phys_id = pid
|
||||||
|
break
|
||||||
|
if not phys_id or not log_id:
|
||||||
|
continue
|
||||||
|
iface = ifaces[phys_id]
|
||||||
|
nets: list[str] = []
|
||||||
|
if iface.pcb_relevant != "NO":
|
||||||
|
comp = graph.components.get(ref)
|
||||||
|
if comp:
|
||||||
|
nets = sorted({n for n in comp.pins.values() if n})
|
||||||
|
out.append(PhysicalBusInstance(
|
||||||
|
instance_id=f"{phys_id}:{ref}",
|
||||||
|
logical_protocol_id=log_id,
|
||||||
|
physical_interface_id=phys_id,
|
||||||
|
pcb_relevant=iface.pcb_relevant,
|
||||||
|
confidence=_EVIDENCE_SCORE["declared"],
|
||||||
|
evidence_kind="declared",
|
||||||
|
recognition_status=(
|
||||||
|
"NOT_APPLICABLE" if iface.pcb_relevant == "NO" else "RECOGNIZED"
|
||||||
|
),
|
||||||
|
nets=nets,
|
||||||
|
host_ref=ref,
|
||||||
|
notes=f"declared {raw}",
|
||||||
|
))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _recognize_usb(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]:
|
||||||
|
instances: list[PhysicalBusInstance] = []
|
||||||
|
connectors = []
|
||||||
|
for comp in graph.components.values():
|
||||||
|
blob = _blob(comp, graph)
|
||||||
|
if _USB_C_RE.search(blob) or _USB_C_RE.search(comp.footprint):
|
||||||
|
connectors.append(comp)
|
||||||
|
pairs = _usb_pairs(list(graph.nets))
|
||||||
|
if not connectors and not pairs:
|
||||||
|
return []
|
||||||
|
|
||||||
|
usb3 = any(_USB3_RE.search(_blob(c, graph)) for c in connectors)
|
||||||
|
usb2_mark = any(_USB2_RE.search(_blob(c, graph)) for c in connectors)
|
||||||
|
hs = any(_USB_HS_RE.search(_blob(c, graph)) for c in connectors)
|
||||||
|
lsfs = any(_USB_LSFS_RE.search(_blob(c, graph)) for c in connectors)
|
||||||
|
ambiguous = False
|
||||||
|
notes = ""
|
||||||
|
if connectors and usb3 and usb2_mark:
|
||||||
|
ambiguous = True
|
||||||
|
notes = "USB2 vs USB3 on the same receptacle — REVIEW, not FAIL."
|
||||||
|
if not hs and not lsfs and pairs:
|
||||||
|
ambiguous = True
|
||||||
|
notes = (notes + " " if notes else "") + "USB2 speed not declared; HS vs LS/FS REVIEW."
|
||||||
|
|
||||||
|
if hs and not lsfs:
|
||||||
|
logical, phys = "usb2-hs", "usb2-hs-dpair"
|
||||||
|
elif lsfs and not hs:
|
||||||
|
logical, phys = "usb2-ls-fs", "usb2-ls-fs-dpair"
|
||||||
|
else:
|
||||||
|
logical, phys = "usb2-hs", "usb2-hs-dpair"
|
||||||
|
ambiguous = True
|
||||||
|
|
||||||
|
groups = [
|
||||||
|
SignalGroup(
|
||||||
|
kind="DIFFERENTIAL_PAIR",
|
||||||
|
id=f"USB2_D_{i}",
|
||||||
|
nets=[a, b],
|
||||||
|
roles={"D+": a, "D-": b},
|
||||||
|
)
|
||||||
|
for i, (a, b) in enumerate(pairs)
|
||||||
|
]
|
||||||
|
nets = sorted({n for g in groups for n in g.nets})
|
||||||
|
host = connectors[0].reference if connectors else (nets[0] if nets else "usb")
|
||||||
|
evidence: EvidenceKind = "part" if connectors else "net_name"
|
||||||
|
if connectors and any(_USB_C_RE.search(_blob(c, graph)) for c in connectors):
|
||||||
|
cphys = "usb-c-usb2-receptacle"
|
||||||
|
ciface = ifaces[cphys]
|
||||||
|
status: RecognitionStatus = "REVIEW" if ambiguous or usb3 else "RECOGNIZED"
|
||||||
|
instances.append(PhysicalBusInstance(
|
||||||
|
instance_id=f"{cphys}:{host}",
|
||||||
|
logical_protocol_id="usb-c-usb2",
|
||||||
|
physical_interface_id=cphys,
|
||||||
|
pcb_relevant=ciface.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 connectors],
|
||||||
|
ambiguous_logical_ids=["usb-c-usb2", "usb2-hs"] if ambiguous else [],
|
||||||
|
notes=notes,
|
||||||
|
))
|
||||||
|
return instances
|
||||||
|
|
||||||
|
iface = ifaces[phys]
|
||||||
|
status = "REVIEW" if ambiguous else "RECOGNIZED"
|
||||||
|
amb = ["usb2-ls-fs", "usb2-hs"] if ambiguous else []
|
||||||
|
instances.append(PhysicalBusInstance(
|
||||||
|
instance_id=f"{phys}:{host}",
|
||||||
|
logical_protocol_id=logical,
|
||||||
|
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 connectors],
|
||||||
|
ambiguous_logical_ids=amb,
|
||||||
|
notes=notes,
|
||||||
|
))
|
||||||
|
return instances
|
||||||
|
|
||||||
|
|
||||||
|
def _ddr_gen_from_text(text: str) -> str | None:
|
||||||
|
if _DDR4_PART.search(text):
|
||||||
|
return "ddr4"
|
||||||
|
for gen, cre in _DDR_GEN:
|
||||||
|
if cre.search(text):
|
||||||
|
return gen
|
||||||
|
return 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 []
|
||||||
|
|
||||||
|
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_axi(graph: DesignGraph, ifaces: dict) -> list[PhysicalBusInstance]:
|
||||||
|
hits: list[Component] = []
|
||||||
|
chip_to_chip = False
|
||||||
|
for comp in graph.components.values():
|
||||||
|
blob = _blob(comp, graph)
|
||||||
|
if _AXI_RE.search(blob):
|
||||||
|
hits.append(comp)
|
||||||
|
if _AXI_C2C_RE.search(blob):
|
||||||
|
chip_to_chip = True
|
||||||
|
declared = any(i == "axi4" or i.startswith("axi4") for _, i, _ in _declared_ids(graph))
|
||||||
|
if not hits and not declared:
|
||||||
|
# Net names alone (AXI_AWVALID) do not invent a PCB AXI bus.
|
||||||
|
return []
|
||||||
|
if chip_to_chip:
|
||||||
|
iface = ifaces["axi4-chip-to-chip-phy"]
|
||||||
|
host = hits[0].reference if hits else "axi4"
|
||||||
|
return [PhysicalBusInstance(
|
||||||
|
instance_id=f"{iface.id}:{host}",
|
||||||
|
logical_protocol_id="axi4",
|
||||||
|
physical_interface_id=iface.id,
|
||||||
|
pcb_relevant=iface.pcb_relevant,
|
||||||
|
confidence=_EVIDENCE_SCORE["silicon"],
|
||||||
|
evidence_kind="silicon",
|
||||||
|
recognition_status="REVIEW",
|
||||||
|
nets=[],
|
||||||
|
host_ref=host,
|
||||||
|
notes="AXI chip-to-chip is CONDITIONAL; PHY docs required. No AXI nets assumed.",
|
||||||
|
)]
|
||||||
|
iface = ifaces["axi4-internal"]
|
||||||
|
host = hits[0].reference if hits else "axi4"
|
||||||
|
return [PhysicalBusInstance(
|
||||||
|
instance_id=f"{iface.id}:{host}",
|
||||||
|
logical_protocol_id="axi4",
|
||||||
|
physical_interface_id=iface.id,
|
||||||
|
pcb_relevant="NO",
|
||||||
|
confidence=_EVIDENCE_SCORE["silicon"],
|
||||||
|
evidence_kind="silicon",
|
||||||
|
recognition_status="NOT_APPLICABLE",
|
||||||
|
nets=[],
|
||||||
|
groups=[],
|
||||||
|
host_ref=host,
|
||||||
|
peer_refs=[c.reference for c in hits],
|
||||||
|
notes="AXI4 is an on-chip interconnect. No PCB nets.",
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_instances(items: list[PhysicalBusInstance]) -> list[PhysicalBusInstance]:
|
||||||
|
by_id: dict[str, PhysicalBusInstance] = {}
|
||||||
|
for inst in items:
|
||||||
|
prev = by_id.get(inst.instance_id)
|
||||||
|
if prev is None or inst.confidence > prev.confidence:
|
||||||
|
by_id[inst.instance_id] = inst
|
||||||
|
return sorted(by_id.values(), key=lambda i: (-i.confidence, i.instance_id))
|
||||||
|
|
||||||
|
|
||||||
|
def protocol_section_for_graph(
|
||||||
|
graph: DesignGraph | None,
|
||||||
|
catalog: ProtocolCatalog | None = None,
|
||||||
|
) -> ProtocolCertificationSection:
|
||||||
|
if graph is None:
|
||||||
|
return empty_protocol_section()
|
||||||
|
instances = recognize_physical_buses(graph, catalog)
|
||||||
|
if not instances:
|
||||||
|
sec = empty_protocol_section()
|
||||||
|
sec.macrophase = PACK_MACROPHASE
|
||||||
|
return sec
|
||||||
|
return ProtocolCertificationSection(
|
||||||
|
schema_version=SCHEMA_VERSION,
|
||||||
|
macrophase=PACK_MACROPHASE,
|
||||||
|
recognized_instances=[i.model_dump() for i in instances],
|
||||||
|
message="",
|
||||||
|
max_level_reached=None,
|
||||||
|
)
|
||||||
@@ -28,7 +28,7 @@ from backend.periscopex.layout_rules import needs_layout_rules_refresh
|
|||||||
from backend.periscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
|
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
|
||||||
)
|
)
|
||||||
from backend.periscopex.protocol_catalog import empty_protocol_section
|
from backend.periscopex.protocol_recognize import protocol_section_for_graph
|
||||||
from backend.periscopex.af_ai_hf import append_investigated, hypotheses_from_rows
|
from backend.periscopex.af_ai_hf import append_investigated, hypotheses_from_rows
|
||||||
from backend.periscopex.af_trace_check import check_af_traces
|
from backend.periscopex.af_trace_check import check_af_traces
|
||||||
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
|
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
|
||||||
@@ -394,7 +394,7 @@ async def run_pcb_pipeline(
|
|||||||
summary=summary,
|
summary=summary,
|
||||||
coverage=coverage,
|
coverage=coverage,
|
||||||
not_reviewed=skipped + si_skip,
|
not_reviewed=skipped + si_skip,
|
||||||
protocol_certification=empty_protocol_section().model_dump(),
|
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
||||||
)
|
)
|
||||||
report_path = ws.local_path("pcb_report.json")
|
report_path = ws.local_path("pcb_report.json")
|
||||||
report_path.write_text(report.model_dump_json(indent=2) + "\n")
|
report_path.write_text(report.model_dump_json(indent=2) + "\n")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from backend.periscopex.interface_class_check import check_interface_classes
|
|||||||
from backend.periscopex.led_current_check import check_led_current
|
from backend.periscopex.led_current_check import check_led_current
|
||||||
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
||||||
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
|
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
|
||||||
from backend.periscopex.protocol_catalog import empty_protocol_section
|
from backend.periscopex.protocol_recognize import protocol_section_for_graph
|
||||||
from backend.periscopex.nc_pin_check import check_nc_pins
|
from backend.periscopex.nc_pin_check import check_nc_pins
|
||||||
from backend.periscopex.parsers import ic_mpn_skip_reason
|
from backend.periscopex.parsers import ic_mpn_skip_reason
|
||||||
from backend.periscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
@@ -229,7 +229,7 @@ async def validate_design_async(
|
|||||||
coverage=_sanitize_coverage(all_coverage),
|
coverage=_sanitize_coverage(all_coverage),
|
||||||
review_errors=dict(review_errors),
|
review_errors=dict(review_errors),
|
||||||
not_reviewed=not_reviewed,
|
not_reviewed=not_reviewed,
|
||||||
protocol_certification=empty_protocol_section().model_dump(),
|
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
report = ValidationReport(
|
report = ValidationReport(
|
||||||
@@ -240,7 +240,7 @@ async def validate_design_async(
|
|||||||
coverage={},
|
coverage={},
|
||||||
review_errors=dict(review_errors),
|
review_errors=dict(review_errors),
|
||||||
not_reviewed=not_reviewed,
|
not_reviewed=not_reviewed,
|
||||||
protocol_certification=empty_protocol_section().model_dump(),
|
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
||||||
)
|
)
|
||||||
report_dict = json.loads(report.model_dump_json(indent=2))
|
report_dict = json.loads(report.model_dump_json(indent=2))
|
||||||
if preserved_comments is not None:
|
if preserved_comments is not None:
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.65.0 — 2026-09-22 — Protocol certification M1 (instance recognition)
|
||||||
|
|
||||||
|
`physical_bus_instance` from declaration / silicon / part / pin / net / pairs. Output: instances, signal groups (DIFF pair, byte lane, clock/address), confidence. Ambiguity is **REVIEW**, never FAIL. AXI4 internal is recognised with `pcb_relevant: NO` and **no PCB nets**. No L0–L3 certifier. No invented ohms.
|
||||||
|
|
||||||
|
- [New] `protocol_recognize.py`; report Protocolli lists recognised instances or stays empty.
|
||||||
|
- [New] pytest `tests/pcb/test_protocol_recognize_m1.py` (USB/DDR fixtures; AXI has no nets).
|
||||||
|
|
||||||
## 2.64.0 — 2026-09-22 — Protocol certification M0 (catalog, no numbers)
|
## 2.64.0 — 2026-09-22 — Protocol certification M0 (catalog, no numbers)
|
||||||
|
|
||||||
Versioned JSON protocol pack + Python loader/validator. Catalog skeletons for USB2 / USB-C-USB2 / 100BASE-TX / HDMI (connector distinct) / DDR / LPDDR / HBM / HyperBus / QSPI-octal / eMMC / UFS / AMBA / Wishbone / Avalon / TileLink / PCIe-CXL. Constraints without an official cite are UNKNOWN / CONTROLLER_DEPENDENT / PHY_DEPENDENT / MISSING_SOURCE — **no invented 90/100/50 Ω**. `logical_protocol_id` ≠ `physical_interface_id`. `pcb_relevant` is YES|NO|CONDITIONAL. RECOMMENDED is not FAIL-mandatory. Typical-as-standard is rejected by the parser. Report section **Protocolli** is empty (`nessun protocollo riconosciuto`). Recognition and L0–L3 certifiers are not in this release.
|
Versioned JSON protocol pack + Python loader/validator. Catalog skeletons for USB2 / USB-C-USB2 / 100BASE-TX / HDMI (connector distinct) / DDR / LPDDR / HBM / HyperBus / QSPI-octal / eMMC / UFS / AMBA / Wishbone / Avalon / TileLink / PCIe-CXL. Constraints without an official cite are UNKNOWN / CONTROLLER_DEPENDENT / PHY_DEPENDENT / MISSING_SOURCE — **no invented 90/100/50 Ω**. `logical_protocol_id` ≠ `physical_interface_id`. `pcb_relevant` is YES|NO|CONDITIONAL. RECOMMENDED is not FAIL-mandatory. Typical-as-standard is rejected by the parser. Report section **Protocolli** is empty (`nessun protocollo riconosciuto`). Recognition and L0–L3 certifiers are not in this release.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "periscope-web",
|
"name": "periscope-web",
|
||||||
"version": "2.64.0",
|
"version": "2.65.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sync-version": "node scripts/sync-version.mjs",
|
"sync-version": "node scripts/sync-version.mjs",
|
||||||
|
|||||||
@@ -7,24 +7,45 @@ export function ProtocolSection({
|
|||||||
}: {
|
}: {
|
||||||
section?: ProtocolCertificationSection | null;
|
section?: ProtocolCertificationSection | null;
|
||||||
}) {
|
}) {
|
||||||
const message = section?.message || "nessun protocollo riconosciuto";
|
|
||||||
const instances = section?.recognized_instances ?? [];
|
const instances = section?.recognized_instances ?? [];
|
||||||
|
const message = section?.message || "nessun protocollo riconosciuto";
|
||||||
return (
|
return (
|
||||||
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
||||||
<h2 className="text-sm font-semibold">Protocolli</h2>
|
<h2 className="text-sm font-semibold">Protocolli</h2>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Logical protocol vs physical interface. M0 loads the catalog only —
|
Logical protocol vs physical interface. Recognition only (M1) —
|
||||||
no recognition, no invented impedance.
|
no L0–L3 certifier, no invented impedance.
|
||||||
</p>
|
</p>
|
||||||
{instances.length === 0 ? (
|
{instances.length === 0 ? (
|
||||||
<p className="text-sm">{message}</p>
|
<p className="text-sm">{message}</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="text-sm space-y-1">
|
<ul className="text-sm space-y-2">
|
||||||
{instances.map((row, i) => (
|
{instances.map((row, i) => {
|
||||||
<li key={i} className="font-mono text-xs">
|
const phys = String(row.physical_interface_id ?? "");
|
||||||
{String(row.physical_interface_id ?? row.logical_protocol_id ?? i)}
|
const log = String(row.logical_protocol_id ?? "");
|
||||||
|
const status = String(row.recognition_status ?? "");
|
||||||
|
const conf = row.confidence;
|
||||||
|
const groups = Array.isArray(row.groups) ? row.groups : [];
|
||||||
|
return (
|
||||||
|
<li key={String(row.instance_id ?? i)} className="space-y-0.5">
|
||||||
|
<p className="font-mono text-xs">
|
||||||
|
{log} → {phys}
|
||||||
|
{status ? ` · ${status}` : ""}
|
||||||
|
{typeof conf === "number" ? ` · ${conf.toFixed(2)}` : ""}
|
||||||
|
</p>
|
||||||
|
{groups.length > 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{groups
|
||||||
|
.map((g) => {
|
||||||
|
const rec = g as Record<string, unknown>;
|
||||||
|
return String(rec.kind ?? rec.id ?? "group");
|
||||||
|
})
|
||||||
|
.join(", ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
|
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
|
||||||
export const APP_VERSION = "2.64.0";
|
export const APP_VERSION = "2.65.0";
|
||||||
export const APP_VERSION_DATE = "2026-09-22";
|
export const APP_VERSION_DATE = "2026-09-22";
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""M1 physical_bus_instance recognition: USB/DDR fixtures; AXI has no PCB nets."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from backend.periscopex.models import (
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
DesignGraph,
|
||||||
|
Net,
|
||||||
|
NetType,
|
||||||
|
PinConnection,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_recognize import (
|
||||||
|
PACK_MACROPHASE,
|
||||||
|
protocol_section_for_graph,
|
||||||
|
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_graph() -> DesignGraph:
|
||||||
|
return DesignGraph(
|
||||||
|
components={
|
||||||
|
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"),
|
||||||
|
"J2": Component(
|
||||||
|
reference="J2", value="USB_C_Receptacle_USB2.0_16P",
|
||||||
|
footprint="USB_C_Receptacle_USB2.0_16P",
|
||||||
|
component_type=ComponentType.CONNECTOR,
|
||||||
|
pins={"A6": "USB_D+", "A7": "USB_D-"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
nets={
|
||||||
|
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")),
|
||||||
|
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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", "10": "DDR4_DM0",
|
||||||
|
"11": "DDR4_CK_P", "12": "DDR4_CK_N",
|
||||||
|
"13": "DDR4_A0", "14": "DDR4_RAS",
|
||||||
|
})
|
||||||
|
u_mem = _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")
|
||||||
|
nets = {n: _net(n, ("U5", p)) for p, n in pins.items()}
|
||||||
|
return DesignGraph(components={"U5": u_mem}, nets=nets)
|
||||||
|
|
||||||
|
|
||||||
|
def _axi_graph() -> DesignGraph:
|
||||||
|
pins = {
|
||||||
|
"1": "AXI_AWVALID",
|
||||||
|
"2": "AXI_WDATA",
|
||||||
|
"3": "AXI_RDATA",
|
||||||
|
"4": "VCCINT",
|
||||||
|
}
|
||||||
|
return DesignGraph(
|
||||||
|
components={
|
||||||
|
"U3": _ic("U3", pins, mpn="XC7A100T", value="AXI4 interconnect"),
|
||||||
|
},
|
||||||
|
nets={
|
||||||
|
"AXI_AWVALID": _net("AXI_AWVALID", ("U3", "1")),
|
||||||
|
"AXI_WDATA": _net("AXI_WDATA", ("U3", "2")),
|
||||||
|
"AXI_RDATA": _net("AXI_RDATA", ("U3", "3")),
|
||||||
|
"VCCINT": _net("VCCINT", ("U3", "4")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_net_names_make_diff_pair_instance():
|
||||||
|
insts = recognize_physical_buses(_usb_graph())
|
||||||
|
usb = [i for i in insts if "usb" in i.logical_protocol_id]
|
||||||
|
assert usb, insts
|
||||||
|
one = usb[0]
|
||||||
|
assert one.physical_interface_id != one.logical_protocol_id
|
||||||
|
assert one.pcb_relevant in {"YES", "CONDITIONAL"}
|
||||||
|
pairs = [g for g in one.groups if g.kind == "DIFFERENTIAL_PAIR"]
|
||||||
|
assert pairs
|
||||||
|
nets = set(pairs[0].nets)
|
||||||
|
assert "USB_D+" in nets and "USB_D-" in nets
|
||||||
|
assert one.recognition_status != "FAIL"
|
||||||
|
assert one.evidence_kind in {"part", "net_name", "silicon"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_speed_ambiguity_is_review_not_fail():
|
||||||
|
insts = recognize_physical_buses(_usb_graph())
|
||||||
|
usb = [i for i in insts if "usb" in i.logical_protocol_id][0]
|
||||||
|
assert usb.recognition_status == "REVIEW"
|
||||||
|
assert "FAIL" not in usb.recognition_status
|
||||||
|
assert usb.ambiguous_logical_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_ddr_net_names_rebuild_byte_lane():
|
||||||
|
insts = recognize_physical_buses(_ddr_graph())
|
||||||
|
ddr = [i for i in insts if i.logical_protocol_id.startswith("ddr")]
|
||||||
|
assert ddr
|
||||||
|
one = ddr[0]
|
||||||
|
assert one.physical_interface_id == "ddr4-sdram-pcb"
|
||||||
|
assert one.logical_protocol_id == "ddr4"
|
||||||
|
lanes = [g for g in one.groups if g.kind == "BYTE_LANE"]
|
||||||
|
assert lanes
|
||||||
|
assert "DDR4_DQ0" in lanes[0].nets
|
||||||
|
assert "DDR4_DQS0_P" in lanes[0].nets
|
||||||
|
clocks = [g for g in one.groups if g.kind == "CLOCK_GROUP"]
|
||||||
|
assert clocks
|
||||||
|
assert one.recognition_status != "FAIL"
|
||||||
|
blob = json.dumps(one.model_dump())
|
||||||
|
assert "90" not in blob
|
||||||
|
assert "ohm" not in blob.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_axi4_internal_does_not_create_pcb_nets():
|
||||||
|
insts = recognize_physical_buses(_axi_graph())
|
||||||
|
axi = [i for i in insts if i.logical_protocol_id == "axi4"]
|
||||||
|
assert axi
|
||||||
|
one = axi[0]
|
||||||
|
assert one.physical_interface_id == "axi4-internal"
|
||||||
|
assert one.pcb_relevant == "NO"
|
||||||
|
assert one.nets == []
|
||||||
|
assert one.groups == []
|
||||||
|
assert one.recognition_status == "NOT_APPLICABLE"
|
||||||
|
assert "AXI_AWVALID" not in one.nets
|
||||||
|
assert "AXI_WDATA" not in one.nets
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_graph_keeps_nessun_protocollo():
|
||||||
|
sec = protocol_section_for_graph(DesignGraph())
|
||||||
|
assert sec.message == "nessun protocollo riconosciuto"
|
||||||
|
assert sec.recognized_instances == []
|
||||||
|
assert sec.macrophase == PACK_MACROPHASE
|
||||||
|
blob = json.dumps(sec.model_dump())
|
||||||
|
assert "ohm" not in blob.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_section_lists_usb_without_z():
|
||||||
|
sec = protocol_section_for_graph(_usb_graph())
|
||||||
|
assert sec.recognized_instances
|
||||||
|
assert sec.max_level_reached is None
|
||||||
|
blob = json.dumps(sec.model_dump())
|
||||||
|
assert "90" not in blob
|
||||||
|
assert "Zdiff" not in blob
|
||||||
|
assert all(row.get("recognition_status") != "FAIL" for row in sec.recognized_instances)
|
||||||
Reference in New Issue
Block a user