mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
- 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.
347 lines
14 KiB
Python
347 lines
14 KiB
Python
#!/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())
|