From d7481e71baeada06fae07278110dcbbd4fd5eb62 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:38:54 -0700 Subject: [PATCH] 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> --- pyproject.toml | 2 +- src/peakrdl_busdecoder/__peakrdl__.py | 4 +- src/peakrdl_busdecoder/cpuif/base_cpuif.py | 6 +- src/peakrdl_busdecoder/cpuif/fanin_gen.py | 11 + src/peakrdl_busdecoder/cpuif/fanout_gen.py | 11 + src/peakrdl_busdecoder/decode_logic_gen.py | 16 ++ src/peakrdl_busdecoder/design_state.py | 55 +++- src/peakrdl_busdecoder/exporter.py | 4 +- src/peakrdl_busdecoder/listener.py | 7 +- src/peakrdl_busdecoder/struct_gen.py | 35 ++- .../cocotb/apb3/smoke/test_variable_depth.py | 211 ++++++++++++++ .../apb3/smoke/test_variable_depth_runner.py | 128 +++++++++ .../cocotb/apb4/smoke/test_variable_depth.py | 227 +++++++++++++++ .../apb4/smoke/test_variable_depth_runner.py | 131 +++++++++ .../axi4lite/smoke/test_variable_depth.py | 271 ++++++++++++++++++ .../smoke/test_variable_depth_runner.py | 128 +++++++++ tests/cocotb_lib/variable_depth.rdl | 31 ++ tests/conftest.py | 2 +- tests/exporter/test_bus_decoder_exporter.py | 3 +- tests/unit/test_external_nested.py | 3 +- tests/unit/test_max_decode_depth.py | 256 +++++++++++++++++ uv.lock | 2 +- 22 files changed, 1518 insertions(+), 26 deletions(-) create mode 100644 tests/cocotb/apb3/smoke/test_variable_depth.py create mode 100644 tests/cocotb/apb3/smoke/test_variable_depth_runner.py create mode 100644 tests/cocotb/apb4/smoke/test_variable_depth.py create mode 100644 tests/cocotb/apb4/smoke/test_variable_depth_runner.py create mode 100644 tests/cocotb/axi4lite/smoke/test_variable_depth.py create mode 100644 tests/cocotb/axi4lite/smoke/test_variable_depth_runner.py create mode 100644 tests/cocotb_lib/variable_depth.rdl create mode 100644 tests/unit/test_max_decode_depth.py diff --git a/pyproject.toml b/pyproject.toml index f5bf35f..8cd9acb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "peakrdl-busdecoder" -version = "0.3.0" +version = "0.4.0" requires-python = ">=3.10" dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"] diff --git a/src/peakrdl_busdecoder/__peakrdl__.py b/src/peakrdl_busdecoder/__peakrdl__.py index b74a2d1..2adeb85 100644 --- a/src/peakrdl_busdecoder/__peakrdl__.py +++ b/src/peakrdl_busdecoder/__peakrdl__.py @@ -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. """, ) diff --git a/src/peakrdl_busdecoder/cpuif/base_cpuif.py b/src/peakrdl_busdecoder/cpuif/base_cpuif.py index ff40b27..519d83a 100644 --- a/src/peakrdl_busdecoder/cpuif/base_cpuif.py +++ b/src/peakrdl_busdecoder/cpuif/base_cpuif.py @@ -25,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: diff --git a/src/peakrdl_busdecoder/cpuif/fanin_gen.py b/src/peakrdl_busdecoder/cpuif/fanin_gen.py index 972dcaf..d5fc598 100644 --- a/src/peakrdl_busdecoder/cpuif/fanin_gen.py +++ b/src/peakrdl_busdecoder/cpuif/fanin_gen.py @@ -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( diff --git a/src/peakrdl_busdecoder/cpuif/fanout_gen.py b/src/peakrdl_busdecoder/cpuif/fanout_gen.py index e877d14..abb513f 100644 --- a/src/peakrdl_busdecoder/cpuif/fanout_gen.py +++ b/src/peakrdl_busdecoder/cpuif/fanout_gen.py @@ -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( diff --git a/src/peakrdl_busdecoder/decode_logic_gen.py b/src/peakrdl_busdecoder/decode_logic_gen.py index d221701..90ff59f 100644 --- a/src/peakrdl_busdecoder/decode_logic_gen.py +++ b/src/peakrdl_busdecoder/decode_logic_gen.py @@ -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;" diff --git a/src/peakrdl_busdecoder/design_state.py b/src/peakrdl_busdecoder/design_state.py index e71fbdf..a646b99 100644 --- a/src/peakrdl_busdecoder/design_state.py +++ b/src/peakrdl_busdecoder/design_state.py @@ -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 diff --git a/src/peakrdl_busdecoder/exporter.py b/src/peakrdl_busdecoder/exporter.py index 1a9467a..83e60c6 100644 --- a/src/peakrdl_busdecoder/exporter.py +++ b/src/peakrdl_busdecoder/exporter.py @@ -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): diff --git a/src/peakrdl_busdecoder/listener.py b/src/peakrdl_busdecoder/listener.py index e861130..0b7c5c2 100644 --- a/src/peakrdl_busdecoder/listener.py +++ b/src/peakrdl_busdecoder/listener.py @@ -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 diff --git a/src/peakrdl_busdecoder/struct_gen.py b/src/peakrdl_busdecoder/struct_gen.py index 117f4fd..f616cf9 100644 --- a/src/peakrdl_busdecoder/struct_gen.py +++ b/src/peakrdl_busdecoder/struct_gen.py @@ -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,30 +16,38 @@ class StructGenerator(BusDecoderListener): ) -> None: super().__init__(ds) - self._stack: deque[Body] = deque() - self._stack.append(StructBody("cpuif_sel_t", True, False)) + 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, 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) @@ -53,5 +61,8 @@ class StructGenerator(BusDecoderListener): 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) diff --git a/tests/cocotb/apb3/smoke/test_variable_depth.py b/tests/cocotb/apb3/smoke/test_variable_depth.py new file mode 100644 index 0000000..c2251e6 --- /dev/null +++ b/tests/cocotb/apb3/smoke/test_variable_depth.py @@ -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" diff --git a/tests/cocotb/apb3/smoke/test_variable_depth_runner.py b/tests/cocotb/apb3/smoke/test_variable_depth_runner.py new file mode 100644 index 0000000..1f9e975 --- /dev/null +++ b/tests/cocotb/apb3/smoke/test_variable_depth_runner.py @@ -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", + ) diff --git a/tests/cocotb/apb4/smoke/test_variable_depth.py b/tests/cocotb/apb4/smoke/test_variable_depth.py new file mode 100644 index 0000000..5080f4d --- /dev/null +++ b/tests/cocotb/apb4/smoke/test_variable_depth.py @@ -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" diff --git a/tests/cocotb/apb4/smoke/test_variable_depth_runner.py b/tests/cocotb/apb4/smoke/test_variable_depth_runner.py new file mode 100644 index 0000000..62825e3 --- /dev/null +++ b/tests/cocotb/apb4/smoke/test_variable_depth_runner.py @@ -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", + ) diff --git a/tests/cocotb/axi4lite/smoke/test_variable_depth.py b/tests/cocotb/axi4lite/smoke/test_variable_depth.py new file mode 100644 index 0000000..0dac3fc --- /dev/null +++ b/tests/cocotb/axi4lite/smoke/test_variable_depth.py @@ -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" diff --git a/tests/cocotb/axi4lite/smoke/test_variable_depth_runner.py b/tests/cocotb/axi4lite/smoke/test_variable_depth_runner.py new file mode 100644 index 0000000..5a6585a --- /dev/null +++ b/tests/cocotb/axi4lite/smoke/test_variable_depth_runner.py @@ -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", + ) diff --git a/tests/cocotb_lib/variable_depth.rdl b/tests/cocotb_lib/variable_depth.rdl new file mode 100644 index 0000000..8c7a9fb --- /dev/null +++ b/tests/cocotb_lib/variable_depth.rdl @@ -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; +}; diff --git a/tests/conftest.py b/tests/conftest.py index 4beba1c..b5663ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/exporter/test_bus_decoder_exporter.py b/tests/exporter/test_bus_decoder_exporter.py index f5bdbaf..a416247 100644 --- a/tests/exporter/test_bus_decoder_exporter.py +++ b/tests/exporter/test_bus_decoder_exporter.py @@ -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" diff --git a/tests/unit/test_external_nested.py b/tests/unit/test_external_nested.py index 461643a..aca9f28 100644 --- a/tests/unit/test_external_nested.py +++ b/tests/unit/test_external_nested.py @@ -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" diff --git a/tests/unit/test_max_decode_depth.py b/tests/unit/test_max_decode_depth.py new file mode 100644 index 0000000..d6c479c --- /dev/null +++ b/tests/unit/test_max_decode_depth.py @@ -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 diff --git a/uv.lock b/uv.lock index 552c16e..214ef75 100644 --- a/uv.lock +++ b/uv.lock @@ -608,7 +608,7 @@ wheels = [ [[package]] name = "peakrdl-busdecoder" -version = "0.3.0" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "jinja2" },