Files
retroSoC/tests/test_tiny_r2_p2.py
T
maksyuki bc48cec74a feat(tiny): implement R2-P7 clock and reset lifecycle
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.
2026-10-14 16:56:01 +08:00

127 lines
5.7 KiB
Python

"""Tiny reset distribution and early timing-feasibility contracts."""
from __future__ import annotations
import os
import json
from pathlib import Path
import shutil
import subprocess
import pytest
from scripts.check_simulation import DEFAULT_FAILURE
from scripts import tiny_r2_feasibility as feasibility
ROOT = Path(__file__).resolve().parents[1]
COMMON = ROOT / "rtl/managed/clusterip/common/rtl"
def run(command: list[str], log: Path, **kwargs) -> str:
result = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
log.write_text(result.stdout)
assert result.returncode == 0, result.stdout
assert not DEFAULT_FAILURE.search(result.stdout), result.stdout
return result.stdout
@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
@pytest.mark.parametrize("fixture,marker", [
("tiny_r2_reset_tb", "SIM_TEST_PASS Tiny R2 reset"),
("tiny_r2_fifo_reset_tb", "SIM_TEST_PASS Tiny R2 FIFO reset"),
])
def test_reset_and_fifo_contracts(tmp_path: Path, simulator: str, fixture: str, marker: str) -> None:
assert shutil.which(simulator), f"required simulator unavailable: {simulator}"
sources = [COMMON / "utils/register.sv", COMMON / "clkrst/rst_sync.sv",
COMMON / "utils/fifo.sv", ROOT / "rtl/tiny/top/tiny_reset_tree.sv",
ROOT / "rtl/ip/core/mgmt_debug_reset.sv", ROOT / f"tests/rtl/{fixture}.sv"]
includes = [f"-I{COMMON}", "-DSV_ASSRT_DISABLE"]
output = tmp_path / "sim"
if simulator == "iverilog":
assert shutil.which("vvp"), "required Icarus runtime unavailable"
command = [simulator, "-g2012", "-s", fixture, *includes,
*(str(p) for p in sources), "-o", str(output)]
launch = ["vvp", str(output)]
else:
command = [simulator, "--binary", "--timing", "--assert", "-Wno-fatal",
"--top-module", fixture, *includes, *(str(p) for p in sources),
"--Mdir", str(tmp_path / "obj"), "-o", str(output)]
launch = [str(output)]
run(command, tmp_path / "compile.log", env={**os.environ, "CCACHE_DISABLE": "1"}, timeout=120)
content = run(launch, tmp_path / "sim.log", timeout=30)
assert marker in content
assert "SIM_TEST_FAIL" not in content and "SIM_TEST_TIMEOUT" not in content
def test_reset_inventory_has_active_domains_and_all_existing_targets() -> None:
topology = json.loads((ROOT / "rtl/tiny/integration/soc_topology.json").read_text())
inventory = json.loads((ROOT / "rtl/tiny/integration/clock_reset_domains.json").read_text())
reset = inventory["reset_distribution"]
assert reset["release_edges"] == 5
assert reset["leaf_names"] == ["fabric", *(target["name"] for target in topology["apb_targets"])]
assert len(reset["leaf_names"]) == 21
assert {domain["name"] for domain in inventory["domains"]} == {
"aon", "system", "memory", "peripheral", "jtag", "audio"}
assert reset["clock_domain"] == "domain-specific" and reset["main_sram_payload_reset"] == "none"
assert inventory["local_memory"]["clock_domain"] == "system"
SDC = ("create_clock -name clk_system -period 41.666666667 $clk_system_pin\n"
"create_clock -name clk_jtag -period 100 $clk_jtag_port\n"
"set_false_path -from [all_inputs]\nset_clock_uncertainty -setup 0.2 [all_clocks]\n")
@pytest.mark.parametrize("mhz", [24, 96, 192, 240])
def test_analysis_only_changes_the_system_period(mhz: int) -> None:
output = feasibility.candidate_sdc(SDC, mhz)
assert output.splitlines()[1:] == SDC.splitlines()[1:]
assert float(output.splitlines()[0].split()[4]) == pytest.approx(1000 / mhz)
if mhz == 24:
assert output == SDC
@pytest.mark.parametrize("sdc,mhz", [(SDC, 48), (SDC + SDC, 96),
(SDC.replace("clk_system", "clk_other"), 96),
(SDC.replace("41.666666667", "5"), 96)])
def test_feasibility_rejects_wrong_source_clock_or_rate(sdc: str, mhz: int) -> None:
with pytest.raises(ValueError):
feasibility.candidate_sdc(sdc, mhz)
@pytest.mark.parametrize("content", ["wns_max=wns max -1.0\n", "", "wns_min=0\nwns_max=nan\ntns_min=0\ntns_max=0\n"])
def test_incomplete_or_invalid_metrics_cannot_pass(tmp_path: Path, content: str) -> None:
report = tmp_path / "metrics.rpt"
report.write_text(content)
with pytest.raises(ValueError):
feasibility.timing_metrics(report)
@pytest.mark.parametrize("mutation", ["none", "missing", "merged"])
def test_reset_loads_require_seventeen_independent_leaves(tmp_path: Path, mutation: str) -> None:
report = tmp_path / "loads.tsv"
lines = ["name\tdrivers\tdirect_loads\tstructural_endpoints", "system\troot/Q\t85\t1000"]
lines += [f"leaf-{i}\tleaf{i}/Q\t10\t20" for i in range(17)]
if mutation == "missing":
lines.pop()
if mutation == "merged":
lines[-1] = "leaf-16\tleaf0/Q\t10\t20"
report.write_text("\n".join(lines))
if mutation == "none":
assert len(feasibility.reset_loads(report, True)) == 18
else:
with pytest.raises(ValueError):
feasibility.reset_loads(report, True)
@pytest.mark.parametrize("content,expected", [(" -1.25 slack (VIOLATED)\n", -1.25),
(" 0.50 slack (MET)\n", 0.5),
("No paths found.\n", None)])
def test_sys_data_slack_is_separate_from_aggregate_recovery(tmp_path, content, expected) -> None:
path = tmp_path / "sys-setup.rpt"
path.write_text(content)
if expected is None:
with pytest.raises(ValueError, match="missing constrained"):
feasibility.path_slack(path)
else:
assert feasibility.path_slack(path) == expected