Add protocol-certification M0 catalog, loader, and empty report (2.64.0).
Versioned JSON skeletons without invented ohms/mm/ps. RECOMMENDED is not FAIL-mandatory; typical-as-standard is rejected. Report Protocolli is empty until recognition (M1+). Native src overlays inherited periscopex on the package path.
This commit is contained in:
+23
-3
@@ -10,9 +10,29 @@ from pathlib import Path
|
|||||||
from pkgutil import extend_path
|
from pkgutil import extend_path
|
||||||
|
|
||||||
_REPO = Path(__file__).resolve().parents[1]
|
_REPO = Path(__file__).resolve().parents[1]
|
||||||
for _p in (_REPO / "periscope" / "src", _REPO / "periscope" / "dependency"):
|
_SRC = _REPO / "periscope" / "src"
|
||||||
|
_DEP = _REPO / "periscope" / "dependency"
|
||||||
|
# Last insert is searched first: native src overlays inherited dependency.
|
||||||
|
for _p in (_DEP, _SRC):
|
||||||
_s = str(_p)
|
_s = str(_p)
|
||||||
if _p.is_dir() and _s not in sys.path:
|
if not _p.is_dir():
|
||||||
|
continue
|
||||||
|
if _s in sys.path:
|
||||||
|
sys.path.remove(_s)
|
||||||
sys.path.insert(0, _s)
|
sys.path.insert(0, _s)
|
||||||
|
|
||||||
__path__ = list(extend_path(__path__, __name__))
|
|
||||||
|
def _prefer_native_backend(paths: list[str]) -> list[str]:
|
||||||
|
src, other, dep = [], [], []
|
||||||
|
for p in paths:
|
||||||
|
norm = p.replace("\\", "/")
|
||||||
|
if "/periscope/src/" in norm:
|
||||||
|
src.append(p)
|
||||||
|
elif "/periscope/dependency/" in norm:
|
||||||
|
dep.append(p)
|
||||||
|
else:
|
||||||
|
other.append(p)
|
||||||
|
return src + other + dep
|
||||||
|
|
||||||
|
|
||||||
|
__path__ = _prefer_native_backend(list(extend_path(__path__, __name__)))
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
"""PinScope-inherited backend package (in-tree dependency)."""
|
"""PinScope-inherited backend package (in-tree dependency)."""
|
||||||
from pkgutil import extend_path
|
from pkgutil import extend_path
|
||||||
|
|
||||||
__path__ = extend_path(__path__, __name__)
|
__path__ = list(extend_path(__path__, __name__))
|
||||||
|
_src, _other, _dep = [], [], []
|
||||||
|
for _p in __path__:
|
||||||
|
_n = str(_p).replace("\\", "/")
|
||||||
|
if "/periscope/src/" in _n:
|
||||||
|
_src.append(_p)
|
||||||
|
elif "/periscope/dependency/" in _n:
|
||||||
|
_dep.append(_p)
|
||||||
|
else:
|
||||||
|
_other.append(_p)
|
||||||
|
__path__[:] = _src + _other + _dep
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
from pkgutil import extend_path
|
from pkgutil import extend_path
|
||||||
|
|
||||||
__path__ = extend_path(__path__, __name__)
|
__path__ = list(extend_path(__path__, __name__))
|
||||||
|
_src, _other, _dep = [], [], []
|
||||||
|
for _p in __path__:
|
||||||
|
_n = str(_p).replace("\\", "/")
|
||||||
|
if "/periscope/src/" in _n:
|
||||||
|
_src.append(_p)
|
||||||
|
elif "/periscope/dependency/" in _n:
|
||||||
|
_dep.append(_p)
|
||||||
|
else:
|
||||||
|
_other.append(_p)
|
||||||
|
__path__[:] = _src + _other + _dep
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ class ValidationReport(BaseModel):
|
|||||||
coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK
|
coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK
|
||||||
review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised
|
review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised
|
||||||
not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF)
|
not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF)
|
||||||
|
protocol_certification: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
class FindingComment(BaseModel):
|
class FindingComment(BaseModel):
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
"""Native Periscope backend modules."""
|
"""Native Periscope backend modules."""
|
||||||
from pkgutil import extend_path
|
from pkgutil import extend_path
|
||||||
|
|
||||||
__path__ = extend_path(__path__, __name__)
|
__path__ = list(extend_path(__path__, __name__))
|
||||||
|
_src, _other, _dep = [], [], []
|
||||||
|
for _p in __path__:
|
||||||
|
_n = str(_p).replace("\\", "/")
|
||||||
|
if "/periscope/src/" in _n:
|
||||||
|
_src.append(_p)
|
||||||
|
elif "/periscope/dependency/" in _n:
|
||||||
|
_dep.append(_p)
|
||||||
|
else:
|
||||||
|
_other.append(_p)
|
||||||
|
__path__[:] = _src + _other + _dep
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
"""Native Periscope core modules (finding engine, PCB, checks)."""
|
"""Native Periscope core modules (finding engine, PCB, checks)."""
|
||||||
from pkgutil import extend_path
|
from pkgutil import extend_path
|
||||||
|
|
||||||
__path__ = extend_path(__path__, __name__)
|
__path__ = list(extend_path(__path__, __name__))
|
||||||
|
_src, _other, _dep = [], [], []
|
||||||
|
for _p in __path__:
|
||||||
|
_n = _p.replace("\\", "/")
|
||||||
|
if "/periscope/src/" in _n:
|
||||||
|
_src.append(_p)
|
||||||
|
elif "/periscope/dependency/" in _n:
|
||||||
|
_dep.append(_p)
|
||||||
|
else:
|
||||||
|
_other.append(_p)
|
||||||
|
__path__[:] = _src + _other + _dep
|
||||||
|
|||||||
@@ -385,6 +385,8 @@ class ValidationReport(BaseModel):
|
|||||||
coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK
|
coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK
|
||||||
review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised
|
review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised
|
||||||
not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF)
|
not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF)
|
||||||
|
# M0 protocol cert: empty instances / "nessun protocollo riconosciuto". No Z.
|
||||||
|
protocol_certification: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
class FindingComment(BaseModel):
|
class FindingComment(BaseModel):
|
||||||
|
|||||||
@@ -0,0 +1,781 @@
|
|||||||
|
"""M0 protocol catalog: versioned JSON pack, loader, validator.
|
||||||
|
|
||||||
|
No invented ohms/mm/ps. Constraints without an official cite stay
|
||||||
|
UNKNOWN / CONTROLLER_DEPENDENT / PHY_DEPENDENT / MISSING_SOURCE /
|
||||||
|
VENDOR_DEPENDENT / NOT_APPLICABLE. logical_protocol_id is never the
|
||||||
|
same string as physical_interface_id. pcb_relevant is YES|NO|CONDITIONAL.
|
||||||
|
RECOMMENDED is not FAIL-mandatory. Typical-as-standard is rejected.
|
||||||
|
|
||||||
|
Recognition and L0–L3 certifiers are M1+ — this module only loads packs
|
||||||
|
and builds the empty report section.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||||
|
|
||||||
|
SCHEMA_VERSION = "1.0.0"
|
||||||
|
PACK_MACROPHASE = "M0"
|
||||||
|
|
||||||
|
CATALOG_DIR = Path(__file__).resolve().parent / "protocol_data"
|
||||||
|
CATALOG_PATH = CATALOG_DIR / "catalog.json"
|
||||||
|
SCHEMA_PATH = CATALOG_DIR / f"schema-{SCHEMA_VERSION}.json"
|
||||||
|
|
||||||
|
EMPTY_PROTOCOL_MESSAGE = "nessun protocollo riconosciuto"
|
||||||
|
|
||||||
|
BusType = Literal[
|
||||||
|
"MEMORY",
|
||||||
|
"PROCESSOR_INTERCONNECT",
|
||||||
|
"PERIPHERAL",
|
||||||
|
"CHIP_TO_CHIP",
|
||||||
|
"STORAGE",
|
||||||
|
"GRAPHICS",
|
||||||
|
"SERIAL",
|
||||||
|
"PARALLEL",
|
||||||
|
]
|
||||||
|
PhysicalLayer = Literal[
|
||||||
|
"INTERNAL",
|
||||||
|
"PARALLEL_SINGLE_ENDED",
|
||||||
|
"PARALLEL_DIFFERENTIAL",
|
||||||
|
"SERIAL_SINGLE_ENDED",
|
||||||
|
"SERIAL_DIFFERENTIAL",
|
||||||
|
"MIXED",
|
||||||
|
]
|
||||||
|
PcbRelevant = Literal["YES", "NO", "CONDITIONAL"]
|
||||||
|
ValueKind = Literal[
|
||||||
|
"NUMERIC",
|
||||||
|
"UNKNOWN",
|
||||||
|
"CONTROLLER_DEPENDENT",
|
||||||
|
"PHY_DEPENDENT",
|
||||||
|
"VENDOR_DEPENDENT",
|
||||||
|
"NOT_APPLICABLE",
|
||||||
|
"MISSING_SOURCE",
|
||||||
|
]
|
||||||
|
MandatoryClass = Literal["MANDATORY", "RECOMMENDED", "OPTIONAL", "INFORMATIONAL"]
|
||||||
|
SourceType = Literal[
|
||||||
|
"STANDARD",
|
||||||
|
"CONNECTOR",
|
||||||
|
"PHY",
|
||||||
|
"CONTROLLER",
|
||||||
|
"MEMORY",
|
||||||
|
"COMPONENT",
|
||||||
|
"CABLE",
|
||||||
|
"PCB",
|
||||||
|
"DERIVED",
|
||||||
|
]
|
||||||
|
SourceClass = Literal["NORMATIVE", "VENDOR", "IMPLEMENTATION", "DERIVED"]
|
||||||
|
ValueOrigin = Literal["SPEC", "TYPICAL", "EXAMPLE", "DERIVED"]
|
||||||
|
MeasurementMethod = Literal[
|
||||||
|
"DATASHEET",
|
||||||
|
"TDR",
|
||||||
|
"FIELD_SOLVER",
|
||||||
|
"PCB_STACKUP",
|
||||||
|
"FABRICATOR",
|
||||||
|
"SIMULATION",
|
||||||
|
"CALCULATION",
|
||||||
|
"EXTRACTION",
|
||||||
|
"MEASUREMENT",
|
||||||
|
]
|
||||||
|
LimitKind = Literal["STANDARD", "DESIGN"]
|
||||||
|
ProtocolResult = Literal[
|
||||||
|
"PASS",
|
||||||
|
"FAIL",
|
||||||
|
"WARNING",
|
||||||
|
"UNKNOWN",
|
||||||
|
"NOT_APPLICABLE",
|
||||||
|
"VENDOR_DEPENDENT",
|
||||||
|
"MISSING_SOURCE",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolCatalogError(ValueError):
|
||||||
|
"""Invalid protocol pack (typical-as-standard, missing source, …)."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintSource(BaseModel):
|
||||||
|
document: str | None = None
|
||||||
|
organization: str | None = None
|
||||||
|
revision: str | None = None
|
||||||
|
section: str | None = None
|
||||||
|
table: str | None = None
|
||||||
|
page: str | None = None
|
||||||
|
url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolConstraint(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
id: str
|
||||||
|
parameter: str
|
||||||
|
value_kind: ValueKind
|
||||||
|
mandatory: MandatoryClass
|
||||||
|
source_type: SourceType
|
||||||
|
source_class: SourceClass
|
||||||
|
value: float | None = None
|
||||||
|
unit: str | None = None
|
||||||
|
source: ConstraintSource | None = None
|
||||||
|
conditions: list[str] = Field(default_factory=list)
|
||||||
|
measurement_method: MeasurementMethod | None = None
|
||||||
|
limit_kind: LimitKind | None = None
|
||||||
|
value_origin: ValueOrigin = "SPEC"
|
||||||
|
typical: bool = False
|
||||||
|
|
||||||
|
@field_validator("source_type")
|
||||||
|
@classmethod
|
||||||
|
def _source_type_required(cls, v: str) -> str:
|
||||||
|
if not v:
|
||||||
|
raise ProtocolCatalogError("constraint without source_type")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def _reject_typical_and_bare_numeric(self) -> ProtocolConstraint:
|
||||||
|
if self.typical or self.value_origin in {"TYPICAL", "EXAMPLE"}:
|
||||||
|
if self.source_type == "STANDARD" or self.source_class == "NORMATIVE":
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"{self.id}: typical/example cannot be STANDARD/NORMATIVE"
|
||||||
|
)
|
||||||
|
if self.mandatory == "MANDATORY":
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"{self.id}: typical/example cannot be MANDATORY"
|
||||||
|
)
|
||||||
|
if self.value_kind == "NUMERIC":
|
||||||
|
if self.value is None:
|
||||||
|
raise ProtocolCatalogError(f"{self.id}: NUMERIC needs a value")
|
||||||
|
if self.source is None or not (
|
||||||
|
self.source.document or self.source.organization
|
||||||
|
):
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"{self.id}: NUMERIC constraint needs an official cite"
|
||||||
|
)
|
||||||
|
elif self.value is not None:
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"{self.id}: non-NUMERIC value_kind cannot carry a number"
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_fail_mandatory(self) -> bool:
|
||||||
|
"""FAIL is allowed only for MANDATORY. RECOMMENDED never FAIL-mandatory."""
|
||||||
|
return self.mandatory == "MANDATORY"
|
||||||
|
|
||||||
|
|
||||||
|
class LogicalProtocol(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
bus_type: BusType
|
||||||
|
family: str = ""
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class PhysicalInterface(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
id: str
|
||||||
|
logical_protocol_id: str
|
||||||
|
bus_type: BusType
|
||||||
|
physical_layer: PhysicalLayer
|
||||||
|
pcb_relevant: PcbRelevant
|
||||||
|
connector: str = "none"
|
||||||
|
recognition_hints: list[str] = Field(default_factory=list)
|
||||||
|
required_checks: list[str] = Field(default_factory=list)
|
||||||
|
constraints: list[ProtocolConstraint] = Field(default_factory=list)
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def _logical_differs_from_physical(self) -> PhysicalInterface:
|
||||||
|
if self.id == self.logical_protocol_id:
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"{self.id}: logical_protocol_id must differ from physical_interface_id"
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolCatalog(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
schema_version: str
|
||||||
|
logical_protocols: list[LogicalProtocol]
|
||||||
|
physical_interfaces: list[PhysicalInterface]
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def _index_integrity(self) -> ProtocolCatalog:
|
||||||
|
if self.schema_version != SCHEMA_VERSION:
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"schema_version {self.schema_version!r} != {SCHEMA_VERSION}"
|
||||||
|
)
|
||||||
|
logical_ids = [p.id for p in self.logical_protocols]
|
||||||
|
if len(logical_ids) != len(set(logical_ids)):
|
||||||
|
raise ProtocolCatalogError("duplicate logical_protocol id")
|
||||||
|
physical_ids = [p.id for p in self.physical_interfaces]
|
||||||
|
if len(physical_ids) != len(set(physical_ids)):
|
||||||
|
raise ProtocolCatalogError("duplicate physical_interface id")
|
||||||
|
logical_set = set(logical_ids)
|
||||||
|
overlap = logical_set & set(physical_ids)
|
||||||
|
if overlap:
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"id used as both logical and physical: {sorted(overlap)}"
|
||||||
|
)
|
||||||
|
for iface in self.physical_interfaces:
|
||||||
|
if iface.logical_protocol_id not in logical_set:
|
||||||
|
raise ProtocolCatalogError(
|
||||||
|
f"{iface.id}: unknown logical_protocol_id "
|
||||||
|
f"{iface.logical_protocol_id}"
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolCertificationSection(BaseModel):
|
||||||
|
"""Report hook. M0: no instances, no Z numbers."""
|
||||||
|
|
||||||
|
schema_version: str = SCHEMA_VERSION
|
||||||
|
macrophase: str = PACK_MACROPHASE
|
||||||
|
recognized_instances: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
message: str = EMPTY_PROTOCOL_MESSAGE
|
||||||
|
max_level_reached: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def empty_protocol_section() -> ProtocolCertificationSection:
|
||||||
|
return ProtocolCertificationSection()
|
||||||
|
|
||||||
|
|
||||||
|
def fail_mandatory(constraint: ProtocolConstraint) -> bool:
|
||||||
|
return constraint.is_fail_mandatory()
|
||||||
|
|
||||||
|
|
||||||
|
def map_protocol_outcome(
|
||||||
|
result: ProtocolResult,
|
||||||
|
mandatory: MandatoryClass,
|
||||||
|
) -> tuple[str, str, str]:
|
||||||
|
"""(finding_class, status, evidence_status) — RECOMMENDED never ERROR FAIL."""
|
||||||
|
if result == "PASS":
|
||||||
|
return "RULE", "INFO", "SUFFICIENT"
|
||||||
|
if result == "NOT_APPLICABLE":
|
||||||
|
return "INFO", "INFO", "SUFFICIENT"
|
||||||
|
if result in {"UNKNOWN", "MISSING_SOURCE"}:
|
||||||
|
return "REVIEW", "WARNING", "INSUFFICIENT"
|
||||||
|
if result == "VENDOR_DEPENDENT":
|
||||||
|
return "REVIEW", "WARNING", "INSUFFICIENT"
|
||||||
|
if result == "FAIL":
|
||||||
|
if mandatory != "MANDATORY":
|
||||||
|
return "RISK", "WARNING", "SUFFICIENT"
|
||||||
|
return "RULE", "ERROR", "SUFFICIENT"
|
||||||
|
if result == "WARNING":
|
||||||
|
return "RISK", "WARNING", "SUFFICIENT"
|
||||||
|
return "REVIEW", "WARNING", "INSUFFICIENT"
|
||||||
|
|
||||||
|
|
||||||
|
def catalog_json_schema() -> dict[str, Any]:
|
||||||
|
return ProtocolCatalog.model_json_schema()
|
||||||
|
|
||||||
|
|
||||||
|
def load_catalog(path: Path | None = None) -> ProtocolCatalog:
|
||||||
|
target = path or CATALOG_PATH
|
||||||
|
raw = json.loads(target.read_text(encoding="utf-8"))
|
||||||
|
return parse_catalog(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_catalog(raw: Any) -> ProtocolCatalog:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ProtocolCatalogError("catalog must be a JSON object")
|
||||||
|
try:
|
||||||
|
return ProtocolCatalog.model_validate(raw)
|
||||||
|
except ProtocolCatalogError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise ProtocolCatalogError(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _c(
|
||||||
|
iface_id: str,
|
||||||
|
parameter: str,
|
||||||
|
value_kind: ValueKind,
|
||||||
|
*,
|
||||||
|
mandatory: MandatoryClass = "MANDATORY",
|
||||||
|
source_type: SourceType = "STANDARD",
|
||||||
|
source_class: SourceClass = "NORMATIVE",
|
||||||
|
conditions: list[str] | None = None,
|
||||||
|
measurement_method: MeasurementMethod | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
row: dict[str, Any] = {
|
||||||
|
"id": f"{iface_id}-{parameter}",
|
||||||
|
"parameter": parameter,
|
||||||
|
"value_kind": value_kind,
|
||||||
|
"mandatory": mandatory,
|
||||||
|
"source_type": source_type,
|
||||||
|
"source_class": source_class,
|
||||||
|
"value": None,
|
||||||
|
"unit": None,
|
||||||
|
"source": None,
|
||||||
|
"conditions": conditions or [],
|
||||||
|
"value_origin": "SPEC",
|
||||||
|
"typical": False,
|
||||||
|
}
|
||||||
|
if measurement_method:
|
||||||
|
row["measurement_method"] = measurement_method
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _iface(
|
||||||
|
pid: str,
|
||||||
|
logical: str,
|
||||||
|
bus_type: BusType,
|
||||||
|
layer: PhysicalLayer,
|
||||||
|
pcb: PcbRelevant,
|
||||||
|
checks: list[str],
|
||||||
|
constraints: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
connector: str = "none",
|
||||||
|
hints: list[str] | None = None,
|
||||||
|
notes: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": pid,
|
||||||
|
"logical_protocol_id": logical,
|
||||||
|
"bus_type": bus_type,
|
||||||
|
"physical_layer": layer,
|
||||||
|
"pcb_relevant": pcb,
|
||||||
|
"connector": connector,
|
||||||
|
"recognition_hints": hints or [],
|
||||||
|
"required_checks": checks,
|
||||||
|
"constraints": constraints,
|
||||||
|
"notes": notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
USB2_CHECKS = [
|
||||||
|
"differential_impedance",
|
||||||
|
"intra_pair_skew",
|
||||||
|
"topology",
|
||||||
|
"length",
|
||||||
|
"vias",
|
||||||
|
"return_path",
|
||||||
|
]
|
||||||
|
ETH_CHECKS = [
|
||||||
|
"differential_impedance",
|
||||||
|
"pair_skew",
|
||||||
|
"length",
|
||||||
|
"magnetics",
|
||||||
|
"return_path",
|
||||||
|
"phy_requirements",
|
||||||
|
]
|
||||||
|
HDMI_CHECKS = [
|
||||||
|
"differential_impedance",
|
||||||
|
"pair_skew",
|
||||||
|
"lane_relationships",
|
||||||
|
"insertion_loss",
|
||||||
|
"return_loss",
|
||||||
|
"vias",
|
||||||
|
"connector",
|
||||||
|
]
|
||||||
|
DDR_CHECKS = [
|
||||||
|
"byte_lane_mapping",
|
||||||
|
"dq_to_dqs_skew",
|
||||||
|
"byte_lane_skew",
|
||||||
|
"ck_to_command_skew",
|
||||||
|
"topology",
|
||||||
|
"impedance",
|
||||||
|
"termination",
|
||||||
|
"via_count",
|
||||||
|
"stub_length",
|
||||||
|
"return_path",
|
||||||
|
]
|
||||||
|
QSPI_CHECKS = ["clock_length", "data_length", "stubs", "vias", "return_path"]
|
||||||
|
OCTAL_CHECKS = [
|
||||||
|
"dq_group",
|
||||||
|
"dqs_relationship_if_present",
|
||||||
|
"clock_to_data_skew",
|
||||||
|
"topology",
|
||||||
|
"impedance_if_required",
|
||||||
|
"length",
|
||||||
|
"vias",
|
||||||
|
"stubs",
|
||||||
|
]
|
||||||
|
EMMC_CHECKS = ["cmd_dat_grouping", "clk_reference", "impedance", "skew", "length"]
|
||||||
|
UFS_CHECKS = [
|
||||||
|
"differential_impedance",
|
||||||
|
"intra_pair_skew",
|
||||||
|
"insertion_loss",
|
||||||
|
"return_loss",
|
||||||
|
"via_transition",
|
||||||
|
"ac_coupling",
|
||||||
|
]
|
||||||
|
PCIE_CHECKS = [
|
||||||
|
"differential_impedance",
|
||||||
|
"intra_pair_skew",
|
||||||
|
"lane_to_lane_skew",
|
||||||
|
"insertion_loss",
|
||||||
|
"return_loss",
|
||||||
|
"vias",
|
||||||
|
"connectors",
|
||||||
|
"return_path",
|
||||||
|
]
|
||||||
|
HYPERBUS_CHECKS = [
|
||||||
|
"dq_to_rwds_skew",
|
||||||
|
"ck_to_dq_relationship",
|
||||||
|
"impedance",
|
||||||
|
"topology",
|
||||||
|
"length",
|
||||||
|
"vias",
|
||||||
|
"stubs",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_m0_catalog() -> dict[str, Any]:
|
||||||
|
"""Skeleton catalog. No numeric USB/ETH/HDMI/DDR values."""
|
||||||
|
logical: list[dict[str, Any]] = []
|
||||||
|
physical: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
def L(pid: str, name: str, bus: BusType, family: str, notes: str = "") -> None:
|
||||||
|
logical.append({
|
||||||
|
"id": pid, "name": name, "bus_type": bus, "family": family, "notes": notes,
|
||||||
|
})
|
||||||
|
|
||||||
|
L("usb2-ls-fs", "USB 2.0 Low/Full Speed", "SERIAL", "USB")
|
||||||
|
L("usb2-hs", "USB 2.0 High Speed", "SERIAL", "USB")
|
||||||
|
L("usb-c-usb2", "USB Type-C carrying USB 2.x only", "SERIAL", "USB")
|
||||||
|
L("usb-c-pd", "USB Power Delivery on CC", "SERIAL", "USB")
|
||||||
|
L("100base-tx", "100BASE-TX Fast Ethernet", "SERIAL", "ETHERNET")
|
||||||
|
L("hdmi-1.4", "HDMI 1.4 TMDS", "SERIAL", "HDMI")
|
||||||
|
L("hdmi-2.0", "HDMI 2.0 TMDS", "SERIAL", "HDMI")
|
||||||
|
L("ddr-family", "DDR family (abstract)", "MEMORY", "DDR")
|
||||||
|
for gen, title in (
|
||||||
|
("ddr1", "DDR1"), ("ddr2", "DDR2"), ("ddr3", "DDR3"),
|
||||||
|
("ddr3l", "DDR3L"), ("ddr4", "DDR4"), ("ddr5", "DDR5"),
|
||||||
|
):
|
||||||
|
L(gen, title, "MEMORY", "DDR")
|
||||||
|
L("lpddr-family", "LPDDR family (abstract)", "MEMORY", "LPDDR")
|
||||||
|
for gen, title in (
|
||||||
|
("lpddr2", "LPDDR2"), ("lpddr3", "LPDDR3"), ("lpddr4", "LPDDR4"),
|
||||||
|
("lpddr4x", "LPDDR4X"), ("lpddr5", "LPDDR5"), ("lpddr5x", "LPDDR5X"),
|
||||||
|
):
|
||||||
|
L(gen, title, "MEMORY", "LPDDR")
|
||||||
|
L("hbm-family", "HBM family (abstract)", "MEMORY", "HBM")
|
||||||
|
for gen, title in (
|
||||||
|
("hbm", "HBM"), ("hbm2", "HBM2"), ("hbm2e", "HBM2E"),
|
||||||
|
("hbm3", "HBM3"), ("hbm3e", "HBM3E"),
|
||||||
|
):
|
||||||
|
L(gen, title, "MEMORY", "HBM")
|
||||||
|
L("hyperbus", "HyperBus", "MEMORY", "HYPERBUS")
|
||||||
|
L("hyperram", "HyperRAM", "MEMORY", "HYPERBUS")
|
||||||
|
L("spi-nor", "SPI NOR", "MEMORY", "SPI")
|
||||||
|
L("qspi", "Quad SPI", "MEMORY", "SPI")
|
||||||
|
L("octal-spi", "Octal SPI", "MEMORY", "SPI")
|
||||||
|
L("opi", "OPI", "MEMORY", "SPI")
|
||||||
|
L("xspi", "xSPI", "MEMORY", "SPI")
|
||||||
|
L("emmc", "eMMC", "STORAGE", "STORAGE")
|
||||||
|
L("ufs", "UFS", "STORAGE", "STORAGE")
|
||||||
|
L("amba", "AMBA (umbrella)", "PROCESSOR_INTERCONNECT", "AMBA")
|
||||||
|
for pid, name in (
|
||||||
|
("axi4", "AXI4"), ("axi4-lite", "AXI4-Lite"), ("axi4-stream", "AXI4-Stream"),
|
||||||
|
("ahb", "AHB"), ("ahb-lite", "AHB-Lite"), ("apb", "APB"),
|
||||||
|
("ace", "ACE"), ("ace-lite", "ACE-Lite"), ("amba-chi", "AMBA CHI"),
|
||||||
|
):
|
||||||
|
L(pid, name, "PROCESSOR_INTERCONNECT", "AMBA")
|
||||||
|
L("wishbone", "Wishbone", "PROCESSOR_INTERCONNECT", "OPEN_BUS")
|
||||||
|
L("avalon-mm", "Avalon-MM", "PROCESSOR_INTERCONNECT", "INTEL_FPGA")
|
||||||
|
L("avalon-st", "Avalon-ST", "PROCESSOR_INTERCONNECT", "INTEL_FPGA")
|
||||||
|
L("tilelink", "TileLink", "PROCESSOR_INTERCONNECT", "RISC-V")
|
||||||
|
L("pcie", "PCI Express (logical)", "CHIP_TO_CHIP", "PCIE")
|
||||||
|
L("cxl", "CXL (logical)", "CHIP_TO_CHIP", "CXL")
|
||||||
|
|
||||||
|
missing_usb = [
|
||||||
|
_c("usb2-hs-dpair", "differential_impedance", "MISSING_SOURCE",
|
||||||
|
measurement_method="PCB_STACKUP"),
|
||||||
|
_c("usb2-hs-dpair", "intra_pair_skew", "MISSING_SOURCE",
|
||||||
|
measurement_method="CALCULATION"),
|
||||||
|
_c("usb2-hs-dpair", "topology", "UNKNOWN"),
|
||||||
|
_c("usb2-hs-dpair", "length", "UNKNOWN"),
|
||||||
|
_c("usb2-hs-dpair", "vias", "UNKNOWN"),
|
||||||
|
_c("usb2-hs-dpair", "return_path", "UNKNOWN", mandatory="RECOMMENDED",
|
||||||
|
source_class="VENDOR"),
|
||||||
|
]
|
||||||
|
physical.append(_iface(
|
||||||
|
"usb2-ls-fs-dpair", "usb2-ls-fs", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
|
||||||
|
USB2_CHECKS,
|
||||||
|
[
|
||||||
|
_c("usb2-ls-fs-dpair", "differential_impedance", "MISSING_SOURCE",
|
||||||
|
measurement_method="PCB_STACKUP"),
|
||||||
|
_c("usb2-ls-fs-dpair", "intra_pair_skew", "MISSING_SOURCE"),
|
||||||
|
_c("usb2-ls-fs-dpair", "topology", "UNKNOWN"),
|
||||||
|
],
|
||||||
|
hints=["USB_D+", "USB_D-", "USB_DP", "USB_DM", "D+", "D-"],
|
||||||
|
notes="LS/FS pair. No invented Z.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"usb2-hs-dpair", "usb2-hs", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
|
||||||
|
USB2_CHECKS, missing_usb,
|
||||||
|
hints=["USB_D+", "USB_D-", "USB_HS"],
|
||||||
|
notes="HS pair. Official USB-IF cite required before NUMERIC Z.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"usb-c-usb2-receptacle", "usb-c-usb2", "SERIAL", "MIXED", "YES",
|
||||||
|
USB2_CHECKS + ["cc_rd_rp", "vbus_gnd", "superspeed_absence"],
|
||||||
|
[
|
||||||
|
_c("usb-c-usb2-receptacle", "differential_impedance", "MISSING_SOURCE"),
|
||||||
|
_c("usb-c-usb2-receptacle", "cc_termination", "MISSING_SOURCE",
|
||||||
|
source_type="CONNECTOR", source_class="NORMATIVE"),
|
||||||
|
_c("usb-c-usb2-receptacle", "superspeed_pairs", "NOT_APPLICABLE",
|
||||||
|
mandatory="INFORMATIONAL",
|
||||||
|
conditions=["usb2_only"]),
|
||||||
|
],
|
||||||
|
connector="Type-C",
|
||||||
|
hints=["CC1", "CC2", "USB_C", "TYPE_C", "A5", "B5"],
|
||||||
|
notes="Type-C carrying USB2. Electrical USB2 + CC. No invented Rd ohms.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"usb-c-pd-cc", "usb-c-pd", "SERIAL", "SERIAL_SINGLE_ENDED", "CONDITIONAL",
|
||||||
|
["cc_pd_contract"],
|
||||||
|
[_c("usb-c-pd-cc", "pd_contract", "MISSING_SOURCE",
|
||||||
|
source_type="STANDARD", conditions=["pd_present"])],
|
||||||
|
connector="Type-C",
|
||||||
|
hints=["CC1", "CC2", "USB_PD"],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"100base-tx-mdi", "100base-tx", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
|
||||||
|
ETH_CHECKS,
|
||||||
|
[
|
||||||
|
_c("100base-tx-mdi", "differential_impedance", "MISSING_SOURCE",
|
||||||
|
source_type="STANDARD"),
|
||||||
|
_c("100base-tx-mdi", "pair_skew", "MISSING_SOURCE"),
|
||||||
|
_c("100base-tx-mdi", "magnetics", "PHY_DEPENDENT",
|
||||||
|
source_type="PHY", source_class="VENDOR"),
|
||||||
|
_c("100base-tx-mdi", "bob_smith", "UNKNOWN", mandatory="RECOMMENDED",
|
||||||
|
source_type="PHY", source_class="VENDOR"),
|
||||||
|
],
|
||||||
|
connector="RJ45",
|
||||||
|
hints=["TD+", "TD-", "RD+", "RD-", "MDI", "RJ45", "100BASE"],
|
||||||
|
notes="IEEE numbers stay MISSING_SOURCE until the spec is on file.",
|
||||||
|
))
|
||||||
|
for hid, lid, conn, note in (
|
||||||
|
("hdmi-1.4-tmds-type-a", "hdmi-1.4", "HDMI-A", "Type A land"),
|
||||||
|
("hdmi-1.4-tmds-type-c-mini", "hdmi-1.4", "HDMI-C-mini",
|
||||||
|
"Mini Type C land; electrical HDMI 1.4, not a second standard"),
|
||||||
|
("hdmi-2.0-tmds-type-a", "hdmi-2.0", "HDMI-A", "Type A land"),
|
||||||
|
("hdmi-2.0-tmds-type-c-mini", "hdmi-2.0", "HDMI-C-mini",
|
||||||
|
"Mini Type C land; electrical HDMI 2.0"),
|
||||||
|
):
|
||||||
|
physical.append(_iface(
|
||||||
|
hid, lid, "SERIAL", "SERIAL_DIFFERENTIAL", "YES", HDMI_CHECKS,
|
||||||
|
[
|
||||||
|
_c(hid, "differential_impedance", "MISSING_SOURCE"),
|
||||||
|
_c(hid, "pair_skew", "MISSING_SOURCE"),
|
||||||
|
_c(hid, "insertion_loss", "MISSING_SOURCE", mandatory="RECOMMENDED"),
|
||||||
|
],
|
||||||
|
connector=conn, hints=["HDMI", "TMDS", "TX0", "TX1", "TX2", "CLK"],
|
||||||
|
notes=note,
|
||||||
|
))
|
||||||
|
|
||||||
|
def _mem_constraints(pid: str, kind: ValueKind, source_type: SourceType) -> list[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",
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
_c(pid, p, kind, source_type=source_type, source_class=cls)
|
||||||
|
for p in params
|
||||||
|
]
|
||||||
|
|
||||||
|
physical.append(_iface(
|
||||||
|
"ddr-family-sdram-pcb", "ddr-family", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
|
||||||
|
DDR_CHECKS, _mem_constraints("ddr-family-sdram-pcb", "CONTROLLER_DEPENDENT", "CONTROLLER"),
|
||||||
|
hints=["DQ", "DQS", "CK", "A0", "RAS", "CAS", "WE"],
|
||||||
|
notes="No universal mm/ps for DDR.",
|
||||||
|
))
|
||||||
|
for gen in ("ddr1", "ddr2", "ddr3", "ddr3l", "ddr4", "ddr5"):
|
||||||
|
kind: ValueKind = (
|
||||||
|
"PHY_DEPENDENT" if gen in {"ddr4", "ddr5"} else "CONTROLLER_DEPENDENT"
|
||||||
|
)
|
||||||
|
src: SourceType = "PHY" if kind == "PHY_DEPENDENT" else "CONTROLLER"
|
||||||
|
physical.append(_iface(
|
||||||
|
f"{gen}-sdram-pcb", gen, "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
|
||||||
|
DDR_CHECKS, _mem_constraints(f"{gen}-sdram-pcb", kind, src),
|
||||||
|
hints=["DQ", "DQS", "CK", gen.upper()],
|
||||||
|
notes="Controller/PHY dependent. Zero universal skew/length.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"lpddr-family-pcb", "lpddr-family", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
|
||||||
|
DDR_CHECKS, _mem_constraints("lpddr-family-pcb", "CONTROLLER_DEPENDENT", "CONTROLLER"),
|
||||||
|
hints=["DQ", "DQS", "CK", "CA", "LPDDR"],
|
||||||
|
))
|
||||||
|
for gen, kind in (
|
||||||
|
("lpddr2", "CONTROLLER_DEPENDENT"),
|
||||||
|
("lpddr3", "CONTROLLER_DEPENDENT"),
|
||||||
|
("lpddr4", "PHY_DEPENDENT"),
|
||||||
|
("lpddr4x", "PHY_DEPENDENT"),
|
||||||
|
("lpddr5", "PHY_DEPENDENT"),
|
||||||
|
("lpddr5x", "PHY_DEPENDENT"),
|
||||||
|
):
|
||||||
|
src = "PHY" if kind == "PHY_DEPENDENT" else "CONTROLLER"
|
||||||
|
physical.append(_iface(
|
||||||
|
f"{gen}-pcb", gen, "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
|
||||||
|
DDR_CHECKS, _mem_constraints(f"{gen}-pcb", kind, src), # type: ignore[arg-type]
|
||||||
|
hints=["DQ", "DQS", "CK", "CA", "WCK", gen.upper()],
|
||||||
|
))
|
||||||
|
for gen in ("hbm", "hbm2", "hbm2e", "hbm3", "hbm3e"):
|
||||||
|
physical.append(_iface(
|
||||||
|
f"{gen}-package", gen, "MEMORY", "INTERNAL", "NO",
|
||||||
|
["package", "silicon"],
|
||||||
|
[
|
||||||
|
_c(f"{gen}-package", "pcb_individual_dq_routing", "NOT_APPLICABLE",
|
||||||
|
mandatory="INFORMATIONAL", source_type="PCB", source_class="IMPLEMENTATION"),
|
||||||
|
],
|
||||||
|
notes="HBM is package/interposer. Do not apply DDR DQ PCB rules.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"hbm-family-package", "hbm-family", "MEMORY", "INTERNAL", "NO",
|
||||||
|
["package", "silicon"],
|
||||||
|
[_c("hbm-family-package", "pcb_individual_dq_routing", "NOT_APPLICABLE",
|
||||||
|
mandatory="INFORMATIONAL", source_type="PCB", source_class="IMPLEMENTATION")],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"hyperbus-pcb", "hyperbus", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
|
||||||
|
HYPERBUS_CHECKS,
|
||||||
|
[
|
||||||
|
_c("hyperbus-pcb", "dq_to_rwds_skew", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
_c("hyperbus-pcb", "impedance", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
],
|
||||||
|
hints=["RWDS", "DQ0", "CK", "HYPERBUS"],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"hyperram-pcb", "hyperram", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
|
||||||
|
HYPERBUS_CHECKS,
|
||||||
|
[_c("hyperram-pcb", "impedance", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR")],
|
||||||
|
hints=["HYPERRAM", "RWDS"],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"spi-nor-pcb", "spi-nor", "MEMORY", "SERIAL_SINGLE_ENDED", "YES",
|
||||||
|
["topology", "length"],
|
||||||
|
[
|
||||||
|
_c("spi-nor-pcb", "topology", "UNKNOWN", mandatory="RECOMMENDED",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
_c("spi-nor-pcb", "impedance", "NOT_APPLICABLE", mandatory="INFORMATIONAL",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
_c("spi-nor-pcb", "length", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
],
|
||||||
|
hints=["SCLK", "CS", "MOSI", "MISO", "SPI"],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"qspi-pcb", "qspi", "MEMORY", "SERIAL_SINGLE_ENDED", "YES", QSPI_CHECKS,
|
||||||
|
[
|
||||||
|
_c("qspi-pcb", "clock_length", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
_c("qspi-pcb", "impedance", "UNKNOWN",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
],
|
||||||
|
hints=["QSPI", "IO0", "IO1", "IO2", "IO3", "SCLK"],
|
||||||
|
))
|
||||||
|
for pid, logical_id in (
|
||||||
|
("octal-spi-pcb", "octal-spi"), ("opi-pcb", "opi"), ("xspi-pcb", "xspi"),
|
||||||
|
):
|
||||||
|
physical.append(_iface(
|
||||||
|
pid, logical_id, "MEMORY", "SERIAL_SINGLE_ENDED", "YES", OCTAL_CHECKS,
|
||||||
|
[
|
||||||
|
_c(pid, "clock_to_data_skew", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
_c(pid, "impedance", "UNKNOWN",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
],
|
||||||
|
hints=["DQ0", "DQS", "OSPI", "XSPI", "OCTAL"],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"emmc-pcb", "emmc", "STORAGE", "PARALLEL_SINGLE_ENDED", "YES", EMMC_CHECKS,
|
||||||
|
[
|
||||||
|
_c("emmc-pcb", "impedance", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
_c("emmc-pcb", "skew", "UNKNOWN", conditions=["mode=HS200|HS400"]),
|
||||||
|
],
|
||||||
|
hints=["eMMC", "DAT0", "CMD", "MMC_CLK"],
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"ufs-mphy-pcb", "ufs", "STORAGE", "SERIAL_DIFFERENTIAL", "YES", UFS_CHECKS,
|
||||||
|
[
|
||||||
|
_c("ufs-mphy-pcb", "differential_impedance", "MISSING_SOURCE"),
|
||||||
|
_c("ufs-mphy-pcb", "intra_pair_skew", "MISSING_SOURCE"),
|
||||||
|
],
|
||||||
|
hints=["UFS", "MPHY", "TX+", "RX+"],
|
||||||
|
))
|
||||||
|
|
||||||
|
amba = [
|
||||||
|
"axi4", "axi4-lite", "axi4-stream", "ahb", "ahb-lite", "apb",
|
||||||
|
"ace", "ace-lite", "amba-chi", "amba",
|
||||||
|
]
|
||||||
|
for pid in amba:
|
||||||
|
physical.append(_iface(
|
||||||
|
f"{pid}-internal", pid, "PROCESSOR_INTERCONNECT", "INTERNAL", "NO",
|
||||||
|
[],
|
||||||
|
[_c(f"{pid}-internal", "pcb_routing", "NOT_APPLICABLE",
|
||||||
|
mandatory="INFORMATIONAL", source_type="PCB",
|
||||||
|
source_class="IMPLEMENTATION")],
|
||||||
|
notes="On-chip interconnect. Recognisable; not PCB routing.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"axi4-chip-to-chip-phy", "axi4", "CHIP_TO_CHIP", "MIXED", "CONDITIONAL",
|
||||||
|
["impedance", "timing", "topology", "voltage"],
|
||||||
|
[
|
||||||
|
_c("axi4-chip-to-chip-phy", "impedance", "PHY_DEPENDENT",
|
||||||
|
source_type="PHY", source_class="VENDOR"),
|
||||||
|
_c("axi4-chip-to-chip-phy", "timing", "VENDOR_DEPENDENT",
|
||||||
|
source_type="COMPONENT", source_class="VENDOR"),
|
||||||
|
],
|
||||||
|
notes="AXI4 name is logical. PCB rules come from the external PHY, not AXI4.",
|
||||||
|
))
|
||||||
|
for pid in ("wishbone", "avalon-mm", "avalon-st", "tilelink"):
|
||||||
|
physical.append(_iface(
|
||||||
|
f"{pid}-internal", pid, "PROCESSOR_INTERCONNECT", "INTERNAL", "NO",
|
||||||
|
[],
|
||||||
|
[_c(f"{pid}-internal", "pcb_routing", "NOT_APPLICABLE",
|
||||||
|
mandatory="INFORMATIONAL", source_type="PCB",
|
||||||
|
source_class="IMPLEMENTATION")],
|
||||||
|
))
|
||||||
|
|
||||||
|
physical.append(_iface(
|
||||||
|
"pcie-phy-pcb", "pcie", "CHIP_TO_CHIP", "SERIAL_DIFFERENTIAL", "YES",
|
||||||
|
PCIE_CHECKS,
|
||||||
|
[
|
||||||
|
_c("pcie-phy-pcb", "differential_impedance", "MISSING_SOURCE"),
|
||||||
|
_c("pcie-phy-pcb", "intra_pair_skew", "MISSING_SOURCE"),
|
||||||
|
_c("pcie-phy-pcb", "insertion_loss", "MISSING_SOURCE"),
|
||||||
|
],
|
||||||
|
hints=["PCIE", "PETX", "PERX", "REFCLK"],
|
||||||
|
notes="Logical vs physical split. Empty numbers until PCI-SIG cite.",
|
||||||
|
))
|
||||||
|
physical.append(_iface(
|
||||||
|
"cxl-phy-pcb", "cxl", "CHIP_TO_CHIP", "SERIAL_DIFFERENTIAL", "YES",
|
||||||
|
PCIE_CHECKS,
|
||||||
|
[
|
||||||
|
_c("cxl-phy-pcb", "differential_impedance", "MISSING_SOURCE"),
|
||||||
|
_c("cxl-phy-pcb", "intra_pair_skew", "MISSING_SOURCE"),
|
||||||
|
],
|
||||||
|
hints=["CXL"],
|
||||||
|
notes="Physical electrical pack empty until official cite.",
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"logical_protocols": logical,
|
||||||
|
"physical_interfaces": physical,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_catalog_files(directory: Path | None = None) -> tuple[Path, Path]:
|
||||||
|
directory = directory or CATALOG_DIR
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog_path = directory / "catalog.json"
|
||||||
|
schema_path = directory / f"schema-{SCHEMA_VERSION}.json"
|
||||||
|
data = build_m0_catalog()
|
||||||
|
parse_catalog(data)
|
||||||
|
catalog_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||||
|
schema_path.write_text(
|
||||||
|
json.dumps(catalog_json_schema(), indent=2) + "\n", encoding="utf-8",
|
||||||
|
)
|
||||||
|
return catalog_path, schema_path
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,422 @@
|
|||||||
|
{
|
||||||
|
"$defs": {
|
||||||
|
"ConstraintSource": {
|
||||||
|
"properties": {
|
||||||
|
"document": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Document"
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Organization"
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Revision"
|
||||||
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Section"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Table"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Page"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Url"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ConstraintSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"LogicalProtocol": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"title": "Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bus_type": {
|
||||||
|
"enum": [
|
||||||
|
"MEMORY",
|
||||||
|
"PROCESSOR_INTERCONNECT",
|
||||||
|
"PERIPHERAL",
|
||||||
|
"CHIP_TO_CHIP",
|
||||||
|
"STORAGE",
|
||||||
|
"GRAPHICS",
|
||||||
|
"SERIAL",
|
||||||
|
"PARALLEL"
|
||||||
|
],
|
||||||
|
"title": "Bus Type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"family": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Family",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Notes",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"bus_type"
|
||||||
|
],
|
||||||
|
"title": "LogicalProtocol",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PhysicalInterface": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"title": "Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logical_protocol_id": {
|
||||||
|
"title": "Logical Protocol Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bus_type": {
|
||||||
|
"enum": [
|
||||||
|
"MEMORY",
|
||||||
|
"PROCESSOR_INTERCONNECT",
|
||||||
|
"PERIPHERAL",
|
||||||
|
"CHIP_TO_CHIP",
|
||||||
|
"STORAGE",
|
||||||
|
"GRAPHICS",
|
||||||
|
"SERIAL",
|
||||||
|
"PARALLEL"
|
||||||
|
],
|
||||||
|
"title": "Bus Type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"physical_layer": {
|
||||||
|
"enum": [
|
||||||
|
"INTERNAL",
|
||||||
|
"PARALLEL_SINGLE_ENDED",
|
||||||
|
"PARALLEL_DIFFERENTIAL",
|
||||||
|
"SERIAL_SINGLE_ENDED",
|
||||||
|
"SERIAL_DIFFERENTIAL",
|
||||||
|
"MIXED"
|
||||||
|
],
|
||||||
|
"title": "Physical Layer",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pcb_relevant": {
|
||||||
|
"enum": [
|
||||||
|
"YES",
|
||||||
|
"NO",
|
||||||
|
"CONDITIONAL"
|
||||||
|
],
|
||||||
|
"title": "Pcb Relevant",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"connector": {
|
||||||
|
"default": "none",
|
||||||
|
"title": "Connector",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"recognition_hints": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Recognition Hints",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"required_checks": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Required Checks",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ProtocolConstraint"
|
||||||
|
},
|
||||||
|
"title": "Constraints",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Notes",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"logical_protocol_id",
|
||||||
|
"bus_type",
|
||||||
|
"physical_layer",
|
||||||
|
"pcb_relevant"
|
||||||
|
],
|
||||||
|
"title": "PhysicalInterface",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ProtocolConstraint": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"title": "Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parameter": {
|
||||||
|
"title": "Parameter",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value_kind": {
|
||||||
|
"enum": [
|
||||||
|
"NUMERIC",
|
||||||
|
"UNKNOWN",
|
||||||
|
"CONTROLLER_DEPENDENT",
|
||||||
|
"PHY_DEPENDENT",
|
||||||
|
"VENDOR_DEPENDENT",
|
||||||
|
"NOT_APPLICABLE",
|
||||||
|
"MISSING_SOURCE"
|
||||||
|
],
|
||||||
|
"title": "Value Kind",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mandatory": {
|
||||||
|
"enum": [
|
||||||
|
"MANDATORY",
|
||||||
|
"RECOMMENDED",
|
||||||
|
"OPTIONAL",
|
||||||
|
"INFORMATIONAL"
|
||||||
|
],
|
||||||
|
"title": "Mandatory",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source_type": {
|
||||||
|
"enum": [
|
||||||
|
"STANDARD",
|
||||||
|
"CONNECTOR",
|
||||||
|
"PHY",
|
||||||
|
"CONTROLLER",
|
||||||
|
"MEMORY",
|
||||||
|
"COMPONENT",
|
||||||
|
"CABLE",
|
||||||
|
"PCB",
|
||||||
|
"DERIVED"
|
||||||
|
],
|
||||||
|
"title": "Source Type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source_class": {
|
||||||
|
"enum": [
|
||||||
|
"NORMATIVE",
|
||||||
|
"VENDOR",
|
||||||
|
"IMPLEMENTATION",
|
||||||
|
"DERIVED"
|
||||||
|
],
|
||||||
|
"title": "Source Class",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Value"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Unit"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ConstraintSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": "Conditions",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"measurement_method": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"DATASHEET",
|
||||||
|
"TDR",
|
||||||
|
"FIELD_SOLVER",
|
||||||
|
"PCB_STACKUP",
|
||||||
|
"FABRICATOR",
|
||||||
|
"SIMULATION",
|
||||||
|
"CALCULATION",
|
||||||
|
"EXTRACTION",
|
||||||
|
"MEASUREMENT"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Measurement Method"
|
||||||
|
},
|
||||||
|
"limit_kind": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"STANDARD",
|
||||||
|
"DESIGN"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Limit Kind"
|
||||||
|
},
|
||||||
|
"value_origin": {
|
||||||
|
"default": "SPEC",
|
||||||
|
"enum": [
|
||||||
|
"SPEC",
|
||||||
|
"TYPICAL",
|
||||||
|
"EXAMPLE",
|
||||||
|
"DERIVED"
|
||||||
|
],
|
||||||
|
"title": "Value Origin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"typical": {
|
||||||
|
"default": false,
|
||||||
|
"title": "Typical",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"parameter",
|
||||||
|
"value_kind",
|
||||||
|
"mandatory",
|
||||||
|
"source_type",
|
||||||
|
"source_class"
|
||||||
|
],
|
||||||
|
"title": "ProtocolConstraint",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"title": "Schema Version",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logical_protocols": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/LogicalProtocol"
|
||||||
|
},
|
||||||
|
"title": "Logical Protocols",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"physical_interfaces": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/PhysicalInterface"
|
||||||
|
},
|
||||||
|
"title": "Physical Interfaces",
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"logical_protocols",
|
||||||
|
"physical_interfaces"
|
||||||
|
],
|
||||||
|
"title": "ProtocolCatalog",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
@@ -28,6 +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.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
|
||||||
@@ -393,6 +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(),
|
||||||
)
|
)
|
||||||
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,6 +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.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 (
|
||||||
@@ -228,6 +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(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
report = ValidationReport(
|
report = ValidationReport(
|
||||||
@@ -238,6 +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(),
|
||||||
)
|
)
|
||||||
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,15 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
- [New] `protocol_catalog.py`, `protocol_data/catalog.json`, schema 1.0.0.
|
||||||
|
- [New] `protocol_certification` on `ValidationReport`; UI section Protocolli.
|
||||||
|
- [New] pytest `tests/pcb/test_protocol_catalog_m0.py`.
|
||||||
|
- [Fixed] Native `periscope/src` wins over inherited `periscopex` on the package path.
|
||||||
|
|
||||||
## 2.63.5 — 2026-09-22 — AF trace analysis (trigger, cascade, visible skips)
|
## 2.63.5 — 2026-09-22 — AF trace analysis (trigger, cascade, visible skips)
|
||||||
|
|
||||||
PCB exam adds **AF trace analysis** after unchanged `run_pcb_checks` (SI/HF stay). Trigger is λ/10 or tr/(6 tpd) from FACT only. Differential pairs are one unit. Z0/Zdiff only vs a **datasheet** window. Missing tr/f/εr/stackup/Z target/via span is a visible INSUFFICIENT finding («Pista ad alta frequenza non controllata per mancanza di …»), not invented 50/90 Ω. Trigger false → no AF finding. Numerical cascade is lossless RLGC sections (ImpedenceFinder Z0); not OpenEMS/FEM. Python. PE-AF-001 AI path unchanged.
|
PCB exam adds **AF trace analysis** after unchanged `run_pcb_checks` (SI/HF stay). Trigger is λ/10 or tr/(6 tpd) from FACT only. Differential pairs are one unit. Z0/Zdiff only vs a **datasheet** window. Missing tr/f/εr/stackup/Z target/via span is a visible INSUFFICIENT finding («Pista ad alta frequenza non controllata per mancanza di …»), not invented 50/90 Ω. Trigger false → no AF finding. Numerical cascade is lossless RLGC sections (ImpedenceFinder Z0); not OpenEMS/FEM. Python. PE-AF-001 AI path unchanged.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "periscope-web",
|
"name": "periscope-web",
|
||||||
"version": "2.63.4",
|
"version": "2.64.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sync-version": "node scripts/sync-version.mjs",
|
"sync-version": "node scripts/sync-version.mjs",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useReviewedFindings } from "@/hooks/use-reviewed-findings";
|
|||||||
import { ReportSummary } from "@/components/report/report-summary";
|
import { ReportSummary } from "@/components/report/report-summary";
|
||||||
import { FindingsList } from "@/components/report/findings-list";
|
import { FindingsList } from "@/components/report/findings-list";
|
||||||
import { FindingFocusView } from "@/components/report/finding-focus-view";
|
import { FindingFocusView } from "@/components/report/finding-focus-view";
|
||||||
|
import { ProtocolSection } from "@/components/report/protocol-section";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Toast, useToast } from "@/components/ui/toast";
|
import { Toast, useToast } from "@/components/ui/toast";
|
||||||
@@ -303,6 +304,7 @@ function ReportContent({ projectId }: { projectId: string }) {
|
|||||||
creditsSpent={creditsSpent}
|
creditsSpent={creditsSpent}
|
||||||
totalCostUsd={totalCostUsd}
|
totalCostUsd={totalCostUsd}
|
||||||
/>
|
/>
|
||||||
|
<ProtocolSection section={report.protocol_certification} />
|
||||||
{failedReviews > 0 && (
|
{failedReviews > 0 && (
|
||||||
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 space-y-2">
|
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 space-y-2">
|
||||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
|
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ProtocolCertificationSection } from "@/lib/types";
|
||||||
|
|
||||||
|
export function ProtocolSection({
|
||||||
|
section,
|
||||||
|
}: {
|
||||||
|
section?: ProtocolCertificationSection | null;
|
||||||
|
}) {
|
||||||
|
const message = section?.message || "nessun protocollo riconosciuto";
|
||||||
|
const instances = section?.recognized_instances ?? [];
|
||||||
|
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.
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -75,6 +75,15 @@ export interface ValidationReport {
|
|||||||
comments?: Record<string, FindingComment[]>;
|
comments?: Record<string, FindingComment[]>;
|
||||||
review_states?: Record<string, FindingReview>;
|
review_states?: Record<string, FindingReview>;
|
||||||
release?: ReportRelease;
|
release?: ReportRelease;
|
||||||
|
protocol_certification?: ProtocolCertificationSection | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtocolCertificationSection {
|
||||||
|
schema_version: string;
|
||||||
|
macrophase: string;
|
||||||
|
recognized_instances: Record<string, unknown>[];
|
||||||
|
message: string;
|
||||||
|
max_level_reached: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NetType = "power" | "ground" | "signal" | "unknown";
|
export type NetType = "power" | "ground" | "signal" | "unknown";
|
||||||
|
|||||||
@@ -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.63.4";
|
export const APP_VERSION = "2.64.0";
|
||||||
export const APP_VERSION_DATE = "2026-09-22";
|
export const APP_VERSION_DATE = "2026-09-22";
|
||||||
|
|||||||
+4
-1
@@ -16,7 +16,10 @@ for p in (
|
|||||||
REPO_ROOT,
|
REPO_ROOT,
|
||||||
):
|
):
|
||||||
s = str(p)
|
s = str(p)
|
||||||
if p.is_dir() and s not in sys.path:
|
if not p.is_dir():
|
||||||
|
continue
|
||||||
|
if s in sys.path:
|
||||||
|
sys.path.remove(s)
|
||||||
sys.path.insert(0, s)
|
sys.path.insert(0, s)
|
||||||
|
|
||||||
from backend.vendor_path import ensure_impedancefinder
|
from backend.vendor_path import ensure_impedancefinder
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""M0 protocol pack: parse, reject typical-as-standard, no invented ohms."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backend.periscopex.models import ValidationReport
|
||||||
|
from backend.periscopex.protocol_catalog import (
|
||||||
|
CATALOG_PATH,
|
||||||
|
SCHEMA_PATH,
|
||||||
|
SCHEMA_VERSION,
|
||||||
|
ProtocolCatalogError,
|
||||||
|
build_m0_catalog,
|
||||||
|
empty_protocol_section,
|
||||||
|
fail_mandatory,
|
||||||
|
load_catalog,
|
||||||
|
map_protocol_outcome,
|
||||||
|
parse_catalog,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_LOGICAL = {
|
||||||
|
"usb2-ls-fs", "usb2-hs", "usb-c-usb2", "100base-tx",
|
||||||
|
"hdmi-1.4", "hdmi-2.0",
|
||||||
|
"ddr-family", "ddr4", "lpddr-family", "lpddr4",
|
||||||
|
"hbm-family", "hbm3", "hyperbus", "qspi", "octal-spi", "emmc", "ufs",
|
||||||
|
"axi4", "axi4-lite", "ahb", "apb", "wishbone", "avalon-mm", "avalon-st",
|
||||||
|
"tilelink", "pcie", "cxl",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_committed_catalog_parses_and_matches_builder():
|
||||||
|
loaded = load_catalog()
|
||||||
|
rebuilt = parse_catalog(build_m0_catalog())
|
||||||
|
assert loaded.schema_version == SCHEMA_VERSION
|
||||||
|
assert loaded.model_dump() == rebuilt.model_dump()
|
||||||
|
assert CATALOG_PATH.is_file()
|
||||||
|
assert SCHEMA_PATH.is_file()
|
||||||
|
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
||||||
|
assert "properties" in schema
|
||||||
|
assert schema["properties"]["schema_version"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_logical_and_physical_ids_are_disjoint():
|
||||||
|
cat = load_catalog()
|
||||||
|
logical = {p.id for p in cat.logical_protocols}
|
||||||
|
physical = {p.id for p in cat.physical_interfaces}
|
||||||
|
assert not (logical & physical)
|
||||||
|
assert REQUIRED_LOGICAL <= logical
|
||||||
|
for iface in cat.physical_interfaces:
|
||||||
|
assert iface.id != iface.logical_protocol_id
|
||||||
|
assert iface.pcb_relevant in {"YES", "NO", "CONDITIONAL"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_amba_internal_is_not_pcb_routing():
|
||||||
|
cat = load_catalog()
|
||||||
|
axi = next(p for p in cat.physical_interfaces if p.id == "axi4-internal")
|
||||||
|
assert axi.pcb_relevant == "NO"
|
||||||
|
assert axi.physical_layer == "INTERNAL"
|
||||||
|
assert axi.logical_protocol_id == "axi4"
|
||||||
|
assert axi.constraints[0].value_kind == "NOT_APPLICABLE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ddr_has_no_universal_mm_or_ps():
|
||||||
|
cat = load_catalog()
|
||||||
|
ddr4 = next(p for p in cat.physical_interfaces if p.id == "ddr4-sdram-pcb")
|
||||||
|
assert ddr4.pcb_relevant == "YES"
|
||||||
|
for c in ddr4.constraints:
|
||||||
|
assert c.value_kind in {
|
||||||
|
"CONTROLLER_DEPENDENT", "PHY_DEPENDENT", "UNKNOWN",
|
||||||
|
"VENDOR_DEPENDENT", "MISSING_SOURCE", "NOT_APPLICABLE",
|
||||||
|
}
|
||||||
|
assert c.value is None
|
||||||
|
assert c.unit is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_hbm_dq_not_pcb_relevant():
|
||||||
|
cat = load_catalog()
|
||||||
|
hbm = next(p for p in cat.physical_interfaces if p.id == "hbm3-package")
|
||||||
|
assert hbm.pcb_relevant == "NO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_hdmi_mini_is_same_logical_different_connector():
|
||||||
|
cat = load_catalog()
|
||||||
|
a = next(p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-a")
|
||||||
|
mini = next(
|
||||||
|
p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-c-mini"
|
||||||
|
)
|
||||||
|
assert a.logical_protocol_id == mini.logical_protocol_id == "hdmi-1.4"
|
||||||
|
assert a.connector == "HDMI-A"
|
||||||
|
assert mini.connector == "HDMI-C-mini"
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_has_no_invented_ohm_numbers():
|
||||||
|
cat = load_catalog()
|
||||||
|
for iface in cat.physical_interfaces:
|
||||||
|
for c in iface.constraints:
|
||||||
|
assert c.value is None
|
||||||
|
assert c.value_kind != "NUMERIC"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recommended_is_not_fail_mandatory():
|
||||||
|
cat = load_catalog()
|
||||||
|
recs = [
|
||||||
|
c
|
||||||
|
for iface in cat.physical_interfaces
|
||||||
|
for c in iface.constraints
|
||||||
|
if c.mandatory == "RECOMMENDED"
|
||||||
|
]
|
||||||
|
assert recs, "catalog should include at least one RECOMMENDED skeleton"
|
||||||
|
for c in recs:
|
||||||
|
assert fail_mandatory(c) is False
|
||||||
|
cls, status, _ = map_protocol_outcome("FAIL", c.mandatory)
|
||||||
|
assert status != "ERROR"
|
||||||
|
assert cls != "RULE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mandatory_fail_can_be_error():
|
||||||
|
cls, status, _ = map_protocol_outcome("FAIL", "MANDATORY")
|
||||||
|
assert cls == "RULE"
|
||||||
|
assert status == "ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_typical_as_standard():
|
||||||
|
raw = build_m0_catalog()
|
||||||
|
iface = raw["physical_interfaces"][0]
|
||||||
|
typical = copy.deepcopy(iface["constraints"][0])
|
||||||
|
typical["id"] = "bad-typical"
|
||||||
|
typical["typical"] = True
|
||||||
|
typical["value_origin"] = "TYPICAL"
|
||||||
|
typical["source_type"] = "STANDARD"
|
||||||
|
typical["source_class"] = "NORMATIVE"
|
||||||
|
typical["mandatory"] = "MANDATORY"
|
||||||
|
typical["value_kind"] = "UNKNOWN"
|
||||||
|
typical["value"] = None
|
||||||
|
iface = copy.deepcopy(iface)
|
||||||
|
iface["id"] = "usb2-hs-typical-bad"
|
||||||
|
iface["constraints"] = [typical]
|
||||||
|
raw["physical_interfaces"].append(iface)
|
||||||
|
with pytest.raises(ProtocolCatalogError, match="typical"):
|
||||||
|
parse_catalog(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_constraint_without_source_type():
|
||||||
|
raw = build_m0_catalog()
|
||||||
|
iface = copy.deepcopy(raw["physical_interfaces"][0])
|
||||||
|
iface["id"] = "usb2-hs-no-source"
|
||||||
|
bad = copy.deepcopy(iface["constraints"][0])
|
||||||
|
bad["id"] = "no-source"
|
||||||
|
del bad["source_type"]
|
||||||
|
iface["constraints"] = [bad]
|
||||||
|
raw["physical_interfaces"].append(iface)
|
||||||
|
with pytest.raises(ProtocolCatalogError):
|
||||||
|
parse_catalog(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_numeric_without_cite():
|
||||||
|
raw = build_m0_catalog()
|
||||||
|
iface = copy.deepcopy(raw["physical_interfaces"][0])
|
||||||
|
iface["id"] = "usb2-hs-fake-z"
|
||||||
|
iface["constraints"] = [{
|
||||||
|
"id": "fake-zdiff",
|
||||||
|
"parameter": "differential_impedance",
|
||||||
|
"value_kind": "NUMERIC",
|
||||||
|
"value": 90.0,
|
||||||
|
"unit": "ohm",
|
||||||
|
"mandatory": "MANDATORY",
|
||||||
|
"source_type": "STANDARD",
|
||||||
|
"source_class": "NORMATIVE",
|
||||||
|
"source": None,
|
||||||
|
"typical": False,
|
||||||
|
"value_origin": "SPEC",
|
||||||
|
"conditions": [],
|
||||||
|
}]
|
||||||
|
raw["physical_interfaces"].append(iface)
|
||||||
|
with pytest.raises(ProtocolCatalogError, match="cite"):
|
||||||
|
parse_catalog(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_boolean_pcb_relevant():
|
||||||
|
raw = build_m0_catalog()
|
||||||
|
iface = copy.deepcopy(raw["physical_interfaces"][0])
|
||||||
|
iface["id"] = "usb2-hs-bool-pcb"
|
||||||
|
iface["pcb_relevant"] = True
|
||||||
|
raw["physical_interfaces"].append(iface)
|
||||||
|
with pytest.raises(ProtocolCatalogError):
|
||||||
|
parse_catalog(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_same_logical_and_physical_id():
|
||||||
|
raw = build_m0_catalog()
|
||||||
|
iface = copy.deepcopy(raw["physical_interfaces"][0])
|
||||||
|
iface["id"] = iface["logical_protocol_id"]
|
||||||
|
raw["physical_interfaces"].append(iface)
|
||||||
|
with pytest.raises(ProtocolCatalogError, match="differ"):
|
||||||
|
parse_catalog(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_report_section_has_no_z():
|
||||||
|
section = empty_protocol_section()
|
||||||
|
dumped = section.model_dump()
|
||||||
|
assert section.message == "nessun protocollo riconosciuto"
|
||||||
|
assert section.recognized_instances == []
|
||||||
|
assert dumped.get("max_level_reached") is None
|
||||||
|
blob = json.dumps(dumped)
|
||||||
|
assert "90" not in blob
|
||||||
|
assert "ohm" not in blob.lower()
|
||||||
|
assert "Zdiff" not in blob
|
||||||
|
assert "z0" not in blob.lower()
|
||||||
|
report = ValidationReport(
|
||||||
|
project="t",
|
||||||
|
timestamp="2026-09-22T00:00:00Z",
|
||||||
|
findings=[],
|
||||||
|
summary={"ERROR": 0, "WARNING": 0, "INFO": 0},
|
||||||
|
protocol_certification=dumped,
|
||||||
|
)
|
||||||
|
assert report.protocol_certification["message"] == "nessun protocollo riconosciuto"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pcie_cxl_physical_packs_are_empty_numbers():
|
||||||
|
cat = load_catalog()
|
||||||
|
for pid in ("pcie-phy-pcb", "cxl-phy-pcb"):
|
||||||
|
iface = next(p for p in cat.physical_interfaces if p.id == pid)
|
||||||
|
assert iface.logical_protocol_id in {"pcie", "cxl"}
|
||||||
|
assert iface.required_checks
|
||||||
|
for c in iface.constraints:
|
||||||
|
assert c.value_kind in {"MISSING_SOURCE", "UNKNOWN"}
|
||||||
|
assert c.value is None
|
||||||
Reference in New Issue
Block a user