Fix max_decode_depth to control decoder hierarchy and port generation (#18)

* Initial plan

* Fix max_decode_depth to properly control decoder hierarchy and port generation

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Fix test that relied on old depth behavior

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Update documentation for max_decode_depth parameter

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* fix format

* Add variable_depth RDL file and smoke tests for max_decode_depth parameter

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Add variable depth tests for APB3 and AXI4-Lite CPUIFs

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* fix

* fix

* bump

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-28 23:38:54 -07:00
committed by GitHub
parent 858a7870ad
commit d7481e71ba
22 changed files with 1518 additions and 26 deletions

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "peakrdl-busdecoder" name = "peakrdl-busdecoder"
version = "0.3.0" version = "0.4.0"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"] dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"]

View File

@@ -116,7 +116,9 @@ class Exporter(ExporterSubcommandPlugin):
type=int, type=int,
default=1, default=1,
help="""Maximum depth for address decoder to descend into nested help="""Maximum depth for address decoder to descend into nested
addressable components. Default is 1. addressable components. Value of 0 decodes all levels (infinite depth).
Value of 1 decodes only top-level children. Value of 2 decodes top-level
and one level deeper, etc. Default is 1.
""", """,
) )

View File

@@ -25,11 +25,7 @@ class BaseCpuif:
@property @property
def addressable_children(self) -> list[AddressableNode]: def addressable_children(self) -> list[AddressableNode]:
return [ return self.exp.ds.get_addressable_children_at_depth(unroll=self.unroll)
child
for child in self.exp.ds.top_node.children(unroll=self.unroll)
if isinstance(child, AddressableNode)
]
@property @property
def addr_width(self) -> int: def addr_width(self) -> int:

View File

@@ -27,6 +27,17 @@ class FaninGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
should_generate = action == WalkerAction.SkipDescendants
if not should_generate and self._ds.max_decode_depth == 0:
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_generate = True
if not should_generate:
return action
if node.array_dimensions: if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions): for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody( fb = ForLoopBody(

View File

@@ -23,6 +23,17 @@ class FanoutGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
should_generate = action == WalkerAction.SkipDescendants
if not should_generate and self._ds.max_decode_depth == 0:
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_generate = True
if not should_generate:
return action
if node.array_dimensions: if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions): for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody( fb = ForLoopBody(

View File

@@ -85,6 +85,20 @@ class DecodeLogicGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
should_decode = action == WalkerAction.SkipDescendants
if not should_decode and self._ds.max_decode_depth == 0:
# When decoding all levels, treat leaf registers as decode boundary
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_decode = True
# Only generate select logic if we're at the decode boundary
if not should_decode:
return action
conditions: list[str] = [] conditions: list[str] = []
conditions.extend(self.cpuif_addr_predicate(node)) conditions.extend(self.cpuif_addr_predicate(node))
conditions.extend(self.cpuif_prot_predicate(node)) conditions.extend(self.cpuif_prot_predicate(node))
@@ -146,6 +160,8 @@ class DecodeLogicGenerator(BusDecoderListener):
def __str__(self) -> str: def __str__(self) -> str:
body = self._decode_stack[-1] body = self._decode_stack[-1]
if isinstance(body, IfBody): if isinstance(body, IfBody):
if len(body) == 0:
return f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"
with body.cm(...) as b: with body.cm(...) as b:
b += f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;" b += f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"

View File

@@ -1,6 +1,6 @@
from typing import TypedDict from typing import TypedDict
from systemrdl.node import AddrmapNode from systemrdl.node import AddressableNode, AddrmapNode
from systemrdl.rdltypes.user_enum import UserEnum from systemrdl.rdltypes.user_enum import UserEnum
from .design_scanner import DesignScanner from .design_scanner import DesignScanner
@@ -72,3 +72,56 @@ class DesignState:
if user_addr_width < self.addr_width: if user_addr_width < self.addr_width:
msg.fatal(f"User-specified address width shall be greater than or equal to {self.addr_width}.") msg.fatal(f"User-specified address width shall be greater than or equal to {self.addr_width}.")
self.addr_width = user_addr_width self.addr_width = user_addr_width
def get_addressable_children_at_depth(self, unroll: bool = False) -> list[AddressableNode]:
"""
Get addressable children at the decode boundary based on max_decode_depth.
max_decode_depth semantics:
- 0: decode all levels (return leaf registers)
- 1: decode only top level (return children at depth 1)
- 2: decode top + 1 level (return children at depth 2)
- N: decode down to depth N (return children at depth N)
Args:
unroll: Whether to unroll arrayed nodes
Returns:
List of addressable nodes at the decode boundary
"""
from systemrdl.node import RegNode
def collect_nodes(node: AddressableNode, current_depth: int) -> list[AddressableNode]:
"""Recursively collect nodes at the decode boundary."""
result: list[AddressableNode] = []
# For depth 0, collect all leaf registers
if self.max_decode_depth == 0:
# If this is a register, it's a leaf
if isinstance(node, RegNode):
result.append(node)
else:
# Recurse into children
for child in node.children(unroll=unroll):
if isinstance(child, AddressableNode):
result.extend(collect_nodes(child, current_depth + 1))
else:
# For depth N, collect children at depth N
if current_depth == self.max_decode_depth:
# We're at the decode boundary - return this node
result.append(node)
elif current_depth < self.max_decode_depth:
# We haven't reached the boundary yet - recurse
for child in node.children(unroll=unroll):
if isinstance(child, AddressableNode):
result.extend(collect_nodes(child, current_depth + 1))
return result
# Start collecting from top node's children
nodes: list[AddressableNode] = []
for child in self.top_node.children(unroll=unroll):
if isinstance(child, AddressableNode):
nodes.extend(collect_nodes(child, 1))
return nodes

View File

@@ -89,7 +89,9 @@ class BusDecoderExporter:
interface. By default, arrayed nodes are kept as arrays. interface. By default, arrayed nodes are kept as arrays.
max_decode_depth: int max_decode_depth: int
Maximum depth for address decoder to descend into nested addressable Maximum depth for address decoder to descend into nested addressable
components. By default, the decoder descends 1 level deep. components. A value of 0 decodes all levels (infinite depth). A value
of 1 decodes only top-level children. A value of 2 decodes top-level
and one level deeper, etc. By default, the decoder descends 1 level deep.
""" """
# If it is the root node, skip to top addrmap # If it is the root node, skip to top addrmap
if isinstance(node, RootNode): if isinstance(node, RootNode):

View File

@@ -15,7 +15,12 @@ class BusDecoderListener(RDLListener):
def should_skip_node(self, node: AddressableNode) -> bool: def should_skip_node(self, node: AddressableNode) -> bool:
"""Check if this node should be skipped (not decoded).""" """Check if this node should be skipped (not decoded)."""
# Check if current depth exceeds max depth # Check if current depth exceeds max depth
if self._depth > self._ds.max_decode_depth: # max_decode_depth semantics:
# - 0 means decode all levels (infinite)
# - 1 means decode only top level (depth 0)
# - 2 means decode top + 1 level (depth 0 and 1)
# - N means decode down to depth N-1
if self._ds.max_decode_depth > 0 and self._depth >= self._ds.max_decode_depth:
return True return True
# Check if this node only contains external addressable children # Check if this node only contains external addressable children

View File

@@ -3,7 +3,7 @@ from collections import deque
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from systemrdl.walker import WalkerAction from systemrdl.walker import WalkerAction
from .body import Body, StructBody from .body import StructBody
from .design_state import DesignState from .design_state import DesignState
from .identifier_filter import kw_filter as kwf from .identifier_filter import kw_filter as kwf
from .listener import BusDecoderListener from .listener import BusDecoderListener
@@ -16,30 +16,38 @@ class StructGenerator(BusDecoderListener):
) -> None: ) -> None:
super().__init__(ds) super().__init__(ds)
self._stack: deque[Body] = deque() self._stack: list[StructBody] = [StructBody("cpuif_sel_t", True, False)]
self._stack.append(StructBody("cpuif_sel_t", True, False)) self._struct_defs: list[StructBody] = []
self._created_struct_stack: deque[bool] = deque() # Track if we created a struct for each node
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
self._skip = False skip = action == WalkerAction.SkipDescendants
if action == WalkerAction.SkipDescendants:
self._skip = True
if node.children(): # Only create nested struct if we're not skipping and node has addressable children
has_addressable_children = any(isinstance(child, AddressableNode) for child in node.children())
if has_addressable_children and not skip:
# Push new body onto stack # Push new body onto stack
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, False) body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, False)
self._stack.append(body) self._stack.append(body)
self._created_struct_stack.append(True)
else:
self._created_struct_stack.append(False)
return action return action
def exit_AddressableComponent(self, node: AddressableNode) -> None: def exit_AddressableComponent(self, node: AddressableNode) -> None:
type = "logic" type = "logic"
if node.children(): # Pop the created_struct flag
created_struct = self._created_struct_stack.pop()
# Only pop struct body if we created one
if created_struct:
body = self._stack.pop() body = self._stack.pop()
if body and isinstance(body, StructBody) and not self._skip: if body:
self._stack.appendleft(body) self._struct_defs.append(body)
type = body.name type = body.name
name = kwf(node.inst_name) name = kwf(node.inst_name)
@@ -53,5 +61,8 @@ class StructGenerator(BusDecoderListener):
super().exit_AddressableComponent(node) super().exit_AddressableComponent(node)
def __str__(self) -> str: def __str__(self) -> str:
self._stack[-1] += "logic cpuif_err;" if "logic cpuif_err;" not in self._stack[-1].lines:
return "\n".join(map(str, self._stack)) self._stack[-1] += "logic cpuif_err;"
bodies = [str(body) for body in self._struct_defs]
bodies.append(str(self._stack[-1]))
return "\n".join(bodies)

View File

@@ -0,0 +1,211 @@
"""APB3 smoke tests for variable depth design testing max_decode_depth parameter."""
import cocotb
from cocotb.triggers import Timer
class _Apb3SlaveShim:
def __init__(self, dut):
prefix = "s_apb"
self.PSEL = getattr(dut, f"{prefix}_PSEL")
self.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
self.PADDR = getattr(dut, f"{prefix}_PADDR")
self.PWDATA = getattr(dut, f"{prefix}_PWDATA")
self.PRDATA = getattr(dut, f"{prefix}_PRDATA")
self.PREADY = getattr(dut, f"{prefix}_PREADY")
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb3MasterShim:
def __init__(self, dut, base: str):
self.PSEL = getattr(dut, f"{base}_PSEL")
self.PENABLE = getattr(dut, f"{base}_PENABLE")
self.PWRITE = getattr(dut, f"{base}_PWRITE")
self.PADDR = getattr(dut, f"{base}_PADDR")
self.PWDATA = getattr(dut, f"{base}_PWDATA")
self.PRDATA = getattr(dut, f"{base}_PRDATA")
self.PREADY = getattr(dut, f"{base}_PREADY")
self.PSLVERR = getattr(dut, f"{base}_PSLVERR")
def _apb3_slave(dut):
return getattr(dut, "s_apb", None) or _Apb3SlaveShim(dut)
def _apb3_master(dut, base: str):
return getattr(dut, base, None) or _Apb3MasterShim(dut, base)
@cocotb.test()
async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only."""
s_apb = _apb3_slave(dut)
# At depth 1, we should have m_apb_inner1 but not deeper interfaces
inner1 = _apb3_master(dut, "m_apb_inner1")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
inner1.PRDATA.value = 0
inner1.PREADY.value = 0
inner1.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select inner1)
inner1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x12345678
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner1.PSEL.value) == 1, "inner1 must be selected"
assert int(inner1.PWRITE.value) == 1, "Write should propagate"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror master"
@cocotb.test()
async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_apb = _apb3_slave(dut)
# At depth 2, we should have m_apb_reg1 and m_apb_inner2
reg1 = _apb3_master(dut, "m_apb_reg1")
inner2 = _apb3_master(dut, "m_apb_inner2")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
reg1.PRDATA.value = 0
reg1.PREADY.value = 0
reg1.PSLVERR.value = 0
inner2.PRDATA.value = 0
inner2.PREADY.value = 0
inner2.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0xABCDEF01
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.PSEL.value) == 0, "inner2 should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select inner2)
inner2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x23456789
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner2.PSEL.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
@cocotb.test()
async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_apb = _apb3_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _apb3_master(dut, "m_apb_reg1")
reg2 = _apb3_master(dut, "m_apb_reg2")
reg2b = _apb3_master(dut, "m_apb_reg2b")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
for master in [reg1, reg2, reg2b]:
master.PRDATA.value = 0
master.PREADY.value = 0
master.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x11111111
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select reg2)
reg2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x22222222
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2.PSEL.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg2.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x14 (should select reg2b)
reg2b.PREADY.value = 1
s_apb.PADDR.value = 0x14
s_apb.PWDATA.value = 0x33333333
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2b.PSEL.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"

View File

@@ -0,0 +1,128 @@
"""Pytest wrapper launching the APB3 cocotb smoke test for variable depth."""
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb3.apb3_cpuif_flat import APB3CpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb3_variable_depth_1(tmp_path: Path) -> None:
"""Test APB3 design with max_decode_depth=1."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB3CpuifFlat,
max_decode_depth=1,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth1.log"),
testcase="test_depth_1",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb3_variable_depth_2(tmp_path: Path) -> None:
"""Test APB3 design with max_decode_depth=2."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB3CpuifFlat,
max_decode_depth=2,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth2.log"),
testcase="test_depth_2",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb3_variable_depth_0(tmp_path: Path) -> None:
"""Test APB3 design with max_decode_depth=0 (all levels)."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB3CpuifFlat,
max_decode_depth=0,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth0.log"),
testcase="test_depth_0",
)

View File

@@ -0,0 +1,227 @@
"""APB4 smoke tests for variable depth design testing max_decode_depth parameter."""
import cocotb
from cocotb.triggers import Timer
class _Apb4SlaveShim:
def __init__(self, dut):
prefix = "s_apb"
self.PSEL = getattr(dut, f"{prefix}_PSEL")
self.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
self.PADDR = getattr(dut, f"{prefix}_PADDR")
self.PPROT = getattr(dut, f"{prefix}_PPROT")
self.PWDATA = getattr(dut, f"{prefix}_PWDATA")
self.PSTRB = getattr(dut, f"{prefix}_PSTRB")
self.PRDATA = getattr(dut, f"{prefix}_PRDATA")
self.PREADY = getattr(dut, f"{prefix}_PREADY")
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb4MasterShim:
def __init__(self, dut, base: str):
self.PSEL = getattr(dut, f"{base}_PSEL")
self.PENABLE = getattr(dut, f"{base}_PENABLE")
self.PWRITE = getattr(dut, f"{base}_PWRITE")
self.PADDR = getattr(dut, f"{base}_PADDR")
self.PPROT = getattr(dut, f"{base}_PPROT")
self.PWDATA = getattr(dut, f"{base}_PWDATA")
self.PSTRB = getattr(dut, f"{base}_PSTRB")
self.PRDATA = getattr(dut, f"{base}_PRDATA")
self.PREADY = getattr(dut, f"{base}_PREADY")
self.PSLVERR = getattr(dut, f"{base}_PSLVERR")
def _apb4_slave(dut):
return getattr(dut, "s_apb", None) or _Apb4SlaveShim(dut)
def _apb4_master(dut, base: str):
return getattr(dut, base, None) or _Apb4MasterShim(dut, base)
@cocotb.test()
async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only."""
s_apb = _apb4_slave(dut)
# At depth 1, we should have m_apb_inner1 but not deeper interfaces
inner1 = _apb4_master(dut, "m_apb_inner1")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0
inner1.PRDATA.value = 0
inner1.PREADY.value = 0
inner1.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select inner1)
inner1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x12345678
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner1.PSEL.value) == 1, "inner1 must be selected"
assert int(inner1.PWRITE.value) == 1, "Write should propagate"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror master"
@cocotb.test()
async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_apb = _apb4_slave(dut)
# At depth 2, we should have m_apb_reg1 and m_apb_inner2
reg1 = _apb4_master(dut, "m_apb_reg1")
inner2 = _apb4_master(dut, "m_apb_inner2")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0
reg1.PRDATA.value = 0
reg1.PREADY.value = 0
reg1.PSLVERR.value = 0
inner2.PRDATA.value = 0
inner2.PREADY.value = 0
inner2.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0xABCDEF01
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.PSEL.value) == 0, "inner2 should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select inner2)
inner2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x23456789
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner2.PSEL.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
@cocotb.test()
async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_apb = _apb4_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _apb4_master(dut, "m_apb_reg1")
reg2 = _apb4_master(dut, "m_apb_reg2")
reg2b = _apb4_master(dut, "m_apb_reg2b")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0
for master in [reg1, reg2, reg2b]:
master.PRDATA.value = 0
master.PREADY.value = 0
master.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x11111111
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select reg2)
reg2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x22222222
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2.PSEL.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg2.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x14 (should select reg2b)
reg2b.PREADY.value = 1
s_apb.PADDR.value = 0x14
s_apb.PWDATA.value = 0x33333333
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2b.PSEL.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"

View File

@@ -0,0 +1,131 @@
"""Pytest wrapper launching the APB4 cocotb smoke test for variable depth."""
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb4_variable_depth_1(tmp_path: Path) -> None:
"""Test APB4 design with max_decode_depth=1."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB4CpuifFlat,
max_decode_depth=1,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
log_file=str(tmp_path / "build_depth_1.log"),
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth1.log"),
testcase="test_depth_1",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb4_variable_depth_2(tmp_path: Path) -> None:
"""Test APB4 design with max_decode_depth=2."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB4CpuifFlat,
max_decode_depth=2,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
log_file=str(tmp_path / "build_depth_2.log"),
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth_2.log"),
testcase="test_depth_2",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb4_variable_depth_0(tmp_path: Path) -> None:
"""Test APB4 design with max_decode_depth=0 (all levels)."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB4CpuifFlat,
max_decode_depth=0,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
log_file=str(tmp_path / "build_depth_0.log"),
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth_0.log"),
testcase="test_depth_0",
)

View File

@@ -0,0 +1,271 @@
"""AXI4-Lite smoke tests for variable depth design testing max_decode_depth parameter."""
import cocotb
from cocotb.triggers import Timer
class _AxilSlaveShim:
def __init__(self, dut):
prefix = "s_axil"
self.AWREADY = getattr(dut, f"{prefix}_AWREADY")
self.AWVALID = getattr(dut, f"{prefix}_AWVALID")
self.AWADDR = getattr(dut, f"{prefix}_AWADDR")
self.AWPROT = getattr(dut, f"{prefix}_AWPROT")
self.WREADY = getattr(dut, f"{prefix}_WREADY")
self.WVALID = getattr(dut, f"{prefix}_WVALID")
self.WDATA = getattr(dut, f"{prefix}_WDATA")
self.WSTRB = getattr(dut, f"{prefix}_WSTRB")
self.BREADY = getattr(dut, f"{prefix}_BREADY")
self.BVALID = getattr(dut, f"{prefix}_BVALID")
self.BRESP = getattr(dut, f"{prefix}_BRESP")
self.ARREADY = getattr(dut, f"{prefix}_ARREADY")
self.ARVALID = getattr(dut, f"{prefix}_ARVALID")
self.ARADDR = getattr(dut, f"{prefix}_ARADDR")
self.ARPROT = getattr(dut, f"{prefix}_ARPROT")
self.RREADY = getattr(dut, f"{prefix}_RREADY")
self.RVALID = getattr(dut, f"{prefix}_RVALID")
self.RDATA = getattr(dut, f"{prefix}_RDATA")
self.RRESP = getattr(dut, f"{prefix}_RRESP")
class _AxilMasterShim:
def __init__(self, dut, base: str):
self.AWREADY = getattr(dut, f"{base}_AWREADY")
self.AWVALID = getattr(dut, f"{base}_AWVALID")
self.AWADDR = getattr(dut, f"{base}_AWADDR")
self.AWPROT = getattr(dut, f"{base}_AWPROT")
self.WREADY = getattr(dut, f"{base}_WREADY")
self.WVALID = getattr(dut, f"{base}_WVALID")
self.WDATA = getattr(dut, f"{base}_WDATA")
self.WSTRB = getattr(dut, f"{base}_WSTRB")
self.BREADY = getattr(dut, f"{base}_BREADY")
self.BVALID = getattr(dut, f"{base}_BVALID")
self.BRESP = getattr(dut, f"{base}_BRESP")
self.ARREADY = getattr(dut, f"{base}_ARREADY")
self.ARVALID = getattr(dut, f"{base}_ARVALID")
self.ARADDR = getattr(dut, f"{base}_ARADDR")
self.ARPROT = getattr(dut, f"{base}_ARPROT")
self.RREADY = getattr(dut, f"{base}_RREADY")
self.RVALID = getattr(dut, f"{base}_RVALID")
self.RDATA = getattr(dut, f"{base}_RDATA")
self.RRESP = getattr(dut, f"{base}_RRESP")
def _axil_slave(dut):
return getattr(dut, "s_axil", None) or _AxilSlaveShim(dut)
def _axil_master(dut, base: str):
return getattr(dut, base, None) or _AxilMasterShim(dut, base)
@cocotb.test()
async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only."""
s_axil = _axil_slave(dut)
# At depth 1, we should have m_axil_inner1 but not deeper interfaces
inner1 = _axil_master(dut, "m_axil_inner1")
# Default slave side inputs
s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0
s_axil.AWPROT.value = 0
s_axil.WVALID.value = 0
s_axil.WDATA.value = 0
s_axil.WSTRB.value = 0
s_axil.BREADY.value = 0
s_axil.ARVALID.value = 0
s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0
inner1.AWREADY.value = 0
inner1.WREADY.value = 0
inner1.BVALID.value = 0
inner1.BRESP.value = 0
inner1.ARREADY.value = 0
inner1.RVALID.value = 0
inner1.RDATA.value = 0
inner1.RRESP.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select inner1)
inner1.AWREADY.value = 1
inner1.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x0
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x12345678
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(inner1.AWVALID.value) == 1, "inner1 write address valid must be set"
assert int(inner1.WVALID.value) == 1, "inner1 write data valid must be set"
@cocotb.test()
async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_axil = _axil_slave(dut)
# At depth 2, we should have m_axil_reg1 and m_axil_inner2
reg1 = _axil_master(dut, "m_axil_reg1")
inner2 = _axil_master(dut, "m_axil_inner2")
# Default slave side inputs
s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0
s_axil.AWPROT.value = 0
s_axil.WVALID.value = 0
s_axil.WDATA.value = 0
s_axil.WSTRB.value = 0
s_axil.BREADY.value = 0
s_axil.ARVALID.value = 0
s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0
for master in [reg1, inner2]:
master.AWREADY.value = 0
master.WREADY.value = 0
master.BVALID.value = 0
master.BRESP.value = 0
master.ARREADY.value = 0
master.RVALID.value = 0
master.RDATA.value = 0
master.RRESP.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.AWREADY.value = 1
reg1.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x0
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0xABCDEF01
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.AWVALID.value) == 0, "inner2 should not be selected"
# Reset
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
reg1.AWREADY.value = 0
reg1.WREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select inner2)
inner2.AWREADY.value = 1
inner2.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x10
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x23456789
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(inner2.AWVALID.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
@cocotb.test()
async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_axil = _axil_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _axil_master(dut, "m_axil_reg1")
reg2 = _axil_master(dut, "m_axil_reg2")
reg2b = _axil_master(dut, "m_axil_reg2b")
# Default slave side inputs
s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0
s_axil.AWPROT.value = 0
s_axil.WVALID.value = 0
s_axil.WDATA.value = 0
s_axil.WSTRB.value = 0
s_axil.BREADY.value = 0
s_axil.ARVALID.value = 0
s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0
for master in [reg1, reg2, reg2b]:
master.AWREADY.value = 0
master.WREADY.value = 0
master.BVALID.value = 0
master.BRESP.value = 0
master.ARREADY.value = 0
master.RVALID.value = 0
master.RDATA.value = 0
master.RRESP.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.AWREADY.value = 1
reg1.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x0
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x11111111
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.AWVALID.value) == 0, "reg2 should not be selected"
assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected"
# Reset
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
reg1.AWREADY.value = 0
reg1.WREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select reg2)
reg2.AWREADY.value = 1
reg2.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x10
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x22222222
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg2.AWVALID.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected"
# Reset
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
reg2.AWREADY.value = 0
reg2.WREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x14 (should select reg2b)
reg2b.AWREADY.value = 1
reg2b.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x14
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x33333333
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg2b.AWVALID.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
assert int(reg2.AWVALID.value) == 0, "reg2 should not be selected"

View File

@@ -0,0 +1,128 @@
"""Pytest wrapper launching the AXI4-Lite cocotb smoke test for variable depth."""
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.axi4lite.axi4_lite_cpuif_flat import AXI4LiteCpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources
@pytest.mark.simulation
@pytest.mark.verilator
def test_axi4lite_variable_depth_1(tmp_path: Path) -> None:
"""Test AXI4-Lite design with max_decode_depth=1."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
max_decode_depth=1,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth1.log"),
testcase="test_depth_1",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_axi4lite_variable_depth_2(tmp_path: Path) -> None:
"""Test AXI4-Lite design with max_decode_depth=2."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
max_decode_depth=2,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth2.log"),
testcase="test_depth_2",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_axi4lite_variable_depth_0(tmp_path: Path) -> None:
"""Test AXI4-Lite design with max_decode_depth=0 (all levels)."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
max_decode_depth=0,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth0.log"),
testcase="test_depth_0",
)

View File

@@ -0,0 +1,31 @@
// Variable depth register hierarchy for testing max_decode_depth parameter
addrmap level2 {
reg {
field {
sw=rw;
hw=r;
} data2[31:0];
} reg2 @ 0x0;
reg {
field {
sw=rw;
hw=r;
} data2b[31:0];
} reg2b @ 0x4;
};
addrmap level1 {
reg {
field {
sw=rw;
hw=r;
} data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap variable_depth {
level1 inner1 @ 0x0;
};

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
collect_ignore_glob = ["cocotb/*/smoke/test_register_access.py"] collect_ignore_glob = ["cocotb/*/smoke/test_register_access.py", "cocotb/*/smoke/test_variable_depth.py"]
import os import os
from collections.abc import Callable from collections.abc import Callable

View File

@@ -89,7 +89,8 @@ class TestBusDecoderExporter:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
output_dir = str(tmp_path) output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) # Use depth=0 to descend all the way to registers
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif, max_decode_depth=0)
# Check that output files are created # Check that output files are created
module_file = tmp_path / "outer_block.sv" module_file = tmp_path / "outer_block.sv"

View File

@@ -101,7 +101,8 @@ def test_non_external_nested_components_are_descended(compile_rdl: Callable[...,
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif) # Use depth=0 to descend all the way down to registers
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=0)
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "outer_block.sv" module_file = Path(tmpdir) / "outer_block.sv"

View File

@@ -0,0 +1,256 @@
"""Test max_decode_depth parameter behavior."""
from collections.abc import Callable
from pathlib import Path
from tempfile import TemporaryDirectory
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
def test_depth_1_generates_top_level_interface_only(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that depth=1 generates interface only for top-level children."""
rdl_source = """
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=1)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interface for inner1 only
assert "m_apb_inner1" in content
# Should NOT have interface for reg1
assert "m_apb_reg1" not in content
# Struct should have inner1 but not nested structure
assert "logic inner1;" in content
def test_depth_2_generates_second_level_interfaces(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that depth=2 generates interfaces for second-level children."""
rdl_source = """
addrmap level2 {
reg {
field { sw=rw; hw=r; } data2[31:0];
} reg2 @ 0x0;
};
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=2)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interfaces for reg1 and inner2
assert "m_apb_reg1" in content
assert "m_apb_inner2" in content
# Should NOT have interface for inner1 or reg2
assert "m_apb_inner1" not in content
assert "m_apb_reg2" not in content
# Struct should be hierarchical with inner1.reg1 and inner1.inner2
assert "cpuif_sel_inner1_t" in content
assert "logic reg1;" in content
assert "logic inner2;" in content
def test_depth_0_decodes_all_levels(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that depth=0 decodes all the way down to registers."""
rdl_source = """
addrmap level2 {
reg {
field { sw=rw; hw=r; } data2[31:0];
} reg2 @ 0x0;
reg {
field { sw=rw; hw=r; } data2b[31:0];
} reg2b @ 0x4;
};
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=0)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interfaces for all leaf registers
assert "m_apb_reg1" in content
assert "m_apb_reg2" in content
assert "m_apb_reg2b" in content
# Should NOT have interfaces for addrmaps
assert "m_apb_inner1" not in content
assert "m_apb_inner2" not in content
# Struct should be fully hierarchical
assert "cpuif_sel_inner1_t" in content
assert "cpuif_sel_inner2_t" in content
def test_depth_affects_decode_logic(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that decode logic changes based on max_decode_depth."""
rdl_source = """
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
# Test depth=1: should set cpuif_wr_sel.inner1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=1)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "cpuif_wr_sel.inner1 = 1'b1;" in content
assert "cpuif_wr_sel.inner1.reg1" not in content
# Test depth=2: should set cpuif_wr_sel.inner1.reg1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=2)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "cpuif_wr_sel.inner1.reg1 = 1'b1;" in content
assert "cpuif_wr_sel.inner1 = 1'b1;" not in content
def test_depth_affects_fanout_fanin(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that fanout/fanin logic changes based on max_decode_depth."""
rdl_source = """
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
# Test depth=1: should have fanout for inner1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=1)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "m_apb_inner1.PSEL" in content
assert "m_apb_reg1.PSEL" not in content
# Test depth=2: should have fanout for reg1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=2)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "m_apb_reg1.PSEL" in content
assert "m_apb_inner1.PSEL" not in content
def test_depth_3_with_deep_hierarchy(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test depth=3 with a 4-level deep hierarchy."""
rdl_source = """
addrmap level3 {
reg {
field { sw=rw; hw=r; } data3[31:0];
} reg3 @ 0x0;
};
addrmap level2 {
reg {
field { sw=rw; hw=r; } data2[31:0];
} reg2 @ 0x0;
level3 inner3 @ 0x10;
};
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=3)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interfaces at depth 3: reg2, inner3
# (reg1 is at depth 2, not 3)
assert "m_apb_reg2" in content
assert "m_apb_inner3" in content
# Should NOT have interfaces at other depths
assert "m_apb_inner1" not in content
assert "m_apb_inner2" not in content
assert "m_apb_reg1" not in content
assert "m_apb_reg3" not in content

2
uv.lock generated
View File

@@ -608,7 +608,7 @@ wheels = [
[[package]] [[package]]
name = "peakrdl-busdecoder" name = "peakrdl-busdecoder"
version = "0.3.0" version = "0.4.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "jinja2" }, { name = "jinja2" },