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