Merge branch 'feat/tiny' into dev

# Conflicts:
#	Makefile
#	README.md
#	publications/README.md
#	rtl/mk/software.mk
#	rtl/rtl_style_audit.json
This commit is contained in:
maksyuki committed 2026-09-27 09:09:53 +08:00
commit a45ebf9688
253 files changed
+15235 -501

No files matched your search

+5 -1
View File
@@ -66,7 +66,11 @@ def policy_issues(root: Path, path: Path, policy: dict[str, object]) -> list[str
text = path.read_text(encoding="utf-8")
issues: list[str] = []
if TINY_TOKEN.search(text):
# Product names in comments/string literals are prose, not retired SDK
# identifiers. Legacy include paths are still checked against the raw text.
code = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'',
" ", text, flags=re.DOTALL)
if TINY_TOKEN.search(code):
issues.append("contains a retired tiny-prefixed identifier or label")
if LEGACY_INCLUDE.search(text):
issues.append("contains a retired legacy include path")
+1
View File
@@ -17,6 +17,7 @@ SELF_OWNED_RTL_ROOTS = {
("rtl", "demo"),
("rtl", "ip"),
("rtl", "mini"),
("rtl", "tiny"),
("rtl", "tech"),
("tests", "rtl"),
}
+2 -2
View File
@@ -193,7 +193,7 @@ def rtl(variant: Path, output: Path, jobs: int, selected: str | None = None) ->
includes = [CRYPTO, COMMON, COMMON / "interface"]
if top == "crypto_dma_p0_tb":
generated = case / "memory_map"
execute(case, "memory-map", [sys.executable, str(ROOT / "rtl/mini/address_map/generate_memory_map.py"),
execute(case, "memory-map", [sys.executable, str(ROOT / "scripts/rtl/generate_memory_map.py"),
"--map", str(ROOT / "rtl/mini/address_map/memory_map.json"),
"--output-dir", str(generated), "--have-sram-if", "NO"])
includes += [generated / "rtl", COMMON / "stream", COMMON / "utils"]
@@ -214,7 +214,7 @@ def rtl(variant: Path, output: Path, jobs: int, selected: str | None = None) ->
fl.write_text("+define+SV_ASSRT_DISABLE\n" +
"\n".join([*[f"+incdir+{p}" for p in includes], *map(str, sources)]) + "\n")
converted = case / "test.v"
execute(case, "convert", [sys.executable, str(ROOT / "rtl/mini/script/convt_sv2v.py"),
execute(case, "convert", [sys.executable, str(ROOT / "scripts/rtl/convt_sv2v.py"),
"-f", str(fl), "--output", str(converted)])
binary = case / "test.vvp"
execute(case, "compile", ["iverilog", "-g2012", "-s", top,
+4
View File
@@ -23,6 +23,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--synth", required=True)
parser.add_argument("--sta", required=True)
parser.add_argument("--pdk", required=True)
parser.add_argument("--soc", choices=("MINI", "TINY"), default="MINI")
parser.add_argument("--have-sram-macro", choices=("YES", "NO"), default="NO")
parser.add_argument("--formal", choices=("YES", "NO"), default="NO")
parser.add_argument("--require-debug-tools", action="store_true")
@@ -73,6 +74,9 @@ def main() -> int:
tools.extend(("openocd", "riscv32-unknown-elf-gdb"))
source_names = ["mpw", "hazard3", "cluster_common", "third_party_ip"]
if args.soc == "TINY":
from scripts.setup_tiny import TINY_SOURCES
source_names = [name for name in TINY_SOURCES if not name.startswith("pdk_")]
pdk_names = {
"IHP130": "pdk_ihp130",
"ICS55": "pdk_ics55",
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Generate Tiny integration bindings and absolute source lists from owned maps."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.rtl.generate_memory_map import generate as generate_memory_map # noqa: E402
from scripts.rtl.generate_pin_map import generate as generate_pin_map # noqa: E402
from scripts.setup_helpers import atomic_write # noqa: E402
TINY = ROOT / "rtl/tiny"
def read_topology(path: Path, memory_map: Path) -> dict:
document = json.loads(path.read_text())
regions = {r["symbol"] for r in json.loads(memory_map.read_text())["regions"]}
if document.get("schema_version") != 1 or document.get("dma_channels") != 4:
raise ValueError("Tiny requires topology schema 1 and four DMA channels")
if document.get("irq_vector_width") != 32 or document.get("external_irq_count") != 30:
raise ValueError("Tiny requires a 32-bit IRQ vector and 30 external inputs")
names: set[str] = set()
bits: set[int] = set()
for irq in document["interrupts"]:
if irq["name"] in names or irq["core_bit"] in bits:
raise ValueError("duplicate Tiny interrupt")
if not 0 <= irq["core_bit"] < 32:
raise ValueError("Tiny IRQ outside vector")
names.add(irq["name"])
bits.add(irq["core_bit"])
assigned: set[str] = set()
for target in document["apb_targets"]:
for region in target["regions"]:
if region not in regions or region in assigned:
raise ValueError(f"invalid or duplicate APB region {region}")
assigned.add(region)
if assigned != {r for r in regions if r.startswith("APB4_")}:
raise ValueError("Tiny topology must route every APB region")
if len(document["apb_targets"]) != 16:
raise ValueError("Tiny's fixed APB bridge requires sixteen targets")
target_names = [target["name"] for target in document["apb_targets"]]
if len(set(target_names)) != 16 or any(re.fullmatch(r"[a-z][a-z0-9_]*", name) is None
for name in target_names):
raise ValueError("Tiny APB target names must be unique identifiers")
if sorted(item["pin"] for item in document["gpio_alt_functions"]) != list(range(32)):
raise ValueError("Tiny GPIO map must contain all 32 pins exactly once")
return document
def generate_bindings(document: dict, output: Path) -> None:
comment = "// Generated by scripts/generate_tiny.py; do not edit.\n"
irq = [comment, "always_comb begin", " s_irq = '0;"]
header = ["#ifndef RETROSOC_GENERATED_IRQ_METADATA_H",
"#define RETROSOC_GENERATED_IRQ_METADATA_H", "#include <stdint.h>",
"#define RS_SOC_IRQ_VECTOR_WIDTH UINT32_C(32)",
"#define RS_SOC_EXTERNAL_IRQ_COUNT UINT32_C(30)",
f"#define RS_SOC_ALLOCATED_IRQ_COUNT UINT32_C({len(document['interrupts']) - 2})"]
for item in document["interrupts"]:
bit, name = item["core_bit"], item["name"].upper()
irq.append(f" s_irq[{bit}] = {item['signal']};")
header.append(f"#define RS_SOC_IRQ_{name} UINT32_C({bit})")
if bit >= 2:
header.append(f"#define RS_SOC_EXT_IRQ_{name} UINT32_C({bit - 2})")
irq.append("end\n")
header.append("#endif\n")
atomic_write(output / "rtl/tiny_irq.svh", "\n".join(irq))
atomic_write(output / "include/retrosoc/generated/irq_metadata.h", "\n".join(header))
atomic_write(output / "include/retrosoc/generated/user_extensions.h",
"#ifndef RETROSOC_GENERATED_USER_EXTENSIONS_H\n"
"#define RETROSOC_GENERATED_USER_EXTENSIONS_H\n"
"#define RS_SOC_USER_CORE_COUNT 0U\n#define RS_SOC_USER_IP_COUNT 0U\n"
"#define RS_SOC_HAS_USER_CORES 0U\n#define RS_SOC_HAS_USER_IP_MUX 0U\n#endif\n")
atomic_write(output / "include/retrosoc/generated/soc_features.h",
"#ifndef RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RS_SOC_TINY 1\n#define RS_SOC_HAS_PSRAM 0U\n"
"#define RS_SOC_DMA_CHANNELS 4U\n#define RS_SOC_ID 0x54494E59U\n#endif\n")
gpio = [comment]
for function in document["gpio_alt_functions"]:
pin = function["pin"]
for mode in ("alt0", "alt1"):
value = function[mode]
gpio.extend(f"assign {signal} = u_gpio_if.di_i[{pin}];" for signal in value["inputs"])
gpio.extend((f"assign u_gpio_if.{mode}_do_i[{pin}] = {value['do']};",
f"assign u_gpio_if.{mode}_oe_i[{pin}] = {value['oe']};"))
atomic_write(output / "rtl/tiny_gpio.svh", "\n".join(gpio) + "\n")
ports, declarations, routes, decode = [], [comment], [comment], [comment]
for index, target in enumerate(document["apb_targets"]):
name = target["name"]
ports.append(f"apb4_if.master {name}")
declarations.append(f"apb4_if u_{name}_apb4_if (.pclk(clk_i), .presetn(s_rst_n));")
routes.append(f".{name}(u_{name}_apb4_if)")
decode.append(f"if ({' || '.join(f'`SOC_ADDR_IS_{r}(s_addr_q)' for r in target['regions'])}) "
f"s_select = {len(document['apb_targets'])}'b{1 << index:016b};")
atomic_write(output / "rtl/tiny_apb_ports.svh", ",\n".join(ports) + "\n")
atomic_write(output / "rtl/tiny_apb_interfaces.svh", "\n".join(declarations) + "\n")
atomic_write(output / "rtl/tiny_apb_connections.svh", ",\n".join(routes[1:]) + "\n")
atomic_write(output / "rtl/tiny_apb_decode.svh", "\n".join(decode) + "\n")
wiring = [comment]
for index, target in enumerate(document["apb_targets"]):
name = target["name"]
for field, value in {"paddr": "s_addr_q", "pwrite": "s_write_q",
"pwdata": "s_wdata_q", "pstrb": "s_wstrb_q", "pprot": "3'd0",
"psel": f"s_select[{index}] && s_access",
"penable": "s_enable"}.items():
wiring.append(f"assign {name}.{field} = {value};")
wiring.extend((f"assign s_ready[{index}] = {name}.pready;",
f"assign s_error[{index}] = {name}.pslverr;",
f"assign s_rdata[{index}] = {name}.prdata;"))
atomic_write(output / "rtl/tiny_apb_wiring.svh", "\n".join(wiring) + "\n")
def generate_filelists(output: Path, generated: Path, defines: list[str]) -> None:
prefixes = {"top": TINY / "top", "dv": TINY / "dv", "ip": ROOT / "rtl/ip",
"tech": ROOT / "rtl/tech", "clusterip": ROOT / "rtl/managed/clusterip",
"hazard3": ROOT / "rtl/managed/hazard3/hdl", "physical": ROOT / "physical"}
templates = [*sorted((TINY / "filelist").glob("*.fl")),
ROOT / "rtl/filelist/pdk_ihp130.fl"]
for template in templates:
content = template.read_text()
for prefix, directory in prefixes.items():
content = re.sub(rf"(?<![^\s+])/{prefix}(?=/|\s|$)", str(directory), content)
atomic_write(output / template.name, content)
include = [generated / "rtl", generated / "memory_map/rtl", generated / "pin_map/rtl",
generated / "archinfo"]
atomic_write(output / "def.fl", " ".join([*defines, *(f"+incdir+{p}" for p in include)]) + "\n")
atomic_write(output / "pdk_local.fl", "")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path)
parser.add_argument("--filelists", type=Path)
parser.add_argument("--define", action="append", default=[])
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
document = read_topology(TINY / "integration/soc_topology.json",
TINY / "address_map/memory_map.json")
if args.check:
return 0
if args.output is None or args.filelists is None:
parser.error("--output and --filelists are required unless --check is used")
if any(not value.startswith("+define+") for value in args.define):
parser.error("--define requires a +define+ token")
output = args.output.resolve()
generate_memory_map(TINY / "address_map/memory_map.json", output / "memory_map", "YES", 128)
generate_pin_map(TINY / "pin_map/pin_map.json", output / "pin_map")
generate_bindings(document, output)
generate_filelists(args.filelists.resolve(), output, args.define)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+12 -4
View File
@@ -17,7 +17,7 @@ from scripts.setup_helpers import atomic_write # noqa: E402
AREA_RE = re.compile(r"Chip area for top module[^:]*:\s*([0-9.eE+-]+)")
HIERARCHY_RE = re.compile(r"^\s*(\d+)\s+[0-9.eE+-]+\s+retrosoc_asic\s*$", re.MULTILINE)
HIERARCHY_RE = re.compile(r"^\s*(\d+)\s+[0-9.eE+-]+\s+retrosoc_(?:tiny_)?asic\s*$", re.MULTILINE)
def collect(args: argparse.Namespace) -> int:
@@ -35,10 +35,18 @@ def collect(args: argparse.Namespace) -> int:
for binary in sorted((root / "sw").glob("*.bin")):
metrics["firmware"][binary.name] = {"bytes": binary.stat().st_size}
area_json = synth_root / "rpt/retrosoc_asic_area.json"
area_report = synth_root / "rpt/retrosoc_asic_area.rpt"
manifest_path = root / "meta/manifest.json"
soc = "MINI"
if manifest_path.is_file():
soc = json.loads(manifest_path.read_text())["configuration"].get("SOC", "MINI")
top = "retrosoc_tiny_asic" if soc == "TINY" else "retrosoc_asic"
area_json = synth_root / "rpt" / f"{top}_area.json"
area_report = synth_root / "rpt" / f"{top}_area.rpt"
if area_json.is_file():
design = json.loads(area_json.read_text(encoding="utf-8"))["design"]
# Yosys -t also prefixes tee report lines in some locked tool builds.
contents = re.sub(r"^\[\d+\.\d+\]\s?", "", area_json.read_text(encoding="utf-8"),
flags=re.MULTILINE)
design = json.loads(contents)["design"]
metrics["synthesis"]["top_area"] = float(design["area"])
metrics["synthesis"]["top_cells"] = int(design["num_cells"])
elif area_report.is_file():
+17 -8
View File
@@ -93,6 +93,8 @@ def main() -> int:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
config = manifest["configuration"]
lock = load_lock(args.lock)
soc = config.get("SOC", "MINI")
product_root = root / "rtl" / soc.lower()
args.output_dir.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(
@@ -100,7 +102,9 @@ def main() -> int:
) as temp:
staging = Path(temp) / "package"
staging.mkdir()
timing_contract = Path(temp) / "commercial_timing_contract.tcl"
timing_contract = Path(temp) / ("tiny_core.sdc" if soc == "TINY" else "commercial_timing_contract.tcl")
timing_generator = (root / "physical/smoke/sta/opensta/generate_sdc.py" if soc == "TINY"
else root / "physical/commercial/scripts/generate_timing_contract.py")
if config.get("MINI_MODE", "PRODUCT") == "MPW":
dynamic_core_filelist = (
args.variant_root
@@ -124,14 +128,11 @@ def main() -> int:
subprocess.run(
[
"python3",
str(
root
/ "physical/commercial/scripts/generate_timing_contract.py"
),
str(timing_generator),
"--domains",
str(root / "rtl/mini/integration/clock_reset_domains.json"),
str(product_root / "integration/clock_reset_domains.json"),
"--pin-map",
str(root / "rtl/mini/pin_map/pin_map.json"),
str(product_root / "pin_map/pin_map.json"),
"--output",
str(timing_contract),
],
@@ -171,8 +172,16 @@ def main() -> int:
"--archinfo-incdir",
str(args.variant_root / "generated/archinfo"),
"--metadata-file",
"contracts/commercial_timing_contract.tcl={0}".format(timing_contract),
f"contracts/{timing_contract.name}={timing_contract}",
]
if soc == "TINY":
if config.get("SYNTH") == "YOSYS":
recipe = config.get("SYNTH_RECIPE", "balanced")
folder = "yosys" if recipe == "balanced" else f"yosys-{recipe}"
source_dir = args.variant_root / "syn" / folder / "filelists"
else:
source_dir = args.variant_root / "sim" / config["SIMU"].lower() / "filelists"
command.extend(("--soc", "TINY", "--source-filelist-dir", str(source_dir)))
for key, flag in (
("HAVE_PLL", "--have-pll"),
("HAVE_SRAM_IF", "--have-sram-if"),
+24 -3
View File
@@ -70,7 +70,16 @@ PR_COMMANDS = (
),
("configs/ci/ihp130.mk", ("STA=OPENSTA", "sta")),
)
RTL_COMMANDS = PR_COMMANDS[:6]
TINY_PR_COMMANDS = (
("configs/ci/ihp130-tiny.mk", RTL_LINT_VALUES),
("configs/ci/ihp130-tiny.mk", ("APP=ci_smoke", "LINK_TYPE=ld2_all_sram", "SIMU=VERILATOR", "SOC_SIM_TIME=1800", "HAVE_CSR=YES", "HAVE_SVA=YES", "firmware", "sim")),
("configs/ci/ihp130-tiny.mk", ("APP=ci_smoke", "SIMU=IVERILOG", "firmware", "sim")),
("configs/ci/ihp130-tiny.mk", ("APP=ci_smoke", "SYNTH=YOSYS", "synth")),
("configs/ci/ihp130-tiny.mk", ("APP=ci_smoke", "SIMU=IVERILOG", "netsim-boot")),
("configs/ci/ihp130-tiny.mk", ("APP=ci_smoke", "STA=OPENSTA", "sta")),
)
RTL_COMMANDS = (*PR_COMMANDS[:6], *TINY_PR_COMMANDS[:3])
PR_COMMANDS = (*PR_COMMANDS, *TINY_PR_COMMANDS)
SMOKE_COMMANDS = (
("configs/ci/ihp130.mk", RTL_LINT_VALUES),
("configs/ci/ihp130.mk", (CI_SMOKE_APP_VALUE, "firmware")),
@@ -106,7 +115,7 @@ NIGHTLY_COMMANDS = (
*PR_COMMANDS,
*NIGHTLY_EXTRA_COMMANDS,
)
PR_PROFILES = ("configs/ci/ihp130.mk",)
PR_PROFILES = ("configs/ci/ihp130.mk", "configs/ci/ihp130-tiny.mk")
SMOKE_PROFILES: tuple[str, ...] = ()
NIGHTLY_PROFILES = PR_PROFILES
@@ -161,7 +170,7 @@ def select_regression(
if suite == "nightly-extra":
if pdk is not None and pdk != "IHP130":
raise ValueError("nightly-extra regression supports only --pdk IHP130")
return NIGHTLY_EXTRA_COMMANDS, NIGHTLY_PROFILES
return NIGHTLY_EXTRA_COMMANDS, ("configs/ci/ihp130.mk",)
if pdk:
if suite == "nightly":
if pdk != "IHP130":
@@ -259,6 +268,7 @@ def main() -> int:
"--suite", choices=("smoke", "rtl", "pr", "nightly", "nightly-extra"), required=True
)
parser.add_argument("--pdk", choices=tuple(PDK_PR_PROFILES), help="run one PDK matrix")
parser.add_argument("--soc", choices=("MINI", "TINY"), help="run one product only")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--netsim-boot-only",
@@ -275,6 +285,17 @@ def main() -> int:
commands, profiles = select_regression(args.suite, args.pdk)
except ValueError as error:
parser.error(str(error))
if args.soc is not None:
tiny_profile = "configs/ci/ihp130-tiny.mk"
if args.soc == "TINY" and args.suite == "smoke":
commands, profiles = TINY_PR_COMMANDS[:2], (tiny_profile,)
else:
commands = tuple((profile, values) for profile, values in commands
if (profile == tiny_profile) == (args.soc == "TINY"))
profiles = tuple(profile for profile in profiles
if (profile == tiny_profile) == (args.soc == "TINY"))
if not commands:
parser.error(f"suite {args.suite} has no {args.soc} configuration for this PDK")
if args.netsim_boot_only:
commands = with_netsim_boot_only(commands)
if args.behavioral_only:
+10
View File
@@ -0,0 +1,10 @@
# Shared RTL Flow Helpers
These product-independent helpers parse filelists, convert SystemVerilog,
generate address/pad artifacts, and prepare simulation inputs. Both products
invoke them here directly, with Python imports through `scripts.rtl`. Product
inputs stay under their owning `rtl/<product>` directory; outputs stay below
`build/`.
Changes require focused generator tests, Ruff and Pytest, plus compilation of
each affected product. `generate_tiny.py` owns Tiny-only integration bindings.
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from scripts.rtl.filelist import parse_filelists, write_filelist # noqa: E402
DEFAULT_OUTPUT = Path(__file__).resolve().parent.parent / ".generated_fl" / "yosys.fl"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Combine synthesis filelists")
parser.add_argument("-f", "--filelist", action="append", type=Path, default=[])
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
return parser.parse_args()
def main() -> int:
args = parse_args()
selected = [path for path in args.filelist if not path.name.startswith("pdk_")]
if not selected:
raise SystemExit("at least one non-PDK -f/--filelist is required")
combined = parse_filelists(selected)
write_filelist(args.output.resolve(), combined)
print(f"generated synthesis filelist: {args.output.resolve()}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from scripts.rtl.filelist import parse_filelists # noqa: E402
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Convert the behavioral RTL with sv2v")
parser.add_argument("-f", "--filelist", action="append", type=Path, default=[])
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--sv2v", default="sv2v", help="sv2v executable")
return parser.parse_args()
def main() -> int:
args = parse_args()
selected = [path for path in args.filelist if not path.name.startswith("pdk_")]
if not selected:
raise SystemExit("at least one non-PDK -f/--filelist is required")
filelist = parse_filelists(selected)
args.output.parent.mkdir(parents=True, exist_ok=True)
command = [args.sv2v]
command.extend(f"-D{item[len('+define+'):]}" for item in filelist.defines)
command.extend(f"-I{path}" for path in filelist.incdirs)
command.extend(str(path) for path in filelist.files)
command.extend(("--write", str(args.output.resolve())))
subprocess.run(command, check=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import shlex
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable
@dataclass
class FileList:
defines: list[str] = field(default_factory=list)
incdirs: list[Path] = field(default_factory=list)
files: list[Path] = field(default_factory=list)
library_files: list[Path] = field(default_factory=list)
options: list[str] = field(default_factory=list)
def extend(self, other: "FileList") -> None:
self.defines.extend(other.defines)
self.incdirs.extend(other.incdirs)
self.files.extend(other.files)
self.library_files.extend(other.library_files)
self.options.extend(other.options)
def deduplicate(self) -> "FileList":
self.defines = _unique(self.defines)
self.incdirs = _unique(self.incdirs)
self.files = _unique(self.files)
self.library_files = _unique(self.library_files)
self.options = _unique(self.options)
return self
def as_tokens(self) -> list[str]:
return [
*self.defines,
*(_quote(f"+incdir+{path}") for path in self.incdirs),
*(item for path in self.library_files for item in ("-v", _quote(str(path)))),
*self.options,
*(_quote(str(path)) for path in self.files),
]
def _unique(values: Iterable):
return list(dict.fromkeys(values))
def _quote(value: str) -> str:
if not any(character.isspace() for character in value):
return value
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def atomic_write(path: Path, content: str) -> bool:
"""Write content atomically and preserve mtime when content is unchanged."""
path = Path(path)
if path.exists() and path.read_text(encoding="utf-8") == content:
return False
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="") as stream:
stream.write(content)
os.replace(temp_name, path)
except Exception:
Path(temp_name).unlink(missing_ok=True)
raise
return True
def tokenize_filelist(path: Path) -> list[str]:
tokens: list[str] = []
for line_number, raw_line in enumerate(
path.read_text(encoding="utf-8").splitlines(), start=1
):
stripped = raw_line.strip()
if not stripped or stripped.startswith("//") or stripped.startswith("#"):
continue
try:
lexer = shlex.shlex(raw_line, posix=True)
lexer.whitespace_split = True
lexer.quotes = '"'
tokens.extend(lexer)
except ValueError as error:
raise ValueError(f"{path}:{line_number}: {error}") from error
return tokens
def parse_filelists(paths: Iterable[Path], *, require_files: bool = True) -> FileList:
result = FileList()
visited: set[Path] = set()
for path in paths:
result.extend(
_parse_filelist(Path(path).resolve(), visited, require_files=require_files)
)
return result.deduplicate()
def _parse_filelist(
path: Path, visited: set[Path], *, require_files: bool
) -> FileList:
if path in visited:
return FileList()
if not path.is_file():
raise FileNotFoundError(f"filelist not found: {path}")
visited.add(path)
result = FileList()
tokens = tokenize_filelist(path)
index = 0
while index < len(tokens):
token = tokens[index]
if token == "-f":
index += 1
if index >= len(tokens):
raise ValueError(f"{path}: -f requires a filelist path")
nested = _resolve(tokens[index], path.parent)
result.extend(
_parse_filelist(nested, visited, require_files=require_files)
)
elif token.startswith("-f") and len(token) > 2:
nested = _resolve(token[2:], path.parent)
result.extend(
_parse_filelist(nested, visited, require_files=require_files)
)
elif token.startswith("+define+"):
result.defines.append(token)
elif token.startswith("+incdir+"):
incdir = _resolve(token[len("+incdir+") :], path.parent)
if require_files and not incdir.is_dir():
raise FileNotFoundError(f"include directory not found: {incdir}")
result.incdirs.append(incdir)
elif token == "-v":
index += 1
if index >= len(tokens):
raise ValueError(f"{path}: -v requires a source path")
library_file = _resolve(tokens[index], path.parent)
_require_source(library_file, require_files)
result.library_files.append(library_file)
elif token.startswith("-") or token.startswith("+"):
result.options.append(token)
else:
source = _resolve(token, path.parent)
_require_source(source, require_files)
result.files.append(source)
index += 1
return result
def _resolve(value: str, base: Path) -> Path:
path = Path(value).expanduser()
return path.resolve() if path.is_absolute() else (base / path).resolve()
def _require_source(path: Path, required: bool) -> None:
if required and not path.is_file():
raise FileNotFoundError(f"source file not found: {path}")
def write_filelist(path: Path, filelist: FileList) -> bool:
content = "\n".join(filelist.as_tokens()) + "\n"
return atomic_write(path, content)
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from scripts.rtl.filelist import atomic_write, parse_filelists # noqa: E402
HEADER_SUFFIXES = {".h", ".vh", ".svh"}
def make_escape(path: Path) -> str:
return (
str(path.resolve())
.replace("\\", "\\\\")
.replace("$", "$$")
.replace(" ", "\\ ")
.replace("#", "\\#")
)
def main() -> int:
parser = argparse.ArgumentParser(description="Generate Make dependencies from RTL filelists")
parser.add_argument("-f", "--filelist", type=Path, action="append", default=[])
parser.add_argument("--extra", type=Path, action="append", default=[])
parser.add_argument("--target", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if not args.filelist:
parser.error("at least one -f/--filelist is required")
parsed = parse_filelists(args.filelist)
dependencies = {
*(path.resolve() for path in args.filelist),
*(path.resolve() for path in parsed.files),
*(path.resolve() for path in parsed.library_files),
*(path.resolve() for path in args.extra),
}
for directory in parsed.incdirs:
if directory.is_dir():
dependencies.update(
path.resolve()
for path in directory.rglob("*")
if path.is_file() and path.suffix.lower() in HEADER_SUFFIXES
)
content = f"{make_escape(args.target)}:"
if dependencies:
content += " \\\n " + " \\\n ".join(make_escape(path) for path in sorted(dependencies))
content += "\n"
atomic_write(args.output, content)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+346
View File
@@ -0,0 +1,346 @@
#!/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 = {"apb4_periph", "apb4_system", "axi4", "ram", "reserved"}
VALID_KINDS = {"active", "reserved"}
VALID_USER_ACCESS = {"none", "ro", "rw"}
VALID_BURST = {"incr1", "incr4", "incr16"}
VALID_SRAM_SIZE_KIB = (4, 16, 32, 64, 128)
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, sram_size_kib: int = 128
) -> tuple[int, list[dict[str, Any]], list[dict[str, Any]]]:
if sram_size_kib not in VALID_SRAM_SIZE_KIB:
raise ValueError(f"sram_size_kib must be one of {VALID_SRAM_SIZE_KIB}")
with path.open(encoding="utf-8") as source:
document = json.load(source)
if document.get("schema_version") != 2:
raise ValueError("schema_version must be 2")
cpu_reset = parse_integer(document.get("cpu_reset"), "cpu_reset")
registers = document.get("sysctrl_registers")
if not isinstance(registers, list) or not registers:
raise ValueError("sysctrl_registers must be a non-empty list")
normalized_registers: list[dict[str, Any]] = []
register_symbols: set[str] = set()
register_offsets: set[int] = set()
for index, register in enumerate(registers):
if not isinstance(register, dict):
raise ValueError(f"sysctrl_registers[{index}] must be an object")
symbol = register.get("symbol")
if not isinstance(symbol, str) or not symbol or not symbol.replace("_", "").isalnum():
raise ValueError(f"sysctrl_registers[{index}].symbol must be an uppercase identifier")
if symbol != symbol.upper() or symbol in register_symbols:
raise ValueError(f"sysctrl_registers[{index}].symbol must be unique and uppercase")
offset = parse_integer(register.get("offset"), f"sysctrl_registers[{index}].offset")
if offset < 0 or offset > 0xFFC or offset % 4 != 0 or offset in register_offsets:
raise ValueError(f"sysctrl_registers[{index}].offset must be unique and 32-bit aligned")
register_symbols.add(symbol)
register_offsets.add(offset)
normalized_registers.append({"symbol": symbol, "offset": offset})
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")
user_access = region.get("user_access")
if user_access not in VALID_USER_ACCESS:
raise ValueError(
f"regions[{index}].user_access must be one of {sorted(VALID_USER_ACCESS)}"
)
if kind == "reserved" and user_access != "none":
raise ValueError(f"regions[{index}] reserved regions must deny user access")
burst = region.get("burst", "incr1")
if burst not in VALID_BURST:
raise ValueError(f"regions[{index}].burst must be one of {sorted(VALID_BURST)}")
if kind == "reserved" and burst != "incr1":
raise ValueError(f"regions[{index}] reserved regions cannot support bursts")
base = parse_integer(region.get("base"), f"regions[{index}].base")
size = parse_integer(region.get("size"), f"regions[{index}].size")
if symbol == "SRAM":
size_options = region.get("size_options")
if not isinstance(size_options, list):
raise ValueError(f"regions[{index}].size_options must be a list")
parsed_options = tuple(
parse_integer(value, f"regions[{index}].size_options") for value in size_options
)
expected_options = tuple(value * 1024 for value in VALID_SRAM_SIZE_KIB)
if parsed_options != expected_options:
raise ValueError(f"regions[{index}].size_options must be {expected_options}")
size = sram_size_kib * 1024
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"),
"user_access": user_access,
"burst": burst,
}
)
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, normalized_registers
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, base: int, end: int) -> str:
conditions: list[str] = []
if base != 0:
conditions.append(f"(addr) >= `SOC_ADDR_{symbol}_BASE")
if end != MAX_ADDRESS - 1:
conditions.append(f"(addr) <= `SOC_ADDR_{symbol}_END")
return "(" + " && ".join(conditions or ["1'b1"]) + ")"
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]], sysctrl_registers: list[dict[str, Any]], legacy_rib: bool = True
) -> str:
lines = [
"// Generated by scripts/rtl/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}_SIZE {hex32(region['size'])}",
f"`define SOC_ADDR_{symbol}_END {hex32(region['end'])}",
f"`define SOC_ADDR_IS_{symbol}(addr) "
f"{range_expression(symbol, region['base'], region['end'])}",
]
lines.append("")
for route in ("apb4_periph", "apb4_system", "axi4", "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)})")
legacy_rib_targets = [
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
for region in regions
if region["route"] in {"apb4_periph", "axi4"}
]
if legacy_rib:
lines.append(f"`define SOC_ADDR_IS_RIB_LEGACY_TARGET(addr) ({join_or(legacy_rib_targets)})")
user_readable = [
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
for region in regions
if region["user_access"] in {"ro", "rw"}
]
user_writable = [
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
for region in regions
if region["user_access"] == "rw"
]
lines += [
"",
f"`define SOC_USER_ADDR_READABLE(addr) ({join_or(user_readable)})",
f"`define SOC_USER_ADDR_WRITABLE(addr) ({join_or(user_writable)})",
]
incr4_regions = [
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
for region in regions
if region["burst"] in {"incr4", "incr16"}
]
lines.append(f"`define SOC_ADDR_SUPPORTS_INCR4(addr) ({join_or(incr4_regions)})")
incr16_regions = [
f"`SOC_ADDR_IS_{region['symbol']}(addr)"
for region in regions
if region["burst"] == "incr16"
]
lines.append(f"`define SOC_ADDR_SUPPORTS_INCR16(addr) ({join_or(incr16_regions)})")
lines.append("")
for register in sysctrl_registers:
macro = f"SOC_SYSCTRL_{register['symbol']}_OFFSET"
lines.append(f"`define {macro:<31} {hex32(register['offset'])}")
lines += ["", "`endif", ""]
return "\n".join(lines)
def render_c(
cpu_reset: int,
regions: list[dict[str, Any]],
sysctrl_registers: list[dict[str, Any]],
have_sram_if: str,
capability_header: str | None = None,
) -> str:
lines = [
"/* Generated by scripts/rtl/generate_memory_map.py; do not edit. */",
"#ifndef RETROSOC_GENERATED_MEMORY_MAP_H",
"#define RETROSOC_GENERATED_MEMORY_MAP_H",
"",
"#ifdef __ASSEMBLER__",
"#define UINT32_C(value) value",
"#else",
"#include <stdint.h>",
"#endif",
"",
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.append("")
lines.extend(
f"#define RS_SOC_SYSCTRL_{register['symbol']}_OFFSET {c_hex32(register['offset'])}"
for register in sysctrl_registers
)
if capability_header is not None:
lines.append(f"#include <{capability_header}>")
lines += ["", "#endif", ""]
return "\n".join(lines)
def render_linker(regions: list[dict[str, Any]]) -> str:
lines = [
"/* Generated by scripts/rtl/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, sram_size_kib: int) -> None:
cpu_reset, regions, sysctrl_registers = read_map(map_path, sram_size_kib)
atomic_write(
output_dir / "rtl" / "mmap_define.svh",
render_rtl(cpu_reset, regions, sysctrl_registers,
json.loads(map_path.read_text()).get("legacy_rib", True)),
)
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, regions, sysctrl_registers, have_sram_if,
json.loads(map_path.read_text()).get("capability_header")),
)
atomic_write(output_dir / "linker" / "memory_regions.ld", render_linker(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("--sram-size-kib", type=int, choices=VALID_SRAM_SIZE_KIB, default=128)
parser.add_argument("--check", action="store_true", help="validate only; write nothing")
arguments = parser.parse_args()
try:
if arguments.check:
read_map(arguments.map, arguments.sram_size_kib)
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.sram_size_kib,
)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+501
View File
@@ -0,0 +1,501 @@
#!/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]+$")
IHP130_POWER_PAD_COUNTS = {"vdd": 24, "vss": 24, "iovdd": 16, "iovss": 16}
POWER_PAD_KINDS = tuple(IHP130_POWER_PAD_COUNTS)
POWER_CONNECTIONS = "`RETROSOC_PAD_POWER_CONNECTIONS"
@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_document(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as source:
return json.load(source)
def parse_power_pads(document: dict[str, Any]) -> dict[str, int]:
counts = document.get("power_pads")
if counts is None:
return dict(IHP130_POWER_PAD_COUNTS)
if not isinstance(counts, dict) or set(counts) != set(POWER_PAD_KINDS):
raise ValueError(f"power_pads must contain exactly {list(POWER_PAD_KINDS)}")
parsed: dict[str, int] = {}
for kind in POWER_PAD_KINDS:
value = counts[kind]
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
raise ValueError(f"power_pads.{kind} must be a positive integer")
parsed[kind] = value
return parsed
def read_power_pad_counts(path: Path) -> dict[str, int]:
return parse_power_pads(read_document(path))
def read_map(path: Path) -> tuple[list[Pad], dict[str, dict[str, str | None]]]:
document = read_document(path)
parse_power_pads(document)
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 scripts/rtl/generate_pin_map.py; do not edit.",
"`ifdef PDK_IHP130",
"`ifdef USE_POWER_PINS",
" inout wire IOVDD,",
" inout wire IOVSS,",
" inout wire VDD,",
" inout wire VSS,",
"`endif",
"`endif",
]
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_core_ports(pads: list[Pad]) -> str:
"""Render the logical-chip port list without technology power pins."""
lines = ["// Generated by scripts/rtl/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_in_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), .p2c({pad.signal}));"
)
if pad.kind == "schmitt_input":
return (
f" tc_io_schmitt_in_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
f".p2c({pad.signal}));"
)
if pad.kind == "tri_output":
return (
f" tc_io_out_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), .c2p({pad.signal}));"
)
if pad.kind == "peripheral_output":
return (
f" tc_io_out_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), .c2p({pad.signal}));"
)
if pad.kind == "peripheral_bidir":
return (
f" tc_io_tri_pad {instance} ({POWER_CONNECTIONS}.pad({pad.name}), "
f".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} ({POWER_CONNECTIONS}.pad({pad.name}), "
f".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} ({POWER_CONNECTIONS}.pad({pad.name}), "
f".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} ({POWER_CONNECTIONS}.pad({pad.name}), "
f".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 ({POWER_CONNECTIONS}.xi_pad({pad.name}), "
f".xo_pad({pad.peer}), .en(1'b1), .clk({pad.signal}));"
)
return None
def render_pad_bindings(
pads: list[Pad], power_counts: dict[str, int] | None = None
) -> str:
counts = IHP130_POWER_PAD_COUNTS if power_counts is None else power_counts
lines = [
"// Generated by scripts/rtl/generate_pin_map.py; do not edit.",
"`ifdef PDK_IHP130",
"`ifdef USE_POWER_PINS",
"`define RETROSOC_PAD_POWER_CONNECTIONS .iovdd(IOVDD), .iovss(IOVSS), .vdd(VDD), .vss(VSS),",
"`else",
"`define RETROSOC_PAD_POWER_CONNECTIONS",
"`endif",
" generate",
]
cell_suffixes = {"vdd": "Vdd", "vss": "Vss", "iovdd": "IOVdd", "iovss": "IOVss"}
for kind, count in counts.items():
lines.extend(
[
f" for (genvar i = 0; i < {count}; i++) begin : {kind}_pads",
' (* keep *) (* dont_touch = "true" *)',
f" sg13g2_IOPad{cell_suffixes[kind]} {kind}_pad (",
" );",
" end",
]
)
lines.extend(
[
" endgenerate",
"`else",
"`define RETROSOC_PAD_POWER_CONNECTIONS",
"`endif",
]
)
for pad in pads:
line = render_pad_instance(pad)
if line is not None:
lines.extend(wrap_feature(pad, line))
lines.append("`undef RETROSOC_PAD_POWER_CONNECTIONS")
return "\n".join(lines) + "\n"
def render_core_binding(pad: Pad) -> str | None:
"""Bind a package-facing core port directly to the logical SoC interface."""
if pad.kind in {"tri_input", "schmitt_input"}:
assert pad.signal is not None
return f" assign {pad.signal} = {pad.name};"
if pad.kind in {"tri_output", "peripheral_output"}:
assert pad.signal is not None
return f" assign {pad.name} = {pad.signal};"
if pad.kind == "peripheral_bidir":
assert pad.input_signal is not None
assert pad.output_signal is not None
assert pad.output_enable_signal is not None
return "\n".join(
[
f" assign {pad.name} = {pad.output_enable_signal} ? {pad.output_signal} : 1'bz;",
f" assign {pad.input_signal} = {pad.name};",
]
)
if pad.kind == "full":
assert pad.interface is not None
assert pad.index is not None
return "\n".join(
[
f" assign {pad.name} = {pad.interface}.oe_o[{pad.index}] ? "
f"{pad.interface}.do_o[{pad.index}] : 1'bz;",
f" assign {pad.interface}.di_i[{pad.index}] = {pad.name};",
]
)
if pad.kind == "xpi_data":
assert pad.index is not None
return "\n".join(
[
f" assign {pad.name} = u_xpi_if.io_oe_o[{pad.index}] ? "
f"u_xpi_if.io_do_o[{pad.index}] : 1'bz;",
f" assign u_xpi_if.io_di_i[{pad.index}] = {pad.name};",
]
)
if pad.kind == "sdram_dq":
assert pad.index is not None
return "\n".join(
[
f" assign {pad.name} = u_sdram_if.oe_o ? u_sdram_if.dq_o[{pad.index}] : 1'bz;",
f" assign u_sdram_if.dq_i[{pad.index}] = {pad.name};",
]
)
if pad.kind == "xtal" and pad.bind:
assert pad.signal is not None
assert pad.peer is not None
return "\n".join(
[
f" assign {pad.signal} = {pad.name};",
f" assign {pad.peer} = 1'b0;",
]
)
return None
def render_core_bindings(pads: list[Pad]) -> str:
lines = ["// Generated by scripts/rtl/generate_pin_map.py; do not edit."]
for pad in pads:
binding = render_core_binding(pad)
if binding is not None:
lines.extend(wrap_feature(pad, binding))
return "\n".join(lines) + "\n"
def ihp130_pad_cell(pad: Pad) -> str | None:
if pad.kind in {"tri_input", "schmitt_input"}:
return "sg13g2_IOPadIn"
if pad.kind in {"tri_output", "peripheral_output"}:
return "sg13g2_IOPadOut4mA"
if pad.kind in {"full", "peripheral_bidir", "sdram_dq", "xpi_data"}:
return "sg13g2_IOPadInOut4mA"
if pad.kind == "xtal" and pad.bind:
return "sg13g2_IOPadIn"
return None
def ihp130_pad_instance(pad: Pad) -> str | None:
cell = ihp130_pad_cell(pad)
if cell is None:
return None
outer = "u_xtal_io_pad" if pad.kind == "xtal" else f"u_{pad.name}"
return f"{outer}.u_{cell}"
def render_profile_bindings(pads: list[Pad], bindings: dict[str, str | None]) -> str:
lines = ["// Generated by scripts/rtl/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)
power_counts = read_power_pad_counts(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, power_counts)
)
atomic_write(rtl_dir / "retrosoc_core_ports.svh", render_core_ports(pads))
atomic_write(rtl_dir / "retrosoc_core_bindings.svh", render_core_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())
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import shutil
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
MINI_DIR = SCRIPT_DIR.parent
ROOT_DIR = SCRIPT_DIR.parents[1]
DEFAULT_MODELS = ROOT_DIR / "rtl" / "managed" / "third_party" / "norflash"
DEFAULT_FIRMWARE = ROOT_DIR / ".sw_build" / "retrosoc_fw.hex"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Prepare NOR Flash simulation files")
parser.add_argument("--sim-dir", type=Path, required=True)
parser.add_argument("--models-dir", type=Path, default=DEFAULT_MODELS)
parser.add_argument("--firmware", type=Path, default=DEFAULT_FIRMWARE)
return parser.parse_args()
def main() -> int:
args = parse_args()
sim_dir = args.sim_dir.resolve()
models_dir = args.models_dir.resolve()
firmware = args.firmware.resolve()
sim_dir.mkdir(parents=True, exist_ok=True)
model_names = ("SECSI.TXT", "SFDP.TXT", "SREG.TXT")
missing_models = [name for name in model_names if not (models_dir / name).is_file()]
if missing_models:
raise FileNotFoundError(
f"NOR Flash model files missing in {models_dir}: {', '.join(missing_models)}"
)
if not firmware.is_file():
raise FileNotFoundError(
f"firmware image not found: {firmware}; run 'make firmware' first"
)
for name in model_names:
shutil.copy2(models_dir / name, sim_dir / name)
memory = sim_dir / "MEM.TXT"
if memory.is_symlink() or memory.exists():
memory.unlink()
memory.symlink_to(firmware)
print(f"prepared NOR Flash files in {sim_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -194,7 +194,7 @@ def _compile(build_dir: Path, bundle: Path) -> tuple[dict[str, Path], dict[str,
conversion = _run_command(
[
sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
str(ROOT / "scripts/rtl/convt_sv2v.py"),
"-f",
str(filelist),
"--output",
+2 -2
View File
@@ -217,7 +217,7 @@ def _verification_sources() -> dict[str, str]:
ROOT / "scripts/apu_codecs.py",
ROOT / "scripts/apu_kws.py",
ROOT / "scripts/apu_kws_convert.py",
ROOT / "rtl/mini/script/convt_sv2v.py",
ROOT / "scripts/rtl/convt_sv2v.py",
Path(__file__).resolve(),
]
return {
@@ -346,7 +346,7 @@ def _compile(build_dir: Path) -> dict[str, Any]:
conversion = _run_command(
[
sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
str(ROOT / "scripts/rtl/convt_sv2v.py"),
"-f",
str(filelist),
"--output",
+1 -1
View File
@@ -303,7 +303,7 @@ def _compile(build_dir: Path, simulator: str) -> dict[str, Any]:
conversion = _run_command(
[
sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
str(ROOT / "scripts/rtl/convt_sv2v.py"),
"-f",
str(filelist),
"--output",
+12 -1
View File
@@ -73,7 +73,7 @@ def main() -> int:
subprocess.run(
[
sys.executable,
str(root / "rtl/mini/address_map/generate_memory_map.py"),
str(root / "scripts/rtl/generate_memory_map.py"),
"--map",
str(root / "rtl/mini/address_map/memory_map.json"),
"--output-dir",
@@ -131,6 +131,17 @@ def main() -> int:
str(executable),
*(str(root / source) for source in TEST_SOURCES),
]
tiny_object = temporary_root / "tiny_dma_math.o"
subprocess.run([
compiler, "-std=c11", "-Wall", "-Wextra", "-Werror", "-DRS_SOC_TINY",
"-Drs_dma_config_validate=rs_tiny_dma_config_validate",
"-Drs_dma_tcd_validate=rs_tiny_dma_tcd_validate",
"-I", str(memory_map_root / "include"),
"-I", str(user_extensions_root / "include"),
"-I", str(root / "crt/include"), "-c", str(root / "crt/src/hal/dma_math.c"),
"-o", str(tiny_object),
], check=True)
command.append(str(tiny_object))
subprocess.run(command, check=True)
subprocess.run([str(executable)], check=True)
return 0
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Prepare only the locked inputs used by the Tiny MCU."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.dependency_lock import source # noqa: E402
from scripts.setup_helpers import ensure_git_repo # noqa: E402
TINY_SOURCES = (
"hazard3", "cluster_common", "cluster_archinfo", "cluster_pwm",
"cluster_rtc", "cluster_wdg", "pdk_ihp130",
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--update", action="store_true")
args = parser.parse_args()
for name in TINY_SOURCES:
dependency = source(name)
ensure_git_repo(
dependency["url"], ROOT / dependency["destination"], dependency["revision"],
recursive=dependency.get("recursive", False), update=args.update,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())