feat: enhance SoC topology and user extensions with APB targets and validation scripts

- Added APB target definitions in soc_topology.json for various peripherals.
- Introduced user_extensions.json to define user core and IP targets.
- Updated generate_filelist.py to include SVA paths.
- Refactored ip_apb_wrapper.sv to utilize generated APB interfaces and bridges.
- Simplified user_core_top.sv and user_ip_top.sv by including generated bindings.
- Implemented check_clock_reset_domains.py to validate clock/reset domains and CDC crossings.
- Updated regress.py to include SVA support in simulation commands.
- Added tests for clock/reset domain validation and user extensions generation.
- Enhanced test coverage for topology generation and APB target ownership validation.
This commit is contained in:
maksyuki committed 2026-07-23 16:42:25 +08:00
1 parent d886cd992c
commit 7d90d2ffb4
22 files changed
+1118 -561

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
"""Tests for the root clock/reset and CDC inventory checker."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOMAIN_MAP = ROOT / "rtl/mini/integration/clock_reset_domains.json"
CHECKER = ROOT / "scripts/check_clock_reset_domains.py"
def check(domain_map: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(CHECKER), "--map", str(domain_map), "--root", str(ROOT)],
text=True,
capture_output=True,
)
def write_invalid_map(tmp_path: Path, document: dict[str, object]) -> Path:
path = tmp_path / "clock_reset_domains.json"
path.write_text(json.dumps(document), encoding="utf-8")
return path
def test_clock_reset_domain_inventory_matches_the_rcu() -> None:
result = check(DOMAIN_MAP)
assert result.returncode == 0, result.stderr
def test_clock_reset_domain_inventory_rejects_unknown_domain_and_instance(tmp_path: Path) -> None:
document = json.loads(DOMAIN_MAP.read_text(encoding="utf-8"))
document["crossings"][0]["destination"] = "unknown"
result = check(write_invalid_map(tmp_path, document))
assert result.returncode != 0
assert "references an unknown domain" in result.stderr
document = json.loads(DOMAIN_MAP.read_text(encoding="utf-8"))
document["domains"][0]["instance"] = "u_missing_rst_sync"
result = check(write_invalid_map(tmp_path, document))
assert result.returncode != 0
assert "does not declare rst_sync instance" in result.stderr
+1
View File
@@ -306,6 +306,7 @@ def test_verilator_simulations_use_uniform_timeout() -> None:
for _, values in regression_commands:
if "SIMU=VERILATOR" in values:
assert not any(value.startswith("SOC_SIM_TIME=") for value in values)
assert "HAVE_SVA=YES" in values
def test_run_flow_writes_structured_result(tmp_path: Path) -> None:
+53 -3
View File
@@ -16,7 +16,7 @@ MEMORY_MAP = ROOT / "rtl/mini/address_map/memory_map.json"
MEMORY_MAP_GENERATOR = ROOT / "rtl/mini/address_map/generate_memory_map.py"
def generate(output_dir: Path) -> None:
def generate(output_dir: Path, ip: str = "NONE") -> None:
subprocess.run(
[
sys.executable,
@@ -27,12 +27,14 @@ def generate(output_dir: Path) -> None:
str(MEMORY_MAP),
"--output-dir",
str(output_dir),
"--ip",
ip,
],
check=True,
)
def validate(topology: Path) -> subprocess.CompletedProcess[str]:
def validate(topology: Path, ip: str = "NONE") -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
sys.executable,
@@ -41,6 +43,8 @@ def validate(topology: Path) -> subprocess.CompletedProcess[str]:
str(topology),
"--memory-map",
str(MEMORY_MAP),
"--ip",
ip,
"--check",
],
text=True,
@@ -54,12 +58,16 @@ def write_invalid_topology(tmp_path: Path, document: dict[str, object]) -> Path:
return path
def test_topology_generates_complete_native_and_gpio_bindings(tmp_path: Path) -> None:
def test_topology_generates_complete_native_apb_and_gpio_bindings(tmp_path: Path) -> None:
generate(tmp_path)
interfaces = (tmp_path / "rtl/soc_nmi_interfaces.svh").read_text(encoding="utf-8")
routes = (tmp_path / "rtl/soc_nmi_routes.svh").read_text(encoding="utf-8")
gpio = (tmp_path / "rtl/soc_gpio_alt_bindings.svh").read_text(encoding="utf-8")
apb_interfaces = (tmp_path / "rtl/soc_apb_interfaces.svh").read_text(encoding="utf-8")
apb_declarations = (tmp_path / "rtl/soc_apb_declarations.svh").read_text(encoding="utf-8")
apb_routes = (tmp_path / "rtl/soc_apb_request_routes.svh").read_text(encoding="utf-8")
apb_response = (tmp_path / "rtl/soc_apb_response_mux.svh").read_text(encoding="utf-8")
filelist = (tmp_path / "soc_topology.fl").read_text(encoding="utf-8")
assert interfaces.count("nmi_if u_") == 18
@@ -69,9 +77,31 @@ def test_topology_generates_complete_native_and_gpio_bindings(tmp_path: Path) ->
assert gpio.count("// GPIO") == 64
assert "assign u_uart1_if.rx_i = u_gpio_if.di_i[0];" in gpio
assert "assign u_gpio_if.alt1_do_i[22] = u_psram_if.nss_o[0];" in gpio
assert apb_interfaces.count("apb4_if u_") == 9
assert "apb4_pure_if u_archinfo_apb_pure_if ();" in apb_interfaces
assert "assign tmr.paddr = nmi.addr;" in apb_routes
assert "({32{s_psel_q[8]}} & tmr.prdata)" in apb_response
assert "localparam int NSLV = 9;" in apb_declarations
assert filelist.startswith("+incdir+")
def test_topology_adds_user_apb_only_for_the_mdd_ip(tmp_path: Path) -> None:
none_output = tmp_path / "none"
mdd_output = tmp_path / "mdd"
generate(none_output)
generate(mdd_output, ip="MDD")
none_interfaces = (none_output / "rtl/soc_apb_interfaces.svh").read_text(encoding="utf-8")
mdd_interfaces = (mdd_output / "rtl/soc_apb_interfaces.svh").read_text(encoding="utf-8")
mdd_declarations = (mdd_output / "rtl/soc_apb_declarations.svh").read_text(encoding="utf-8")
mdd_response = (mdd_output / "rtl/soc_apb_response_mux.svh").read_text(encoding="utf-8")
assert "u_user_ip_apb_if" not in none_interfaces
assert "apb4_if u_user_ip_apb_if (clk_i, rst_n_i);" in mdd_interfaces
assert "localparam int NSLV = 10;" in mdd_declarations
assert "s_psel_q[9]" in mdd_response
def test_topology_rejects_unknown_or_non_native_regions(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["native_targets"][0]["regions"] = ["UNKNOWN"]
@@ -100,6 +130,26 @@ def test_topology_rejects_duplicate_region_and_disabled_owner(tmp_path: Path) ->
assert "disabled but declares regions" in result.stderr
def test_topology_rejects_invalid_apb_target_ownership(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["apb_targets"][0]["region"] = "SRAM"
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "not an active APB region" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["apb_targets"][8]["slot"] = 0
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "APB target slot 0 is duplicated" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["apb_targets"][9]["requires_ip"] = None
result = validate(write_invalid_topology(tmp_path, document), ip="MDD")
assert result.returncode != 0
assert "requires_ip does not match" in result.stderr
def test_topology_rejects_invalid_gpio_coverage_and_expression(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["gpio_alt_functions"][1]["pin"] = 0
+73
View File
@@ -0,0 +1,73 @@
"""Tests for the generated scalar user-extension bindings."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EXTENSIONS = ROOT / "rtl/mini/integration/user_extensions.json"
GENERATOR = ROOT / "rtl/mini/integration/generate_user_extensions.py"
def generate(output_dir: Path) -> None:
subprocess.run(
[
sys.executable,
str(GENERATOR),
"--map",
str(EXTENSIONS),
"--output-dir",
str(output_dir),
],
check=True,
)
def validate(extensions: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(GENERATOR), "--map", str(extensions), "--check"],
text=True,
capture_output=True,
)
def write_invalid_extensions(tmp_path: Path, document: dict[str, object]) -> Path:
path = tmp_path / "invalid_extensions.json"
path.write_text(json.dumps(document), encoding="utf-8")
return path
def test_extensions_generate_isolated_scalar_bindings(tmp_path: Path) -> None:
generate(tmp_path)
core = (tmp_path / "rtl/user_core_bindings.svh").read_text(encoding="utf-8")
ip = (tmp_path / "rtl/user_ip_bindings.svh").read_text(encoding="utf-8")
filelist = (tmp_path / "user_extensions.fl").read_text(encoding="utf-8")
assert core.count("nmi_if u_user_") == 6
assert "nmi.valid = '0;" in core
assert "5'd6: begin" in core
assert "user_core_design_username1 #(" in core
assert ip.count("user_gpio_if #(`USER_GPIO_NUM)") == 2
assert "gpio.do_o = '0;" in ip
assert "8'd2: begin" in ip
assert "u_user_2_apb_if.psel = apb.psel;" in ip
assert filelist.startswith("+incdir+")
def test_extensions_reject_noncontiguous_slots_and_invalid_modules(tmp_path: Path) -> None:
document = json.loads(EXTENSIONS.read_text(encoding="utf-8"))
document["core_targets"][2]["slot"] = 7
result = validate(write_invalid_extensions(tmp_path, document))
assert result.returncode != 0
assert "slots must be contiguous from one" in result.stderr
document = json.loads(EXTENSIONS.read_text(encoding="utf-8"))
document["ip_targets"][0]["module"] = "user-ip"
result = validate(write_invalid_extensions(tmp_path, document))
assert result.returncode != 0
assert "must be a SystemVerilog identifier" in result.stderr