mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat: add Tiny R2 reset distribution and feasibility analysis
- Implemented a new TCL script for Tiny R2 feasibility checks, reporting clock properties, path delays, and reset loads. - Created a SystemVerilog module for the Tiny R2 reset tree, managing multiple reset signals for the system and its leaves. - Developed a Python script for capturing and analyzing feasibility metrics, ensuring compliance with reset-load and clock frequency requirements. - Added testbenches for the Tiny R2 reset tree and FIFO, validating reset behavior and timing contracts. - Introduced unit tests for feasibility analysis, ensuring correct handling of clock rates and reset load conditions.
This commit is contained in:
24 files changed
+1209
-66
No files matched your search
@@ -91,15 +91,18 @@ def generate_bindings(document: dict, output: Path) -> None:
|
||||
f"assign u_gpio_if.{mode}_oe_i[{pin}] = {value['oe']};"))
|
||||
atomic_write(output / "rtl/tiny_gpio.svh", "\n".join(gpio) + "\n")
|
||||
ports, declarations, routes, decode = [], [comment], [comment], [comment]
|
||||
reset_assignments = []
|
||||
for index, target in enumerate(document["apb_targets"]):
|
||||
name = target["name"]
|
||||
ports.append(f"apb4_if.master {name}")
|
||||
declarations.append(f"apb4_if u_{name}_apb4_if (.pclk(clk_i), .presetn(s_rst_n));")
|
||||
declarations.append(f"logic s_{name}_rst_n;")
|
||||
declarations.append(f"apb4_if u_{name}_apb4_if (.pclk(clk_i), .presetn(s_{name}_rst_n));")
|
||||
reset_assignments.append(f"assign s_{name}_rst_n = s_leaf_rst_n[{index + 1}];")
|
||||
routes.append(f".{name}(u_{name}_apb4_if)")
|
||||
decode.append(f"if ({' || '.join(f'`SOC_ADDR_IS_{r}(s_addr_q)' for r in target['regions'])}) "
|
||||
f"s_select = {len(document['apb_targets'])}'b{1 << index:016b};")
|
||||
atomic_write(output / "rtl/tiny_apb_ports.svh", ",\n".join(ports) + "\n")
|
||||
atomic_write(output / "rtl/tiny_apb_interfaces.svh", "\n".join(declarations) + "\n")
|
||||
atomic_write(output / "rtl/tiny_apb_interfaces.svh", "\n".join(declarations + reset_assignments) + "\n")
|
||||
atomic_write(output / "rtl/tiny_apb_connections.svh", ",\n".join(routes[1:]) + "\n")
|
||||
atomic_write(output / "rtl/tiny_apb_decode.svh", "\n".join(decode) + "\n")
|
||||
wiring = [comment]
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/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["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())
|
||||
Reference in new issue
Block a user