Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7481e71ba | ||
|
|
858a7870ad | ||
|
|
3d823572cc | ||
|
|
f829e3894f | ||
|
|
74eb2344b1 |
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@@ -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;",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
142
src/peakrdl_busdecoder/cpuif/fanin_intermediate_gen.py
Normal file
142
src/peakrdl_busdecoder/cpuif/fanin_intermediate_gen.py
Normal 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
|
||||
@@ -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(
|
||||
|
||||
@@ -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;"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
211
tests/cocotb/apb3/smoke/test_variable_depth.py
Normal file
211
tests/cocotb/apb3/smoke/test_variable_depth.py
Normal 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"
|
||||
128
tests/cocotb/apb3/smoke/test_variable_depth_runner.py
Normal file
128
tests/cocotb/apb3/smoke/test_variable_depth_runner.py
Normal 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",
|
||||
)
|
||||
227
tests/cocotb/apb4/smoke/test_variable_depth.py
Normal file
227
tests/cocotb/apb4/smoke/test_variable_depth.py
Normal 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"
|
||||
131
tests/cocotb/apb4/smoke/test_variable_depth_runner.py
Normal file
131
tests/cocotb/apb4/smoke/test_variable_depth_runner.py
Normal 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",
|
||||
)
|
||||
271
tests/cocotb/axi4lite/smoke/test_variable_depth.py
Normal file
271
tests/cocotb/axi4lite/smoke/test_variable_depth.py
Normal 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"
|
||||
128
tests/cocotb/axi4lite/smoke/test_variable_depth_runner.py
Normal file
128
tests/cocotb/axi4lite/smoke/test_variable_depth_runner.py
Normal 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",
|
||||
)
|
||||
31
tests/cocotb_lib/variable_depth.rdl
Normal file
31
tests/cocotb_lib/variable_depth.rdl
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
131
tests/generator/test_questa_compatibility.py
Normal file
131
tests/generator/test_questa_compatibility.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
256
tests/unit/test_max_decode_depth.py
Normal file
256
tests/unit/test_max_decode_depth.py
Normal 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
|
||||
@@ -9,7 +9,6 @@ invocation constructed from stdin tokens.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
Reference in New Issue
Block a user