mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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:
253 files changed
+15235
-501
No files matched your search
@@ -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")
|
||||
|
||||
@@ -17,6 +17,7 @@ SELF_OWNED_RTL_ROOTS = {
|
||||
("rtl", "demo"),
|
||||
("rtl", "ip"),
|
||||
("rtl", "mini"),
|
||||
("rtl", "tiny"),
|
||||
("rtl", "tech"),
|
||||
("tests", "rtl"),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user