mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat(mpw): consume native RIBP generator
This commit is contained in:
1 parent
fc6a46c5ee
commit
35e855d1e2
10 files changed
+143
-321
No files matched your search
@@ -3,7 +3,7 @@
|
||||
"sources": {
|
||||
"mpw": {
|
||||
"url": "https://github.com/retroSoC/mini-ver-mpw.git",
|
||||
"revision": "41955b19fe17d8adbb4b6634ae59709e118f70f9",
|
||||
"revision": "08ad5eecb469e7b25c9a4165982925b965745ae7",
|
||||
"destination": "rtl/managed/mpw",
|
||||
"license": "NOASSERTION"
|
||||
},
|
||||
|
||||
@@ -10,3 +10,4 @@ packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73
|
||||
typing-extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
|
||||
typing-inspect==0.9.0 --hash=sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f
|
||||
click==8.1.8 --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2
|
||||
pyslang==11.0.0 --hash=sha256:3346f50648c0d62ac63856908f86c850d9f8c3d2b07d7a4f36d315f5afa2b5d4
|
||||
+1
-1
@@ -43,7 +43,7 @@ $(MPW_STAMP):
|
||||
|
||||
$(MPW_VARIANT_STAMP): $(MPW_STAMP) $(ROOT_PATH)/scripts/generate_mpw.py
|
||||
$(FLOW_PYTHON) $(ROOT_PATH)/scripts/generate_mpw.py --root $(ROOT_PATH) \
|
||||
--output $(MPW_OUTPUT_DIR) --simu $(SIMU) \
|
||||
--output $(MPW_OUTPUT_DIR) \
|
||||
--lock-file $(CACHE_ROOT)/locks/mpw.lock
|
||||
@mkdir -p $(@D)
|
||||
@touch $@
|
||||
|
||||
@@ -35,14 +35,15 @@ without an interrupt entry are driven low. Firmware does not expose a generic
|
||||
external interrupt API until the SoC includes a claim/complete interrupt
|
||||
controller.
|
||||
|
||||
`user_extensions.json` separately defines the supported scalar user-core and
|
||||
user-IP slots. `generate_user_extensions.py` creates isolated default routing
|
||||
and one explicit instance per selected slot for `user_core_top.sv` and
|
||||
`user_ip_top.sv`. Adding an extension therefore requires a reviewed JSON entry
|
||||
rather than manual edits to the SoC muxes. Each core declares whether its reset
|
||||
is synchronous or asynchronous so a generated legacy RIB adapter uses the same
|
||||
reset style and does not introduce mixed-reset paths. The module port contract
|
||||
remains the existing user design template contract.
|
||||
`user_extensions.json` defines the SoC-level user-core and user-IP slots.
|
||||
The locked mini-ver-mpw `mpw.toml` remains the source of truth for enabled
|
||||
design IDs, module names, slots, and reset type. `scripts/generate_mpw.py`
|
||||
verifies that both descriptions agree before RTL generation.
|
||||
`generate_user_extensions.py` then creates isolated default routing and one
|
||||
explicit instance per selected slot for `user_core_top.sv` and `user_ip_top.sv`.
|
||||
User cores use `ribp_if.master ribp`; user IPs use `user_gpio_if.user_ip gpio`
|
||||
and APB4. This prevents legacy RIB/NMI and electrical pad-control contracts
|
||||
from entering the SoC muxes.
|
||||
|
||||
`clock_reset_domains.json` is the checked inventory of root clock domains,
|
||||
reset synchronizers, and RCU PLL control crossings. It records the primitive,
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
"ip_selector_width": 8,
|
||||
"gpio_width": 32,
|
||||
"core_targets": [
|
||||
{"slot": 0, "module": "user_core_design_username1", "instance": "u_user_core_design_username1", "bus": "ribp", "reset": "sync"},
|
||||
{"slot": 1, "module": "user_core_design_username2", "instance": "u_user_core_design_username2", "bus": "ribp", "reset": "sync"},
|
||||
{"slot": 2, "module": "user_core_design_username3", "instance": "u_user_core_design_username3", "bus": "ribp", "reset": "async"},
|
||||
{"slot": 3, "module": "user_core_design_username4", "instance": "u_user_core_design_username4", "bus": "ribp", "reset": "async"},
|
||||
{"slot": 4, "module": "user_core_design_username5", "instance": "u_user_core_design_username5", "bus": "ribp", "reset": "async"},
|
||||
{"slot": 5, "module": "user_core_design_username6", "instance": "u_user_core_design_username6", "bus": "ribp", "reset": "async"}
|
||||
{"slot": 0, "module": "mpw_core_kianv_rv32i_user_core_design", "instance": "u_mpw_core_kianv_rv32i", "bus": "ribp", "reset": "sync"},
|
||||
{"slot": 1, "module": "mpw_core_kianv_rv32ima_user_core_design", "instance": "u_mpw_core_kianv_rv32ima", "bus": "ribp", "reset": "sync"},
|
||||
{"slot": 2, "module": "mpw_core_hazard3_user_core_design", "instance": "u_mpw_core_hazard3", "bus": "ribp", "reset": "async"},
|
||||
{"slot": 3, "module": "mpw_core_serv_user_core_design", "instance": "u_mpw_core_serv", "bus": "ribp", "reset": "async"},
|
||||
{"slot": 4, "module": "mpw_core_femtorv32_user_core_design", "instance": "u_mpw_core_femtorv32", "bus": "ribp", "reset": "async"},
|
||||
{"slot": 5, "module": "mpw_core_darkriscv_user_core_design", "instance": "u_mpw_core_darkriscv", "bus": "ribp", "reset": "async"}
|
||||
],
|
||||
"ip_targets": [
|
||||
{"slot": 1, "module": "user_ip_design_username1", "instance": "u_user_ip_design_username1"},
|
||||
{"slot": 2, "module": "user_ip_design_username2", "instance": "u_user_ip_design_username2"}
|
||||
{"slot": 1, "module": "mpw_ip_tiny_timer_user_ip_design", "instance": "u_mpw_ip_tiny_timer"},
|
||||
{"slot": 2, "module": "mpw_ip_tiny_gpio_user_ip_design", "instance": "u_mpw_ip_tiny_gpio"}
|
||||
]
|
||||
}
|
||||
+66
-220
@@ -1,240 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate native RIBP MPW sources from the locked mini-ver-mpw manifest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import fcntl
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Iterator, TextIO
|
||||
|
||||
|
||||
def tree_digest(root: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
if not root.exists():
|
||||
return ""
|
||||
for path in sorted(
|
||||
item for item in root.rglob("*") if item.is_file() and item.name != ".stamp"
|
||||
):
|
||||
digest.update(path.relative_to(root).as_posix().encode("utf-8"))
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run(root: Path, log: TextIO, script: Path, *args: str) -> None:
|
||||
command = ["python3", str(script), *args]
|
||||
log.write("+ " + " ".join(command) + "\n")
|
||||
log.flush()
|
||||
subprocess.run(
|
||||
command,
|
||||
cwd=root,
|
||||
check=True,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def rewrite_filelists(root: Path, replacements: tuple[tuple[Path, Path], ...]) -> None:
|
||||
for filelist in root.rglob("*.fl"):
|
||||
content = filelist.read_text(encoding="utf-8")
|
||||
for old_prefix, new_prefix in replacements:
|
||||
content = content.replace(str(old_prefix.resolve()), str(new_prefix.resolve()))
|
||||
filelist.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def migrate_user_extension_includes(root: Path) -> list[Path]:
|
||||
"""Migrate legacy user-extension includes in isolated MPW sources."""
|
||||
legacy_include = re.compile(r'`include\s+"mdd_config\.svh"')
|
||||
migrated: list[Path] = []
|
||||
for source in sorted(root.rglob("*.sv")):
|
||||
content = source.read_text(encoding="utf-8")
|
||||
content, replacements = legacy_include.subn('`include "user_extensions.svh"', content)
|
||||
if replacements == 0:
|
||||
continue
|
||||
source.write_text(content, encoding="utf-8")
|
||||
migrated.append(source)
|
||||
return migrated
|
||||
|
||||
|
||||
def migrate_user_gpio_interfaces(root: Path) -> list[Path]:
|
||||
"""Migrate legacy MPW user-IP GPIO ports in isolated build sources."""
|
||||
legacy_port = re.compile(r"\bgpio_if\.dut\s+gpio\b")
|
||||
legacy_pad_control = re.compile(
|
||||
r"^[ \t]*assign[ \t]+gpio\.gpio_(?:cs|pu|pd)[ \t]*=[^;]*;[^\n]*\n",
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
migrated: list[Path] = []
|
||||
for source in sorted(root.rglob("*.sv")):
|
||||
content = source.read_text(encoding="utf-8")
|
||||
if legacy_port.search(content) is None:
|
||||
continue
|
||||
|
||||
content, port_count = legacy_port.subn("user_gpio_if.user_ip gpio", content)
|
||||
if port_count != 1:
|
||||
raise ValueError(f"expected one legacy GPIO port in {source}")
|
||||
content = legacy_pad_control.sub("", content)
|
||||
for legacy_name, interface_name in (
|
||||
("gpio.gpio_oe", "gpio.oe_o"),
|
||||
("gpio.gpio_out", "gpio.do_o"),
|
||||
("gpio.gpio_in", "gpio.di_i"),
|
||||
):
|
||||
content = content.replace(legacy_name, interface_name)
|
||||
if "gpio.gpio_" in content:
|
||||
raise ValueError(f"unsupported legacy GPIO signal in {source}")
|
||||
source.write_text(content, encoding="utf-8")
|
||||
migrated.append(source)
|
||||
return migrated
|
||||
|
||||
|
||||
def migrate_user_core_ribp_interfaces(root: Path) -> list[Path]:
|
||||
"""Migrate isolated user-core sources from the retired RIB spelling."""
|
||||
migrated: list[Path] = []
|
||||
for source in sorted(
|
||||
path for path in root.rglob("*") if path.is_file() and path.suffix in {".sv", ".fl"}
|
||||
):
|
||||
content = source.read_text(encoding="utf-8")
|
||||
updated = content.replace("rib_if", "ribp_if")
|
||||
updated = re.sub(
|
||||
r"\bribp_if\.(master|slave)\s+rib\b", r"ribp_if.\1 ribp", updated
|
||||
)
|
||||
updated = re.sub(r"\brib\.", "ribp.", updated)
|
||||
updated = re.sub(r"\.rib(\s*\()", r".ribp\1", updated)
|
||||
updated = re.sub(r"\bahbl2rib\b", "ahbl2ribp", updated)
|
||||
updated = re.sub(r"\brib\b", "ribp", updated)
|
||||
if updated == content:
|
||||
continue
|
||||
source.write_text(updated, encoding="utf-8")
|
||||
migrated.append(source)
|
||||
return migrated
|
||||
|
||||
|
||||
def remove_legacy_picorv32_entry(filelist: Path, management_core_dir: Path) -> None:
|
||||
"""Remove the PicoRV32 line emitted by the legacy user-core generator."""
|
||||
excluded = {
|
||||
str((management_core_dir / "picorv32.v").resolve()),
|
||||
str((management_core_dir / "picorv32_ver.v").resolve()),
|
||||
}
|
||||
content = filelist.read_text(encoding="utf-8")
|
||||
filtered = [line for line in content.splitlines() if line.strip() not in excluded]
|
||||
filelist.write_text("\n".join(filtered) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def remove_tree(path: Path) -> None:
|
||||
"""Remove an MPW workspace, tolerating transient NFS directory entries."""
|
||||
for attempt in range(3):
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
return
|
||||
except OSError as error:
|
||||
if error.errno != errno.ENOTEMPTY or attempt == 2:
|
||||
raise
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_workspace(parent: Path, prefix: str) -> Iterator[Path]:
|
||||
workspace = Path(tempfile.mkdtemp(prefix=prefix, dir=parent))
|
||||
try:
|
||||
yield workspace
|
||||
finally:
|
||||
if workspace.exists():
|
||||
remove_tree(workspace)
|
||||
|
||||
|
||||
def prepare_legacy_mpw_workspace(root: Path, source: Path, workspace: Path) -> Path:
|
||||
legacy_mpw = workspace / "rtl" / "mini" / "mpw"
|
||||
legacy_mpw.mkdir(parents=True, exist_ok=True)
|
||||
for name in ("common.py", "core.py", "info.py", "ip.py", "user_design_info.h"):
|
||||
shutil.copy2(source / name, legacy_mpw / name)
|
||||
for name in ("core", "ip"):
|
||||
shutil.copytree(source / name, legacy_mpw / name)
|
||||
(legacy_mpw / ".build").mkdir()
|
||||
|
||||
legacy_core = workspace / "rtl" / "mini" / "core"
|
||||
legacy_core.mkdir(parents=True, exist_ok=True)
|
||||
picorv32 = root / "rtl" / "managed" / "picorv32" / "rtl"
|
||||
shutil.copy2(picorv32 / "picorv32.v", legacy_core / "picorv32.v")
|
||||
shutil.copy2(picorv32 / "picorv32_ver.v", legacy_core / "picorv32_ver.v")
|
||||
return legacy_mpw
|
||||
|
||||
|
||||
def generate(args: argparse.Namespace) -> None:
|
||||
root = args.root.resolve()
|
||||
def generate(root: Path, output: Path) -> None:
|
||||
managed_mpw = root / "rtl/managed/mpw"
|
||||
output = args.output.resolve()
|
||||
log_path = output.parent / f"{output.name}-generation.log"
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with (
|
||||
log_path.open("w", encoding="utf-8") as log,
|
||||
temporary_workspace(output.parent, f".{output.name}.") as workspace,
|
||||
):
|
||||
log.write(f"simulator={args.simu} user-extensions=enabled\n")
|
||||
candidate = workspace / "output"
|
||||
candidate.mkdir()
|
||||
mpw = prepare_legacy_mpw_workspace(root, managed_mpw, workspace)
|
||||
shared = mpw / ".build"
|
||||
(shared / "user_design_info.h").unlink(missing_ok=True)
|
||||
|
||||
run(workspace, log, mpw / "ip.py")
|
||||
run(workspace, log, mpw / "info.py", "IP")
|
||||
shutil.copytree(shared / "ip", candidate / "ip")
|
||||
migrated_includes = migrate_user_extension_includes(candidate / "ip")
|
||||
migrated_gpio = migrate_user_gpio_interfaces(candidate / "ip")
|
||||
log.write(f"migrated user-extension includes: {len(migrated_includes)}\n")
|
||||
log.write(f"migrated user GPIO interfaces: {len(migrated_gpio)}\n")
|
||||
run(workspace, log, mpw / "core.py", args.simu)
|
||||
run(workspace, log, mpw / "info.py", "CORE")
|
||||
shutil.copytree(shared / "core", candidate / "core")
|
||||
migrated_core_ribp = migrate_user_core_ribp_interfaces(candidate / "core")
|
||||
log.write(f"migrated user-core RIBP interfaces: {len(migrated_core_ribp)}\n")
|
||||
info = shared / "user_design_info.h"
|
||||
if info.is_file():
|
||||
shutil.copy2(info, candidate / info.name)
|
||||
|
||||
rewrite_filelists(
|
||||
candidate,
|
||||
(
|
||||
(shared, output),
|
||||
(workspace / "rtl" / "mini" / "core", root / "rtl/managed/picorv32/rtl"),
|
||||
),
|
||||
)
|
||||
remove_legacy_picorv32_entry(candidate / "core/core.fl", root / "rtl/managed/picorv32/rtl")
|
||||
if tree_digest(candidate) == tree_digest(output):
|
||||
print(f"[mpw] generated output unchanged: {output} (log: {log_path})")
|
||||
return
|
||||
if output.exists():
|
||||
shutil.rmtree(output)
|
||||
os.replace(candidate, output)
|
||||
print(f"[mpw] generated isolated output: {output} (log: {log_path})")
|
||||
manifest = managed_mpw / "mpw.toml"
|
||||
if not manifest.is_file():
|
||||
raise FileNotFoundError(f"MPW v2 manifest is missing: {manifest}")
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"mpwgen",
|
||||
"generate",
|
||||
"--manifest",
|
||||
str(manifest),
|
||||
"--output",
|
||||
str(output),
|
||||
]
|
||||
subprocess.run(command, cwd=managed_mpw, check=True)
|
||||
validate_extension_bindings(root, output)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generate isolated MPW build sources")
|
||||
parser.add_argument("--root", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--simu", choices=("VCS", "VERILATOR", "IVERILOG"), required=True)
|
||||
parser.add_argument("--lock-file", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
def validate_extension_bindings(root: Path, output: Path) -> None:
|
||||
"""Require SoC slot bindings to match the generated MPW v2 manifest."""
|
||||
extensions_path = root / "rtl/mini/integration/user_extensions.json"
|
||||
manifest_path = output / "manifest.json"
|
||||
extensions = json.loads(extensions_path.read_text(encoding="utf-8"))
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
designs = manifest.get("designs")
|
||||
if not isinstance(designs, list):
|
||||
raise ValueError("generated MPW manifest has no design list")
|
||||
for kind, target_key in (("core", "core_targets"), ("ip", "ip_targets")):
|
||||
expected: dict[object, object] = {}
|
||||
for design in designs:
|
||||
if not isinstance(design, dict) or design.get("kind") != kind:
|
||||
continue
|
||||
slot = design.get("slot")
|
||||
if slot in expected:
|
||||
raise ValueError(f"generated MPW manifest duplicates {kind} slot {slot}")
|
||||
expected[slot] = design
|
||||
targets = extensions.get(target_key)
|
||||
if not isinstance(targets, list):
|
||||
raise ValueError(f"{extensions_path}: {target_key} must be a list")
|
||||
actual: dict[object, object] = {}
|
||||
for target in targets:
|
||||
if not isinstance(target, dict):
|
||||
raise ValueError(f"{extensions_path}: {target_key} contains a non-object entry")
|
||||
slot = target.get("slot")
|
||||
if slot in actual:
|
||||
raise ValueError(f"{extensions_path}: {target_key} duplicates slot {slot}")
|
||||
actual[slot] = target
|
||||
if set(actual) != set(expected):
|
||||
raise ValueError(f"{extensions_path}: {target_key} slots do not match MPW v2")
|
||||
for slot, design in expected.items():
|
||||
target = actual[slot]
|
||||
if target.get("module") != design.get("top"):
|
||||
raise ValueError(
|
||||
f"{extensions_path}: {kind} slot {slot} does not match {design.get('top')}"
|
||||
)
|
||||
if kind == "core" and target.get("reset") != design.get("reset"):
|
||||
raise ValueError(f"{extensions_path}: core slot {slot} reset type does not match MPW v2")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
args.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.lock_file.open("a+", encoding="utf-8") as lock:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--lock-file", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
arguments.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with arguments.lock_file.open("a+", encoding="utf-8") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
generate(args)
|
||||
generate(arguments.root.resolve(), arguments.output.resolve())
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+19
-9
@@ -10,6 +10,11 @@ import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # Python 3.10
|
||||
import tomli as tomllib
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts.dependency_lock import archive, source # noqa: E402
|
||||
@@ -53,6 +58,18 @@ def extract_serv() -> Path:
|
||||
return extracted
|
||||
|
||||
|
||||
def manifest_design_dir(identifier: str) -> Path:
|
||||
manifest = MPW_DIR / "mpw.toml"
|
||||
document = tomllib.loads(manifest.read_text(encoding="utf-8"))
|
||||
for design in document.get("design", []):
|
||||
if design.get("id") == identifier:
|
||||
source_dir = Path(design["source_dir"])
|
||||
if source_dir.is_absolute() or ".." in source_dir.parts:
|
||||
raise RuntimeError(f"unsafe source directory for MPW design {identifier}")
|
||||
return MPW_DIR / source_dir
|
||||
raise RuntimeError(f"MPW manifest does not define {identifier}")
|
||||
|
||||
|
||||
def prepare(update: bool) -> None:
|
||||
if not (MPW_DIR / ".git").is_dir():
|
||||
raise SystemExit("MPW generator is missing; run 'python3 setup.py' first")
|
||||
@@ -62,11 +79,6 @@ def prepare(update: bool) -> None:
|
||||
hazard3["url"], ROOT / hazard3["destination"], hazard3["revision"],
|
||||
update=update,
|
||||
)
|
||||
ibex = source("ibex")
|
||||
ensure_git_repo(
|
||||
ibex["url"], ROOT / ibex["destination"], ibex["revision"],
|
||||
update=update,
|
||||
)
|
||||
serv_archive = archive("serv")
|
||||
download_file(
|
||||
serv_archive["url"], ROOT / serv_archive["destination"],
|
||||
@@ -74,10 +86,8 @@ def prepare(update: bool) -> None:
|
||||
)
|
||||
serv = extract_serv()
|
||||
|
||||
sync_tree(MPW_DIR / "Hazard3/hdl", MPW_DIR / "core/username3/Hazard3")
|
||||
sync_tree(serv / "rtl", MPW_DIR / "core/username4/serv")
|
||||
sync_tree(MPW_DIR / "ibex/rtl", MPW_DIR / "core/username7/ibex")
|
||||
(MPW_DIR / ".build").mkdir(exist_ok=True)
|
||||
sync_tree(MPW_DIR / "Hazard3/hdl", manifest_design_dir("hazard3") / "Hazard3")
|
||||
sync_tree(serv / "rtl", manifest_design_dir("serv") / "serv")
|
||||
print("pinned MPW core sources are ready")
|
||||
|
||||
|
||||
|
||||
@@ -193,12 +193,12 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--dynamic-core-filelist",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "rtl/managed/mpw/.build/core/core.fl",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dynamic-ip-filelist",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "rtl/managed/mpw/.build/ip/ip.fl",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument("--output-dir", type=Path, default=REPO_ROOT / "export")
|
||||
return parser.parse_args()
|
||||
|
||||
+34
-71
@@ -14,6 +14,8 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from zipfile import ZipFile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FILELIST_SCRIPT_DIR = ROOT / "rtl/mini/script"
|
||||
@@ -31,12 +33,7 @@ from scripts.analyze_warnings import normalize # noqa: E402
|
||||
from scripts.check_c_warnings import self_owned_warnings # noqa: E402
|
||||
from scripts.check_format import format_files # noqa: E402
|
||||
from scripts.dependency_lock import LockError, load_lock # noqa: E402
|
||||
from scripts.generate_mpw import ( # noqa: E402
|
||||
migrate_user_core_ribp_interfaces,
|
||||
migrate_user_extension_includes,
|
||||
migrate_user_gpio_interfaces,
|
||||
remove_legacy_picorv32_entry,
|
||||
)
|
||||
from scripts.generate_mpw import validate_extension_bindings # noqa: E402
|
||||
from scripts.install_toolchain import safe_extract # noqa: E402
|
||||
from scripts import regress # noqa: E402
|
||||
from scripts import run_flow # noqa: E402
|
||||
@@ -294,76 +291,42 @@ def test_formal_result_summary_requires_every_passing_step(tmp_path: Path) -> No
|
||||
assert set(result["proofs"]) == set(proofs)
|
||||
|
||||
|
||||
def test_legacy_user_gpio_interface_is_migrated_in_generated_source(tmp_path: Path) -> None:
|
||||
source = tmp_path / "user_ip_design.sv"
|
||||
source.write_text(
|
||||
"""`include "mdd_config.svh"
|
||||
module user_ip_design (gpio_if.dut gpio);
|
||||
logic value;
|
||||
assign gpio.gpio_oe = value;
|
||||
assign gpio.gpio_cs = value;
|
||||
assign gpio.gpio_pu = value;
|
||||
assign gpio.gpio_pd = value;
|
||||
assign gpio.gpio_out = value;
|
||||
always_comb value = gpio.gpio_in[0];
|
||||
endmodule
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
def test_mpw_generator_uses_only_the_native_v2_contract() -> None:
|
||||
source = (ROOT / "scripts/generate_mpw.py").read_text(encoding="utf-8")
|
||||
|
||||
assert migrate_user_extension_includes(tmp_path) == [source]
|
||||
assert migrate_user_gpio_interfaces(tmp_path) == [source]
|
||||
|
||||
migrated = source.read_text(encoding="utf-8")
|
||||
assert "gpio_if.dut" not in migrated
|
||||
assert "gpio.gpio_" not in migrated
|
||||
assert '`include "user_extensions.svh"' in migrated
|
||||
assert "user_gpio_if.user_ip gpio" in migrated
|
||||
assert "gpio.oe_o" in migrated
|
||||
assert "gpio.do_o" in migrated
|
||||
assert "gpio.di_i" in migrated
|
||||
assert "migrate_user_" not in source
|
||||
assert "prepare_legacy_mpw_workspace" not in source
|
||||
assert '"-m",\n "mpwgen"' in source
|
||||
|
||||
|
||||
def test_user_core_ribp_interface_is_migrated_in_generated_source(tmp_path: Path) -> None:
|
||||
source = tmp_path / "user_core_design.sv"
|
||||
source.write_text(
|
||||
"""module user_core_design (rib_if.master rib);
|
||||
ahbl2rib u_bridge (ahbl, rib);
|
||||
assign rib.addr = 32'h0;
|
||||
endmodule
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
def test_mpw_extension_bindings_match_generated_manifest(tmp_path: Path) -> None:
|
||||
extensions_path = tmp_path / "rtl/mini/integration/user_extensions.json"
|
||||
extensions_path.parent.mkdir(parents=True)
|
||||
extensions = {
|
||||
"core_targets": [
|
||||
{"slot": 0, "module": "mpw_core_zero", "reset": "sync"},
|
||||
],
|
||||
"ip_targets": [
|
||||
{"slot": 1, "module": "mpw_ip_one"},
|
||||
],
|
||||
}
|
||||
extensions_path.write_text(json.dumps(extensions), encoding="utf-8")
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
manifest = {
|
||||
"designs": [
|
||||
{"kind": "core", "slot": 0, "top": "mpw_core_zero", "reset": "sync"},
|
||||
{"kind": "ip", "slot": 1, "top": "mpw_ip_one"},
|
||||
]
|
||||
}
|
||||
(output / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
assert migrate_user_core_ribp_interfaces(tmp_path) == [source]
|
||||
validate_extension_bindings(tmp_path, output)
|
||||
|
||||
migrated = source.read_text(encoding="utf-8")
|
||||
assert "rib_if" not in migrated
|
||||
assert "ribp_if.master ribp" in migrated
|
||||
assert "ahbl2ribp" in migrated
|
||||
assert "ahbl, ribp" in migrated
|
||||
assert "ribp.addr" in migrated
|
||||
|
||||
|
||||
def test_generated_user_core_filelist_excludes_the_legacy_picorv32_entry(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
management_core_dir = tmp_path / "rtl/managed/picorv32/rtl"
|
||||
management_core_dir.mkdir(parents=True)
|
||||
picorv32 = management_core_dir / "picorv32.v"
|
||||
picorv32_ver = management_core_dir / "picorv32_ver.v"
|
||||
picorv32.touch()
|
||||
picorv32_ver.touch()
|
||||
filelist = tmp_path / "generated/mpw/core/core.fl"
|
||||
filelist.parent.mkdir(parents=True)
|
||||
filelist.write_text(
|
||||
f"{picorv32.resolve()}\n{picorv32_ver.resolve()}\nuser_core_design.sv\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
remove_legacy_picorv32_entry(filelist, management_core_dir)
|
||||
|
||||
assert filelist.read_text(encoding="utf-8") == "user_core_design.sv\n"
|
||||
extensions["core_targets"][0]["module"] = "wrong_core"
|
||||
extensions_path.write_text(json.dumps(extensions), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="does not match"):
|
||||
validate_extension_bindings(tmp_path, output)
|
||||
|
||||
|
||||
def test_prepare_norflash_and_missing_firmware(tmp_path: Path) -> None:
|
||||
|
||||
@@ -57,7 +57,8 @@ def test_extensions_generate_isolated_scalar_bindings(tmp_path: Path) -> None:
|
||||
assert "5'd0: begin" in core
|
||||
assert "5'd5: begin" in core
|
||||
assert "core_reset_i[0]" in core
|
||||
assert "user_core_design_username1 #(" in core
|
||||
assert "mpw_core_kianv_rv32i_user_core_design #(0)" in core
|
||||
assert "mpw_ip_tiny_timer_user_ip_design #(1)" in ip
|
||||
assert "User core 0 uses the RIBP contract" in core
|
||||
assert ip.count("user_gpio_if #(`USER_GPIO_NUM)") == 2
|
||||
assert "gpio.do_o = '0;" in ip
|
||||
|
||||
Reference in new issue
Block a user