mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
Integrate the Tiny RCU, normalized REF24 control, external-input profiles, single PLL backend, SYS/MEM/PCLK clocks, CPU-only WFI gating, peripheral lifecycle control and retained debug behavior. Keep CPU and all 32 main SRAM macros on SYS and the six private Crypto macros on PCLK. Add the handwritten RCU register/HAL contract, RS_EBUSY, bounded SDK operations and focused firmware. Preserve shared-IP defaults and the ICS55/PLL SAFE24 boot and explicit IHP130/no-PLL compatibility profiles. Use APB/AXI domain bridges and source-owned response holders to preserve backpressured responses, complete aborted bursts and prevent stale replay. Verify production DMA source-reset drain and common XPI endpoint reset with extracted integration bindings and an independent scoreboard. Add source-bound macro/clock audits, SAFE24 observational STA constraints and commercial-adapter host checks. No P8 functionality or operating-point qualification is included. See TINY-R2-P7 in docs/ip/tiny-soc.md. Validation: 54 P7 directed tests pass, including both simulators, ten source-reset ratio runs and two expected-negative drain mutations. RTL format/style, clock/reset inventory, Ruff and both Tiny PDK PR dry-runs pass. Retained 06-00 synthesis and strict netlist boots pass for both PDKs; STA remains negative and observational. Source comparison confirms only the follow-up tests changed after that backend campaign. Evidence: build/ics55-tiny-2026-10-14-06-00-ff4b5cef6eb5/meta/ tiny-r2-p7-source-reset/summary.json. Earlier firmware, HAL and compatibility results and the non-clean full-suite attempt retain their original attribution. Full Icarus firmware is deferred to final Tiny qualification; this commit does not constitute human phase acceptance or physical signoff.
196 lines
8.5 KiB
Python
196 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the declared Mini SoC root clock/reset domains and CDC crossings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
RESET_PRIMITIVES = {"rst_sync"}
|
|
CDC_PRIMITIVES = {
|
|
"apb4_async_bridge",
|
|
"async_fifo",
|
|
"async_reqack",
|
|
"axi4_async_bridge",
|
|
"tiny_axi4_async_bridge",
|
|
"tiny_apb_async_bridge",
|
|
"cdc_2phase",
|
|
"cdc_fifo",
|
|
"cdc_sync",
|
|
"cdc_fifo_warm_flush",
|
|
"edge_det",
|
|
"hazard3_apb_async_bridge",
|
|
}
|
|
DATA_PLANE_FAULT_RESET_CONTRACT = {
|
|
"source_reset": "abort_accepted_mailbox_item",
|
|
"destination_reset": "abort_accepted_item_backpressure_unaccepted_source",
|
|
}
|
|
IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
|
|
HIERARCHICAL_PIN_RE = re.compile(
|
|
r"[A-Za-z_][A-Za-z0-9_.]*(?:/[A-Za-z_][A-Za-z0-9_.]*)+$"
|
|
)
|
|
|
|
|
|
def require_object(value: Any, field: str) -> dict[str, Any]:
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"{field} must be an object")
|
|
return value
|
|
|
|
|
|
def require_string(value: Any, field: str) -> str:
|
|
if not isinstance(value, str) or not value:
|
|
raise ValueError(f"{field} must be a non-empty string")
|
|
return value
|
|
|
|
|
|
def require_identifier(value: Any, field: str) -> str:
|
|
identifier = require_string(value, field)
|
|
if IDENTIFIER_RE.fullmatch(identifier) is None:
|
|
raise ValueError(f"{field} must be a SystemVerilog identifier")
|
|
return identifier
|
|
|
|
|
|
def require_positive_number(value: Any, field: str) -> float:
|
|
if not isinstance(value, (int, float)) or isinstance(value, bool) or value <= 0:
|
|
raise ValueError(f"{field} must be a positive number")
|
|
return float(value)
|
|
|
|
|
|
def require_hierarchical_pin(value: Any, field: str) -> str:
|
|
pin = require_string(value, field)
|
|
if HIERARCHICAL_PIN_RE.fullmatch(pin) is None:
|
|
raise ValueError(f"{field} must be a hierarchical instance pin")
|
|
return pin
|
|
|
|
|
|
def require_instance(path: Path, primitive: str, instance: str, field: str) -> None:
|
|
if not path.is_file():
|
|
raise ValueError(f"{field}.path does not exist: {path}")
|
|
source = path.read_text(encoding="utf-8")
|
|
expression = re.compile(rf"\b{re.escape(primitive)}\b[\s\S]*?\b{re.escape(instance)}\b")
|
|
if expression.search(source) is None:
|
|
raise ValueError(f"{field} does not declare {primitive} instance {instance}")
|
|
|
|
|
|
def validate(document_path: Path, root: Path) -> None:
|
|
document = require_object(json.loads(document_path.read_text(encoding="utf-8")), "domain map")
|
|
if document.get("schema_version") != 2:
|
|
raise ValueError("schema_version must be 2")
|
|
|
|
raw_domains = document.get("domains")
|
|
if not isinstance(raw_domains, list) or not raw_domains:
|
|
raise ValueError("domains must be a non-empty list")
|
|
names: set[str] = set()
|
|
sta_sources: dict[str, str | None] = {}
|
|
for index, value in enumerate(raw_domains):
|
|
field = f"domains[{index}]"
|
|
domain = require_object(value, field)
|
|
name = require_identifier(domain.get("name"), f"{field}.name")
|
|
if name in names:
|
|
raise ValueError(f"domain {name} is duplicated")
|
|
names.add(name)
|
|
require_identifier(domain.get("clock"), f"{field}.clock")
|
|
require_identifier(domain.get("reset"), f"{field}.reset")
|
|
require_identifier(domain.get("reset_source"), f"{field}.reset_source")
|
|
primitive = require_string(domain.get("reset_primitive"), f"{field}.reset_primitive")
|
|
if primitive not in RESET_PRIMITIVES:
|
|
raise ValueError(f"{field}.reset_primitive is not supported")
|
|
path = root / require_string(domain.get("path"), f"{field}.path")
|
|
instance = require_identifier(domain.get("instance"), f"{field}.instance")
|
|
require_instance(path, primitive, instance, field)
|
|
sta = require_object(domain.get("sta"), f"{field}.sta")
|
|
source_port = sta.get("source_port")
|
|
source_domain = sta.get("source_domain")
|
|
if (source_port is None) == (source_domain is None):
|
|
raise ValueError(f"{field}.sta must declare exactly one source")
|
|
if source_port is not None:
|
|
require_identifier(source_port, f"{field}.sta.source_port")
|
|
sta_sources[name] = None
|
|
else:
|
|
sta_sources[name] = require_identifier(source_domain, f"{field}.sta.source_domain")
|
|
object_type = sta.get("object_type", "pin")
|
|
if object_type not in {"net_driver", "pin", "port"}:
|
|
raise ValueError(f"{field}.sta.object_type must be net_driver, pin, or port")
|
|
if object_type == "pin":
|
|
require_hierarchical_pin(sta.get("pin"), f"{field}.sta.pin")
|
|
elif object_type == "net_driver":
|
|
require_identifier(sta.get("net"), f"{field}.sta.net")
|
|
elif source_port is None:
|
|
raise ValueError(f"{field}.sta port object requires source_port")
|
|
require_positive_number(sta.get("period_ns"), f"{field}.sta.period_ns")
|
|
require_identifier(sta.get("async_group"), f"{field}.sta.async_group")
|
|
|
|
for name, source_domain in sta_sources.items():
|
|
if source_domain is not None and source_domain not in names:
|
|
raise ValueError(f"domain {name} STA source references unknown domain {source_domain}")
|
|
|
|
raw_crossings = document.get("crossings")
|
|
if not isinstance(raw_crossings, list):
|
|
raise ValueError("crossings must be a list")
|
|
generated = document.get("generated_apb_crossings")
|
|
if generated is not None:
|
|
if (document.get("clock_contract") != "TINY-R2-P7" or
|
|
generated.get("generator") != "scripts/generate_tiny.py" or
|
|
generated.get("primitive") != "tiny_apb_async_bridge" or
|
|
generated.get("source") != "system"):
|
|
raise ValueError("unrecognized generated APB crossing contract")
|
|
expected = {"aon": ["clint", "archinfo"], "memory": ["xpi"],
|
|
"peripheral": ["gpio", "uart0", "timer0", "timer1", "i2c0", "dma",
|
|
"pwm", "rtc", "wdg", "rng", "crc", "ws2812", "crypto", "i2s"]}
|
|
if (generated.get("destinations") != expected or
|
|
generated.get("same_clock_isolation") != ["sdio"] or
|
|
generated.get("native_system") != ["sysctrl", "sram"] or
|
|
not set(expected).issubset(names)):
|
|
raise ValueError("P7 generated APB domain assignment mismatch")
|
|
crossing_names: set[str] = set()
|
|
for index, value in enumerate(raw_crossings):
|
|
field = f"crossings[{index}]"
|
|
crossing = require_object(value, field)
|
|
name = require_identifier(crossing.get("name"), f"{field}.name")
|
|
if name in crossing_names:
|
|
raise ValueError(f"crossing {name} is duplicated")
|
|
crossing_names.add(name)
|
|
source = require_identifier(crossing.get("source"), f"{field}.source")
|
|
destination = require_identifier(crossing.get("destination"), f"{field}.destination")
|
|
if source not in names or destination not in names:
|
|
raise ValueError(f"{field} references an unknown domain")
|
|
if source == destination:
|
|
raise ValueError(f"{field} must cross between distinct domains")
|
|
primitive = require_string(crossing.get("primitive"), f"{field}.primitive")
|
|
if primitive not in CDC_PRIMITIVES:
|
|
raise ValueError(f"{field}.primitive is not an approved CDC primitive")
|
|
path = root / require_string(crossing.get("path"), f"{field}.path")
|
|
instance = require_identifier(crossing.get("instance"), f"{field}.instance")
|
|
if name == "data_plane_fault":
|
|
if primitive != "async_reqack" or instance != "u_fault_mailbox":
|
|
raise ValueError(
|
|
f"{field} must use async_reqack instance u_fault_mailbox"
|
|
)
|
|
reset_contract = crossing.get("unilateral_reset")
|
|
if (not isinstance(reset_contract, dict) or
|
|
reset_contract != DATA_PLANE_FAULT_RESET_CONTRACT):
|
|
raise ValueError(
|
|
f"{field}.unilateral_reset must declare the data-plane fault reset contract"
|
|
)
|
|
require_instance(path, primitive, instance, field)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--map", required=True, type=Path)
|
|
parser.add_argument("--root", required=True, type=Path)
|
|
arguments = parser.parse_args()
|
|
try:
|
|
validate(arguments.map, arguments.root)
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
parser.error(str(error))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|