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 (
|
||||
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_trace_check import check_af_traces
|
||||
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
|
||||
@@ -394,7 +394,7 @@ async def run_pcb_pipeline(
|
||||
summary=summary,
|
||||
coverage=coverage,
|
||||
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.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.lifecycle import check_lifecycle, load_lifecycle_dir
|
||||
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.parsers import ic_mpn_skip_reason
|
||||
from backend.periscopex.passive_rail_check import (
|
||||
@@ -229,7 +229,7 @@ async def validate_design_async(
|
||||
coverage=_sanitize_coverage(all_coverage),
|
||||
review_errors=dict(review_errors),
|
||||
not_reviewed=not_reviewed,
|
||||
protocol_certification=empty_protocol_section().model_dump(),
|
||||
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
||||
)
|
||||
except Exception:
|
||||
report = ValidationReport(
|
||||
@@ -240,7 +240,7 @@ async def validate_design_async(
|
||||
coverage={},
|
||||
review_errors=dict(review_errors),
|
||||
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))
|
||||
if preserved_comments is not None:
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
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)
|
||||
|
||||
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",
|
||||
"version": "2.64.0",
|
||||
"version": "2.65.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"sync-version": "node scripts/sync-version.mjs",
|
||||
|
||||
@@ -7,24 +7,45 @@ export function ProtocolSection({
|
||||
}: {
|
||||
section?: ProtocolCertificationSection | null;
|
||||
}) {
|
||||
const message = section?.message || "nessun protocollo riconosciuto";
|
||||
const instances = section?.recognized_instances ?? [];
|
||||
const message = section?.message || "nessun protocollo riconosciuto";
|
||||
return (
|
||||
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
||||
<h2 className="text-sm font-semibold">Protocolli</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Logical protocol vs physical interface. M0 loads the catalog only —
|
||||
no recognition, no invented impedance.
|
||||
Logical protocol vs physical interface. Recognition only (M1) —
|
||||
no L0–L3 certifier, no invented impedance.
|
||||
</p>
|
||||
{instances.length === 0 ? (
|
||||
<p className="text-sm">{message}</p>
|
||||
) : (
|
||||
<ul className="text-sm space-y-1">
|
||||
{instances.map((row, i) => (
|
||||
<li key={i} className="font-mono text-xs">
|
||||
{String(row.physical_interface_id ?? row.logical_protocol_id ?? i)}
|
||||
</li>
|
||||
))}
|
||||
<ul className="text-sm space-y-2">
|
||||
{instances.map((row, i) => {
|
||||
const phys = String(row.physical_interface_id ?? "");
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/** 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";
|
||||
|
||||
@@ -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