mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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:
56 files changed
+1681
-178
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user