mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-06 22:55:14 +00:00
333 lines
12 KiB
Python
333 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Export the configured retroSoC RTL as a tarball or a single SV file."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import re
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
SCRIPT_DIR = REPO_ROOT / "rtl/mini/script"
|
|
INTEGRATION_DIR = REPO_ROOT / "rtl/mini/integration"
|
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
sys.path.insert(0, str(INTEGRATION_DIR))
|
|
from filelist import FileList, atomic_write, parse_filelists # noqa: E402
|
|
from generate_filelist import generate_all # noqa: E402
|
|
from generate_user_extensions import generate as generate_user_extensions # noqa: E402
|
|
|
|
|
|
INCLUDE_RE = re.compile(r'^\s*`include\s+"([^"]+)"')
|
|
HAZARD3_UPDATE_FUNCTION_RE = re.compile(
|
|
r"function\s+\[XLEN-1:0\]\s+update_nonconst\s*;\s*"
|
|
r"input\s+\[XLEN-1:0\]\s+prev\s*;\s*"
|
|
r"input\s+\[XLEN-1:0\]\s+nonconst\s*;\s*"
|
|
r"begin\s*"
|
|
r"update_nonconst\s*=\s*\(wdata_update\s*&\s*nonconst\)\s*\|\s*"
|
|
r"\(prev\s*&\s*~nonconst\)\s*;\s*"
|
|
r"end\s*endfunction"
|
|
)
|
|
HAZARD3_UPDATE_CALL_RE = re.compile(
|
|
r"update_nonconst\(\s*(?P<previous>[A-Za-z_][A-Za-z0-9_$]*)\s*,\s*"
|
|
r"(?P<writable>[A-Za-z_][A-Za-z0-9_$]*)\s*\)"
|
|
)
|
|
|
|
|
|
def build_defines(args: argparse.Namespace) -> list[str]:
|
|
defines = [
|
|
f"+define+PDK_{args.pdk}",
|
|
f"+define+SIMU_{args.simu}",
|
|
"+define+SYNTHESIS",
|
|
f"+define+SOC_EXT_CLK_HZ={getattr(args, 'ext_clk_hz', 72_000_000)}",
|
|
f"+define+SOC_AUD_CLK_HZ={getattr(args, 'aud_clk_hz', 18_432_000)}",
|
|
f"+define+SOC_CLINT_TIMEBASE_HZ={getattr(args, 'clint_timebase_hz', 1_000_000)}",
|
|
]
|
|
if args.have_pll:
|
|
defines.append("+define+HAVE_PLL")
|
|
if args.have_sram_if:
|
|
defines.append("+define+HAVE_SRAM_IF")
|
|
if args.have_sram_macro:
|
|
defines.append("+define+HAVE_SRAM_MACRO")
|
|
if not args.have_sva:
|
|
defines.append("+define+SV_ASSRT_DISABLE")
|
|
idcode = int(args.jtag_idcode, 16)
|
|
if idcode >= (1 << 31):
|
|
idcode -= 1 << 32
|
|
defines.append(f"+define+SOC_JTAG_IDCODE={idcode}")
|
|
return defines
|
|
|
|
|
|
def configured_filelist(
|
|
args: argparse.Namespace,
|
|
generated_dir: Path,
|
|
user_extensions_dir: Path,
|
|
*,
|
|
require_files: bool = True,
|
|
) -> FileList:
|
|
names: list[str | Path] = []
|
|
for name in (
|
|
"memory_map_filelist",
|
|
"soc_topology_filelist",
|
|
"user_extensions_filelist",
|
|
):
|
|
path = getattr(args, name, None)
|
|
if path is not None:
|
|
names.append(path)
|
|
names.extend(
|
|
[
|
|
"def.fl",
|
|
"sys_def.fl",
|
|
"commonip.fl",
|
|
"inc.fl",
|
|
]
|
|
)
|
|
pin_map = getattr(args, "pin_map_filelist", None)
|
|
if pin_map is not None:
|
|
names.append(pin_map)
|
|
names.extend(["clusterip.fl", "ip.fl", "tech.fl"])
|
|
names.append("core_hazard3.fl")
|
|
dynamic_core = args.dynamic_core_filelist
|
|
if not dynamic_core.is_file():
|
|
raise FileNotFoundError(f"user core filelist is not generated: {dynamic_core}")
|
|
names.append(str(dynamic_core))
|
|
dynamic_ip = args.dynamic_ip_filelist
|
|
if not dynamic_ip.is_file():
|
|
raise FileNotFoundError(f"user IP filelist is not generated: {dynamic_ip}")
|
|
names.append(str(dynamic_ip))
|
|
if getattr(args, "user_extensions_filelist", None) is None:
|
|
names.append(str(user_extensions_dir / "user_extensions.fl"))
|
|
names.append("top.fl")
|
|
paths = [Path(name) if Path(name).is_absolute() else generated_dir / name for name in names]
|
|
filelist = parse_filelists(paths, require_files=require_files)
|
|
archinfo_incdir = getattr(args, "archinfo_incdir", None)
|
|
if archinfo_incdir is not None:
|
|
archinfo_incdir = archinfo_incdir.resolve()
|
|
if require_files and not archinfo_incdir.is_dir():
|
|
raise FileNotFoundError(f"archinfo include directory not found: {archinfo_incdir}")
|
|
filelist.incdirs.append(archinfo_incdir)
|
|
filelist.deduplicate()
|
|
return filelist
|
|
|
|
|
|
def resolve_include(name: str, incdirs: list[Path], current_file: Path) -> Path | None:
|
|
candidates = [current_file.parent / name, *(directory / name for directory in incdirs)]
|
|
for candidate in candidates:
|
|
if candidate.is_file():
|
|
return candidate.resolve()
|
|
return None
|
|
|
|
|
|
def collect_includes(files: list[Path], incdirs: list[Path]) -> list[Path]:
|
|
headers: set[Path] = set()
|
|
|
|
def visit(path: Path) -> None:
|
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
match = INCLUDE_RE.search(line)
|
|
if not match:
|
|
continue
|
|
header = resolve_include(match.group(1), incdirs, path)
|
|
if header is None:
|
|
continue
|
|
if header not in headers:
|
|
headers.add(header)
|
|
visit(header)
|
|
|
|
for source in files:
|
|
visit(source)
|
|
return sorted(headers)
|
|
|
|
|
|
def inline_file(path: Path, incdirs: list[Path], stack: tuple[Path, ...] = ()) -> list[str]:
|
|
resolved = path.resolve()
|
|
if resolved in stack:
|
|
cycle = " -> ".join(str(item) for item in (*stack, resolved))
|
|
raise RuntimeError(f"cyclic `include detected: {cycle}")
|
|
|
|
output: list[str] = []
|
|
for line in resolved.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
match = INCLUDE_RE.search(line)
|
|
if not match:
|
|
output.append(line)
|
|
continue
|
|
header = resolve_include(match.group(1), incdirs, resolved)
|
|
if header is None:
|
|
output.append(line)
|
|
continue
|
|
output.append(f'// begin `include "{match.group(1)}"')
|
|
output.extend(inline_file(header, incdirs, (*stack, resolved)))
|
|
output.append(f'// end `include "{match.group(1)}"')
|
|
return output
|
|
|
|
|
|
def defines_to_sv(defines: list[str]) -> list[str]:
|
|
output = []
|
|
for define in defines:
|
|
body = define[len("+define+") :]
|
|
name, separator, value = body.partition("=")
|
|
output.append(f"`define {name}{' ' + value if separator else ''}")
|
|
return output
|
|
|
|
|
|
def sanitize_librelane_source(source: str) -> str:
|
|
"""Inline the Hazard3 CSR helper that yosys-slang mis-models with hierarchy."""
|
|
source, definitions = HAZARD3_UPDATE_FUNCTION_RE.subn("", source, count=1)
|
|
if definitions != 1:
|
|
raise RuntimeError("expected exactly one Hazard3 update_nonconst definition")
|
|
|
|
def inline_update(match: re.Match[str]) -> str:
|
|
previous = match.group("previous")
|
|
writable = match.group("writable")
|
|
return f"((wdata_update & {writable}) | ({previous} & ~{writable}))"
|
|
|
|
source, calls = HAZARD3_UPDATE_CALL_RE.subn(inline_update, source)
|
|
if calls != 2 or "update_nonconst" in source:
|
|
raise RuntimeError("expected exactly two Hazard3 update_nonconst calls")
|
|
return source
|
|
|
|
|
|
def write_single_sv(filelist: FileList, output: Path, *, librelane_safe: bool = False) -> None:
|
|
lines = [
|
|
"// Generated by physical/smoke/syn/tools/export_soc_sources.py",
|
|
*defines_to_sv(filelist.defines),
|
|
"",
|
|
]
|
|
for source in [*filelist.library_files, *filelist.files]:
|
|
lines.append(f"// ===== file: {source.relative_to(REPO_ROOT)} =====")
|
|
lines.extend(inline_file(source, filelist.incdirs))
|
|
lines.append("")
|
|
source = "\n".join(lines) + "\n"
|
|
if librelane_safe:
|
|
source = sanitize_librelane_source(source)
|
|
atomic_write(output, source)
|
|
|
|
|
|
def bundle_relative(path: Path) -> Path:
|
|
try:
|
|
relative = path.resolve().relative_to(REPO_ROOT)
|
|
except ValueError:
|
|
return Path("_abs") / path.as_posix().lstrip("/")
|
|
if relative.parts and relative.parts[0] == "rtl":
|
|
return Path(*relative.parts[1:])
|
|
return Path("repo") / relative
|
|
|
|
|
|
def write_tar(
|
|
filelist: FileList,
|
|
export_dir: Path,
|
|
soc: str,
|
|
metadata_files: dict[Path, Path] | None = None,
|
|
) -> Path:
|
|
export_dir.mkdir(parents=True, exist_ok=True)
|
|
sources = [*filelist.library_files, *filelist.files]
|
|
bundled_files: dict[Path, Path] = {}
|
|
for source in [*sources, *collect_includes(sources, filelist.incdirs)]:
|
|
bundled_files[bundle_relative(source)] = source.resolve()
|
|
|
|
manifest = [*filelist.defines]
|
|
manifest.extend(f"+incdir+{bundle_relative(path)}" for path in filelist.incdirs)
|
|
manifest.extend(str(bundle_relative(path)) for path in sources)
|
|
manifest_data = ("\n".join(manifest) + "\n").encode()
|
|
|
|
tar_path = export_dir / f"retrosoc_{soc.lower()}_sources.tar.gz"
|
|
with tarfile.open(tar_path, "w:gz") as archive:
|
|
for relative, source in sorted(bundled_files.items()):
|
|
archive.add(source, arcname=str(Path("rtl") / relative), recursive=False)
|
|
for relative, source in sorted((metadata_files or {}).items()):
|
|
archive.add(source, arcname=str(Path("rtl") / relative), recursive=False)
|
|
filelist_info = tarfile.TarInfo("rtl/filelist.fl")
|
|
filelist_info.size = len(manifest_data)
|
|
filelist_info.mode = 0o644
|
|
archive.addfile(filelist_info, io.BytesIO(manifest_data))
|
|
return tar_path
|
|
|
|
|
|
def positive_integer(value: str) -> int:
|
|
parsed = int(value)
|
|
if parsed <= 0:
|
|
raise argparse.ArgumentTypeError("value must be positive")
|
|
return parsed
|
|
|
|
|
|
def metadata_file(value: str) -> tuple[Path, Path]:
|
|
destination, separator, source_text = value.partition("=")
|
|
relative = Path(destination)
|
|
source = Path(source_text)
|
|
if (
|
|
not separator
|
|
or not destination
|
|
or relative.is_absolute()
|
|
or ".." in relative.parts
|
|
or not source.is_file()
|
|
):
|
|
raise argparse.ArgumentTypeError("metadata must use a safe DESTINATION=SOURCE_FILE mapping")
|
|
return relative, source.resolve()
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Export configured retroSoC RTL sources")
|
|
parser.add_argument("mode", choices=("tar", "sv"))
|
|
parser.add_argument("--soc", default="MINI", choices=("MINI",))
|
|
parser.add_argument("--pdk", default="IHP130", choices=("IHP130", "ICS55", "SKY130", "GF180"))
|
|
parser.add_argument("--simu", default="VCS", choices=("VCS", "VERILATOR", "IVERILOG"))
|
|
parser.add_argument("--have-pll", action="store_true")
|
|
parser.add_argument("--have-sram-if", action="store_true")
|
|
parser.add_argument("--have-sram-macro", action="store_true")
|
|
parser.add_argument("--have-sva", action="store_true")
|
|
parser.add_argument("--jtag-idcode", default="DEADBEEF")
|
|
parser.add_argument("--ext-clk-hz", type=positive_integer, default=72_000_000)
|
|
parser.add_argument("--aud-clk-hz", type=positive_integer, default=18_432_000)
|
|
parser.add_argument("--clint-timebase-hz", type=positive_integer, default=1_000_000)
|
|
parser.add_argument(
|
|
"--dynamic-core-filelist",
|
|
type=Path,
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
"--dynamic-ip-filelist",
|
|
type=Path,
|
|
required=True,
|
|
)
|
|
parser.add_argument("--memory-map-filelist", type=Path)
|
|
parser.add_argument("--soc-topology-filelist", type=Path)
|
|
parser.add_argument("--user-extensions-filelist", type=Path)
|
|
parser.add_argument("--pin-map-filelist", type=Path)
|
|
parser.add_argument("--archinfo-incdir", type=Path)
|
|
parser.add_argument("--librelane-safe", action="store_true")
|
|
parser.add_argument("--metadata-file", type=metadata_file, action="append", default=[])
|
|
parser.add_argument("--output-dir", type=Path, default=REPO_ROOT / "export")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
if not re.fullmatch(r"[0-9a-fA-F]{8}", args.jtag_idcode):
|
|
raise SystemExit("--jtag-idcode must be exactly eight hexadecimal digits")
|
|
export_dir = args.output_dir.resolve()
|
|
export_dir.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix="retrosoc-filelists-") as temporary:
|
|
generated_dir = Path(temporary)
|
|
user_extensions_dir = generated_dir / "user_extensions"
|
|
generate_all(generated_dir, build_defines(args))
|
|
if args.user_extensions_filelist is None:
|
|
generate_user_extensions(
|
|
REPO_ROOT / "rtl/mini/integration/user_extensions.json",
|
|
user_extensions_dir,
|
|
)
|
|
filelist = configured_filelist(args, generated_dir, user_extensions_dir)
|
|
if args.mode == "sv":
|
|
output = export_dir / "retrosoc_asic_sources.sv"
|
|
write_single_sv(filelist, output, librelane_safe=args.librelane_safe)
|
|
else:
|
|
output = write_tar(filelist, export_dir, args.soc, dict(args.metadata_file))
|
|
print(f"generated: {output}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|