Files
retroSoC/rtl/mini/integration/generate_user_extensions.py
T
maksyuki 7a604ec86b feat: refactor NMI interface and remove unused variables
- Removed CORE and IP variables from common.tcl and synth.tcl scripts.
- Updated yosys.mk to exclude CORE and IP from environment variables.
- Modified bus_fault_tb to enhance NMI interface with management and user NMI signals.
- Added user access denial handling in bus_fault_tb.
- Updated pll_ctrl_tb and sysctrl_fault_tb to reflect changes in NMI interface.
- Adjusted test_gpio_user_mux.py to remove IP_MDD definition.
- Enhanced memory map tests to include user extensions and ensure proper generation.
- Updated test_script_tools.py to reflect changes in core management and user extensions.
- Refined topology tests to ensure user APB target is always added.
- Adjusted tech cell configuration tests to use simplified profile paths.
- Improved user extensions tests for scalar bindings and slot validation.
2026-07-26 09:03:33 +08:00

305 lines
11 KiB
Python

#!/usr/bin/env python3
"""Generate scalar user core and user IP integration bindings."""
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
IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
APB_REQUEST_SIGNALS = ("paddr", "pprot", "psel", "penable", "pwrite", "pwdata", "pstrb")
@dataclass(frozen=True)
class ExtensionTarget:
slot: int
module: str
instance: str
@dataclass(frozen=True)
class ExtensionMap:
core_selector_width: int
ip_selector_width: int
gpio_width: int
core_targets: tuple[ExtensionTarget, ...]
ip_targets: tuple[ExtensionTarget, ...]
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_identifier(value: Any, field: str) -> str:
if not isinstance(value, str) or IDENTIFIER_RE.fullmatch(value) is None:
raise ValueError(f"{field} must be a SystemVerilog identifier")
return value
def require_positive_integer(value: Any, field: str) -> int:
if not isinstance(value, int) or value <= 0:
raise ValueError(f"{field} must be a positive integer")
return value
def parse_targets(
value: Any, field: str, selector_width: int, first_slot: int
) -> tuple[ExtensionTarget, ...]:
if not isinstance(value, list) or not value:
raise ValueError(f"{field} must be a non-empty list")
targets: list[ExtensionTarget] = []
slots: set[int] = set()
instances: set[str] = set()
for index, entry in enumerate(value):
target = require_object(entry, f"{field}[{index}]")
slot = target.get("slot")
if not isinstance(slot, int) or not first_slot <= slot < (1 << selector_width):
raise ValueError(f"{field}[{index}].slot must be within the selector range")
module = require_identifier(target.get("module"), f"{field}[{index}].module")
instance = require_identifier(target.get("instance"), f"{field}[{index}].instance")
if slot in slots:
raise ValueError(f"{field} slot {slot} is duplicated")
if instance in instances:
raise ValueError(f"{field} instance {instance} is duplicated")
slots.add(slot)
instances.add(instance)
targets.append(ExtensionTarget(slot, module, instance))
ordered = tuple(sorted(targets, key=lambda item: item.slot))
if [target.slot for target in ordered] != list(range(first_slot, first_slot + len(ordered))):
raise ValueError(f"{field} slots must be contiguous from {first_slot}")
return ordered
def read_extensions(path: Path) -> ExtensionMap:
document = require_object(json.loads(path.read_text(encoding="utf-8")), "user extensions")
if document.get("schema_version") != 1:
raise ValueError("schema_version must be 1")
core_selector_width = require_positive_integer(
document.get("core_selector_width"), "core_selector_width"
)
ip_selector_width = require_positive_integer(
document.get("ip_selector_width"), "ip_selector_width"
)
gpio_width = require_positive_integer(document.get("gpio_width"), "gpio_width")
return ExtensionMap(
core_selector_width,
ip_selector_width,
gpio_width,
parse_targets(document.get("core_targets"), "core_targets", core_selector_width, 0),
parse_targets(document.get("ip_targets"), "ip_targets", ip_selector_width, 1),
)
def render_core_bindings(extensions: ExtensionMap) -> str:
lines = ["// Generated by rtl/mini/integration/generate_user_extensions.py; do not edit."]
for target in extensions.core_targets:
lines.extend(
[
f" nmi_if u_user_{target.slot}_nmi_if ();",
f" logic [31:0] s_user_{target.slot}_irq;",
]
)
lines.extend(
[
"",
" always_comb begin",
" nmi.valid = '0;",
" nmi.addr = '0;",
" nmi.wdata = '0;",
" nmi.wstrb = '0;",
]
)
for target in extensions.core_targets:
lines.extend(
[
f" u_user_{target.slot}_nmi_if.rdata = '0;",
f" u_user_{target.slot}_nmi_if.ready = '0;",
f" s_user_{target.slot}_irq = '0;",
]
)
lines.append(" unique case (sel_i)")
for target in extensions.core_targets:
lines.extend(
[
f" {extensions.core_selector_width}'d{target.slot}: begin",
f" nmi.valid = u_user_{target.slot}_nmi_if.valid;",
f" nmi.addr = u_user_{target.slot}_nmi_if.addr;",
f" nmi.wdata = u_user_{target.slot}_nmi_if.wdata;",
f" nmi.wstrb = u_user_{target.slot}_nmi_if.wstrb;",
f" u_user_{target.slot}_nmi_if.rdata = nmi.rdata;",
f" u_user_{target.slot}_nmi_if.ready = nmi.ready;",
f" s_user_{target.slot}_irq = irq_i;",
" end",
]
)
lines.extend([" default: ;", " endcase", " end", ""])
for target in extensions.core_targets:
lines.extend(
[
f" {target.module} #({target.slot}) {target.instance} (",
" .clk_i (clk_i),",
f" .rst_n_i(rst_n_i && ~core_reset_i[{target.slot}]),",
f" .irq_i (s_user_{target.slot}_irq),",
f" .nmi (u_user_{target.slot}_nmi_if)",
" );",
"",
]
)
return "\n".join(lines)
def render_config(extensions: ExtensionMap) -> str:
return "\n".join(
[
"// Generated by rtl/mini/integration/generate_user_extensions.py; do not edit.",
"`ifndef RETROSOC_USER_EXTENSIONS_CONFIG_SVH",
"`define RETROSOC_USER_EXTENSIONS_CONFIG_SVH",
"",
f"`define USER_CORESEL_WIDTH {extensions.core_selector_width}",
f"`define USER_CORE_COUNT {len(extensions.core_targets)}",
f"`define USER_IPSEL_WIDTH {extensions.ip_selector_width}",
f"`define USER_IP_COUNT {len(extensions.ip_targets)}",
f"`define USER_GPIO_NUM {extensions.gpio_width}",
"",
"`endif",
"",
]
)
def render_c_config(extensions: ExtensionMap) -> str:
return "\n".join(
[
"/* Generated by rtl/mini/integration/generate_user_extensions.py; do not edit. */",
"#ifndef RETROSOC_GENERATED_USER_EXTENSIONS_H",
"#define RETROSOC_GENERATED_USER_EXTENSIONS_H",
"",
"#include <stdint.h>",
"",
f"#define RS_SOC_USER_CORE_COUNT UINT32_C({len(extensions.core_targets)})",
f"#define RS_SOC_USER_CORESEL_WIDTH UINT32_C({extensions.core_selector_width})",
f"#define RS_SOC_USER_IP_COUNT UINT32_C({len(extensions.ip_targets)})",
"",
"#endif",
"",
]
)
def render_ip_bindings(extensions: ExtensionMap) -> str:
lines = ["// Generated by rtl/mini/integration/generate_user_extensions.py; do not edit."]
for target in extensions.ip_targets:
lines.extend(
[
f" user_gpio_if #(`USER_GPIO_NUM) u_user_{target.slot}_gpio_if ();",
f" apb4_if u_user_{target.slot}_apb_if (clk_i, rst_n_i);",
]
)
lines.extend(
[
"",
" always_comb begin",
" gpio.do_o = '0;",
" gpio.oe_o = '0;",
" apb.pready = '0;",
" apb.prdata = '0;",
" apb.pslverr = '0;",
]
)
for target in extensions.ip_targets:
lines.extend(
f" u_user_{target.slot}_apb_if.{signal} = '0;" for signal in APB_REQUEST_SIGNALS
)
lines.append(" unique case (sel_i)")
for target in extensions.ip_targets:
lines.extend(
[
f" {extensions.ip_selector_width}'d{target.slot}: begin",
f" gpio.do_o = u_user_{target.slot}_gpio_if.do_o;",
f" gpio.oe_o = u_user_{target.slot}_gpio_if.oe_o;",
f" apb.pready = u_user_{target.slot}_apb_if.pready;",
f" apb.prdata = u_user_{target.slot}_apb_if.prdata;",
f" apb.pslverr = u_user_{target.slot}_apb_if.pslverr;",
]
)
lines.extend(
f" u_user_{target.slot}_apb_if.{signal} = apb.{signal};"
for signal in APB_REQUEST_SIGNALS
)
lines.append(" end")
lines.extend([" default: ;", " endcase", " end", ""])
for target in extensions.ip_targets:
lines.append(f" assign u_user_{target.slot}_gpio_if.di_i = gpio.di_i;")
lines.append("")
for target in extensions.ip_targets:
lines.extend(
[
f" {target.module} #({target.slot}) {target.instance} (",
" .clk_i (clk_i),",
" .rst_n_i(rst_n_i),",
f" .gpio (u_user_{target.slot}_gpio_if),",
f" .apb (u_user_{target.slot}_apb_if)",
" );",
"",
]
)
return "\n".join(lines)
def generate(source: Path, output_dir: Path) -> None:
extensions = read_extensions(source)
rtl_dir = output_dir / "rtl"
atomic_write(rtl_dir / "user_core_bindings.svh", render_core_bindings(extensions))
atomic_write(rtl_dir / "user_ip_bindings.svh", render_ip_bindings(extensions))
atomic_write(rtl_dir / "user_extensions_config.svh", render_config(extensions))
atomic_write(
output_dir / "include" / "retrosoc" / "generated" / "user_extensions.h",
render_c_config(extensions),
)
atomic_write(output_dir / "user_extensions.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_extensions(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())