mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
- Updated to include support for the ICS55 PDK, allowing for conditional source inputs and configuration checks. - Added new test cases for the ICS55 platform, including PLL backend and GPIO readback functionality. - Modified existing tests to accommodate changes in configuration and source input handling. - Introduced new parameters in the testbench for better configurability. - Ensured compatibility checks for the IHP130 and ICS55 PDKs in the feasibility script. - Enhanced the publication tests to validate hardware changes more robustly.
242 lines
13 KiB
Python
242 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Retain reset-load and same-frequency CPU/SRAM feasibility; never qualify a rate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from datetime import datetime, timezone
|
|
import math
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
from scripts import tiny_r2_baseline as baseline # noqa: E402
|
|
|
|
PHASE = "TINY-R2-P2"
|
|
RATES = (24, 96, 192, 240)
|
|
CORNERS = {
|
|
"slow": ("slow_1p08V_125C", "slow_1p08V_3p0V_125C", "slow_1p08V_125C"),
|
|
"typ": ("typ_1p20V_25C", "typ_1p2V_3p3V_25C", "typ_1p20V_25C"),
|
|
"fast": ("fast_1p32V_m40C", "fast_1p32V_3p6V_m40C", "fast_1p32V_m55C"),
|
|
}
|
|
MACRO = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
|
|
SDC_CLOCK = re.compile(r"^(create_clock -name clk_system -period )([0-9.]+)( \$clk_system_pin)$", re.MULTILINE)
|
|
|
|
|
|
def source_inputs(root: Path) -> dict:
|
|
inputs = baseline.source_inputs(root)
|
|
for path in (root / "tests").rglob("*"):
|
|
if path.is_file() and path.suffix in (".py", ".sv", ".svh"):
|
|
inputs[str(path.relative_to(root))] = baseline.sha256(path)
|
|
return inputs
|
|
|
|
|
|
def generated_inputs(root: Path, variant: Path) -> dict:
|
|
return {str(p.relative_to(root)): baseline.sha256(p)
|
|
for p in (variant / "generated/tiny").rglob("*") if p.is_file()}
|
|
|
|
|
|
def require_variant(root: Path, variant: Path) -> dict:
|
|
if not variant.is_relative_to(root / "build"):
|
|
raise ValueError("evidence must remain below repository build variants")
|
|
manifest = baseline.configuration(variant)
|
|
config = manifest["configuration"]
|
|
if config["PDK"] != "IHP130" or config["APP"] != "ci_smoke" or config["MGMT_CPU_CLK_HZ"] != "24000000":
|
|
raise ValueError("feasibility requires the 24 MHz Tiny ci_smoke variant")
|
|
return manifest
|
|
|
|
|
|
def candidate_sdc(original: str, mhz: int) -> str:
|
|
if mhz not in RATES or len(SDC_CLOCK.findall(original)) != 1:
|
|
raise ValueError("expected one baseline system clock and a frozen candidate rate")
|
|
if abs(float(SDC_CLOCK.search(original)[2]) - 1000 / 24) > 1e-6:
|
|
raise ValueError("source SDC is not the 24 MHz baseline")
|
|
if mhz == 24:
|
|
return original
|
|
return SDC_CLOCK.sub(lambda m: m[1] + f"{1000 / mhz:.12g}" + m[3], original)
|
|
|
|
|
|
def timing_metrics(path: Path) -> dict:
|
|
values = {}
|
|
for line in path.read_text().splitlines():
|
|
key, _, value = line.partition("=")
|
|
values[key] = float(value.split()[-1])
|
|
if (set(values) != {"wns_min", "wns_max", "tns_min", "tns_max"}
|
|
or not all(math.isfinite(value) for value in values.values())):
|
|
raise ValueError("incomplete STA measurements")
|
|
return values
|
|
|
|
|
|
def reset_loads(path: Path, candidate: bool) -> dict:
|
|
rows = {}
|
|
for line in path.read_text().splitlines()[1:]:
|
|
name, driver, direct, endpoints = line.split("\t")
|
|
if name in rows or min(int(direct), int(endpoints)) < 0:
|
|
raise ValueError("invalid reset-load accounting")
|
|
rows[name] = {"driver": driver, "direct_loads": int(direct), "structural_endpoints": int(endpoints)}
|
|
expected = {"system"} | ({f"leaf-{index}" for index in range(17)} if candidate else set())
|
|
if set(rows) != expected or len({r["driver"] for r in rows.values()}) != len(rows):
|
|
raise ValueError("reset drivers merged or missing")
|
|
return rows
|
|
|
|
|
|
def path_slack(path: Path) -> float:
|
|
match = re.search(r"(?m)^\s*([-+]?\d+(?:\.\d+)?)\s+slack\b", path.read_text())
|
|
if match is None:
|
|
raise ValueError(f"missing constrained SYS path: {path}")
|
|
return float(match[1])
|
|
|
|
|
|
def capture(args: argparse.Namespace) -> int:
|
|
root, variant = args.root.resolve(), args.variant_root.resolve()
|
|
manifest = require_variant(root, variant)
|
|
parent = variant / "meta/tiny-r2-p2"
|
|
directory = baseline.new_attempt(parent, "inputs-")
|
|
inputs = source_inputs(root) | generated_inputs(root, variant)
|
|
record = {"phase": PHASE, "created_at": datetime.now(timezone.utc).isoformat(),
|
|
"manifest": manifest, "tools": baseline.tools_identity(root),
|
|
"source": baseline.snapshot(root, directory, inputs)}
|
|
baseline.dump(directory / "identity.json", record)
|
|
baseline.dump(parent / "latest-inputs.json", {"path": str(directory / "identity.json")})
|
|
print(f"Tiny P2 captured candidate inputs: {directory}")
|
|
return 0
|
|
|
|
|
|
def consumed_files(variant: Path) -> dict:
|
|
return {name: baseline.artifact(variant / name) for name in (
|
|
"meta/manifest.json", "syn/yosys/result-synth.json", "sta/opensta/result-sta.json",
|
|
"syn/yosys/out/retrosoc_tiny_asic_yosys.v", "syn/yosys/out/retrosoc_tiny_asic_yosys.config",
|
|
"sta/opensta/retrosoc_core.sdc")}
|
|
|
|
|
|
def run(args: argparse.Namespace) -> int:
|
|
root, variant = args.root.resolve(), args.variant_root.resolve()
|
|
old_variant = args.baseline_root.resolve()
|
|
require_variant(root, variant)
|
|
require_variant(root, old_variant)
|
|
parent = variant / "meta/tiny-r2-p2"
|
|
directory = baseline.new_attempt(parent, "feasibility-")
|
|
output = directory / "report.json"
|
|
baseline.dump(parent / "latest-feasibility.json", {"path": str(output)})
|
|
record = {"phase": PHASE, "status": "running", "points": [],
|
|
"qualification": "early unextracted feasibility only; no qualified operating point"}
|
|
try:
|
|
identity_path = Path(baseline.read_json(parent / "latest-inputs.json")["path"])
|
|
identity = baseline.read_json(identity_path)
|
|
before = source_inputs(root) | generated_inputs(root, variant)
|
|
if before != baseline.read_json(baseline.check_artifact(identity["source"]["inputs"])):
|
|
raise ValueError("candidate inputs changed after capture")
|
|
for key in ("inputs", "snapshot", "patch"):
|
|
baseline.check_artifact(identity["source"][key])
|
|
record.update(candidate_identity=baseline.artifact(identity_path), tools=baseline.tools_identity(root))
|
|
original = baseline.read_json(old_variant / "meta/tiny-r2-p2/baseline-artifacts.json")
|
|
for key in ("inputs", "snapshot", "patch"):
|
|
baseline.check_artifact(original["source"][key])
|
|
for artifact in original["artifacts"].values():
|
|
baseline.check_artifact(artifact)
|
|
record["baseline_identity"] = baseline.artifact(old_variant / "meta/tiny-r2-p2/baseline-artifacts.json")
|
|
record["inputs"] = {"baseline": consumed_files(old_variant), "candidate": consumed_files(variant)}
|
|
for role, files in record["inputs"].items():
|
|
for key in ("syn/yosys/result-synth.json", "sta/opensta/result-sta.json"):
|
|
result = baseline.read_json(Path(files[key]["path"]))
|
|
if result.get("status") != "passed" or result.get("exit_code") != 0:
|
|
raise ValueError(f"{role} lacks completed synthesis/STA")
|
|
if role == "candidate":
|
|
synth = baseline.read_json(Path(files["syn/yosys/result-synth.json"]["path"]))
|
|
if datetime.fromisoformat(synth["started_at"]) < datetime.fromisoformat(identity["created_at"]):
|
|
raise ValueError("candidate synthesis predates its captured inputs")
|
|
libraries = root / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref"
|
|
script = root / "physical/smoke/sta/opensta/tiny_r2_feasibility.tcl"
|
|
record["script"] = baseline.artifact(script)
|
|
record["macro_views"] = {}
|
|
for corner, (std, io, ram) in CORNERS.items():
|
|
paths = [libraries / f"sg13g2_stdcell/lib/sg13g2_stdcell_{std}.lib",
|
|
libraries / f"sg13g2_io/lib/sg13g2_io_{io}.lib",
|
|
libraries / f"sg13g2_sram/lib/{MACRO}_{ram}.lib"]
|
|
text = paths[2].read_text()
|
|
record["macro_views"][corner] = {
|
|
"libraries": [baseline.artifact(p) for p in paths],
|
|
"explicit_minimum_period": bool(re.search(r"\b(?:minimum_period|min_period)\b", text)),
|
|
"setup": "setup_rising" in text, "hold": "hold_rising" in text,
|
|
"clock_to_output": "rising_edge" in text, "pulse_width": "min_pulse_width" in text,
|
|
"temperature_pairing": ram + "; stdcell " + std,
|
|
}
|
|
for role, files in record["inputs"].items():
|
|
original_sdc = Path(files["sta/opensta/retrosoc_core.sdc"]["path"]).read_text()
|
|
for mhz in RATES:
|
|
point = directory / role / corner / str(mhz)
|
|
point.mkdir(parents=True)
|
|
sdc = point / "analysis.sdc"
|
|
baseline.atomic_write(sdc, candidate_sdc(original_sdc, mhz))
|
|
environment = {
|
|
"OPENSTA_TOP": "retrosoc_tiny_asic",
|
|
"OPENSTA_NETLIST": files["syn/yosys/out/retrosoc_tiny_asic_yosys.v"]["path"],
|
|
"OPENSTA_LIBERTY": str(paths[0]), "OPENSTA_LINK_LIBS": str(paths[1]),
|
|
"OPENSTA_SRAM_LIBS": str(paths[2]), "OPENSTA_SDC": str(sdc),
|
|
"OPENSTA_REPORT": str(point / "timing.rpt"),
|
|
"OPENSTA_METRICS": str(point / "timing_metrics.rpt"),
|
|
"TINY_R2_OUTPUT": str(point), "TINY_R2_CANDIDATE": str(int(role == "candidate")),
|
|
}
|
|
command = [sys.executable, str(root / "scripts/run_flow.py"), "--tool", "opensta-p2",
|
|
"--log", str(point / "sta.log"), "--result", str(point / "result.json")]
|
|
for key, value in environment.items():
|
|
command += ["--env", f"{key}={value}"]
|
|
command += ["--", record["tools"]["opensta"]["executable"]["path"],
|
|
"-no_init", "-exit", "-threads", "2", str(script)]
|
|
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=1800)
|
|
log = (point / "sta.log").read_text()
|
|
if "TINY_R2_FEASIBILITY_PASS:" not in log or re.search(r"(?im)^Error:|\bFATAL\b", log):
|
|
raise ValueError(f"STA audit incomplete: {point}")
|
|
metrics = timing_metrics(point / "timing_metrics.rpt")
|
|
record["points"].append({"role": role, "corner": corner, "sys_mhz": mhz,
|
|
"metrics": metrics,
|
|
"sys_setup_path_slack_ns": path_slack(point / "sys-setup.rpt"),
|
|
"sys_hold_path_slack_ns": path_slack(point / "sys-hold.rpt"),
|
|
"reset_loads": reset_loads(point / "reset-loads.tsv", role == "candidate"),
|
|
"timing": "failed" if any(v < 0 for v in metrics.values()) else "nonnegative_reported_slack",
|
|
"artifacts": {p.name: baseline.artifact(p) for p in point.iterdir() if p.is_file()}})
|
|
baseline.dump(output, record)
|
|
print(f"{role} {corner} SYS={mhz} MHz: WNS={metrics['wns_max']} ns; not qualification", flush=True)
|
|
if before != source_inputs(root) | generated_inputs(root, variant):
|
|
raise ValueError("candidate inputs changed during feasibility")
|
|
for files in record["inputs"].values():
|
|
for artifact in files.values():
|
|
baseline.check_artifact(artifact)
|
|
for views in record["macro_views"].values():
|
|
for artifact in views["libraries"]:
|
|
baseline.check_artifact(artifact)
|
|
for key in ("candidate_identity", "baseline_identity", "script"):
|
|
baseline.check_artifact(record[key])
|
|
for tool in record["tools"].values():
|
|
baseline.check_artifact(tool["executable"])
|
|
record["status"] = "completed"
|
|
except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
|
|
record.update(status="failed", error=str(error))
|
|
print(f"Tiny P2 feasibility: {error}", file=sys.stderr)
|
|
finally:
|
|
baseline.dump(output, record)
|
|
return int(record["status"] != "completed")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", type=Path, default=ROOT)
|
|
parser.add_argument("--variant-root", type=Path, required=True)
|
|
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
subparsers.add_parser("capture")
|
|
execute = subparsers.add_parser("run")
|
|
execute.add_argument("--baseline-root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
return capture(args) if args.action == "capture" else run(args)
|
|
except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
|
|
print(f"Tiny P2: {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|