feat: enhance Tiny R2 Baseline and ICS55 Support

- 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.
This commit is contained in:
maksyuki committed 2026-10-08 09:44:39 +08:00
1 parent ecac3559b0
commit e3ddeacea3
56 files changed
+1681 -178

No files matched your search

+28
View File
@@ -25,6 +25,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--pdk", required=True)
parser.add_argument("--soc", choices=("MINI", "TINY"), default="MINI")
parser.add_argument("--have-sram-macro", choices=("YES", "NO"), default="NO")
parser.add_argument("--have-pll", choices=("YES", "NO"), default="NO")
parser.add_argument("--formal", choices=("YES", "NO"), default="NO")
parser.add_argument("--require-debug-tools", action="store_true")
parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK)
@@ -85,7 +86,23 @@ def main() -> int:
}
if args.pdk in pdk_names:
source_names.append(pdk_names[args.pdk])
if args.pdk == "ICS55" and args.soc == "TINY" and args.have_pll == "YES":
source_names.append("pdk_ics55_pll")
paths = {name: root / lock["sources"][name]["destination"] for name in source_names}
if args.pdk == "ICS55":
for corner in ("tt", "ss"):
paths[f"ics55_cells_{corner}"] = root / f".cache/retrosoc/pdk/ics55/ics55_h7cr_{corner}.lib"
if args.have_sram_macro == "YES":
for size in ("1024", "4096"):
name = f"ics55_ecos_sram_{size}x32_m8"
for view in (f"verilog/{name}_core.v", f"verilog/{name}_stub.v",
f"lib/{name}_tt1p2v25cctyp.lib", f"lib/{name}_ss1p08v125ccmax.lib"):
paths[f"{name}/{view}"] = root / ".cache/retrosoc/pdk/ics55/sram" / name / view
if args.soc == "TINY" and args.have_pll == "YES":
from physical.pdk.setup import PLL_VIEWS
pll = root / lock["sources"]["pdk_ics55_pll"]["destination"]
for view in PLL_VIEWS:
paths[f"pll/{view}"] = pll / view
if args.pdk == "SKY130" and args.have_sram_macro == "YES":
paths["sky130_openram_sram"] = root / ".cache/retrosoc/pdk/sky130/openram"
tool_results = {
@@ -97,6 +114,17 @@ def main() -> int:
}
failures = sum(value["path"] is None for value in tool_results.values())
failures += sum(not value["exists"] for value in path_results.values())
if "pdk_ics55_pll" in paths and paths["pdk_ics55_pll"].exists():
try:
revision = subprocess.check_output(["git", "-C", str(paths["pdk_ics55_pll"]),
"rev-parse", "HEAD"], text=True).strip()
dirty = subprocess.check_output(["git", "-C", str(paths["pdk_ics55_pll"]),
"status", "--porcelain"], text=True).strip()
matching = revision == lock["sources"]["pdk_ics55_pll"]["revision"] and not dirty
except subprocess.CalledProcessError:
matching = False
path_results["pdk_ics55_pll"]["locked_clean_checkout"] = bool(matching)
failures += int(not matching)
report = {
"schema_version": 1,
"ok": failures == 0,
+44 -9
View File
@@ -53,7 +53,17 @@ def read_topology(path: Path, memory_map: Path) -> dict:
return document
def generate_bindings(document: dict, output: Path) -> None:
def platform_values(pdk: str, have_pll: bool = False) -> dict[str, int]:
if (pdk, have_pll) not in (("IHP130", False), ("ICS55", True)):
raise ValueError("unsupported Tiny PDK/PLL combination")
return {"technology": 0x02040037 if pdk == "ICS55" else 0x02010082,
"features0": 0x7FFE | int(have_pll), "pll_present": int(have_pll),
"gpio_pulls": int(pdk == "ICS55")}
def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
have_pll: bool = False) -> None:
platform = platform_values(pdk, have_pll)
comment = "// Generated by scripts/generate_tiny.py; do not edit.\n"
irq = [comment, "always_comb begin", " s_irq = '0;"]
header = ["#ifndef RETROSOC_GENERATED_IRQ_METADATA_H",
@@ -80,7 +90,13 @@ def generate_bindings(document: dict, output: Path) -> None:
"#ifndef RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RS_SOC_TINY 1\n#define RS_SOC_HAS_PSRAM 0U\n"
"#define RS_SOC_DMA_CHANNELS 4U\n#define RS_SOC_ID 0x54494E59U\n#endif\n")
"#define RS_SOC_DMA_CHANNELS 4U\n#define RS_SOC_ID 0x54494E59U\n"
f"#define RS_SOC_TECHNOLOGY_ID 0x{platform['technology']:08X}U\n"
f"#define RS_SOC_PLL_PRESENT {platform['pll_present']}U\n"
f"#define RS_SOC_GPIO_HAS_PULLS {platform['gpio_pulls']}U\n#endif\n")
atomic_write(output / "rtl/tiny_platform.svh", comment +
f"`define RETROSOC_TINY__TECHNOLOGY 32'h{platform['technology']:08x}\n"
f"`define RETROSOC_TINY__FEATURES0 32'h{platform['features0']:08x}\n")
gpio = [comment]
for function in document["gpio_alt_functions"]:
pin = function["pin"]
@@ -119,18 +135,33 @@ def generate_bindings(document: dict, output: Path) -> None:
atomic_write(output / "rtl/tiny_apb_wiring.svh", "\n".join(wiring) + "\n")
def generate_filelists(output: Path, generated: Path, defines: list[str]) -> None:
def generate_filelists(output: Path, generated: Path, defines: list[str],
pdk: str = "IHP130", simulator: str = "VERILATOR",
synthesis: str = "NONE") -> None:
prefixes = {"top": TINY / "top", "dv": TINY / "dv", "ip": ROOT / "rtl/ip",
"tech": ROOT / "rtl/tech", "clusterip": ROOT / "rtl/managed/clusterip",
"hazard3": ROOT / "rtl/managed/hazard3/hdl", "physical": ROOT / "physical"}
"hazard3": ROOT / "rtl/managed/hazard3/hdl", "physical": ROOT / "physical",
"cache": ROOT / ".cache"}
suffix = "_yosys" if synthesis == "YOSYS" else "_verilator" if simulator == "VERILATOR" else ""
pdk_name = "pdk_ihp130.fl" if pdk == "IHP130" else f"pdk_ics55{suffix}.fl"
templates = [*sorted((TINY / "filelist").glob("*.fl")),
ROOT / "rtl/filelist/pdk_ihp130.fl"]
ROOT / "rtl/filelist" / pdk_name]
for template in templates:
content = template.read_text()
for prefix, directory in prefixes.items():
content = re.sub(rf"(?<![^\s+])/{prefix}(?=/|\s|$)", str(directory), content)
if template.name == pdk_name and pdk == "ICS55":
# The shared tc_sram file also declares an unused 16 KiB wrapper.
# Keep its official interface visible to parsers; Tiny must never
# instantiate it (the mapped-netlist audit rejects that case).
content = content.replace("ics55_ecos_sram_4096x32_m8_core.v",
"ics55_ecos_sram_4096x32_m8_stub.v")
atomic_write(output / template.name, content)
include = [generated / "rtl", generated / "memory_map/rtl", generated / "pin_map/rtl",
if template.name == pdk_name:
if pdk == "ICS55" and synthesis != "YOSYS":
content += str(ROOT / ".cache/retrosoc/sources/ics55_ecos_pll/verilog/PLL_TOP.behavioral.v") + "\n"
atomic_write(output / "pdk_selected.fl", content)
include = [TINY / "dv", generated / "rtl", generated / "memory_map/rtl", generated / "pin_map/rtl",
generated / "archinfo"]
atomic_write(output / "def.fl", " ".join([*defines, *(f"+incdir+{p}" for p in include)]) + "\n")
atomic_write(output / "pdk_local.fl", "")
@@ -142,6 +173,9 @@ def main() -> int:
parser.add_argument("--filelists", type=Path)
parser.add_argument("--define", action="append", default=[])
parser.add_argument("--check", action="store_true")
parser.add_argument("--pdk", choices=("IHP130", "ICS55"), default="IHP130")
parser.add_argument("--simulator", choices=("IVERILOG", "VERILATOR"), default="VERILATOR")
parser.add_argument("--synthesis", choices=("NONE", "YOSYS"), default="NONE")
args = parser.parse_args()
document = read_topology(TINY / "integration/soc_topology.json",
TINY / "address_map/memory_map.json")
@@ -153,9 +187,10 @@ def main() -> int:
parser.error("--define requires a +define+ token")
output = args.output.resolve()
generate_memory_map(TINY / "address_map/memory_map.json", output / "memory_map", "YES", 128)
generate_pin_map(TINY / "pin_map/pin_map.json", output / "pin_map")
generate_bindings(document, output)
generate_filelists(args.filelists.resolve(), output, args.define)
generate_pin_map(TINY / "pin_map/pin_map.json", output / "pin_map", read_while_driving=True)
generate_bindings(document, output, args.pdk, "+define+HAVE_PLL" in args.define)
generate_filelists(args.filelists.resolve(), output, args.define,
args.pdk, args.simulator, args.synthesis)
return 0
+26 -10
View File
@@ -79,6 +79,14 @@ TINY_PR_COMMANDS = (
("configs/ci/ihp130-tiny.mk", ("APP=ci_smoke", "STA=OPENSTA", "sta")),
)
RTL_COMMANDS = (*PR_COMMANDS[:6], *TINY_PR_COMMANDS[:3])
TINY_PROFILES = {"IHP130": "configs/ci/ihp130-tiny.mk", "ICS55": "configs/ci/ics55-tiny.mk"}
def tiny_commands(pdk: str) -> tuple:
return tuple((TINY_PROFILES[pdk], (*values, "TINY_SIM_ARGS=+tiny_jtag_smoke")
if "sim" in values else values) for _, values in TINY_PR_COMMANDS)
PR_COMMANDS = (*PR_COMMANDS, *TINY_PR_COMMANDS)
SMOKE_COMMANDS = (
("configs/ci/ihp130.mk", RTL_LINT_VALUES),
@@ -179,6 +187,8 @@ def select_regression(
if pdk == "IHP130":
return PR_COMMANDS, PR_PROFILES
profile = PDK_PR_PROFILES[pdk]
if pdk == "ICS55":
return (*pdk_pr_commands(profile), *tiny_commands(pdk)), (profile, TINY_PROFILES[pdk])
return pdk_pr_commands(profile), (profile,)
if suite == "pr":
return PR_COMMANDS, PR_PROFILES
@@ -282,18 +292,24 @@ def main() -> int:
)
args = parser.parse_args()
try:
commands, profiles = select_regression(args.suite, args.pdk)
if args.soc == "TINY":
pdk = args.pdk or "ICS55"
if pdk not in TINY_PROFILES or args.suite == "nightly-extra":
raise ValueError("Tiny supports IHP130/ICS55 smoke, rtl, pr and nightly suites")
commands = tiny_commands(pdk)
if args.suite == "smoke":
commands = commands[:2]
elif args.suite == "rtl":
commands = commands[:3]
profiles = (TINY_PROFILES[pdk],)
else:
commands, profiles = select_regression(args.suite, args.pdk)
except ValueError as error:
parser.error(str(error))
if args.soc is not None:
tiny_profile = "configs/ci/ihp130-tiny.mk"
if args.soc == "TINY" and args.suite == "smoke":
commands, profiles = TINY_PR_COMMANDS[:2], (tiny_profile,)
else:
commands = tuple((profile, values) for profile, values in commands
if (profile == tiny_profile) == (args.soc == "TINY"))
profiles = tuple(profile for profile in profiles
if (profile == tiny_profile) == (args.soc == "TINY"))
if args.soc == "MINI":
commands = tuple((profile, values) for profile, values in commands
if profile not in TINY_PROFILES.values())
profiles = tuple(profile for profile in profiles if profile not in TINY_PROFILES.values())
if not commands:
parser.error(f"suite {args.suite} has no {args.soc} configuration for this PDK")
if args.netsim_boot_only:
+7 -6
View File
@@ -271,7 +271,7 @@ def render_core_ports(pads: list[Pad]) -> str:
return "\n".join(lines) + "\n"
def render_pad_instance(pad: Pad) -> str | None:
def render_pad_instance(pad: Pad, read_while_driving: bool = False) -> str | None:
instance = f"u_{pad.name}"
if pad.kind == "tri_input":
return (
@@ -297,8 +297,9 @@ def render_pad_instance(pad: Pad) -> str | None:
f".c2p_en({pad.output_enable_signal}), .p2c({pad.input_signal}));"
)
if pad.kind == "full":
parameters = " #(.ReadWhileDriving(1'b1))" if read_while_driving else ""
return (
f" tc_io_tri_full_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
f" tc_io_tri_full_pad{parameters} {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
f".c2p({pad.interface}.do_o[{pad.index}]), "
f".c2p_en({pad.interface}.oe_o[{pad.index}]), .p2c({pad.interface}.di_i[{pad.index}]), "
f".cs({pad.interface}.cs_o[{pad.index}]), .pu({pad.interface}.pu_o[{pad.index}]), "
@@ -325,7 +326,7 @@ def render_pad_instance(pad: Pad) -> str | None:
def render_pad_bindings(
pads: list[Pad], power_counts: dict[str, int] | None = None
pads: list[Pad], power_counts: dict[str, int] | None = None, read_while_driving: bool = False
) -> str:
counts = IHP130_POWER_PAD_COUNTS if power_counts is None else power_counts
lines = [
@@ -358,7 +359,7 @@ def render_pad_bindings(
]
)
for pad in pads:
line = render_pad_instance(pad)
line = render_pad_instance(pad, read_while_driving)
if line is not None:
lines.extend(wrap_feature(pad, line))
lines.append("`undef RETROSOC_PAD_POWER_CONNECTIONS")
@@ -461,13 +462,13 @@ def render_profile_bindings(pads: list[Pad], bindings: dict[str, str | None]) ->
return "\n".join(lines) + "\n"
def generate(map_path: Path, output_dir: Path) -> None:
def generate(map_path: Path, output_dir: Path, *, read_while_driving: bool = False) -> None:
pads, profiles = read_map(map_path)
power_counts = read_power_pad_counts(map_path)
rtl_dir = output_dir / "rtl"
atomic_write(rtl_dir / "retrosoc_asic_ports.svh", render_ports(pads))
atomic_write(
rtl_dir / "retrosoc_asic_pad_bindings.svh", render_pad_bindings(pads, power_counts)
rtl_dir / "retrosoc_asic_pad_bindings.svh", render_pad_bindings(pads, power_counts, read_while_driving)
)
atomic_write(rtl_dir / "retrosoc_core_ports.svh", render_core_ports(pads))
atomic_write(rtl_dir / "retrosoc_core_bindings.svh", render_core_bindings(pads))
+5 -1
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
@@ -21,13 +22,16 @@ TINY_SOURCES = (
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--update", action="store_true")
parser.add_argument("--pdk", choices=("IHP130", "ICS55"), default="ICS55")
args = parser.parse_args()
for name in TINY_SOURCES:
for name in (name for name in TINY_SOURCES if not name.startswith("pdk_")):
dependency = source(name)
ensure_git_repo(
dependency["url"], ROOT / dependency["destination"], dependency["revision"],
recursive=dependency.get("recursive", False), update=args.update,
)
subprocess.run([sys.executable, str(ROOT / "physical/pdk/setup.py"), "--pdk", args.pdk,
*(["--update"] if args.update else [])], check=True)
return 0
+275
View File
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""Source-bound Tiny ICS55 evidence and a fail-closed SAFE24 netlist audit."""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import hashlib
from pathlib import Path
import re
import sys
import tarfile
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts import tiny_r2_baseline as b # noqa: E402
from scripts.config_key import variant_id # noqa: E402
from scripts.tiny_r2_feasibility import timing_metrics, path_slack # noqa: E402
PHASE = "TINY-ICS55-P1"
RAM = "ics55_ecos_sram_1024x32_m8"
def verify_locked_views(root: Path) -> dict:
"""Check derived cache bytes against the unchanged locked archives."""
lock = b.read_json(root / "dependencies/dependencies.lock.json")
verified = {}
for key in ("pdk_ics55_h7cr_liberty", "pdk_ics55_sram_1024x32_m8", "pdk_ics55_sram_4096x32_m8"):
spec = lock["archives"][key]
path = root / spec["destination"]
if b.sha256(path) != spec["sha256"]:
raise ValueError(f"locked archive changed: {key}")
package = key.replace("pdk_ics55_sram_", "ics55_ecos_sram_")
seen = set()
with tarfile.open(path) as archive:
for member in archive:
if not member.isfile():
continue
relative = Path(member.name)
if relative.is_absolute() or ".." in relative.parts:
raise ValueError("unsafe archive view name")
target = None
if key == "pdk_ics55_h7cr_liberty":
for corner, tokens in (("tt", ("tt", "1p2", "25")), ("ss", ("ss", "1p08", "125"))):
if member.name.endswith(".lib") and all(t in member.name.lower() for t in tokens):
target = root / f".cache/retrosoc/pdk/ics55/ics55_h7cr_{corner}.lib"
else:
if relative.parts[0] == package:
relative = Path(*relative.parts[1:])
if relative.parts and relative.parts[0] in ("lib", "verilog"):
target = root / ".cache/retrosoc/pdk/ics55/sram" / package / relative
if target is not None:
if not target.is_file() or hashlib.sha256(archive.extractfile(member).read()).hexdigest() != b.sha256(target):
raise ValueError(f"cached view differs from locked archive: {target}")
seen.add(str(target))
if len(seen) != (2 if key == "pdk_ics55_h7cr_liberty" else 11):
raise ValueError(f"incomplete locked view set: {key}")
verified[key] = {"archive": b.artifact(path), "views": sorted(seen)}
from physical.pdk.prepare_ics55_sim_model import patch_muxi2_models, VERILATOR_LINT_OFF, VERILATOR_LINT_ON
pdk = root / lock["sources"]["pdk_ics55"]["destination"]
original = pdk / "IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/verilog/ics55_LLSC_H7CR.v"
patched, _ = patch_muxi2_models(original.read_text())
expected = VERILATOR_LINT_OFF + patched.rstrip() + "\n" + VERILATOR_LINT_ON
model = root / ".cache/retrosoc/pdk/ics55/ics55_h7cr_functional.v"
if model.read_text() != expected:
raise ValueError("standard-cell functional model differs from the owned locked-source transform")
verified["functional_cells"] = b.artifact(model)
return verified
def inspect_netlist(document: dict) -> dict:
modules = document["modules"]
top = "retrosoc_tiny_asic"
leaves = []
aliases = {}
def canonical(bit):
if bit in aliases:
aliases[bit] = canonical(aliases[bit])
return aliases[bit]
return bit
def alias(a, other):
a, other = canonical(a), canonical(other)
if a == other:
return
if a in ("0", "1", "x", "z"):
a, other = other, a
if a in ("0", "1", "x", "z"):
raise ValueError("conflicting constants at a hierarchy boundary")
aliases[a] = other
def visit(module_name, scope, bound):
module = modules[module_name]
mapping = {}
for port, bits in bound.items():
declaration = module["ports"][port]
for local, external in zip(declaration["bits"], bits):
if isinstance(local, str):
if declaration["direction"] == "output":
alias(external, local)
elif local in mapping:
alias(external, mapping[local])
else:
mapping[local] = external
def net(bit):
return bit if isinstance(bit, str) else mapping.get(bit, f"{scope}:{bit}")
for name, cell in module.get("cells", {}).items():
ports = {port: [net(bit) for bit in bits] for port, bits in cell["connections"].items()}
kind = cell["type"]
path = f"{scope}/{name}"
attrs = modules.get(kind, {}).get("attributes", {})
if kind in modules and not int(str(attrs.get("blackbox", "0")), 2):
visit(kind, path, ports)
else:
leaves.append({"path": path, "type": kind, "ports": ports})
visit(top, top, {})
for c in leaves:
for port, bits in c["ports"].items():
c["ports"][port] = [canonical(bit) for bit in bits]
clock = modules[top]["cells"]["u_clock_buffer"]["connections"]["clk_o"]
if len(clock) != 1 or not isinstance(clock[0], int):
raise ValueError("missing live SYS clock")
system = canonical(f"{top}:{clock[0]}")
if system in ("0", "1", "x", "z"):
raise ValueError("SYS is not a live clock net")
ram = [c for c in leaves if c["type"] == RAM]
pll = [c for c in leaves if c["type"] == "PLL_TOP"]
if any(c["type"] == "ics55_ecos_sram_4096x32_m8" for c in leaves):
raise ValueError("Tiny instantiated the unsupported 16 KiB SRAM interface stub")
if len(ram) != 32 or len(pll) != 1:
raise ValueError("expected 32 main SRAM macros and one PLL")
if any(c["ports"].get("CLK") != [system] for c in ram):
raise ValueError("main SRAM does not share direct SYS")
constants = {"0": 0, "1": 1}
for c in leaves:
if c["type"] in ("TIELOH7R", "TIEHIH7R"):
for bit in c["ports"]["Z"]:
constants[bit] = int(c["type"] == "TIEHIH7R")
enable = pll[0]["ports"].get("EN", [])
if len(enable) != 1 or constants.get(enable[0]) != 0:
raise ValueError("PLL enable is not proven constant zero")
cpu = [c for c in leaves if "u_hazard3_cpu_1port" in c["path"]
and ("CK" in c["ports"] or "CKN" in c["ports"])]
if not cpu or any(c["ports"].get("CK", c["ports"].get("CKN")) != [system] for c in cpu):
raise ValueError("CPU sequential clocks do not share direct SYS")
pll_outputs = {bit for name in ("CKOUT1", "CKOUT2", "CKTST") for bit in pll[0]["ports"].get(name, [])}
if system in pll_outputs:
raise ValueError("PLL drives SYS")
return {"system_net": system, "cpu_clocked_cells": len(cpu),
"sram": [{"path": c["path"], "clock": c["ports"]["CLK"]} for c in ram],
"pll": pll[0], "pll_enable": 0, "qualification": "SAFE24 digital connectivity only"}
def check_configuration(variant: Path, path: Path) -> None:
manifest = b.configuration(variant)
if manifest["configuration"]["PDK"] != "ICS55":
raise ValueError("ICS55 platform evidence cannot consume another PDK")
config = dict(line.split("=", 1) for line in path.read_text().splitlines() if "=" in line)
for key, value in {"PDK": "ICS55", "SOC": "TINY", "HAVE_PLL": "YES",
"HAVE_SRAM_MACRO": "YES", "SRAM_SIZE_KIB": "128",
"TOP_DESIGN": "retrosoc_tiny_asic", "PERIOD_PS": "41667"}.items():
if config.get(key) != value:
raise ValueError(f"consumed netlist configuration mismatch: {key}")
values = [f"{k}={v}" for k, v in manifest["configuration"].items()
if k not in ("SIMU", "SYNTH", "SYNTH_RECIPE", "STA")]
expected = variant_id(manifest["profile"], values, ROOT / "dependencies/dependencies.lock.json",
"2000-01-01-00-00").rsplit("-", 1)[1]
if config.get("CONFIG_DIGEST") != expected or variant.name.rsplit("-", 1)[1] != expected:
raise ValueError("consumed configuration digest mismatch")
def verify_netlist(variant: Path, netlist: Path, config: Path, output: Path) -> dict:
check_configuration(variant, config)
report = b.read_json(output)
if report.get("status") != "passed" or report.get("phase") != PHASE:
raise ValueError("missing successful structural audit")
if report.get("audit_script") != b.artifact(Path(__file__)):
raise ValueError("structural audit was produced by another script")
expected = [netlist, Path(str(netlist) + ".json"), config]
if report.get("inputs") != [b.artifact(path) for path in expected]:
raise ValueError("missing or changed consumed netlist artifacts")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--variant-root", type=Path, required=True)
parser.add_argument("action", choices=("capture", "audit-netlist", "verify-netlist", "report"))
parser.add_argument("--netlist", type=Path)
parser.add_argument("--config", type=Path)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
variant = args.variant_root.resolve()
parent = variant / "meta/tiny-ics55-p1"
try:
if args.action == "capture":
directory = b.new_attempt(parent, "inputs-")
b.dump(parent / "latest-inputs.json", {"path": str(directory / "identity.json")})
b.dump(directory / "identity.json", {"phase": PHASE, "status": "capturing"})
inputs = b.source_inputs(ROOT, "ICS55")
for path in (ROOT / "tests").rglob("*"):
if path.is_file() and path.suffix in (".py", ".sv", ".svh"):
inputs[str(path.relative_to(ROOT))] = b.sha256(path)
views = verify_locked_views(ROOT)
identity = {"phase": PHASE, "status": "captured", "created_at": datetime.now(timezone.utc).isoformat(), "views": views,
"source": b.snapshot(ROOT, directory, inputs), "tools": b.tools_identity(ROOT)}
if any(b.sha256(ROOT / name) != digest for name, digest in inputs.items()):
raise ValueError("source changed while capturing inputs")
b.dump(directory / "identity.json", identity)
b.dump(parent / "latest-inputs.json", {"path": str(directory / "identity.json")})
elif args.action in ("audit-netlist", "verify-netlist"):
if not all((args.netlist, args.config, args.output)):
parser.error("netlist actions require --netlist, --config and --output")
check_configuration(variant, args.config)
if args.action == "audit-netlist":
net_json = Path(str(args.netlist) + ".json")
report = {"phase": PHASE, "status": "passed", "audit_script": b.artifact(Path(__file__)),
"binding": inspect_netlist(b.read_json(net_json)),
"inputs": [b.artifact(p) for p in (args.netlist, net_json, args.config)]}
b.dump(args.output, report)
else:
verify_netlist(variant, args.netlist, args.config, args.output)
else:
b.dump(parent / "report.json", {"phase": PHASE, "status": "incomplete"})
identity = b.read_json(Path(b.read_json(parent / "latest-inputs.json")["path"]))
if identity.get("status") != "captured":
raise ValueError("missing successful source capture")
inputs = b.read_json(b.check_artifact(identity["source"]["inputs"]))
if any(b.sha256(ROOT / name) != digest for name, digest in inputs.items()):
raise ValueError("captured source changed")
flows = {}
for key in ("snapshot", "patch"):
b.check_artifact(identity["source"][key])
for tool in identity["tools"].values():
b.check_artifact(tool["executable"])
b.check_artifact(tool["archive"])
for name in ("syn/yosys/result-synth.json", "sta/opensta/result-sta.json"):
path = variant / name
result = b.read_json(path)
if result.get("status") != "passed" or result.get("exit_code") != 0:
raise ValueError(f"incomplete flow: {name}")
if datetime.fromisoformat(result["started_at"]) < datetime.fromisoformat(identity["created_at"]):
raise ValueError(f"flow predates captured source: {name}")
flows[name] = b.artifact(path)
sta = variant / "sta/opensta"
mapped = variant / "syn/yosys/out/retrosoc_tiny_asic_yosys.v"
verify_netlist(variant, mapped, mapped.with_suffix(".config"),
variant / "syn/yosys/tiny-ics55-netlist.json")
log = (sta / "opensta.log").read_text()
if "TINY_ICS55_STA_AUDIT_PASS:" not in log or re.search(r"(?im)^Error:|\bFATAL\b|%Error", log):
raise ValueError("incomplete or erroneous STA audit")
metrics = timing_metrics(sta / "timing_metrics.rpt")
report = {"phase": PHASE, "status": "completed", "source": identity,
"flows": flows, "qualification": "SAFE24 synthesis/STA observation only; functional acceptance is separate",
"timing": metrics,
"timing_status": "failed" if any(v < 0 for v in metrics.values()) else "nonnegative_reported_slack",
"sys_setup_slack_ns": path_slack(sta / "sys-setup.rpt"),
"sys_hold_slack_ns": path_slack(sta / "sys-hold.rpt"),
"sta_artifacts": {p.name: b.artifact(p) for p in sta.iterdir() if p.is_file()},
"macro_audit": b.artifact(variant / "syn/yosys/tiny-ics55-netlist.json")}
b.dump(parent / "report.json", report)
print(f"{PHASE} {args.action}: PASS (not physical qualification)")
return 0
except (OSError, ValueError, KeyError) as error:
if args.action == "capture" and "directory" in locals():
b.dump(directory / "identity.json", {"phase": PHASE, "status": "failed", "error": str(error)})
print(f"{PHASE}: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+29 -13
View File
@@ -38,7 +38,7 @@ TOOL_PROGRAMS = {"verilator": "verilator", "verible": "verible-verilog-format",
SOURCE_PATHS = ("Makefile", "configs/ci/ihp130-tiny.mk", "dependencies/dependencies.lock.json",
"requirements", "rtl/tiny", "rtl/ip", "rtl/tech", "rtl/mk/software.mk",
"crt", "app/apps/ci_smoke", "app/apps/bringup", "scripts",
"physical/smoke", "physical/librelane/tiny")
"physical/smoke", "physical/librelane/tiny", "physical/pdk")
CASE_FIELDS = {"id", "name", "cycles", "instructions", "kernel_cycles", "kernel_calls",
"payload_bytes", "checksum", "cpu_wait", "dma_wait", "sram_reads",
"sram_writes", "sram_rbeats", "sram_wbeats", "sram_stalls", "sram_errors"}
@@ -75,7 +75,7 @@ def digest_map(value: dict) -> str:
return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
def source_inputs(root: Path) -> dict[str, str]:
def source_inputs(root: Path, pdk: str = "IHP130") -> dict[str, str]:
names = subprocess.check_output(
["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--",
*SOURCE_PATHS], cwd=root,
@@ -83,7 +83,14 @@ def source_inputs(root: Path) -> dict[str, str]:
paths = {root / name for name in names if name and not name.endswith(
(".md", ".png", ".jpg", ".svg", ".pdf"))}
lock = read_json(root / "dependencies/dependencies.lock.json")
for name in TINY_SOURCES:
selected_sources = list(TINY_SOURCES)
if pdk == "ICS55":
selected_sources = [name if name != "pdk_ihp130" else "pdk_ics55" for name in selected_sources]
selected_sources.append("pdk_ics55_pll")
paths.add(root / "configs/ci/ics55-tiny.mk")
paths.update(p for p in (root / ".cache/retrosoc/pdk/ics55").rglob("*")
if p.is_file() and p.suffix in (".v", ".lib"))
for name in selected_sources:
spec = lock["sources"][name]
checkout = root / spec["destination"]
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=checkout,
@@ -100,7 +107,7 @@ def source_inputs(root: Path) -> dict[str, str]:
paths.update((base / "sg13g2_stdcell/lib").glob("*slow_1p08V_125C.lib"))
paths.update((base / "sg13g2_io/lib").glob("*slow_1p08V_3p0V_125C.lib"))
else:
for suffix in ("*.v", "*.sv", "*.vh", "*.svh", "*.c", "*.h"):
for suffix in ("*.v", "*.sv", "*.vh", "*.svh", "*.c", "*.h", "*.lib", "*.lef"):
paths.update(checkout.rglob(suffix))
return {str(path.relative_to(root)): sha256(path) for path in sorted(paths) if path.is_file()}
@@ -126,13 +133,22 @@ def runtime_inputs(root: Path, variant: Path, simulator: str) -> dict[str, str]:
def configuration(variant: Path) -> dict:
manifest = read_json(variant / "meta/manifest.json")
config = manifest["configuration"]
if manifest["profile"] != "ihp130-tiny" or any(
config.get(key) != expected for key, expected in COMPATIBILITY.items()
expected = dict(COMPATIBILITY)
if manifest["profile"] == "ics55-tiny":
expected.update(PDK="ICS55", HAVE_PLL="YES")
if manifest["profile"] not in ("ihp130-tiny", "ics55-tiny") or any(
config.get(key) != value for key, value in expected.items()
):
raise ValueError("baseline requires the committed Tiny/IHP130 24 MHz macro profile")
raise ValueError("baseline requires an explicit Tiny 24 MHz macro profile")
return manifest
def variant_source_inputs(root: Path, variant: Path) -> dict:
if configuration(variant)["configuration"]["PDK"] == "ICS55":
return source_inputs(root, "ICS55")
return source_inputs(root)
def tools_identity(root: Path) -> dict:
cache_value = os.environ.get("RETROSOC_DEVELOPMENT_CACHE")
if not cache_value:
@@ -325,7 +341,7 @@ def build_image(args: argparse.Namespace) -> int:
try:
manifest = configuration(variant)
tools = tools_identity(root)
before = source_inputs(root)
before = variant_source_inputs(root, variant)
# Archive generated C headers and the preprocessed flat linker input too.
generated = {str(path.relative_to(root)): sha256(path)
for path in (variant / "generated/tiny").rglob("*") if path.is_file()}
@@ -355,7 +371,7 @@ def build_image(args: argparse.Namespace) -> int:
ebss = re.search(r"^([0-9a-f]+)(?: [0-9a-f]+)? [A-Za-z] _ebss$", symbols, re.MULTILINE)
if ebss is None or not 0x30000000 <= int(ebss[1], 16) <= 0x3001F000:
raise ValueError("image does not preserve at least 4 KiB of stack headroom")
if before != source_inputs(root):
if before != variant_source_inputs(root, variant):
raise ValueError("source changed while compiling the retained image")
if any(not (root / name).is_file() or sha256(root / name) != digest
for name, digest in generated.items()):
@@ -421,12 +437,12 @@ def simulate(args: argparse.Namespace) -> int:
firmware_source=image["source"], manifest=configuration(variant), tools=tools_identity(root))
if args.simulator == "iverilog":
record["runtime_tool"] = vvp_identity(args.vvp, record["tools"]["iverilog"])
before = source_inputs(root)
before = variant_source_inputs(root, variant)
command = args.command[1:] if args.command[:1] == ["--"] else args.command
if not command:
raise ValueError("a fresh simulator compilation command is required")
execute(root, directory, "compile-model", command, quiet=True)
if before != source_inputs(root):
if before != variant_source_inputs(root, variant):
raise ValueError("source changed while compiling the simulator")
runtime = runtime_inputs(root, variant, args.simulator)
record["rtl_source"] = snapshot(root, directory / "inputs", before | runtime)
@@ -472,7 +488,7 @@ def simulate(args: argparse.Namespace) -> int:
dump(output, record)
if errors:
raise ValueError("; ".join(errors))
if before != source_inputs(root) or runtime != runtime_inputs(root, variant, args.simulator):
if before != variant_source_inputs(root, variant) or runtime != runtime_inputs(root, variant, args.simulator):
raise ValueError("source or generated inputs changed during simulation")
validate_runs(record["runs"])
validate_runtime_tool(root, record)
@@ -511,7 +527,7 @@ def report(args: argparse.Namespace) -> int:
check_artifact(attempt["model"])
source = attempt["rtl_source"]
inputs = read_json(check_artifact(source["inputs"]))
if inputs != source_inputs(root) | runtime_inputs(root, variant, simulator):
if inputs != variant_source_inputs(root, variant) | runtime_inputs(root, variant, simulator):
raise ValueError(f"stale {simulator} source inputs")
check_artifact(source["snapshot"])
validate_runs(attempt["runs"])
+1 -1
View File
@@ -44,7 +44,7 @@ def require_variant(root: Path, variant: Path) -> dict:
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":
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