Files
retroSoC/rtl/mini/integration/generate_soc_topology.py
T
maksyuki d886cd992c feat: refactor SoC topology and NMI integration
- Updated the  file to improve code readability and maintainability by removing commented-out code and adding generated GPIO alternate-function wiring.
- Modified  to streamline simulation command configurations and removed unnecessary variables.
- Introduced a new testbench  to validate NMI routing functionality within the SoC topology.
- Added comprehensive tests in  to ensure the correctness of generated bindings and validate topology constraints.
- Adjusted  to reflect changes in regression command imports and ensure uniform timeout settings for Verilator simulations.
2026-07-23 12:07:45 +08:00

375 lines
15 KiB
Python

#!/usr/bin/env python3
"""Generate Mini SoC internal integration bindings from one topology 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
SV_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
SV_REFERENCE_RE = re.compile(
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*(?:\[[0-9]+\])?$"
)
SV_CONSTANT_RE = re.compile(r"(?:1'[bB][01]|'[01])$")
@dataclass(frozen=True)
class NativeTarget:
slot: int
name: str
interface: str
regions: tuple[str, ...]
disabled: bool
@dataclass(frozen=True)
class GpioMode:
inputs: tuple[str, ...]
do: str
oe: str
@dataclass(frozen=True)
class GpioFunction:
pin: int
alt0: GpioMode
alt1: GpioMode
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_object(value: Any, field: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ValueError(f"{field} must be an object")
return value
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 require_identifier(value: Any, field: str) -> str:
identifier = require_string(value, field)
if SV_IDENTIFIER_RE.fullmatch(identifier) is None:
raise ValueError(f"{field} must be a SystemVerilog identifier")
return identifier
def require_reference(value: Any, field: str) -> str:
reference = require_string(value, field)
if SV_REFERENCE_RE.fullmatch(reference) is None:
raise ValueError(f"{field} must be a scalar SystemVerilog reference")
return reference
def require_value(value: Any, field: str) -> str:
rendered = require_string(value, field)
if SV_REFERENCE_RE.fullmatch(rendered) is None and SV_CONSTANT_RE.fullmatch(rendered) is None:
raise ValueError(f"{field} must be a scalar SystemVerilog reference or constant")
return rendered
def read_memory_regions(path: Path) -> dict[str, dict[str, Any]]:
document = require_object(json.loads(path.read_text(encoding="utf-8")), "memory map")
regions = document.get("regions")
if not isinstance(regions, list) or not regions:
raise ValueError("memory map regions must be a non-empty list")
parsed: dict[str, dict[str, Any]] = {}
for index, entry in enumerate(regions):
region = require_object(entry, f"memory map regions[{index}]")
symbol = require_identifier(region.get("symbol"), f"memory map regions[{index}].symbol")
if symbol in parsed:
raise ValueError(f"memory map region {symbol} is duplicated")
parsed[symbol] = region
return parsed
def parse_regions(value: Any, field: str) -> tuple[str, ...]:
if not isinstance(value, list) or not value:
raise ValueError(f"{field} must be a non-empty list")
regions = tuple(require_identifier(item, f"{field} entry") for item in value)
if len(regions) != len(set(regions)):
raise ValueError(f"{field} contains duplicate regions")
return regions
def parse_native_targets(value: Any, memory_regions: dict[str, dict[str, Any]]) -> list[NativeTarget]:
if not isinstance(value, list) or not value:
raise ValueError("native_targets must be a non-empty list")
targets: list[NativeTarget] = []
claimed_regions: set[str] = set()
names: set[str] = set()
interfaces: set[str] = set()
slots: set[int] = set()
for index, entry in enumerate(value):
target = require_object(entry, f"native_targets[{index}]")
slot = target.get("slot")
if not isinstance(slot, int) or slot < 0:
raise ValueError(f"native_targets[{index}].slot must be a non-negative integer")
name = require_identifier(target.get("name"), f"native_targets[{index}].name")
interface = require_identifier(
target.get("interface"), f"native_targets[{index}].interface"
)
disabled = target.get("disabled", False)
if not isinstance(disabled, bool):
raise ValueError(f"native_targets[{index}].disabled must be boolean")
if disabled:
if "regions" in target:
raise ValueError(f"native_targets[{index}] is disabled but declares regions")
regions: tuple[str, ...] = ()
else:
regions = parse_regions(target.get("regions"), f"native_targets[{index}].regions")
for region_name in regions:
region = memory_regions.get(region_name)
if region is None:
raise ValueError(
f"native_targets[{index}] references unknown region {region_name}"
)
if region.get("route") != "native" or region.get("kind") != "active":
raise ValueError(
f"native_targets[{index}] region {region_name} is not an active native region"
)
if region_name in claimed_regions:
raise ValueError(f"native region {region_name} has multiple targets")
claimed_regions.add(region_name)
if name in names:
raise ValueError(f"native target name {name} is duplicated")
if interface in interfaces:
raise ValueError(f"native target interface {interface} is duplicated")
if slot in slots:
raise ValueError(f"native target slot {slot} is duplicated")
names.add(name)
interfaces.add(interface)
slots.add(slot)
targets.append(NativeTarget(slot, name, interface, regions, disabled))
ordered = sorted(targets, key=lambda item: item.slot)
if [target.slot for target in ordered] != list(range(len(ordered))):
raise ValueError("native target slots must be contiguous from zero")
active_native_regions = {
symbol
for symbol, region in memory_regions.items()
if region.get("route") == "native" and region.get("kind") == "active"
}
if claimed_regions != active_native_regions:
missing = sorted(active_native_regions - claimed_regions)
extra = sorted(claimed_regions - active_native_regions)
details: list[str] = []
if missing:
details.append(f"missing {', '.join(missing)}")
if extra:
details.append(f"extra {', '.join(extra)}")
raise ValueError(f"native target regions do not cover the memory map ({'; '.join(details)})")
return ordered
def parse_gpio_mode(value: Any, field: str) -> GpioMode:
mode = require_object(value, field)
inputs_value = mode.get("inputs")
if not isinstance(inputs_value, list):
raise ValueError(f"{field}.inputs must be a list")
inputs = tuple(
require_reference(item, f"{field}.inputs[{index}]")
for index, item in enumerate(inputs_value)
)
if len(inputs) != len(set(inputs)):
raise ValueError(f"{field}.inputs contains duplicates")
return GpioMode(
inputs=inputs,
do=require_value(mode.get("do"), f"{field}.do"),
oe=require_value(mode.get("oe"), f"{field}.oe"),
)
def parse_gpio_functions(value: Any, gpio_pins: int) -> list[GpioFunction]:
if not isinstance(value, list) or len(value) != gpio_pins:
raise ValueError("gpio_alt_functions must contain one entry for every GPIO pin")
parsed: list[GpioFunction] = []
pins: set[int] = set()
for index, entry in enumerate(value):
function = require_object(entry, f"gpio_alt_functions[{index}]")
pin = function.get("pin")
if not isinstance(pin, int) or not 0 <= pin < gpio_pins:
raise ValueError(f"gpio_alt_functions[{index}].pin is out of range")
if pin in pins:
raise ValueError(f"gpio pin {pin} is duplicated")
pins.add(pin)
parsed.append(
GpioFunction(
pin=pin,
alt0=parse_gpio_mode(function.get("alt0"), f"gpio_alt_functions[{index}].alt0"),
alt1=parse_gpio_mode(function.get("alt1"), f"gpio_alt_functions[{index}].alt1"),
)
)
if pins != set(range(gpio_pins)):
raise ValueError("gpio_alt_functions must cover contiguous pins from zero")
return sorted(parsed, key=lambda item: item.pin)
def read_topology(
topology_path: Path, memory_map_path: Path
) -> tuple[list[NativeTarget], list[GpioFunction]]:
document = require_object(json.loads(topology_path.read_text(encoding="utf-8")), "topology")
if document.get("schema_version") != 1:
raise ValueError("schema_version must be 1")
gpio_pins = document.get("gpio_pins")
if not isinstance(gpio_pins, int) or gpio_pins <= 0:
raise ValueError("gpio_pins must be a positive integer")
memory_regions = read_memory_regions(memory_map_path)
return (
parse_native_targets(document.get("native_targets"), memory_regions),
parse_gpio_functions(document.get("gpio_alt_functions"), gpio_pins),
)
def render_nmi_interfaces(targets: list[NativeTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
lines.extend(f" nmi_if {target.interface} ();" for target in targets)
return "\n".join(lines) + "\n"
def render_region_select(region: str, memory_region: dict[str, Any]) -> str:
base = int(require_string(memory_region.get("base"), f"memory map region {region}.base"), 0)
if base == 0:
return f"(nmi.addr <= `SOC_ADDR_{region}_END)"
return f"`SOC_ADDR_IS_{region}(nmi.addr)"
def render_nmi_routes(
targets: list[NativeTarget], memory_regions: dict[str, dict[str, Any]]
) -> str:
count = len(targets)
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
lines.extend(
[
f" logic [{count - 1}:0] s_slv_sel_d, s_slv_sel_q;",
f" logic [{count - 1}:0] s_slv_ready;",
f" logic [31:0] s_slv_rdata [0:{count - 1}];",
"",
]
)
for target in targets:
if not target.disabled:
lines.append(
f" logic [{len(target.regions) - 1}:0] s_{target.name}_region_sel;"
)
lines.append("")
for target in targets:
if target.disabled:
lines.append(f" assign {target.interface}.valid = 1'b0;")
else:
for index, region in enumerate(target.regions):
lines.append(
f" assign s_{target.name}_region_sel[{index}] = "
f"{render_region_select(region, memory_regions[region])};"
)
lines.append(
f" assign {target.interface}.valid = nmi.valid && (|s_{target.name}_region_sel);"
)
lines.extend(
[
f" assign {target.interface}.addr = nmi.addr;",
f" assign {target.interface}.wdata = nmi.wdata;",
f" assign {target.interface}.wstrb = nmi.wstrb;",
f" assign s_slv_sel_d[{target.slot}] = {target.interface}.valid;",
f" assign s_slv_ready[{target.slot}] = {target.interface}.ready;",
f" assign s_slv_rdata[{target.slot}] = {target.interface}.rdata;",
"",
]
)
lines.extend(
[
f" dffr #({count}) u_slv_sel_dffr (",
" clk_i,",
" rst_n_i,",
" s_slv_sel_d,",
" s_slv_sel_q",
" );",
"",
" assign nmi.ready = |(s_slv_sel_q & s_slv_ready);",
"",
" always_comb begin",
" nmi.rdata = '0;",
f" for (int index = 0; index < {count}; index++) begin",
" if (s_slv_sel_q[index]) begin",
" nmi.rdata = nmi.rdata | s_slv_rdata[index];",
" end",
" end",
" end",
]
)
return "\n".join(lines) + "\n"
def render_gpio_bindings(functions: list[GpioFunction]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for function in functions:
for mode_name, mode in (("alt0", function.alt0), ("alt1", function.alt1)):
lines.append(f" // GPIO{function.pin} {mode_name}")
lines.extend(
f" assign {input_signal} = u_gpio_if.di_i[{function.pin}];"
for input_signal in mode.inputs
)
lines.extend(
[
f" assign u_gpio_if.{mode_name}_do_i[{function.pin}] = {mode.do};",
f" assign u_gpio_if.{mode_name}_oe_i[{function.pin}] = {mode.oe};",
"",
]
)
return "\n".join(lines) + "\n"
def generate(topology_path: Path, memory_map_path: Path, output_dir: Path) -> None:
targets, gpio_functions = read_topology(topology_path, memory_map_path)
memory_regions = read_memory_regions(memory_map_path)
rtl_dir = output_dir / "rtl"
atomic_write(rtl_dir / "soc_nmi_interfaces.svh", render_nmi_interfaces(targets))
atomic_write(rtl_dir / "soc_nmi_routes.svh", render_nmi_routes(targets, memory_regions))
atomic_write(rtl_dir / "soc_gpio_alt_bindings.svh", render_gpio_bindings(gpio_functions))
atomic_write(output_dir / "soc_topology.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("--memory-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_topology(arguments.map, arguments.memory_map)
else:
if arguments.output_dir is None:
parser.error("--output-dir is required unless --check is used")
generate(arguments.map, arguments.memory_map, arguments.output_dir)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())