5 Commits

Author SHA1 Message Date
Copilot
d7481e71ba 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>
2025-10-28 23:38:54 -07:00
Arnav Sacheti
858a7870ad version bump 2025-10-28 22:06:49 -07:00
Copilot
3d823572cc Fix nonconstant index errors in Questa by using unpacked structs and interface array intermediates (#17)
* Initial plan

* Fix nonconstant index error by using unpacked structs and arrays

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

* Add comprehensive tests for Questa compatibility with instance arrays

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

* Add intermediate signals for interface array fanin to fix Questa compatibility

Questa simulator rejects indexing interface arrays with non-constant variables in procedural blocks (always_comb). This fix creates intermediate unpacked array signals that are assigned from interface arrays using generate loops (with genvar), then references those intermediates in the fanin logic.

Changes:
- Added fanin_intermediate_gen.py to generate intermediate signals
- Modified APB3/APB4/AXI4-Lite cpuif classes to use intermediates for interface arrays
- Updated templates to include intermediate signal section
- Intermediate signals use genvar indexing (legal) instead of variable indexing (illegal in Questa)

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

* Fix type checking and formatting issues in fanin intermediate generator

- Added proper null checks for node.array_dimensions to avoid type errors
- Used getattr() to safely access is_interface and _interface attributes
- Added early returns when interface is None to prevent errors
- Fixed formatting issues in fanin_intermediate_gen.py
- All type checks now pass (only pre-existing errors remain in other files)
- All 67 tests still pass

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

* Fix test collection when cocotb is not installed

The cocotb test files (test_runner.py) have imports that fail at module level when cocotb is not installed, causing pytest collection to fail. This prevents running tests in environments without cocotb dependencies.

Solution: Updated conftest.py to conditionally ignore cocotb tests when cocotb is not available, allowing tests to pass in both environments (with and without cocotb).

- When cocotb is available: only ignore test_register_access.py files (existing behavior)
- When cocotb is not available: ignore all files under cocotb/** directories

This allows CI to run all 67 non-simulation tests successfully without requiring cocotb/verilator dependencies for basic testing.

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

* add default for unpacked struct

* cocotb is test dependancy

* Refactor fanin intermediate signal logic to cpuif level

Moved the interface-specific signal assignment logic from fanin_intermediate_gen.py to individual cpuif classes (APB3Cpuif, APB4Cpuif, AXI4LiteCpuif). This follows better architecture principles where each cpuif knows which signals it needs.

Changes:
- Added fanin_intermediate_assignments() method to BaseCpuif
- Implemented fanin_intermediate_assignments() in APB3Cpuif, APB4Cpuif, and AXI4LiteCpuif
- Updated FaninIntermediateGenerator to call the cpuif method instead of checking interface type
- Removed interface type checking logic from fanin_intermediate_gen.py

This makes the code more maintainable and follows the single responsibility principle - each cpuif class knows its own signal requirements.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-28 22:03:57 -07:00
Arnav Sacheti
f829e3894f format xargs 2025-10-27 20:34:41 -07:00
Arnav Sacheti
74eb2344b1 change to pypi gh action 2025-10-26 19:53:13 -07:00
36 changed files with 1940 additions and 51 deletions

View File

@@ -40,7 +40,6 @@ jobs:
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags/')
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: uvx twine upload dist/*
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true

View File

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

View File

@@ -116,7 +116,9 @@ class Exporter(ExporterSubcommandPlugin):
type=int,
default=1,
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

@@ -52,8 +52,16 @@ class APB3Cpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -62,6 +70,23 @@ class APB3Cpuif(BaseCpuif):
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB3 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -26,6 +26,13 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -55,8 +55,16 @@ class APB4Cpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -65,6 +73,23 @@ class APB4Cpuif(BaseCpuif):
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB4 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -29,6 +29,13 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -4,14 +4,14 @@ from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteSVInterface
from .axi4_lite_interface import AXI4LiteSVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class AXI4LiteCpuif(BaseCpuif):
template_path = "axi4lite_tmpl.sv"
template_path = "axi4_lite_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
@@ -68,9 +68,17 @@ class AXI4LiteCpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
@@ -79,6 +87,23 @@ class AXI4LiteCpuif(BaseCpuif):
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for AXI4-Lite interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.RVALID;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.RRESP[1];",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.RDATA;",
]

View File

@@ -4,7 +4,7 @@ from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteFlatInterface
from .axi4_lite_interface import AXI4LiteFlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
class AXI4LiteCpuifFlat(BaseCpuif):
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
template_path = "axi4lite_tmpl.sv"
template_path = "axi4_lite_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)

View File

@@ -53,6 +53,13 @@ assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -7,6 +7,7 @@ from systemrdl.node import AddressableNode
from ..utils import clog2, get_indexed_path, is_pow2, roundup_pow2
from .fanin_gen import FaninGenerator
from .fanin_intermediate_gen import FaninIntermediateGenerator
from .fanout_gen import FanoutGenerator
if TYPE_CHECKING:
@@ -24,11 +25,7 @@ class BaseCpuif:
@property
def addressable_children(self) -> list[AddressableNode]:
return [
child
for child in self.exp.ds.top_node.children(unroll=self.unroll)
if isinstance(child, AddressableNode)
]
return self.exp.ds.get_addressable_children_at_depth(unroll=self.unroll)
@property
def addr_width(self) -> int:
@@ -97,6 +94,7 @@ class BaseCpuif:
"ds": self.exp.ds,
"fanout": FanoutGenerator,
"fanin": FaninGenerator,
"fanin_intermediate": FaninIntermediateGenerator,
}
template = jj_env.get_template(self.template_path)
@@ -116,3 +114,24 @@ class BaseCpuif:
def readback(self, node: AddressableNode | None = None) -> str:
raise NotImplementedError
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for interface array fanin.
This method should be implemented by cpuif classes that use interfaces.
It returns a list of assignment strings that copy signals from interface
arrays to intermediate unpacked arrays using constant (genvar) indexing.
Args:
node: The addressable node
inst_name: Instance name for the intermediate signals
array_idx: Array index string (e.g., "[gi0][gi1]")
master_prefix: Master interface prefix
indexed_path: Indexed path to the interface element
Returns:
List of assignment strings
"""
return [] # Default: no intermediate assignments needed

View File

@@ -27,6 +27,17 @@ class FaninGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
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:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(

View File

@@ -0,0 +1,142 @@
"""Generator for intermediate signals needed for interface array fanin.
When using SystemVerilog interface arrays, we cannot use variable indices
in procedural blocks (like always_comb). This generator creates intermediate
signals that copy from interface arrays using generate loops, which can then
be safely accessed with variable indices in the fanin logic.
"""
from collections import deque
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
from systemrdl.walker import WalkerAction
from ..body import Body, ForLoopBody
from ..design_state import DesignState
from ..listener import BusDecoderListener
from ..utils import get_indexed_path
if TYPE_CHECKING:
from .base_cpuif import BaseCpuif
class FaninIntermediateGenerator(BusDecoderListener):
"""Generates intermediate signals for interface array fanin."""
def __init__(self, ds: DesignState, cpuif: "BaseCpuif") -> None:
super().__init__(ds)
self._cpuif = cpuif
self._declarations: list[str] = []
self._stack: deque[Body] = deque()
self._stack.append(Body())
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node)
# Only generate intermediates for interface arrays
# Check if cpuif has is_interface attribute (some implementations don't)
is_interface = getattr(self._cpuif, "is_interface", False)
if not is_interface or not node.array_dimensions:
return action
# Generate intermediate signal declarations
self._generate_intermediate_declarations(node)
# Generate assignment logic using generate loops
if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(
"genvar",
f"gi{i}",
dim,
)
self._stack.append(fb)
# Generate assignments from interface array to intermediates
self._stack[-1] += self._generate_intermediate_assignments(node)
return action
def exit_AddressableComponent(self, node: AddressableNode) -> None:
is_interface = getattr(self._cpuif, "is_interface", False)
if is_interface and node.array_dimensions:
for _ in node.array_dimensions:
b = self._stack.pop()
if not b:
continue
self._stack[-1] += b
super().exit_AddressableComponent(node)
def _generate_intermediate_declarations(self, node: AddressableNode) -> None:
"""Generate intermediate signal declarations for a node."""
inst_name = node.inst_name
# Array dimensions should be checked before calling this function
if not node.array_dimensions:
return
# Calculate total array size
array_size = 1
for dim in node.array_dimensions:
array_size *= dim
# Create array dimension string
array_str = "".join(f"[{dim}]" for dim in node.array_dimensions)
# Generate declarations for each fanin signal
# For APB3/4: PREADY, PSLVERR, PRDATA
# These are the signals read in fanin
self._declarations.append(f"logic {inst_name}_fanin_ready{array_str};")
self._declarations.append(f"logic {inst_name}_fanin_err{array_str};")
self._declarations.append(
f"logic [{self._cpuif.data_width - 1}:0] {inst_name}_fanin_data{array_str};"
)
def _generate_intermediate_assignments(self, node: AddressableNode) -> str:
"""Generate assignments from interface array to intermediate signals."""
inst_name = node.inst_name
indexed_path = get_indexed_path(node.parent, node, "gi", skip_kw_filter=True)
# Get master prefix - use getattr to avoid type errors
interface = getattr(self._cpuif, "_interface", None)
if interface is None:
return ""
master_prefix = interface.get_master_prefix()
# Array dimensions should be checked before calling this function
if not node.array_dimensions:
return ""
# Create indexed signal names for left-hand side
array_idx = "".join(f"[gi{i}]" for i in range(len(node.array_dimensions)))
# Delegate to cpuif to get the appropriate assignments for this interface type
assignments = self._cpuif.fanin_intermediate_assignments(
node, inst_name, array_idx, master_prefix, indexed_path
)
return "\n".join(assignments)
def get_declarations(self) -> str:
"""Get all intermediate signal declarations."""
if not self._declarations:
return ""
return "\n".join(self._declarations)
def __str__(self) -> str:
"""Get all intermediate signal declarations and assignments."""
if not self._declarations:
return ""
# Output declarations first
output = "\n".join(self._declarations)
output += "\n\n"
# Then output assignments
body_str = "\n".join(map(str, self._stack))
if body_str and body_str.strip():
output += body_str
return output

View File

@@ -23,6 +23,17 @@ class FanoutGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
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:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(

View File

@@ -85,6 +85,20 @@ class DecodeLogicGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
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.extend(self.cpuif_addr_predicate(node))
conditions.extend(self.cpuif_prot_predicate(node))
@@ -146,6 +160,8 @@ class DecodeLogicGenerator(BusDecoderListener):
def __str__(self) -> str:
body = self._decode_stack[-1]
if isinstance(body, IfBody):
if len(body) == 0:
return f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"
with body.cm(...) as b:
b += f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"

View File

@@ -1,6 +1,6 @@
from typing import TypedDict
from systemrdl.node import AddrmapNode
from systemrdl.node import AddressableNode, AddrmapNode
from systemrdl.rdltypes.user_enum import UserEnum
from .design_scanner import DesignScanner
@@ -72,3 +72,56 @@ class DesignState:
if user_addr_width < 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
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.
max_decode_depth: int
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 isinstance(node, RootNode):

View File

@@ -15,7 +15,12 @@ class BusDecoderListener(RDLListener):
def should_skip_node(self, node: AddressableNode) -> bool:
"""Check if this node should be skipped (not decoded)."""
# 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
# Check if this node only contains external addressable children

View File

@@ -51,7 +51,7 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
always_comb begin
// Default all write select signals to 0
cpuif_wr_sel = '0;
cpuif_wr_sel = '{default: '0};
if (cpuif_req && cpuif_wr_en) begin
// A write request is pending
@@ -66,7 +66,7 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
always_comb begin
// Default all read select signals to 0
cpuif_rd_sel = '0;
cpuif_rd_sel = '{default: '0};
if (cpuif_req && cpuif_rd_en) begin
// A read request is pending

View File

@@ -3,7 +3,7 @@ from collections import deque
from systemrdl.node import AddressableNode
from systemrdl.walker import WalkerAction
from .body import Body, StructBody
from .body import StructBody
from .design_state import DesignState
from .identifier_filter import kw_filter as kwf
from .listener import BusDecoderListener
@@ -16,42 +16,53 @@ class StructGenerator(BusDecoderListener):
) -> None:
super().__init__(ds)
self._stack: deque[Body] = deque()
self._stack.append(StructBody("cpuif_sel_t", True, True))
self._stack: list[StructBody] = [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:
action = super().enter_AddressableComponent(node)
self._skip = False
if action == WalkerAction.SkipDescendants:
self._skip = True
skip = action == WalkerAction.SkipDescendants
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
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, True)
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, False)
self._stack.append(body)
self._created_struct_stack.append(True)
else:
self._created_struct_stack.append(False)
return action
def exit_AddressableComponent(self, node: AddressableNode) -> None:
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()
if body and isinstance(body, StructBody) and not self._skip:
self._stack.appendleft(body)
if body:
self._struct_defs.append(body)
type = body.name
name = kwf(node.inst_name)
if node.array_dimensions:
for dim in node.array_dimensions:
name = f"[{dim - 1}:0]{name}"
name = f"{name}[{dim}]"
self._stack[-1] += f"{type} {name};"
super().exit_AddressableComponent(node)
def __str__(self) -> str:
self._stack[-1] += "logic cpuif_err;"
return "\n".join(map(str, self._stack))
if "logic cpuif_err;" not in self._stack[-1].lines:
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
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
from collections.abc import Callable

View File

@@ -89,7 +89,8 @@ class TestBusDecoderExporter:
exporter = BusDecoderExporter()
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
module_file = tmp_path / "outer_block.sv"

View File

@@ -0,0 +1,131 @@
"""Test Questa simulator compatibility for instance arrays."""
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_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that instance arrays generate Questa-compatible code.
This test ensures that:
- Struct members for arrays use unpacked array syntax (name[dim])
- NOT packed bit-vector syntax ([dim-1:0]name)
- Struct is unpacked (not packed)
- Array indexing with loop variables works correctly
This fixes the error: "Nonconstant index into instance array"
"""
rdl_source = """
addrmap test_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[4] @ 0x0 += 0x10;
};
"""
top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text()
# Should use unpacked struct
assert "typedef struct {" in content
assert "typedef struct packed" not in content
# Should use unpacked array syntax for array members
assert "logic my_reg[4];" in content
# Should NOT use packed bit-vector syntax
assert "[3:0]my_reg" not in content
# Should have proper array indexing in decode logic
assert "cpuif_wr_sel.my_reg[i0] = 1'b1;" in content
assert "cpuif_rd_sel.my_reg[i0] = 1'b1;" in content
# Should have proper array indexing in fanout/fanin logic
assert "cpuif_wr_sel.my_reg[gi0]" in content or "cpuif_rd_sel.my_reg[gi0]" in content
assert "cpuif_wr_sel.my_reg[i0]" in content or "cpuif_rd_sel.my_reg[i0]" in content
def test_multidimensional_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that multidimensional instance arrays generate Questa-compatible code."""
rdl_source = """
addrmap test_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[2][3] @ 0x0 += 0x10;
};
"""
top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text()
# Should use unpacked struct with multidimensional array
assert "typedef struct {" in content
# Should use unpacked array syntax for multidimensional arrays
assert "logic my_reg[2][3];" in content
# Should NOT use packed bit-vector syntax
assert "[1:0][2:0]my_reg" not in content
assert "[5:0]my_reg" not in content
def test_nested_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that nested instance arrays generate Questa-compatible code."""
rdl_source = """
addrmap inner_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg[2] @ 0x0 += 0x10;
};
addrmap outer_map {
inner_map inner[3] @ 0x0 += 0x100;
};
"""
top = compile_rdl(rdl_source, top="outer_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "outer_map.sv"
content = module_file.read_text()
# Should use unpacked struct
assert "typedef struct {" in content
# Inner should be an array
# The exact syntax may vary, but it should be unpacked
# Look for the pattern of unpacked arrays, not packed bit-vectors
assert "inner[3]" in content or "logic inner" in content
# Should NOT use packed bit-vector syntax like [2:0]inner
assert "[2:0]inner" not in content

View File

@@ -96,3 +96,10 @@ class TestStructGenerator:
assert "[" in result and "]" in result
# Should reference the register
assert "my_regs" in result
# Should use unpacked array syntax (name[size]), not packed bit-vector ([size:0]name)
assert "my_regs[4]" in result
# Should NOT use packed bit-vector syntax
assert "[3:0]my_regs" not in result
# Should be unpacked struct, not packed
assert "typedef struct {" in result
assert "typedef struct packed" not in result

View File

@@ -41,9 +41,9 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
assert "cpuif_rd_sel.multicast.response" not in content
# Verify struct is flat (no nested structs for external children)
assert "typedef struct packed" in content
assert "typedef struct" in content
assert "logic multicast;" in content
assert "logic [15:0]port;" in content
assert "logic port[16];" in content
def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None:
@@ -101,7 +101,8 @@ def test_non_external_nested_components_are_descended(compile_rdl: Callable[...,
with TemporaryDirectory() as tmpdir:
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
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

View File

@@ -9,7 +9,6 @@ invocation constructed from stdin tokens.
from __future__ import annotations
import os
import subprocess
import sys

2
uv.lock generated
View File

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