mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-05 06:32:37 +00:00
feat: refactor test scripts to update paths and improve organization
- Updated paths in test files to reflect new directory structure for RTL and script files. - Changed references from rtl/mini to rtl/ip and scripts/rtl where applicable. - Added a new test file for Tiny product isolation, ensuring independent ABI and bus acceptance. - Enhanced tests for memory map generation and protocol contracts for Tiny architecture. - Ensured that all tests are aligned with the latest directory structure and naming conventions.
This commit is contained in:
194 files changed
+5534
-449
No files matched your search
@@ -0,0 +1,10 @@
|
||||
# Shared RTL Flow Helpers
|
||||
|
||||
These product-independent helpers parse filelists, convert SystemVerilog,
|
||||
generate address/pad artifacts, and prepare simulation inputs. Both products
|
||||
invoke them here directly, with Python imports through `scripts.rtl`. Product
|
||||
inputs stay under their owning `rtl/<product>` directory; outputs stay below
|
||||
`build/`.
|
||||
|
||||
Changes require focused generator tests, Ruff and Pytest, plus compilation of
|
||||
each affected product. `generate_tiny.py` owns Tiny-only integration bindings.
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts.rtl.filelist import parse_filelists, write_filelist # noqa: E402
|
||||
|
||||
|
||||
DEFAULT_OUTPUT = Path(__file__).resolve().parent.parent / ".generated_fl" / "yosys.fl"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Combine synthesis filelists")
|
||||
parser.add_argument("-f", "--filelist", action="append", type=Path, default=[])
|
||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
selected = [path for path in args.filelist if not path.name.startswith("pdk_")]
|
||||
if not selected:
|
||||
raise SystemExit("at least one non-PDK -f/--filelist is required")
|
||||
combined = parse_filelists(selected)
|
||||
write_filelist(args.output.resolve(), combined)
|
||||
print(f"generated synthesis filelist: {args.output.resolve()}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts.rtl.filelist import parse_filelists # noqa: E402
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Convert the behavioral RTL with sv2v")
|
||||
parser.add_argument("-f", "--filelist", action="append", type=Path, default=[])
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--sv2v", default="sv2v", help="sv2v executable")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
selected = [path for path in args.filelist if not path.name.startswith("pdk_")]
|
||||
if not selected:
|
||||
raise SystemExit("at least one non-PDK -f/--filelist is required")
|
||||
|
||||
filelist = parse_filelists(selected)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [args.sv2v]
|
||||
command.extend(f"-D{item[len('+define+'):]}" for item in filelist.defines)
|
||||
command.extend(f"-I{path}" for path in filelist.incdirs)
|
||||
command.extend(str(path) for path in filelist.files)
|
||||
command.extend(("--write", str(args.output.resolve())))
|
||||
subprocess.run(command, check=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileList:
|
||||
defines: list[str] = field(default_factory=list)
|
||||
incdirs: list[Path] = field(default_factory=list)
|
||||
files: list[Path] = field(default_factory=list)
|
||||
library_files: list[Path] = field(default_factory=list)
|
||||
options: list[str] = field(default_factory=list)
|
||||
|
||||
def extend(self, other: "FileList") -> None:
|
||||
self.defines.extend(other.defines)
|
||||
self.incdirs.extend(other.incdirs)
|
||||
self.files.extend(other.files)
|
||||
self.library_files.extend(other.library_files)
|
||||
self.options.extend(other.options)
|
||||
|
||||
def deduplicate(self) -> "FileList":
|
||||
self.defines = _unique(self.defines)
|
||||
self.incdirs = _unique(self.incdirs)
|
||||
self.files = _unique(self.files)
|
||||
self.library_files = _unique(self.library_files)
|
||||
self.options = _unique(self.options)
|
||||
return self
|
||||
|
||||
def as_tokens(self) -> list[str]:
|
||||
return [
|
||||
*self.defines,
|
||||
*(_quote(f"+incdir+{path}") for path in self.incdirs),
|
||||
*(item for path in self.library_files for item in ("-v", _quote(str(path)))),
|
||||
*self.options,
|
||||
*(_quote(str(path)) for path in self.files),
|
||||
]
|
||||
|
||||
|
||||
def _unique(values: Iterable):
|
||||
return list(dict.fromkeys(values))
|
||||
|
||||
|
||||
def _quote(value: str) -> str:
|
||||
if not any(character.isspace() for character in value):
|
||||
return value
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def atomic_write(path: Path, content: str) -> bool:
|
||||
"""Write content atomically and preserve mtime when content is unchanged."""
|
||||
path = Path(path)
|
||||
if path.exists() and path.read_text(encoding="utf-8") == content:
|
||||
return False
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8", newline="") as stream:
|
||||
stream.write(content)
|
||||
os.replace(temp_name, path)
|
||||
except Exception:
|
||||
Path(temp_name).unlink(missing_ok=True)
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def tokenize_filelist(path: Path) -> list[str]:
|
||||
tokens: list[str] = []
|
||||
for line_number, raw_line in enumerate(
|
||||
path.read_text(encoding="utf-8").splitlines(), start=1
|
||||
):
|
||||
stripped = raw_line.strip()
|
||||
if not stripped or stripped.startswith("//") or stripped.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
lexer = shlex.shlex(raw_line, posix=True)
|
||||
lexer.whitespace_split = True
|
||||
lexer.quotes = '"'
|
||||
tokens.extend(lexer)
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{path}:{line_number}: {error}") from error
|
||||
return tokens
|
||||
|
||||
|
||||
def parse_filelists(paths: Iterable[Path], *, require_files: bool = True) -> FileList:
|
||||
result = FileList()
|
||||
visited: set[Path] = set()
|
||||
for path in paths:
|
||||
result.extend(
|
||||
_parse_filelist(Path(path).resolve(), visited, require_files=require_files)
|
||||
)
|
||||
return result.deduplicate()
|
||||
|
||||
|
||||
def _parse_filelist(
|
||||
path: Path, visited: set[Path], *, require_files: bool
|
||||
) -> FileList:
|
||||
if path in visited:
|
||||
return FileList()
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"filelist not found: {path}")
|
||||
visited.add(path)
|
||||
|
||||
result = FileList()
|
||||
tokens = tokenize_filelist(path)
|
||||
index = 0
|
||||
while index < len(tokens):
|
||||
token = tokens[index]
|
||||
if token == "-f":
|
||||
index += 1
|
||||
if index >= len(tokens):
|
||||
raise ValueError(f"{path}: -f requires a filelist path")
|
||||
nested = _resolve(tokens[index], path.parent)
|
||||
result.extend(
|
||||
_parse_filelist(nested, visited, require_files=require_files)
|
||||
)
|
||||
elif token.startswith("-f") and len(token) > 2:
|
||||
nested = _resolve(token[2:], path.parent)
|
||||
result.extend(
|
||||
_parse_filelist(nested, visited, require_files=require_files)
|
||||
)
|
||||
elif token.startswith("+define+"):
|
||||
result.defines.append(token)
|
||||
elif token.startswith("+incdir+"):
|
||||
incdir = _resolve(token[len("+incdir+") :], path.parent)
|
||||
if require_files and not incdir.is_dir():
|
||||
raise FileNotFoundError(f"include directory not found: {incdir}")
|
||||
result.incdirs.append(incdir)
|
||||
elif token == "-v":
|
||||
index += 1
|
||||
if index >= len(tokens):
|
||||
raise ValueError(f"{path}: -v requires a source path")
|
||||
library_file = _resolve(tokens[index], path.parent)
|
||||
_require_source(library_file, require_files)
|
||||
result.library_files.append(library_file)
|
||||
elif token.startswith("-") or token.startswith("+"):
|
||||
result.options.append(token)
|
||||
else:
|
||||
source = _resolve(token, path.parent)
|
||||
_require_source(source, require_files)
|
||||
result.files.append(source)
|
||||
index += 1
|
||||
return result
|
||||
|
||||
|
||||
def _resolve(value: str, base: Path) -> Path:
|
||||
path = Path(value).expanduser()
|
||||
return path.resolve() if path.is_absolute() else (base / path).resolve()
|
||||
|
||||
|
||||
def _require_source(path: Path, required: bool) -> None:
|
||||
if required and not path.is_file():
|
||||
raise FileNotFoundError(f"source file not found: {path}")
|
||||
|
||||
|
||||
def write_filelist(path: Path, filelist: FileList) -> bool:
|
||||
content = "\n".join(filelist.as_tokens()) + "\n"
|
||||
return atomic_write(path, content)
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts.rtl.filelist import atomic_write, parse_filelists # noqa: E402
|
||||
|
||||
|
||||
HEADER_SUFFIXES = {".h", ".vh", ".svh"}
|
||||
|
||||
|
||||
def make_escape(path: Path) -> str:
|
||||
return (
|
||||
str(path.resolve())
|
||||
.replace("\\", "\\\\")
|
||||
.replace("$", "$$")
|
||||
.replace(" ", "\\ ")
|
||||
.replace("#", "\\#")
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Generate Make dependencies from RTL filelists")
|
||||
parser.add_argument("-f", "--filelist", type=Path, action="append", default=[])
|
||||
parser.add_argument("--extra", type=Path, action="append", default=[])
|
||||
parser.add_argument("--target", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if not args.filelist:
|
||||
parser.error("at least one -f/--filelist is required")
|
||||
|
||||
parsed = parse_filelists(args.filelist)
|
||||
dependencies = {
|
||||
*(path.resolve() for path in args.filelist),
|
||||
*(path.resolve() for path in parsed.files),
|
||||
*(path.resolve() for path in parsed.library_files),
|
||||
*(path.resolve() for path in args.extra),
|
||||
}
|
||||
for directory in parsed.incdirs:
|
||||
if directory.is_dir():
|
||||
dependencies.update(
|
||||
path.resolve()
|
||||
for path in directory.rglob("*")
|
||||
if path.is_file() and path.suffix.lower() in HEADER_SUFFIXES
|
||||
)
|
||||
content = f"{make_escape(args.target)}:"
|
||||
if dependencies:
|
||||
content += " \\\n " + " \\\n ".join(make_escape(path) for path in sorted(dependencies))
|
||||
content += "\n"
|
||||
atomic_write(args.output, content)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate RTL, SDK and linker address-map artifacts from one JSON source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
MAX_ADDRESS = 1 << 32
|
||||
VALID_ROUTES = {"apb4_periph", "apb4_system", "axi4", "ram", "reserved"}
|
||||
VALID_KINDS = {"active", "reserved"}
|
||||
VALID_USER_ACCESS = {"none", "ro", "rw"}
|
||||
VALID_BURST = {"incr1", "incr4", "incr16"}
|
||||
VALID_SRAM_SIZE_KIB = (4, 16, 32, 64, 128)
|
||||
|
||||
|
||||
def parse_integer(value: Any, field: str) -> int:
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value, 0)
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{field} must be an integer: {value!r}") from error
|
||||
raise ValueError(f"{field} must be an integer")
|
||||
|
||||
|
||||
def read_map(
|
||||
path: Path, sram_size_kib: int = 128
|
||||
) -> tuple[int, list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
if sram_size_kib not in VALID_SRAM_SIZE_KIB:
|
||||
raise ValueError(f"sram_size_kib must be one of {VALID_SRAM_SIZE_KIB}")
|
||||
with path.open(encoding="utf-8") as source:
|
||||
document = json.load(source)
|
||||
if document.get("schema_version") != 2:
|
||||
raise ValueError("schema_version must be 2")
|
||||
cpu_reset = parse_integer(document.get("cpu_reset"), "cpu_reset")
|
||||
registers = document.get("sysctrl_registers")
|
||||
if not isinstance(registers, list) or not registers:
|
||||
raise ValueError("sysctrl_registers must be a non-empty list")
|
||||
normalized_registers: list[dict[str, Any]] = []
|
||||
register_symbols: set[str] = set()
|
||||
register_offsets: set[int] = set()
|
||||
for index, register in enumerate(registers):
|
||||
if not isinstance(register, dict):
|
||||
raise ValueError(f"sysctrl_registers[{index}] must be an object")
|
||||
symbol = register.get("symbol")
|
||||
if not isinstance(symbol, str) or not symbol or not symbol.replace("_", "").isalnum():
|
||||
raise ValueError(f"sysctrl_registers[{index}].symbol must be an uppercase identifier")
|
||||
if symbol != symbol.upper() or symbol in register_symbols:
|
||||
raise ValueError(f"sysctrl_registers[{index}].symbol must be unique and uppercase")
|
||||
offset = parse_integer(register.get("offset"), f"sysctrl_registers[{index}].offset")
|
||||
if offset < 0 or offset > 0xFFC or offset % 4 != 0 or offset in register_offsets:
|
||||
raise ValueError(f"sysctrl_registers[{index}].offset must be unique and 32-bit aligned")
|
||||
register_symbols.add(symbol)
|
||||
register_offsets.add(offset)
|
||||
normalized_registers.append({"symbol": symbol, "offset": offset})
|
||||
regions = document.get("regions")
|
||||
if not isinstance(regions, list) or not regions:
|
||||
raise ValueError("regions must be a non-empty list")
|
||||
normalized: list[dict[str, Any]] = []
|
||||
symbols: set[str] = set()
|
||||
for index, region in enumerate(regions):
|
||||
if not isinstance(region, dict):
|
||||
raise ValueError(f"regions[{index}] must be an object")
|
||||
symbol = region.get("symbol")
|
||||
if not isinstance(symbol, str) or not symbol or not symbol.replace("_", "").isalnum():
|
||||
raise ValueError(f"regions[{index}].symbol must be an uppercase identifier")
|
||||
if symbol != symbol.upper() or symbol in symbols:
|
||||
raise ValueError(f"regions[{index}].symbol must be unique and uppercase")
|
||||
symbols.add(symbol)
|
||||
route = region.get("route")
|
||||
kind = region.get("kind")
|
||||
if route not in VALID_ROUTES or kind not in VALID_KINDS:
|
||||
raise ValueError(f"regions[{index}] has invalid route or kind")
|
||||
if (route == "reserved") != (kind == "reserved"):
|
||||
raise ValueError(f"regions[{index}] reserved route and kind must agree")
|
||||
user_access = region.get("user_access")
|
||||
if user_access not in VALID_USER_ACCESS:
|
||||
raise ValueError(
|
||||
f"regions[{index}].user_access must be one of {sorted(VALID_USER_ACCESS)}"
|
||||
)
|
||||
if kind == "reserved" and user_access != "none":
|
||||
raise ValueError(f"regions[{index}] reserved regions must deny user access")
|
||||
burst = region.get("burst", "incr1")
|
||||
if burst not in VALID_BURST:
|
||||
raise ValueError(f"regions[{index}].burst must be one of {sorted(VALID_BURST)}")
|
||||
if kind == "reserved" and burst != "incr1":
|
||||
raise ValueError(f"regions[{index}] reserved regions cannot support bursts")
|
||||
base = parse_integer(region.get("base"), f"regions[{index}].base")
|
||||
size = parse_integer(region.get("size"), f"regions[{index}].size")
|
||||
if symbol == "SRAM":
|
||||
size_options = region.get("size_options")
|
||||
if not isinstance(size_options, list):
|
||||
raise ValueError(f"regions[{index}].size_options must be a list")
|
||||
parsed_options = tuple(
|
||||
parse_integer(value, f"regions[{index}].size_options") for value in size_options
|
||||
)
|
||||
expected_options = tuple(value * 1024 for value in VALID_SRAM_SIZE_KIB)
|
||||
if parsed_options != expected_options:
|
||||
raise ValueError(f"regions[{index}].size_options must be {expected_options}")
|
||||
size = sram_size_kib * 1024
|
||||
if base < 0 or size <= 0 or base + size > MAX_ADDRESS:
|
||||
raise ValueError(f"regions[{index}] exceeds the 32-bit address space")
|
||||
if base % 4 != 0 or size % 4 != 0:
|
||||
raise ValueError(f"regions[{index}] must be 32-bit aligned")
|
||||
normalized.append(
|
||||
{
|
||||
"symbol": symbol,
|
||||
"base": base,
|
||||
"size": size,
|
||||
"end": base + size - 1,
|
||||
"route": route,
|
||||
"kind": kind,
|
||||
"public": region.get("public", False) is True,
|
||||
"linker": region.get("linker"),
|
||||
"user_access": user_access,
|
||||
"burst": burst,
|
||||
}
|
||||
)
|
||||
previous: dict[str, Any] | None = None
|
||||
for region in sorted(normalized, key=lambda item: item["base"]):
|
||||
if previous is not None and region["base"] <= previous["end"]:
|
||||
raise ValueError(f"address-map overlap: {previous['symbol']} and {region['symbol']}")
|
||||
previous = region
|
||||
return cpu_reset, normalized, normalized_registers
|
||||
|
||||
|
||||
def hex32(value: int) -> str:
|
||||
return f"32'h{value:08X}"
|
||||
|
||||
|
||||
def c_hex32(value: int) -> str:
|
||||
return f"UINT32_C(0x{value:08X})"
|
||||
|
||||
|
||||
def range_expression(symbol: str, base: int, end: int) -> str:
|
||||
conditions: list[str] = []
|
||||
if base != 0:
|
||||
conditions.append(f"(addr) >= `SOC_ADDR_{symbol}_BASE")
|
||||
if end != MAX_ADDRESS - 1:
|
||||
conditions.append(f"(addr) <= `SOC_ADDR_{symbol}_END")
|
||||
return "(" + " && ".join(conditions or ["1'b1"]) + ")"
|
||||
|
||||
|
||||
def join_or(expressions: list[str]) -> str:
|
||||
return " || \\\n ".join(expressions) if expressions else "1'b0"
|
||||
|
||||
|
||||
def render_rtl(
|
||||
cpu_reset: int, regions: list[dict[str, Any]], sysctrl_registers: list[dict[str, Any]], legacy_rib: bool = True
|
||||
) -> str:
|
||||
lines = [
|
||||
"// Generated by scripts/rtl/generate_memory_map.py; do not edit.",
|
||||
"`ifndef RETROSOC_GENERATED_MMAP_DEFINE_SVH",
|
||||
"`define RETROSOC_GENERATED_MMAP_DEFINE_SVH",
|
||||
"",
|
||||
f"`define SOC_CPU_RESET_ADDR {hex32(cpu_reset)}",
|
||||
"`define SOC_IRQ_START_ADDR `SOC_CPU_RESET_ADDR",
|
||||
"// Compatibility aliases for managed MPW cores.",
|
||||
"`define CPU_RESET_ADDR `SOC_CPU_RESET_ADDR",
|
||||
"`define IRQ_START_ADDR `SOC_IRQ_START_ADDR",
|
||||
"",
|
||||
]
|
||||
for region in regions:
|
||||
symbol = region["symbol"]
|
||||
lines += [
|
||||
f"`define SOC_ADDR_{symbol}_BASE {hex32(region['base'])}",
|
||||
f"`define SOC_ADDR_{symbol}_SIZE {hex32(region['size'])}",
|
||||
f"`define SOC_ADDR_{symbol}_END {hex32(region['end'])}",
|
||||
f"`define SOC_ADDR_IS_{symbol}(addr) "
|
||||
f"{range_expression(symbol, region['base'], region['end'])}",
|
||||
]
|
||||
lines.append("")
|
||||
for route in ("apb4_periph", "apb4_system", "axi4", "ram", "reserved"):
|
||||
selected = [
|
||||
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
|
||||
for region in regions
|
||||
if region["route"] == route
|
||||
]
|
||||
lines.append(f"`define SOC_ADDR_IS_{route.upper()}(addr) ({join_or(selected)})")
|
||||
legacy_rib_targets = [
|
||||
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
|
||||
for region in regions
|
||||
if region["route"] in {"apb4_periph", "axi4"}
|
||||
]
|
||||
if legacy_rib:
|
||||
lines.append(f"`define SOC_ADDR_IS_RIB_LEGACY_TARGET(addr) ({join_or(legacy_rib_targets)})")
|
||||
user_readable = [
|
||||
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
|
||||
for region in regions
|
||||
if region["user_access"] in {"ro", "rw"}
|
||||
]
|
||||
user_writable = [
|
||||
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
|
||||
for region in regions
|
||||
if region["user_access"] == "rw"
|
||||
]
|
||||
lines += [
|
||||
"",
|
||||
f"`define SOC_USER_ADDR_READABLE(addr) ({join_or(user_readable)})",
|
||||
f"`define SOC_USER_ADDR_WRITABLE(addr) ({join_or(user_writable)})",
|
||||
]
|
||||
incr4_regions = [
|
||||
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
|
||||
for region in regions
|
||||
if region["burst"] in {"incr4", "incr16"}
|
||||
]
|
||||
lines.append(f"`define SOC_ADDR_SUPPORTS_INCR4(addr) ({join_or(incr4_regions)})")
|
||||
incr16_regions = [
|
||||
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
|
||||
for region in regions
|
||||
if region["burst"] == "incr16"
|
||||
]
|
||||
lines.append(f"`define SOC_ADDR_SUPPORTS_INCR16(addr) ({join_or(incr16_regions)})")
|
||||
lines.append("")
|
||||
for register in sysctrl_registers:
|
||||
macro = f"SOC_SYSCTRL_{register['symbol']}_OFFSET"
|
||||
lines.append(f"`define {macro:<31} {hex32(register['offset'])}")
|
||||
lines += ["", "`endif", ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_c(
|
||||
cpu_reset: int,
|
||||
regions: list[dict[str, Any]],
|
||||
sysctrl_registers: list[dict[str, Any]],
|
||||
have_sram_if: str,
|
||||
capability_header: str | None = None,
|
||||
) -> str:
|
||||
lines = [
|
||||
"/* Generated by scripts/rtl/generate_memory_map.py; do not edit. */",
|
||||
"#ifndef RETROSOC_GENERATED_MEMORY_MAP_H",
|
||||
"#define RETROSOC_GENERATED_MEMORY_MAP_H",
|
||||
"",
|
||||
"#ifdef __ASSEMBLER__",
|
||||
"#define UINT32_C(value) value",
|
||||
"#else",
|
||||
"#include <stdint.h>",
|
||||
"#endif",
|
||||
"",
|
||||
f"#define RS_SOC_CPU_RESET_ADDR {c_hex32(cpu_reset)}",
|
||||
f"#define RS_SOC_HAS_SRAM {1 if have_sram_if == 'YES' else 0}U",
|
||||
"",
|
||||
]
|
||||
for region in regions:
|
||||
if not region["public"]:
|
||||
continue
|
||||
symbol = region["symbol"]
|
||||
lines += [
|
||||
f"#define RS_SOC_{symbol}_BASE {c_hex32(region['base'])}",
|
||||
f"#define RS_SOC_{symbol}_SIZE {c_hex32(region['size'])}",
|
||||
f"#define RS_SOC_{symbol}_END {c_hex32(region['end'])}",
|
||||
]
|
||||
lines.append("")
|
||||
lines.extend(
|
||||
f"#define RS_SOC_SYSCTRL_{register['symbol']}_OFFSET {c_hex32(register['offset'])}"
|
||||
for register in sysctrl_registers
|
||||
)
|
||||
if capability_header is not None:
|
||||
lines.append(f"#include <{capability_header}>")
|
||||
lines += ["", "#endif", ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_linker(regions: list[dict[str, Any]]) -> str:
|
||||
lines = [
|
||||
"/* Generated by scripts/rtl/generate_memory_map.py; do not edit. */",
|
||||
"MEMORY",
|
||||
"{",
|
||||
]
|
||||
for region in regions:
|
||||
linker = region["linker"]
|
||||
if linker is None:
|
||||
continue
|
||||
attributes = "rx" if linker == "FLASH" else "wxa!ri"
|
||||
lines.append(
|
||||
f" {linker} ({attributes}) : ORIGIN = 0x{region['base']:08X}, "
|
||||
f"LENGTH = 0x{region['size']:08X}"
|
||||
)
|
||||
lines += ["}", ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def atomic_write(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists() and path.read_text(encoding="utf-8") == content:
|
||||
return
|
||||
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output:
|
||||
output.write(content)
|
||||
os.replace(temporary_name, path)
|
||||
except BaseException:
|
||||
Path(temporary_name).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def generate(map_path: Path, output_dir: Path, have_sram_if: str, sram_size_kib: int) -> None:
|
||||
cpu_reset, regions, sysctrl_registers = read_map(map_path, sram_size_kib)
|
||||
atomic_write(
|
||||
output_dir / "rtl" / "mmap_define.svh",
|
||||
render_rtl(cpu_reset, regions, sysctrl_registers,
|
||||
json.loads(map_path.read_text()).get("legacy_rib", True)),
|
||||
)
|
||||
atomic_write(output_dir / "memory_map.fl", f"+incdir+{output_dir / 'rtl'}\n")
|
||||
atomic_write(
|
||||
output_dir / "include" / "retrosoc" / "generated" / "memory_map.h",
|
||||
render_c(cpu_reset, regions, sysctrl_registers, have_sram_if,
|
||||
json.loads(map_path.read_text()).get("capability_header")),
|
||||
)
|
||||
atomic_write(output_dir / "linker" / "memory_regions.ld", render_linker(regions))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--map", required=True, type=Path)
|
||||
parser.add_argument("--output-dir", type=Path)
|
||||
parser.add_argument("--have-sram-if", choices=("YES", "NO"), default="NO")
|
||||
parser.add_argument("--sram-size-kib", type=int, choices=VALID_SRAM_SIZE_KIB, default=128)
|
||||
parser.add_argument("--check", action="store_true", help="validate only; write nothing")
|
||||
arguments = parser.parse_args()
|
||||
try:
|
||||
if arguments.check:
|
||||
read_map(arguments.map, arguments.sram_size_kib)
|
||||
else:
|
||||
if arguments.output_dir is None:
|
||||
parser.error("--output-dir is required unless --check is used")
|
||||
generate(
|
||||
arguments.map,
|
||||
arguments.output_dir,
|
||||
arguments.have_sram_if,
|
||||
arguments.sram_size_kib,
|
||||
)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
parser.error(str(error))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate SoC pad declarations and platform bindings from one JSON map."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
VALID_FEATURES = {"HAVE_PLL"}
|
||||
VALID_KINDS = {
|
||||
"full",
|
||||
"peripheral_output",
|
||||
"peripheral_bidir",
|
||||
"schmitt_input",
|
||||
"sdram_dq",
|
||||
"tri_input",
|
||||
"tri_output",
|
||||
"xpi_data",
|
||||
"xtal",
|
||||
}
|
||||
REQUIRED_PROFILES = {"fpga_mini", "tb", "verilator"}
|
||||
CONNECTION_RE = re.compile(r"(?:[A-Za-z_][A-Za-z0-9_]*)(?:\[[0-9]+\])?$")
|
||||
CONSTANT_RE = re.compile(r"[0-9]+'[bBdDhHoO][0-9a-fA-F_xXzZ]+$")
|
||||
IHP130_POWER_PAD_COUNTS = {"vdd": 24, "vss": 24, "iovdd": 16, "iovss": 16}
|
||||
POWER_CONNECTIONS = "`RETROSOC_PAD_POWER_CONNECTIONS"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pad:
|
||||
name: str
|
||||
direction: str
|
||||
feature: str | None
|
||||
kind: str
|
||||
index: int | None
|
||||
signal: str | None
|
||||
input_signal: str | None
|
||||
output_signal: str | None
|
||||
output_enable_signal: str | None
|
||||
interface: str | None
|
||||
peer: str | None
|
||||
bind: bool
|
||||
|
||||
|
||||
def atomic_write(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists() and path.read_text(encoding="utf-8") == content:
|
||||
return
|
||||
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output:
|
||||
output.write(content)
|
||||
os.replace(temporary_name, path)
|
||||
except BaseException:
|
||||
Path(temporary_name).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def require_string(value: Any, field: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError(f"{field} must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
def parse_feature(value: Any, field: str) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
feature = require_string(value, field)
|
||||
if feature not in VALID_FEATURES:
|
||||
raise ValueError(f"{field} must be one of {sorted(VALID_FEATURES)}")
|
||||
return feature
|
||||
|
||||
|
||||
def format_template(value: str | None, index: int | None, field: str) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(f"{field} must be a string")
|
||||
if "{index}" in value:
|
||||
if index is None:
|
||||
raise ValueError(f"{field} uses {{index}} without an indexed pad")
|
||||
return value.format(index=index)
|
||||
return value
|
||||
|
||||
|
||||
def parse_pad(entry: dict[str, Any], defaults: dict[str, Any], index: int | None) -> Pad:
|
||||
data = {**defaults, **entry}
|
||||
name = require_string(format_template(data.get("name"), index, "pad.name"), "pad.name")
|
||||
direction = require_string(data.get("direction"), f"{name}.direction")
|
||||
if direction not in {"input", "output", "inout"}:
|
||||
raise ValueError(f"{name}.direction must be input, output, or inout")
|
||||
kind = require_string(data.get("kind"), f"{name}.kind")
|
||||
if kind not in VALID_KINDS:
|
||||
raise ValueError(f"{name}.kind is not supported")
|
||||
feature = parse_feature(data.get("feature"), f"{name}.feature")
|
||||
signal = format_template(data.get("signal"), index, f"{name}.signal")
|
||||
input_signal = format_template(data.get("input"), index, f"{name}.input")
|
||||
output_signal = format_template(data.get("output"), index, f"{name}.output")
|
||||
output_enable_signal = format_template(
|
||||
data.get("output_enable"), index, f"{name}.output_enable"
|
||||
)
|
||||
interface = format_template(data.get("interface"), index, f"{name}.interface")
|
||||
peer = format_template(data.get("peer"), index, f"{name}.peer")
|
||||
bind = data.get("bind", True)
|
||||
if not isinstance(bind, bool):
|
||||
raise ValueError(f"{name}.bind must be boolean")
|
||||
if kind in {"tri_input", "schmitt_input", "tri_output", "peripheral_output"} and signal is None:
|
||||
raise ValueError(f"{name}.signal is required for {kind}")
|
||||
if kind == "full" and (interface is None or index is None):
|
||||
raise ValueError(f"{name} requires an interface and index")
|
||||
if kind in {"xpi_data", "sdram_dq"} and index is None:
|
||||
raise ValueError(f"{name} requires an index")
|
||||
if kind == "xtal" and bind and (signal is None or peer is None):
|
||||
raise ValueError(f"{name} requires signal and peer")
|
||||
if kind == "peripheral_bidir" and (
|
||||
input_signal is None or output_signal is None or output_enable_signal is None
|
||||
):
|
||||
raise ValueError(f"{name} requires input, output, and output_enable")
|
||||
return Pad(
|
||||
name,
|
||||
direction,
|
||||
feature,
|
||||
kind,
|
||||
index,
|
||||
signal,
|
||||
input_signal,
|
||||
output_signal,
|
||||
output_enable_signal,
|
||||
interface,
|
||||
peer,
|
||||
bind,
|
||||
)
|
||||
|
||||
|
||||
def expand_group(group: Any, index: int) -> list[Pad]:
|
||||
if not isinstance(group, dict):
|
||||
raise ValueError(f"pads[{index}] must be an object")
|
||||
defaults = {
|
||||
key: value
|
||||
for key, value in group.items()
|
||||
if key not in {"ports", "count", "prefix", "suffix"}
|
||||
}
|
||||
ports = group.get("ports")
|
||||
if ports is not None:
|
||||
if not isinstance(ports, list) or not ports:
|
||||
raise ValueError(f"pads[{index}].ports must be a non-empty list")
|
||||
if not all(isinstance(port, dict) for port in ports):
|
||||
raise ValueError(f"pads[{index}].ports entries must be objects")
|
||||
return [parse_pad(port, defaults, None) for port in ports]
|
||||
count = group.get("count")
|
||||
if not isinstance(count, int) or count <= 0:
|
||||
raise ValueError(f"pads[{index}].count must be a positive integer")
|
||||
prefix = require_string(group.get("prefix"), f"pads[{index}].prefix")
|
||||
suffix = require_string(group.get("suffix"), f"pads[{index}].suffix")
|
||||
return [
|
||||
parse_pad({"name": f"{prefix}{pad_index}{suffix}"}, defaults, pad_index)
|
||||
for pad_index in range(count)
|
||||
]
|
||||
|
||||
|
||||
def validate_connection(value: Any, field: str) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
connection = require_string(value, field)
|
||||
if CONNECTION_RE.fullmatch(connection) or CONSTANT_RE.fullmatch(connection):
|
||||
return connection
|
||||
raise ValueError(f"{field} must be a signal, bit-select, constant, or null")
|
||||
|
||||
|
||||
def read_map(path: Path) -> tuple[list[Pad], dict[str, dict[str, str | None]]]:
|
||||
with path.open(encoding="utf-8") as source:
|
||||
document = json.load(source)
|
||||
if document.get("schema_version") != 1:
|
||||
raise ValueError("schema_version must be 1")
|
||||
groups = document.get("pads")
|
||||
if not isinstance(groups, list) or not groups:
|
||||
raise ValueError("pads must be a non-empty list")
|
||||
pads: list[Pad] = []
|
||||
for index, group in enumerate(groups):
|
||||
expanded = expand_group(group, index)
|
||||
if len(expanded) == 0:
|
||||
raise ValueError(f"pads[{index}] does not expand to any pads")
|
||||
pads.extend(expanded)
|
||||
names = [pad.name for pad in pads]
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("pad names must be unique")
|
||||
known_names = set(names)
|
||||
for pad in pads:
|
||||
if pad.peer is not None and pad.peer not in known_names:
|
||||
raise ValueError(f"{pad.name}.peer is not a known pad")
|
||||
profiles = document.get("profiles")
|
||||
if not isinstance(profiles, dict) or set(profiles) != REQUIRED_PROFILES:
|
||||
raise ValueError(f"profiles must contain exactly {sorted(REQUIRED_PROFILES)}")
|
||||
normalized_profiles: dict[str, dict[str, str | None]] = {}
|
||||
for profile, profile_data in profiles.items():
|
||||
if not isinstance(profile_data, dict):
|
||||
raise ValueError(f"profiles.{profile} must be an object")
|
||||
bindings = profile_data.get("bindings", {})
|
||||
if not isinstance(bindings, dict):
|
||||
raise ValueError(f"profiles.{profile}.bindings must be an object")
|
||||
if any(name not in known_names for name in bindings):
|
||||
raise ValueError(f"profiles.{profile} contains an unknown pad")
|
||||
normalized_profiles[profile] = {
|
||||
name: validate_connection(value, f"profiles.{profile}.bindings.{name}")
|
||||
for name, value in bindings.items()
|
||||
}
|
||||
return pads, normalized_profiles
|
||||
|
||||
|
||||
def wrap_feature(pad: Pad, line: str) -> list[str]:
|
||||
if pad.feature is None:
|
||||
return [line]
|
||||
return [f"`ifdef {pad.feature}", line, "`endif"]
|
||||
|
||||
|
||||
def render_ports(pads: list[Pad]) -> str:
|
||||
lines = [
|
||||
"// Generated by scripts/rtl/generate_pin_map.py; do not edit.",
|
||||
"`ifdef PDK_IHP130",
|
||||
"`ifdef USE_POWER_PINS",
|
||||
" inout wire IOVDD,",
|
||||
" inout wire IOVSS,",
|
||||
" inout wire VDD,",
|
||||
" inout wire VSS,",
|
||||
"`endif",
|
||||
"`endif",
|
||||
]
|
||||
for index, pad in enumerate(pads):
|
||||
suffix = "," if index != len(pads) - 1 else ""
|
||||
lines.extend(wrap_feature(pad, f" {pad.direction} {pad.name}{suffix}"))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_core_ports(pads: list[Pad]) -> str:
|
||||
"""Render the logical-chip port list without technology power pins."""
|
||||
lines = ["// Generated by scripts/rtl/generate_pin_map.py; do not edit."]
|
||||
for index, pad in enumerate(pads):
|
||||
suffix = "," if index != len(pads) - 1 else ""
|
||||
lines.extend(wrap_feature(pad, f" {pad.direction} {pad.name}{suffix}"))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_pad_instance(pad: Pad) -> str | None:
|
||||
instance = f"u_{pad.name}"
|
||||
if pad.kind == "tri_input":
|
||||
return (
|
||||
f" tc_io_in_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), .p2c({pad.signal}));"
|
||||
)
|
||||
if pad.kind == "schmitt_input":
|
||||
return (
|
||||
f" tc_io_schmitt_in_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
|
||||
f".p2c({pad.signal}));"
|
||||
)
|
||||
if pad.kind == "tri_output":
|
||||
return (
|
||||
f" tc_io_out_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), .c2p({pad.signal}));"
|
||||
)
|
||||
if pad.kind == "peripheral_output":
|
||||
return (
|
||||
f" tc_io_out_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), .c2p({pad.signal}));"
|
||||
)
|
||||
if pad.kind == "peripheral_bidir":
|
||||
return (
|
||||
f" tc_io_tri_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
|
||||
f".c2p({pad.output_signal}), "
|
||||
f".c2p_en({pad.output_enable_signal}), .p2c({pad.input_signal}));"
|
||||
)
|
||||
if pad.kind == "full":
|
||||
return (
|
||||
f" tc_io_tri_full_pad {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}]), "
|
||||
f".pd({pad.interface}.pd_o[{pad.index}]));"
|
||||
)
|
||||
if pad.kind == "xpi_data":
|
||||
return (
|
||||
f" tc_io_tri_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
|
||||
f".c2p(u_xpi_if.io_do_o[{pad.index}]), "
|
||||
f".c2p_en(u_xpi_if.io_oe_o[{pad.index}]), .p2c(u_xpi_if.io_di_i[{pad.index}]));"
|
||||
)
|
||||
if pad.kind == "sdram_dq":
|
||||
return (
|
||||
f" tc_io_tri_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
|
||||
f".c2p(u_sdram_if.dq_o[{pad.index}]), "
|
||||
f".c2p_en(u_sdram_if.oe_o), .p2c(u_sdram_if.dq_i[{pad.index}]));"
|
||||
)
|
||||
if pad.kind == "xtal" and pad.bind:
|
||||
return (
|
||||
f" tc_io_xtl_pad u_xtal_io_pad ({POWER_CONNECTIONS}.xi_pad({pad.name}), "
|
||||
f".xo_pad({pad.peer}), .en(1'b1), .clk({pad.signal}));"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def render_pad_bindings(pads: list[Pad]) -> str:
|
||||
lines = [
|
||||
"// Generated by scripts/rtl/generate_pin_map.py; do not edit.",
|
||||
"`ifdef PDK_IHP130",
|
||||
"`ifdef USE_POWER_PINS",
|
||||
"`define RETROSOC_PAD_POWER_CONNECTIONS .iovdd(IOVDD), .iovss(IOVSS), .vdd(VDD), .vss(VSS),",
|
||||
"`else",
|
||||
"`define RETROSOC_PAD_POWER_CONNECTIONS",
|
||||
"`endif",
|
||||
" generate",
|
||||
]
|
||||
cell_suffixes = {"vdd": "Vdd", "vss": "Vss", "iovdd": "IOVdd", "iovss": "IOVss"}
|
||||
for kind, count in IHP130_POWER_PAD_COUNTS.items():
|
||||
lines.extend(
|
||||
[
|
||||
f" for (genvar i = 0; i < {count}; i++) begin : {kind}_pads",
|
||||
' (* keep *) (* dont_touch = "true" *)',
|
||||
f" sg13g2_IOPad{cell_suffixes[kind]} {kind}_pad (",
|
||||
" );",
|
||||
" end",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
" endgenerate",
|
||||
"`else",
|
||||
"`define RETROSOC_PAD_POWER_CONNECTIONS",
|
||||
"`endif",
|
||||
]
|
||||
)
|
||||
for pad in pads:
|
||||
line = render_pad_instance(pad)
|
||||
if line is not None:
|
||||
lines.extend(wrap_feature(pad, line))
|
||||
lines.append("`undef RETROSOC_PAD_POWER_CONNECTIONS")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_core_binding(pad: Pad) -> str | None:
|
||||
"""Bind a package-facing core port directly to the logical SoC interface."""
|
||||
if pad.kind in {"tri_input", "schmitt_input"}:
|
||||
assert pad.signal is not None
|
||||
return f" assign {pad.signal} = {pad.name};"
|
||||
if pad.kind in {"tri_output", "peripheral_output"}:
|
||||
assert pad.signal is not None
|
||||
return f" assign {pad.name} = {pad.signal};"
|
||||
if pad.kind == "peripheral_bidir":
|
||||
assert pad.input_signal is not None
|
||||
assert pad.output_signal is not None
|
||||
assert pad.output_enable_signal is not None
|
||||
return "\n".join(
|
||||
[
|
||||
f" assign {pad.name} = {pad.output_enable_signal} ? {pad.output_signal} : 1'bz;",
|
||||
f" assign {pad.input_signal} = {pad.name};",
|
||||
]
|
||||
)
|
||||
if pad.kind == "full":
|
||||
assert pad.interface is not None
|
||||
assert pad.index is not None
|
||||
return "\n".join(
|
||||
[
|
||||
f" assign {pad.name} = {pad.interface}.oe_o[{pad.index}] ? "
|
||||
f"{pad.interface}.do_o[{pad.index}] : 1'bz;",
|
||||
f" assign {pad.interface}.di_i[{pad.index}] = {pad.name};",
|
||||
]
|
||||
)
|
||||
if pad.kind == "xpi_data":
|
||||
assert pad.index is not None
|
||||
return "\n".join(
|
||||
[
|
||||
f" assign {pad.name} = u_xpi_if.io_oe_o[{pad.index}] ? "
|
||||
f"u_xpi_if.io_do_o[{pad.index}] : 1'bz;",
|
||||
f" assign u_xpi_if.io_di_i[{pad.index}] = {pad.name};",
|
||||
]
|
||||
)
|
||||
if pad.kind == "sdram_dq":
|
||||
assert pad.index is not None
|
||||
return "\n".join(
|
||||
[
|
||||
f" assign {pad.name} = u_sdram_if.oe_o ? u_sdram_if.dq_o[{pad.index}] : 1'bz;",
|
||||
f" assign u_sdram_if.dq_i[{pad.index}] = {pad.name};",
|
||||
]
|
||||
)
|
||||
if pad.kind == "xtal" and pad.bind:
|
||||
assert pad.signal is not None
|
||||
assert pad.peer is not None
|
||||
return "\n".join(
|
||||
[
|
||||
f" assign {pad.signal} = {pad.name};",
|
||||
f" assign {pad.peer} = 1'b0;",
|
||||
]
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def render_core_bindings(pads: list[Pad]) -> str:
|
||||
lines = ["// Generated by scripts/rtl/generate_pin_map.py; do not edit."]
|
||||
for pad in pads:
|
||||
binding = render_core_binding(pad)
|
||||
if binding is not None:
|
||||
lines.extend(wrap_feature(pad, binding))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def ihp130_pad_cell(pad: Pad) -> str | None:
|
||||
if pad.kind in {"tri_input", "schmitt_input"}:
|
||||
return "sg13g2_IOPadIn"
|
||||
if pad.kind in {"tri_output", "peripheral_output"}:
|
||||
return "sg13g2_IOPadOut4mA"
|
||||
if pad.kind in {"full", "peripheral_bidir", "sdram_dq", "xpi_data"}:
|
||||
return "sg13g2_IOPadInOut4mA"
|
||||
if pad.kind == "xtal" and pad.bind:
|
||||
return "sg13g2_IOPadIn"
|
||||
return None
|
||||
|
||||
|
||||
def ihp130_pad_instance(pad: Pad) -> str | None:
|
||||
cell = ihp130_pad_cell(pad)
|
||||
if cell is None:
|
||||
return None
|
||||
outer = "u_xtal_io_pad" if pad.kind == "xtal" else f"u_{pad.name}"
|
||||
return f"{outer}.u_{cell}"
|
||||
|
||||
|
||||
def render_profile_bindings(pads: list[Pad], bindings: dict[str, str | None]) -> str:
|
||||
lines = ["// Generated by scripts/rtl/generate_pin_map.py; do not edit."]
|
||||
for index, pad in enumerate(pads):
|
||||
connection = bindings.get(pad.name)
|
||||
suffix = "," if index != len(pads) - 1 else ""
|
||||
value = "" if connection is None else connection
|
||||
lines.extend(wrap_feature(pad, f" .{pad.name}({value}){suffix}"))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def generate(map_path: Path, output_dir: Path) -> None:
|
||||
pads, profiles = read_map(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))
|
||||
atomic_write(rtl_dir / "retrosoc_core_ports.svh", render_core_ports(pads))
|
||||
atomic_write(rtl_dir / "retrosoc_core_bindings.svh", render_core_bindings(pads))
|
||||
for profile, bindings in profiles.items():
|
||||
atomic_write(
|
||||
rtl_dir / f"retrosoc_asic_{profile}_bindings.svh",
|
||||
render_profile_bindings(pads, bindings),
|
||||
)
|
||||
atomic_write(output_dir / "pin_map.fl", f"+incdir+{rtl_dir}\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--map", required=True, type=Path)
|
||||
parser.add_argument("--output-dir", type=Path)
|
||||
parser.add_argument("--check", action="store_true", help="validate only; write nothing")
|
||||
arguments = parser.parse_args()
|
||||
try:
|
||||
if arguments.check:
|
||||
read_map(arguments.map)
|
||||
else:
|
||||
if arguments.output_dir is None:
|
||||
parser.error("--output-dir is required unless --check is used")
|
||||
generate(arguments.map, arguments.output_dir)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
parser.error(str(error))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
MINI_DIR = SCRIPT_DIR.parent
|
||||
ROOT_DIR = SCRIPT_DIR.parents[1]
|
||||
DEFAULT_MODELS = ROOT_DIR / "rtl" / "managed" / "third_party" / "norflash"
|
||||
DEFAULT_FIRMWARE = ROOT_DIR / ".sw_build" / "retrosoc_fw.hex"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Prepare NOR Flash simulation files")
|
||||
parser.add_argument("--sim-dir", type=Path, required=True)
|
||||
parser.add_argument("--models-dir", type=Path, default=DEFAULT_MODELS)
|
||||
parser.add_argument("--firmware", type=Path, default=DEFAULT_FIRMWARE)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
sim_dir = args.sim_dir.resolve()
|
||||
models_dir = args.models_dir.resolve()
|
||||
firmware = args.firmware.resolve()
|
||||
sim_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
model_names = ("SECSI.TXT", "SFDP.TXT", "SREG.TXT")
|
||||
missing_models = [name for name in model_names if not (models_dir / name).is_file()]
|
||||
if missing_models:
|
||||
raise FileNotFoundError(
|
||||
f"NOR Flash model files missing in {models_dir}: {', '.join(missing_models)}"
|
||||
)
|
||||
if not firmware.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"firmware image not found: {firmware}; run 'make firmware' first"
|
||||
)
|
||||
|
||||
for name in model_names:
|
||||
shutil.copy2(models_dir / name, sim_dir / name)
|
||||
|
||||
memory = sim_dir / "MEM.TXT"
|
||||
if memory.is_symlink() or memory.exists():
|
||||
memory.unlink()
|
||||
memory.symlink_to(firmware)
|
||||
print(f"prepared NOR Flash files in {sim_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user