mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-06 14:55:14 +00:00
- Introduced a new testbench for the SDIO wrapper () to validate APB interactions. - Updated to include IRQ handling for SDIO0 and SDIO1. - Enhanced to account for performance management of SDIO0 and SDIO1. - Modified memory map tests to include SDIO base addresses and performance wait offsets. - Updated pin mapping tests to include SDIO1 signals. - Created a comprehensive suite of SDIO tests () covering various functionalities including CRC, command engine, DMA, and standalone operations. - Added a new test for verifying SDIO register parity against the RTL definition. - Adjusted SOC topology tests to reflect the addition of SDIO in the IRQ and APB fabric.
332 lines
13 KiB
Python
332 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate SoC pad declarations and platform bindings from one JSON 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
|
|
|
|
|
|
VALID_FEATURES = {"HAVE_PLL"}
|
|
VALID_KINDS = {
|
|
"full",
|
|
"peripheral_output",
|
|
"peripheral_bidir",
|
|
"schmitt_input",
|
|
"sdram_dq",
|
|
"tri_input",
|
|
"tri_output",
|
|
"xpi_data",
|
|
"xtal",
|
|
}
|
|
REQUIRED_PROFILES = {"fpga_mini", "tb", "verilator"}
|
|
CONNECTION_RE = re.compile(r"(?:[A-Za-z_][A-Za-z0-9_]*)(?:\[[0-9]+\])?$")
|
|
CONSTANT_RE = re.compile(r"[0-9]+'[bBdDhHoO][0-9a-fA-F_xXzZ]+$")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Pad:
|
|
name: str
|
|
direction: str
|
|
feature: str | None
|
|
kind: str
|
|
index: int | None
|
|
signal: str | None
|
|
input_signal: str | None
|
|
output_signal: str | None
|
|
output_enable_signal: str | None
|
|
interface: str | None
|
|
peer: str | None
|
|
bind: bool
|
|
|
|
|
|
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_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 parse_feature(value: Any, field: str) -> str | None:
|
|
if value is None:
|
|
return None
|
|
feature = require_string(value, field)
|
|
if feature not in VALID_FEATURES:
|
|
raise ValueError(f"{field} must be one of {sorted(VALID_FEATURES)}")
|
|
return feature
|
|
|
|
|
|
def format_template(value: str | None, index: int | None, field: str) -> str | None:
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, str):
|
|
raise ValueError(f"{field} must be a string")
|
|
if "{index}" in value:
|
|
if index is None:
|
|
raise ValueError(f"{field} uses {{index}} without an indexed pad")
|
|
return value.format(index=index)
|
|
return value
|
|
|
|
|
|
def parse_pad(entry: dict[str, Any], defaults: dict[str, Any], index: int | None) -> Pad:
|
|
data = {**defaults, **entry}
|
|
name = require_string(format_template(data.get("name"), index, "pad.name"), "pad.name")
|
|
direction = require_string(data.get("direction"), f"{name}.direction")
|
|
if direction not in {"input", "output", "inout"}:
|
|
raise ValueError(f"{name}.direction must be input, output, or inout")
|
|
kind = require_string(data.get("kind"), f"{name}.kind")
|
|
if kind not in VALID_KINDS:
|
|
raise ValueError(f"{name}.kind is not supported")
|
|
feature = parse_feature(data.get("feature"), f"{name}.feature")
|
|
signal = format_template(data.get("signal"), index, f"{name}.signal")
|
|
input_signal = format_template(data.get("input"), index, f"{name}.input")
|
|
output_signal = format_template(data.get("output"), index, f"{name}.output")
|
|
output_enable_signal = format_template(
|
|
data.get("output_enable"), index, f"{name}.output_enable"
|
|
)
|
|
interface = format_template(data.get("interface"), index, f"{name}.interface")
|
|
peer = format_template(data.get("peer"), index, f"{name}.peer")
|
|
bind = data.get("bind", True)
|
|
if not isinstance(bind, bool):
|
|
raise ValueError(f"{name}.bind must be boolean")
|
|
if (
|
|
kind in {"tri_input", "schmitt_input", "tri_output", "peripheral_output"}
|
|
and signal is None
|
|
):
|
|
raise ValueError(f"{name}.signal is required for {kind}")
|
|
if kind == "full" and (interface is None or index is None):
|
|
raise ValueError(f"{name} requires an interface and index")
|
|
if kind in {"xpi_data", "sdram_dq"} and index is None:
|
|
raise ValueError(f"{name} requires an index")
|
|
if kind == "xtal" and bind and (signal is None or peer is None):
|
|
raise ValueError(f"{name} requires signal and peer")
|
|
if kind == "peripheral_bidir" and (
|
|
input_signal is None or output_signal is None or output_enable_signal is None
|
|
):
|
|
raise ValueError(f"{name} requires input, output, and output_enable")
|
|
return Pad(
|
|
name,
|
|
direction,
|
|
feature,
|
|
kind,
|
|
index,
|
|
signal,
|
|
input_signal,
|
|
output_signal,
|
|
output_enable_signal,
|
|
interface,
|
|
peer,
|
|
bind,
|
|
)
|
|
|
|
|
|
def expand_group(group: Any, index: int) -> list[Pad]:
|
|
if not isinstance(group, dict):
|
|
raise ValueError(f"pads[{index}] must be an object")
|
|
defaults = {
|
|
key: value
|
|
for key, value in group.items()
|
|
if key not in {"ports", "count", "prefix", "suffix"}
|
|
}
|
|
ports = group.get("ports")
|
|
if ports is not None:
|
|
if not isinstance(ports, list) or not ports:
|
|
raise ValueError(f"pads[{index}].ports must be a non-empty list")
|
|
if not all(isinstance(port, dict) for port in ports):
|
|
raise ValueError(f"pads[{index}].ports entries must be objects")
|
|
return [parse_pad(port, defaults, None) for port in ports]
|
|
count = group.get("count")
|
|
if not isinstance(count, int) or count <= 0:
|
|
raise ValueError(f"pads[{index}].count must be a positive integer")
|
|
prefix = require_string(group.get("prefix"), f"pads[{index}].prefix")
|
|
suffix = require_string(group.get("suffix"), f"pads[{index}].suffix")
|
|
return [
|
|
parse_pad({"name": f"{prefix}{pad_index}{suffix}"}, defaults, pad_index)
|
|
for pad_index in range(count)
|
|
]
|
|
|
|
|
|
def validate_connection(value: Any, field: str) -> str | None:
|
|
if value is None:
|
|
return None
|
|
connection = require_string(value, field)
|
|
if CONNECTION_RE.fullmatch(connection) or CONSTANT_RE.fullmatch(connection):
|
|
return connection
|
|
raise ValueError(f"{field} must be a signal, bit-select, constant, or null")
|
|
|
|
|
|
def read_map(path: Path) -> tuple[list[Pad], dict[str, dict[str, str | None]]]:
|
|
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")
|
|
groups = document.get("pads")
|
|
if not isinstance(groups, list) or not groups:
|
|
raise ValueError("pads must be a non-empty list")
|
|
pads: list[Pad] = []
|
|
for index, group in enumerate(groups):
|
|
expanded = expand_group(group, index)
|
|
if len(expanded) == 0:
|
|
raise ValueError(f"pads[{index}] does not expand to any pads")
|
|
pads.extend(expanded)
|
|
names = [pad.name for pad in pads]
|
|
if len(names) != len(set(names)):
|
|
raise ValueError("pad names must be unique")
|
|
known_names = set(names)
|
|
for pad in pads:
|
|
if pad.peer is not None and pad.peer not in known_names:
|
|
raise ValueError(f"{pad.name}.peer is not a known pad")
|
|
profiles = document.get("profiles")
|
|
if not isinstance(profiles, dict) or set(profiles) != REQUIRED_PROFILES:
|
|
raise ValueError(f"profiles must contain exactly {sorted(REQUIRED_PROFILES)}")
|
|
normalized_profiles: dict[str, dict[str, str | None]] = {}
|
|
for profile, profile_data in profiles.items():
|
|
if not isinstance(profile_data, dict):
|
|
raise ValueError(f"profiles.{profile} must be an object")
|
|
bindings = profile_data.get("bindings", {})
|
|
if not isinstance(bindings, dict):
|
|
raise ValueError(f"profiles.{profile}.bindings must be an object")
|
|
if any(name not in known_names for name in bindings):
|
|
raise ValueError(f"profiles.{profile} contains an unknown pad")
|
|
normalized_profiles[profile] = {
|
|
name: validate_connection(value, f"profiles.{profile}.bindings.{name}")
|
|
for name, value in bindings.items()
|
|
}
|
|
return pads, normalized_profiles
|
|
|
|
|
|
def wrap_feature(pad: Pad, line: str) -> list[str]:
|
|
if pad.feature is None:
|
|
return [line]
|
|
return [f"`ifdef {pad.feature}", line, "`endif"]
|
|
|
|
|
|
def render_ports(pads: list[Pad]) -> str:
|
|
lines = ["// Generated by rtl/mini/pin_map/generate_pin_map.py; do not edit."]
|
|
for index, pad in enumerate(pads):
|
|
suffix = "," if index != len(pads) - 1 else ""
|
|
lines.extend(wrap_feature(pad, f" {pad.direction} {pad.name}{suffix}"))
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def render_pad_instance(pad: Pad) -> str | None:
|
|
instance = f"u_{pad.name}"
|
|
if pad.kind == "tri_input":
|
|
return (
|
|
f" tc_io_tri_pad {instance} (.pad({pad.name}), .c2p(1'b0), .c2p_en(1'b0), "
|
|
f".p2c({pad.signal}));"
|
|
)
|
|
if pad.kind == "schmitt_input":
|
|
return (
|
|
f" tc_io_tri_schmitt_pad {instance} (.pad({pad.name}), .c2p(1'b0), "
|
|
f".c2p_en(1'b0), .p2c({pad.signal}));"
|
|
)
|
|
if pad.kind == "tri_output":
|
|
return (
|
|
f" tc_io_tri_pad {instance} (.pad({pad.name}), .c2p({pad.signal}), .c2p_en(1'b1), "
|
|
".p2c());"
|
|
)
|
|
if pad.kind == "peripheral_output":
|
|
return f" tc_io_out_pad {instance} (.pad({pad.name}), .c2p({pad.signal}));"
|
|
if pad.kind == "peripheral_bidir":
|
|
return (
|
|
f" tc_io_tri_pad {instance} (.pad({pad.name}), .c2p({pad.output_signal}), "
|
|
f".c2p_en({pad.output_enable_signal}), .p2c({pad.input_signal}));"
|
|
)
|
|
if pad.kind == "full":
|
|
return (
|
|
f" tc_io_tri_full_pad {instance} (.pad({pad.name}), .c2p({pad.interface}.do_o[{pad.index}]), "
|
|
f".c2p_en({pad.interface}.oe_o[{pad.index}]), .p2c({pad.interface}.di_i[{pad.index}]), "
|
|
f".cs({pad.interface}.cs_o[{pad.index}]), .pu({pad.interface}.pu_o[{pad.index}]), "
|
|
f".pd({pad.interface}.pd_o[{pad.index}]));"
|
|
)
|
|
if pad.kind == "xpi_data":
|
|
return (
|
|
f" tc_io_tri_pad {instance} (.pad({pad.name}), .c2p(u_xpi_if.io_do_o[{pad.index}]), "
|
|
f".c2p_en(u_xpi_if.io_oe_o[{pad.index}]), .p2c(u_xpi_if.io_di_i[{pad.index}]));"
|
|
)
|
|
if pad.kind == "sdram_dq":
|
|
return (
|
|
f" tc_io_tri_pad {instance} (.pad({pad.name}), .c2p(u_sdram_if.dq_o[{pad.index}]), "
|
|
f".c2p_en(u_sdram_if.oe_o), .p2c(u_sdram_if.dq_i[{pad.index}]));"
|
|
)
|
|
if pad.kind == "xtal" and pad.bind:
|
|
return (
|
|
f" tc_io_xtl_pad u_xtal_io_pad (.xi_pad({pad.name}), .xo_pad({pad.peer}), "
|
|
f".en(1'b1), .clk({pad.signal}));"
|
|
)
|
|
return None
|
|
|
|
|
|
def render_pad_bindings(pads: list[Pad]) -> str:
|
|
lines = ["// Generated by rtl/mini/pin_map/generate_pin_map.py; do not edit."]
|
|
for pad in pads:
|
|
line = render_pad_instance(pad)
|
|
if line is not None:
|
|
lines.extend(wrap_feature(pad, line))
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def render_profile_bindings(pads: list[Pad], bindings: dict[str, str | None]) -> str:
|
|
lines = ["// Generated by rtl/mini/pin_map/generate_pin_map.py; do not edit."]
|
|
for index, pad in enumerate(pads):
|
|
connection = bindings.get(pad.name)
|
|
suffix = "," if index != len(pads) - 1 else ""
|
|
value = "" if connection is None else connection
|
|
lines.extend(wrap_feature(pad, f" .{pad.name}({value}){suffix}"))
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def generate(map_path: Path, output_dir: Path) -> None:
|
|
pads, profiles = read_map(map_path)
|
|
rtl_dir = output_dir / "rtl"
|
|
atomic_write(rtl_dir / "retrosoc_asic_ports.svh", render_ports(pads))
|
|
atomic_write(rtl_dir / "retrosoc_asic_pad_bindings.svh", render_pad_bindings(pads))
|
|
for profile, bindings in profiles.items():
|
|
atomic_write(
|
|
rtl_dir / f"retrosoc_asic_{profile}_bindings.svh",
|
|
render_profile_bindings(pads, bindings),
|
|
)
|
|
atomic_write(output_dir / "pin_map.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_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)
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
parser.error(str(error))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|