Files
retroSoC/rtl/mini/address_map/generate_memory_map.py
T
maksyuki fc898bc4ab feat: add PLL controller and related testbench functionality
- Removed legacy clock definitions and added a dynamic PLL timing template in retrosoc_pll.sdc.
- Introduced a new SystemVerilog testbench (pll_ctrl_tb.sv) for the PLL controller, including clock generation, register read/write tasks, and validation of PLL configuration and timing.
- Updated sysctrl_fault_tb.sv to include PLL controller interface and initialization.
- Enhanced memory map tests to verify PLL configuration and status offsets.
- Added tests for PLL controller dynamic reconfiguration and fallback to safe clock.
2026-07-20 22:12:17 +08:00

250 lines
9.5 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 = {"native", "apb", "ram", "reserved"}
VALID_KINDS = {"active", "reserved"}
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) -> tuple[int, list[dict[str, Any]]]:
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")
cpu_reset = parse_integer(document.get("cpu_reset"), "cpu_reset")
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")
requires_ip = region.get("requires_ip")
if requires_ip is not None and requires_ip != "MDD":
raise ValueError(f"regions[{index}].requires_ip must be MDD when present")
base = parse_integer(region.get("base"), f"regions[{index}].base")
size = parse_integer(region.get("size"), f"regions[{index}].size")
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"),
"requires_ip": requires_ip,
}
)
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
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) -> str:
return f"((addr) >= `SOC_ADDR_{symbol}_BASE && (addr) <= `SOC_ADDR_{symbol}_END)"
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]]) -> str:
lines = [
"// Generated by rtl/mini/address_map/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}_END {hex32(region['end'])}",
f"`define SOC_ADDR_IS_{symbol}(addr) {range_expression(symbol)}",
]
lines.append("")
for route in ("native", "apb", "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)})")
lines += [
"",
"`define SOC_SYSCTRL_PLL_CFG_OFFSET 32'h00000008",
"`define SOC_SYSCTRL_PLL_CMD_OFFSET 32'h0000000C",
"`define SOC_SYSCTRL_FAULT_STATUS_OFFSET 32'h00000010",
"`define SOC_SYSCTRL_FAULT_ADDR_OFFSET 32'h00000014",
"`define SOC_SYSCTRL_FAULT_COUNT_OFFSET 32'h00000018",
"`define SOC_SYSCTRL_PLL_STATUS_OFFSET 32'h0000001C",
"",
"`endif",
"",
]
return "\n".join(lines)
def render_c(cpu_reset: int, regions: list[dict[str, Any]], have_sram_if: str) -> str:
lines = [
"/* Generated by rtl/mini/address_map/generate_memory_map.py; do not edit. */",
"#ifndef RETROSOC_GENERATED_MEMORY_MAP_H",
"#define RETROSOC_GENERATED_MEMORY_MAP_H",
"",
"#include <stdint.h>",
"",
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 += [
"",
"#define RS_SOC_SYSCTRL_PLL_CFG_OFFSET UINT32_C(0x00000008)",
"#define RS_SOC_SYSCTRL_PLL_CMD_OFFSET UINT32_C(0x0000000C)",
"#define RS_SOC_SYSCTRL_FAULT_STATUS_OFFSET UINT32_C(0x00000010)",
"#define RS_SOC_SYSCTRL_FAULT_ADDR_OFFSET UINT32_C(0x00000014)",
"#define RS_SOC_SYSCTRL_FAULT_COUNT_OFFSET UINT32_C(0x00000018)",
"#define RS_SOC_SYSCTRL_PLL_STATUS_OFFSET UINT32_C(0x0000001C)",
"",
"#endif",
"",
]
return "\n".join(lines)
def render_linker(regions: list[dict[str, Any]]) -> str:
lines = [
"/* Generated by rtl/mini/address_map/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, ip: str) -> None:
cpu_reset, regions = read_map(map_path)
enabled_regions = [
region for region in regions if region["requires_ip"] is None or region["requires_ip"] == ip
]
atomic_write(output_dir / "rtl" / "mmap_define.svh", render_rtl(cpu_reset, enabled_regions))
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, enabled_regions, have_sram_if),
)
atomic_write(output_dir / "linker" / "memory_regions.ld", render_linker(enabled_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("--ip", choices=("NONE", "MDD"), default="NONE")
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, arguments.have_sram_if, arguments.ip)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())