mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-06 14:55:14 +00:00
- 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.
250 lines
9.5 KiB
Python
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())
|