11 Commits

Author SHA1 Message Date
Arnav Sacheti
ae17384b3b version bump 2025-10-26 19:06:29 -07:00
Copilot
b80f166997 Export master interface address widths in package parameters (#16)
* Initial plan

* Add master address width parameters to exported package

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-26 19:05:45 -07:00
Copilot
95fda3abaa Refactor cpuif classes to use Interface abstraction (#14)
* Initial plan

* Refactor cpuif classes to use Interface abstraction

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

* Fix type annotation consistency in Interface.signal()

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

* Add runtime validation and documentation for indexer types

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

* Remove unused variable in SVInterface.signal()

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

* Fix master port directions in APB3 and APB4 flat interfaces

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

* Fix AXI4LiteCpuifFlat and apply code formatting

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

* PSELx -> PSEL

* cleanup marker warnings

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-26 18:47:11 -07:00
Arnav Sacheti
1eababe1ab remove cocotb -sim.yml 2025-10-26 18:00:53 -07:00
Arnav Sacheti
b1f1bf983a Refactor tests (better grouping + cocotb support) (#15)
* initial refactor

* fix cocotb tests

* fix typecheck

* install verilator
2025-10-26 17:56:35 -07:00
Arnav Sacheti
93276ff616 fix (#13)
* fix

* fix pyrefly

* remove tests

* Update tests/unit/test_exporter.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/peakrdl_busdecoder/listener.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/unit/test_exporter.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix iter

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-26 14:40:03 -07:00
Copilot
c9addd6ac2 Fix decoder generation for external nested addressable components and add max-decode-depth parameter (#12)
* Initial plan

* Fix bus decoder to skip external nested components

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

* Optimize external children check using generator expressions

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

* Add max-decode-depth CLI argument

Added --max-decode-depth argument that:
- Is added to CLI arguments in __peakrdl__.py
- Piped into design state via ExporterKwargs and DesignStateKwargs
- Used to control max depth in listener.py
- All 66 tests pass including new test for the parameter

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-25 19:56:49 -07:00
Copilot
04971bdb8e Fix non-synthesizable code generation for nested addrmaps with arrays (#11)
* Initial plan

* Fix non-synthesizable code for nested addrmaps with arrays

Fixed bug where array dimensions were used instead of strides in decode logic.
For nested addrmaps with arrays like inner[4] @ 0x0 += 0x100, the generated
code was incorrectly using the dimension (4) instead of the stride (0x100).
This resulted in non-synthesizable SystemVerilog with incorrect address decoding.

The fix calculates proper strides for each dimension, including support for
multi-dimensional arrays like [2][3] where each dimension has a different stride.

Added comprehensive tests to prevent regression.

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

* Improve code comments for stride calculation clarity

Added more detailed comments explaining the stride calculation logic,
including a concrete example showing how strides are calculated for
multi-dimensional arrays.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-24 10:35:28 -07:00
Copilot
9b6dbc30e2 Fix APB4 assertion syntax for Questa 2025 compatibility (#10)
* Initial plan

* Fix APB4 assertion syntax for Questa 2025 compatibility

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-24 09:00:32 -07:00
Copilot
4dc61d24ca Add cocotb testbench for validating generated bus decoder RTL across APB3, APB4, and AXI4-Lite interfaces (#9)
* Initial plan

* Add cocotb test infrastructure and testbenches for APB3, APB4, and AXI4-Lite

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

* Add integration tests, examples, and documentation for cocotb testbenches

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

* Address code review feedback: use relative imports and update installation docs

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

* Add implementation summary document

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

* Merge cocotb dependencies into test group

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

* Add optional cocotb simulation workflow with Icarus Verilog

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-23 23:46:51 -07:00
Arnav Sacheti
0b98165ccc update tmpl 2025-10-23 23:42:17 -07:00
71 changed files with 3201 additions and 1724 deletions

View File

@@ -7,6 +7,9 @@ on:
branches: [ main ]
workflow_dispatch:
workflow_call:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: '0 0 * * 1'
jobs:
test:
@@ -24,10 +27,16 @@ jobs:
uses: astral-sh/setup-uv@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install Verilator
run: |
sudo apt-get update
sudo apt-get install -y verilator
verilator --version
- name: Install dependencies
run: |
uv sync --group test
uv sync --all-extras --group test
- name: Run tests
run: uv run pytest tests/ -v --cov=peakrdl_busdecoder --cov-report=xml --cov-report=term

View File

@@ -2,6 +2,10 @@ interface apb3_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
// Clocking
logic PCLK;
logic PRESETn;
// Command
logic PSEL;
logic PENABLE;
@@ -15,6 +19,9 @@ interface apb3_intf #(
logic PSLVERR;
modport master (
input PCLK,
input PRESETn,
output PSEL,
output PENABLE,
output PWRITE,
@@ -27,6 +34,9 @@ interface apb3_intf #(
);
modport slave (
input PCLK,
input PRESETn,
input PSEL,
input PENABLE,
input PWRITE,

View File

@@ -2,6 +2,10 @@ interface apb4_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
// Clocking
logic PCLK;
logic PRESETn;
// Command
logic PSEL;
logic PENABLE;
@@ -17,6 +21,9 @@ interface apb4_intf #(
logic PSLVERR;
modport master (
input PCLK,
input PRESETn,
output PSEL,
output PENABLE,
output PWRITE,
@@ -31,6 +38,9 @@ interface apb4_intf #(
);
modport slave (
input PCLK,
input PRESETn,
input PSEL,
input PENABLE,
input PWRITE,

View File

@@ -2,6 +2,9 @@ interface axi4lite_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
logic ACLK;
logic ARESETn;
logic AWREADY;
logic AWVALID;
logic [ADDR_WIDTH-1:0] AWADDR;
@@ -27,6 +30,9 @@ interface axi4lite_intf #(
logic [1:0] RRESP;
modport master (
input ACLK,
input ARESETn,
input AWREADY,
output AWVALID,
output AWADDR,
@@ -53,15 +59,18 @@ interface axi4lite_intf #(
);
modport slave (
input ACLK,
input ARESETn,
output AWREADY,
// input AWVALID,
// input AWADDR,
input AWVALID,
input AWADDR,
input AWPROT,
output WREADY,
// input WVALID,
// input WDATA,
// input WSTRB,
input WVALID,
input WDATA,
input WSTRB,
input BREADY,
output BVALID,
@@ -73,8 +82,8 @@ interface axi4lite_intf #(
input ARPROT,
input RREADY,
// output RVALID,
// output RDATA,
// output RRESP
output RVALID,
output RDATA,
output RRESP
);
endinterface

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "peakrdl-busdecoder"
version = "0.1.0"
version = "0.2.0"
requires-python = ">=3.10"
dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"]
@@ -59,13 +59,15 @@ test = [
"pytest>=7.4.4",
"pytest-cov>=4.1.0",
"pytest-xdist>=3.5.0",
"cocotb>=1.8.0",
"cocotb-bus>=0.2.1",
]
tools = ["pyrefly>=0.37.0", "ruff>=0.14.0"]
[project.entry-points."peakrdl.exporters"]
busdecoder = "peakrdl_busdecoder.__peakrdl__:Exporter"
# ---------------------- RUFF ----------------------
[tool.ruff]
line-length = 110
target-version = "py310"
@@ -102,5 +104,13 @@ python-version = "3.10"
# Default behavior: check bodies of untyped defs & infer return types.
untyped-def-behavior = "check-and-infer-return-type"
project-includes = ["**/*"]
project-includes = ["src/**/*"]
project-excludes = ["**/__pycache__", "**/*venv/**/*"]
# ---------------------- PYTEST ----------------------
[tool.pytest.ini_options]
python_files = ["test_*.py", "*_test.py"]
markers = [
"simulation: marks tests as requiring cocotb simulation (deselect with '-m \"not simulation\"')",
"verilator: marks tests as requiring verilator simulator (deselect with '-m \"not verilator\"')",
]

View File

@@ -111,6 +111,15 @@ class Exporter(ExporterSubcommandPlugin):
""",
)
arg_group.add_argument(
"--max-decode-depth",
type=int,
default=1,
help="""Maximum depth for address decoder to descend into nested
addressable components. Default is 1.
""",
)
def do_export(self, top_node: "AddrmapNode", options: "argparse.Namespace") -> None:
cpuifs = self.get_cpuifs()
@@ -123,4 +132,5 @@ class Exporter(ExporterSubcommandPlugin):
package_name=options.package_name,
address_width=options.addr_width,
cpuif_unroll=options.unroll,
max_decode_depth=options.max_decode_depth,
)

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from textwrap import indent
from types import EllipsisType
@@ -50,7 +48,7 @@ class IfBody(Body):
# --- Context manager for a branch ---
class _BranchCtx:
def __init__(self, outer: IfBody, condition: SupportsStr | None) -> None:
def __init__(self, outer: "IfBody", condition: SupportsStr | None) -> None:
self._outer = outer
# route through __getitem__ to reuse validation logic
self._body = outer[Ellipsis if condition is None else condition]
@@ -66,7 +64,7 @@ class IfBody(Body):
) -> bool:
return False
def cm(self, condition: SupportsStr | None) -> IfBody._BranchCtx:
def cm(self, condition: SupportsStr | None) -> "IfBody._BranchCtx":
"""Use with: with ifb.cm('cond') as b: ... ; use None for else."""
return IfBody._BranchCtx(self, condition)

View File

@@ -1,47 +1,36 @@
from typing import overload
from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .apb3_interface import APB3SVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB3Cpuif(BaseCpuif):
template_path = "apb3_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"apb3_intf.master m_apb_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB3SVInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
slave_ports: list[str] = ["apb3_intf.slave s_apb"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_apb", "m_apb_")
@overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_apb.{signal}"
# Master signal
return f"m_apb_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}

View File

@@ -1,46 +1,29 @@
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .apb3_interface import APB3FlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB3CpuifFlat(BaseCpuif):
template_path = "apb3_tmpl.sv"
is_interface = False
def _port_declaration(self, child: AddressableNode) -> list[str]:
return [
f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}",
]
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB3FlatInterface(self)
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
slave_ports: list[str] = [
f"input logic {self.signal('PCLK')}",
f"input logic {self.signal('PRESETn')}",
f"input logic {self.signal('PSELx')}",
f"input logic {self.signal('PENABLE')}",
f"input logic {self.signal('PWRITE')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR')}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA')}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA')}",
f"output logic {self.signal('PREADY')}",
f"output logic {self.signal('PSLVERR')}",
]
master_ports: list[str] = []
for child in self.addressable_children:
master_ports.extend(self._port_declaration(child))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_apb_", "m_apb_")
def signal(
self,
@@ -48,26 +31,11 @@ class APB3CpuifFlat(BaseCpuif):
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb_{signal}"
# Master signal
base = f"m_apb_{node.inst_name}"
if not self.check_is_array(node):
# Not an array or an unrolled element
if node.current_idx is not None:
# This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}"
# Is an array
if idx is not None:
return f"{base}_{signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
return self._interface.signal(signal, node, idx)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}
fanout[self.signal("PSELx", node)] = (
fanout[self.signal("PSEL", node)] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}"
)
fanout[self.signal("PENABLE", node)] = self.signal("PENABLE")

View File

@@ -0,0 +1,56 @@
"""APB3-specific interface implementations."""
from systemrdl.node import AddressableNode
from ..interface import FlatInterface, SVInterface
class APB3SVInterface(SVInterface):
"""APB3 SystemVerilog interface."""
def get_interface_type(self) -> str:
return "apb3_intf"
def get_slave_name(self) -> str:
return "s_apb"
def get_master_prefix(self) -> str:
return "m_apb_"
class APB3FlatInterface(FlatInterface):
"""APB3 flat signal interface."""
def get_slave_prefix(self) -> str:
return "s_apb_"
def get_master_prefix(self) -> str:
return "m_apb_"
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
return [
f"input logic {slave_prefix}PCLK",
f"input logic {slave_prefix}PRESETn",
f"input logic {slave_prefix}PSEL",
f"input logic {slave_prefix}PENABLE",
f"input logic {slave_prefix}PWRITE",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}PADDR",
f"input logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PWDATA",
f"output logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PRDATA",
f"output logic {slave_prefix}PREADY",
f"output logic {slave_prefix}PSLVERR",
]
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
return [
f"output logic {self.signal('PCLK', child)}",
f"output logic {self.signal('PRESETn', child)}",
f"output logic {self.signal('PSEL', child)}",
f"output logic {self.signal('PENABLE', child)}",
f"output logic {self.signal('PWRITE', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"input logic [{self.cpuif.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"input logic {self.signal('PREADY', child)}",
f"input logic {self.signal('PSLVERR', child)}",
]

View File

@@ -1,48 +1,37 @@
from typing import overload
from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .apb4_interface import APB4SVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB4Cpuif(BaseCpuif):
template_path = "apb4_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"apb4_intf.master m_apb_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB4SVInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the APB4 interface."""
slave_ports: list[str] = ["apb4_intf.slave s_apb"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_apb", "m_apb_")
@overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_apb.{signal}"
# Master signal
return f"m_apb_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}

View File

@@ -1,50 +1,29 @@
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .apb4_interface import APB4FlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB4CpuifFlat(BaseCpuif):
template_path = "apb4_tmpl.sv"
is_interface = False
def _port_declaration(self, child: AddressableNode) -> list[str]:
return [
f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [2:0] {self.signal('PPROT', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}",
]
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB4FlatInterface(self)
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
slave_ports: list[str] = [
f"input logic {self.signal('PCLK')}",
f"input logic {self.signal('PRESETn')}",
f"input logic {self.signal('PSELx')}",
f"input logic {self.signal('PENABLE')}",
f"input logic {self.signal('PWRITE')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR')}",
f"input logic [2:0] {self.signal('PPROT')}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA')}",
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB')}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA')}",
f"output logic {self.signal('PREADY')}",
f"output logic {self.signal('PSLVERR')}",
]
master_ports: list[str] = []
for child in self.addressable_children:
master_ports.extend(self._port_declaration(child))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_apb_", "m_apb_")
def signal(
self,
@@ -52,26 +31,11 @@ class APB4CpuifFlat(BaseCpuif):
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb_{signal}"
# Master signal
base = f"m_apb_{node.inst_name}"
if not self.check_is_array(node):
# Not an array or an unrolled element
if node.current_idx is not None:
# This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}"
# Is an array
if idx is not None:
return f"{base}_{signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
return self._interface.signal(signal, node, idx)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}
fanout[self.signal("PSELx", node)] = (
fanout[self.signal("PSEL", node)] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}"
)
fanout[self.signal("PENABLE", node)] = self.signal("PENABLE")

View File

@@ -0,0 +1,60 @@
"""APB4-specific interface implementations."""
from systemrdl.node import AddressableNode
from ..interface import FlatInterface, SVInterface
class APB4SVInterface(SVInterface):
"""APB4 SystemVerilog interface."""
def get_interface_type(self) -> str:
return "apb4_intf"
def get_slave_name(self) -> str:
return "s_apb"
def get_master_prefix(self) -> str:
return "m_apb_"
class APB4FlatInterface(FlatInterface):
"""APB4 flat signal interface."""
def get_slave_prefix(self) -> str:
return "s_apb_"
def get_master_prefix(self) -> str:
return "m_apb_"
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
return [
f"input logic {slave_prefix}PCLK",
f"input logic {slave_prefix}PRESETn",
f"input logic {slave_prefix}PSEL",
f"input logic {slave_prefix}PENABLE",
f"input logic {slave_prefix}PWRITE",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}PADDR",
f"input logic [2:0] {slave_prefix}PPROT",
f"input logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PWDATA",
f"input logic [{self.cpuif.data_width // 8 - 1}:0] {slave_prefix}PSTRB",
f"output logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PRDATA",
f"output logic {slave_prefix}PREADY",
f"output logic {slave_prefix}PSLVERR",
]
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
return [
f"output logic {self.signal('PCLK', child)}",
f"output logic {self.signal('PRESETn', child)}",
f"output logic {self.signal('PSEL', child)}",
f"output logic {self.signal('PENABLE', child)}",
f"output logic {self.signal('PWRITE', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"output logic [2:0] {self.signal('PPROT', child)}",
f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"output logic [{self.cpuif.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",
f"input logic [{self.cpuif.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"input logic {self.signal('PREADY', child)}",
f"input logic {self.signal('PSLVERR', child)}",
]

View File

@@ -6,7 +6,7 @@
assert_bad_data_width: assert($bits({{cpuif.signal("PWDATA")}}) == {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH)
else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("PWDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end
assert_wr_sel: assert (@(posedge {{cpuif.signal("PCLK")}}) {{cpuif.signal("PSEL")}} && {{cpuif.signal("PWRITE")}} |-> ##1 ({{cpuif.signal("PREADY")}} || {{cpuif.signal("PSLVERR")}}))
assert_wr_sel: assert property (@(posedge {{cpuif.signal("PCLK")}}) {{cpuif.signal("PSEL")}} && {{cpuif.signal("PWRITE")}} |-> ##1 ({{cpuif.signal("PREADY")}} || {{cpuif.signal("PSLVERR")}}))
else $error("APB4 Slave port SEL implies that cpuif_wr_sel must be one-hot encoded");
`endif
{%- endif %}

View File

@@ -1,48 +1,37 @@
from typing import overload
from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteSVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class AXI4LiteCpuif(BaseCpuif):
template_path = "axi4lite_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"axi4lite_intf.master m_axil_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = AXI4LiteSVInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the AXI4-Lite interface."""
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_axil", "m_axil_")
@overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
def signal(self, signal: str, node: AddressableNode, indexer: str | None = None) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_axil.{signal}"
# Master signal
return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}

View File

@@ -1,48 +1,39 @@
from typing import overload
from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteFlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class AXI4LiteCpuifFlat(BaseCpuif):
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
template_path = "axi4lite_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"axi4lite_intf.master m_axil_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = AXI4LiteFlatInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the AXI4-Lite interface."""
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_axil_", "m_axil_")
@overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
def signal(self, signal: str, node: AddressableNode, indexer: str | None = None) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_axil.{signal}"
# Master signal
return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}

View File

@@ -0,0 +1,84 @@
"""AXI4-Lite-specific interface implementations."""
from systemrdl.node import AddressableNode
from ..interface import FlatInterface, SVInterface
class AXI4LiteSVInterface(SVInterface):
"""AXI4-Lite SystemVerilog interface."""
def get_interface_type(self) -> str:
return "axi4lite_intf"
def get_slave_name(self) -> str:
return "s_axil"
def get_master_prefix(self) -> str:
return "m_axil_"
class AXI4LiteFlatInterface(FlatInterface):
"""AXI4-Lite flat signal interface."""
def get_slave_prefix(self) -> str:
return "s_axil_"
def get_master_prefix(self) -> str:
return "m_axil_"
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
return [
# Write address channel
f"input logic {slave_prefix}AWVALID",
f"output logic {slave_prefix}AWREADY",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}AWADDR",
f"input logic [2:0] {slave_prefix}AWPROT",
# Write data channel
f"input logic {slave_prefix}WVALID",
f"output logic {slave_prefix}WREADY",
f"input logic [{self.cpuif.data_width - 1}:0] {slave_prefix}WDATA",
f"input logic [{self.cpuif.data_width // 8 - 1}:0] {slave_prefix}WSTRB",
# Write response channel
f"output logic {slave_prefix}BVALID",
f"input logic {slave_prefix}BREADY",
f"output logic [1:0] {slave_prefix}BRESP",
# Read address channel
f"input logic {slave_prefix}ARVALID",
f"output logic {slave_prefix}ARREADY",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}ARADDR",
f"input logic [2:0] {slave_prefix}ARPROT",
# Read data channel
f"output logic {slave_prefix}RVALID",
f"input logic {slave_prefix}RREADY",
f"output logic [{self.cpuif.data_width - 1}:0] {slave_prefix}RDATA",
f"output logic [1:0] {slave_prefix}RRESP",
]
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
return [
# Write address channel
f"output logic {self.signal('AWVALID', child)}",
f"input logic {self.signal('AWREADY', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('AWADDR', child)}",
f"output logic [2:0] {self.signal('AWPROT', child)}",
# Write data channel
f"output logic {self.signal('WVALID', child)}",
f"input logic {self.signal('WREADY', child)}",
f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('WDATA', child)}",
f"output logic [{self.cpuif.data_width // 8 - 1}:0] {self.signal('WSTRB', child)}",
# Write response channel
f"input logic {self.signal('BVALID', child)}",
f"output logic {self.signal('BREADY', child)}",
f"input logic [1:0] {self.signal('BRESP', child)}",
# Read address channel
f"output logic {self.signal('ARVALID', child)}",
f"input logic {self.signal('ARREADY', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('ARADDR', child)}",
f"output logic [2:0] {self.signal('ARPROT', child)}",
# Read data channel
f"input logic {self.signal('RVALID', child)}",
f"output logic {self.signal('RREADY', child)}",
f"input logic [{self.cpuif.data_width - 1}:0] {self.signal('RDATA', child)}",
f"input logic [1:0] {self.signal('RRESP', child)}",
]

View File

@@ -36,18 +36,17 @@ class FaninGenerator(BusDecoderListener):
)
self._stack.append(fb)
if action == WalkerAction.Continue:
ifb = IfBody()
with ifb.cm(
f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)} || cpuif_wr_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}"
) as b:
b += self._cpuif.fanin(node)
self._stack[-1] += ifb
ifb = IfBody()
with ifb.cm(
f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)} || cpuif_wr_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}"
) as b:
b += self._cpuif.fanin(node)
self._stack[-1] += ifb
ifb = IfBody()
with ifb.cm(f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}") as b:
b += self._cpuif.readback(node)
self._stack[-1] += ifb
ifb = IfBody()
with ifb.cm(f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}") as b:
b += self._cpuif.readback(node)
self._stack[-1] += ifb
return action

View File

@@ -32,8 +32,7 @@ class FanoutGenerator(BusDecoderListener):
)
self._stack.append(fb)
if action == WalkerAction.Continue:
self._stack[-1] += self._cpuif.fanout(node)
self._stack[-1] += self._cpuif.fanout(node)
return action

View File

@@ -0,0 +1,190 @@
"""Interface abstraction for handling flat and non-flat signal declarations."""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
if TYPE_CHECKING:
from .base_cpuif import BaseCpuif
class Interface(ABC):
"""Abstract base class for interface signal handling."""
def __init__(self, cpuif: "BaseCpuif") -> None:
self.cpuif = cpuif
@property
@abstractmethod
def is_interface(self) -> bool:
"""Whether this uses SystemVerilog interfaces."""
...
@abstractmethod
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""
Generate port declarations for the interface.
Args:
slave_name: Name of the slave interface/signal prefix
master_prefix: Prefix for master interfaces/signals
Returns:
Port declarations as a string
"""
...
@abstractmethod
def signal(
self,
signal: str,
node: AddressableNode | None = None,
indexer: str | int | None = None,
) -> str:
"""
Generate signal reference.
Args:
signal: Signal name
node: Optional addressable node for master signals
indexer: Optional indexer for arrays.
For SVInterface: str like "i" or "gi" for loop indices
For FlatInterface: str or int for array subscript
Returns:
Signal reference as a string
"""
...
class SVInterface(Interface):
"""SystemVerilog interface-based signal handling."""
@property
def is_interface(self) -> bool:
return True
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""Generate SystemVerilog interface port declarations."""
slave_ports: list[str] = [f"{self.get_interface_type()}.slave {slave_name}"]
master_ports: list[str] = []
for child in self.cpuif.addressable_children:
base = f"{self.get_interface_type()}.master {master_prefix}{child.inst_name}"
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.cpuif.check_is_array(child):
assert child.array_dimensions is not None
base = f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
master_ports.append(base)
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
indexer: str | int | None = None,
) -> str:
"""Generate SystemVerilog interface signal reference."""
from ..utils import get_indexed_path
# SVInterface only supports string indexers (loop variable names like "i", "gi")
if indexer is not None and not isinstance(indexer, str):
raise TypeError(f"SVInterface.signal() requires string indexer, got {type(indexer).__name__}")
if node is None or indexer is None:
# Node is none, so this is a slave signal
slave_name = self.get_slave_name()
return f"{slave_name}.{signal}"
# Master signal
master_prefix = self.get_master_prefix()
return f"{master_prefix}{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
@abstractmethod
def get_interface_type(self) -> str:
"""Get the SystemVerilog interface type name."""
...
@abstractmethod
def get_slave_name(self) -> str:
"""Get the slave interface instance name."""
...
@abstractmethod
def get_master_prefix(self) -> str:
"""Get the master interface name prefix."""
...
class FlatInterface(Interface):
"""Flat signal-based interface handling."""
@property
def is_interface(self) -> bool:
return False
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""Generate flat port declarations."""
slave_ports = self._get_slave_port_declarations(slave_name)
master_ports: list[str] = []
for child in self.cpuif.addressable_children:
master_ports.extend(self._get_master_port_declarations(child, master_prefix))
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
indexer: str | int | None = None,
) -> str:
"""Generate flat signal reference."""
if node is None:
# Node is none, so this is a slave signal
slave_prefix = self.get_slave_prefix()
return f"{slave_prefix}{signal}"
# Master signal
master_prefix = self.get_master_prefix()
base = f"{master_prefix}{node.inst_name}"
if not self.cpuif.check_is_array(node):
# Not an array or an unrolled element
if node.current_idx is not None:
# This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}"
# Is an array
if indexer is not None:
return f"{base}_{signal}[{indexer}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
@abstractmethod
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
"""Get slave port declarations."""
...
@abstractmethod
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
"""Get master port declarations for a child node."""
...
@abstractmethod
def get_slave_prefix(self) -> str:
"""Get the slave signal name prefix."""
...
@abstractmethod
def get_master_prefix(self) -> str:
"""Get the master signal name prefix."""
...

View File

@@ -63,11 +63,16 @@ class DecodeLogicGenerator(BusDecoderListener):
l_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})")
u_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})")
# Generate Conditions
return [
f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})",
f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})",
]
lower_expr = f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})"
upper_expr = f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})"
predicates: list[str] = []
# Avoid generating a redundant >= 0 comparison, which triggers Verilator warnings.
if not (l_bound.value == 0 and len(l_bound_comp) == 1):
predicates.append(lower_expr)
predicates.append(upper_expr)
return predicates
def cpuif_prot_predicate(self, node: AddressableNode) -> list[str]:
if self._flavor == DecodeLogicFlavor.READ:

View File

@@ -14,6 +14,7 @@ class DesignStateKwargs(TypedDict, total=False):
package_name: str
address_width: int
cpuif_unroll: bool
max_decode_depth: int
class DesignState:
@@ -35,6 +36,7 @@ class DesignState:
user_addr_width: int | None = kwargs.pop("address_width", None)
self.cpuif_unroll: bool = kwargs.pop("cpuif_unroll", False)
self.max_decode_depth: int = kwargs.pop("max_decode_depth", 1)
# ------------------------
# Info about the design

View File

@@ -17,6 +17,7 @@ from .identifier_filter import kw_filter as kwf
from .listener import BusDecoderListener
from .struct_gen import StructGenerator
from .sv_int import SVInt
from .utils import clog2
from .validate_design import DesignValidator
@@ -27,6 +28,7 @@ class ExporterKwargs(TypedDict, total=False):
address_width: int
cpuif_unroll: bool
reuse_hwif_typedefs: bool
max_decode_depth: int
if TYPE_CHECKING:
@@ -59,6 +61,7 @@ class BusDecoderExporter:
)
self.jj_env.filters["kwf"] = kwf # type: ignore
self.jj_env.filters["walk"] = self.walk # type: ignore
self.jj_env.filters["clog2"] = clog2 # type: ignore
def export(self, node: RootNode | AddrmapNode, output_dir: str, **kwargs: Unpack[ExporterKwargs]) -> None:
"""
@@ -84,6 +87,9 @@ class BusDecoderExporter:
cpuif_unroll: bool
Unroll arrayed addressable nodes into separate instances in the CPU
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.
"""
# If it is the root node, skip to top addrmap
if isinstance(node, RootNode):

View File

@@ -1,6 +1,6 @@
from collections import deque
from systemrdl.node import AddressableNode
from systemrdl.node import AddressableNode, RegNode
from systemrdl.walker import RDLListener, WalkerAction
from .design_state import DesignState
@@ -12,15 +12,39 @@ class BusDecoderListener(RDLListener):
self._ds = ds
self._depth = 0
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:
return True
# Check if this node only contains external addressable children
if node != self._ds.top_node and not isinstance(node, RegNode):
if any(isinstance(c, AddressableNode) for c in node.children()) and all(
c.external for c in node.children() if isinstance(c, AddressableNode)
):
return True
return False
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
if node.array_dimensions:
assert node.array_stride is not None, "Array stride should be defined for arrayed components"
self._array_stride_stack.extend(node.array_dimensions)
current_stride = node.array_stride
self._array_stride_stack.append(current_stride)
# Work backwards from rightmost to leftmost dimension (fastest to slowest changing)
# Each dimension's stride is the product of its size and the previous dimension's stride
for dim in node.array_dimensions[-1:0:-1]:
current_stride = current_stride * dim
self._array_stride_stack.appendleft(current_stride)
self._depth += 1
if self._depth > 1:
# Check if we should skip this node's descendants
if self.should_skip_node(node):
return WalkerAction.SkipDescendants
return WalkerAction.Continue
def exit_AddressableComponent(self, node: AddressableNode) -> None:

View File

@@ -1,13 +1,12 @@
//==========================================================
// Module: {{ds.module_name}}
// Description: CPU Interface Bus Decoder
// Author: PeakRDL-busdecoder
// Author: PeakRDL-BusDecoder
// License: LGPL-3.0
// Date: {{current_date}}
// Version: {{version}}
// Links:
// - https://github.com/arnavsacheti/PeakRDL-busdecoder
// - https://github.com/arnavsacheti/PeakRDL-BusDecoder
//==========================================================
@@ -17,7 +16,6 @@ module {{ds.module_name}}
) {%- endif %} (
{{cpuif.port_declaration|indent(4)}}
);
//--------------------------------------------------------------------------
// CPU Bus interface logic
//--------------------------------------------------------------------------
@@ -46,7 +44,7 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
// Slave <-> Internal CPUIF <-> Master
//--------------------------------------------------------------------------
{{-cpuif.get_implementation()|indent(4)}}
{{cpuif.get_implementation()|indent(4)}}
//--------------------------------------------------------------------------
// Write Address Decoder

View File

@@ -1,10 +1,21 @@
// Generated by PeakRDL-busdecoder - A free and open-source SystemVerilog generator
// https://github.com/arnavsacheti/PeakRDL-busdecoder
//==========================================================
// Package: {{ds.package_name}}
// Description: CPU Interface Bus Decoder Package
// Author: PeakRDL-BusDecoder
// License: LGPL-3.0
// Date: {{current_date}}
// Version: {{version}}
// Links:
// - https://github.com/arnavsacheti/PeakRDL-BusDecoder
//==========================================================
package {{ds.package_name}};
localparam {{ds.module_name.upper()}}_DATA_WIDTH = {{ds.cpuif_data_width}};
localparam {{ds.module_name.upper()}}_MIN_ADDR_WIDTH = {{ds.addr_width}};
localparam {{ds.module_name.upper()}}_SIZE = {{SVInt(ds.top_node.size)}};
{%- for child in cpuif.addressable_children %}
localparam {{ds.module_name.upper()}}_{{child.inst_name.upper()}}_ADDR_WIDTH = {{child.size|clog2}};
{%- endfor %}
endpackage
{# (eof newline anchor) #}

View File

@@ -1,19 +1,27 @@
# Unit tests
# Tests
The bus decoder exporter now ships with a small unit test suite built around
`pytest`. The tests exercise the Python implementation directly and use the
[`systemrdl-compiler`](https://github.com/SystemRDL/systemrdl-compiler)
The bus decoder exporter includes comprehensive test suites to validate both the
Python implementation and the generated SystemVerilog RTL.
## Unit Tests
The unit test suite is built around `pytest` and exercises the Python implementation
directly using the [`systemrdl-compiler`](https://github.com/SystemRDL/systemrdl-compiler)
package to elaborate inline SystemRDL snippets.
## Install dependencies
### Install dependencies
Create an isolated environment if desired and install the minimal requirements:
```bash
python -m pip install -r tests/requirements.txt
# Using uv (recommended)
uv sync --group test
# Or using pip
python -m pip install -e . parameterized pytest pytest-cov pytest-xdist
```
## Running the suite
### Running the suite
Invoke `pytest` from the repository root (or the `tests` directory) and point it
at the unit tests:
@@ -25,3 +33,67 @@ pytest tests/unit
Pytest will automatically discover tests that follow the `test_*.py` naming
pattern and can make use of the `compile_rdl` fixture defined in
`tests/unit/conftest.py` to compile inline SystemRDL sources.
## Cocotb Integration Tests
The cocotb test suite validates the functionality of generated SystemVerilog RTL
through simulation. These tests generate bus decoders for different CPU interfaces
(APB3, APB4, AXI4-Lite) and verify that read/write operations work correctly.
### Install dependencies
```bash
# Install with cocotb support using uv (recommended)
uv sync --group test
# Or using pip
python -m pip install -e . parameterized pytest pytest-cov pytest-xdist cocotb cocotb-bus
# Install HDL simulator (choose one)
apt-get install iverilog # Icarus Verilog
apt-get install verilator # Verilator
```
### Running the tests
#### Integration tests (no simulator required)
These tests validate code generation without requiring an HDL simulator:
```bash
pytest tests/cocotb/testbenches/test_integration.py -v
```
#### Example code generation
Run examples to see generated code for different configurations:
```bash
python tests/cocotb/examples.py
```
#### Simulation layout
Simulation-oriented tests are grouped by CPU interface under
`tests/cocotb/<cpuif>/<group>/`. For example, the APB4 smoke test lives in
`tests/cocotb/apb4/smoke/` alongside its pytest runner module. Each runner
compiles the appropriate SystemRDL design, adds the interface wrapper from
`hdl-src/`, and invokes cocotb via Verilator.
#### Full simulation tests (requires simulator)
To execute the smoke tests for every supported interface:
```bash
pytest tests/cocotb/*/smoke/test_runner.py -v
```
To target a single interface, point pytest at that runner module:
```bash
pytest tests/cocotb/apb3/smoke/test_runner.py -v
pytest tests/cocotb/apb4/smoke/test_runner.py -v
pytest tests/cocotb/axi4lite/smoke/test_runner.py -v
```
For more information about cocotb tests, see [`tests/cocotb/README.md`](cocotb/README.md).

0
tests/body/__init__.py Normal file
View File

50
tests/body/test_body.py Normal file
View File

@@ -0,0 +1,50 @@
from peakrdl_busdecoder.body import Body
class TestBody:
"""Test the base Body class."""
def test_empty_body(self) -> None:
"""Test empty body returns empty string."""
body = Body()
assert str(body) == ""
assert not body # Should be falsy when empty
def test_add_single_line(self) -> None:
"""Test adding a single line to body."""
body = Body()
body += "line1"
assert str(body) == "line1"
assert body # Should be truthy when not empty
def test_add_multiple_lines(self) -> None:
"""Test adding multiple lines to body."""
body = Body()
body += "line1"
body += "line2"
body += "line3"
expected = "line1\nline2\nline3"
assert str(body) == expected
def test_add_returns_self(self) -> None:
"""Test that add operation returns self for chaining."""
body = Body()
body += "line1"
body += "line2"
# Chaining works because += returns self
assert len(body.lines) == 2
def test_add_nested_body(self) -> None:
"""Test adding another body as a line."""
outer = Body()
inner = Body()
inner += "inner1"
inner += "inner2"
outer += "outer1"
outer += inner
outer += "outer2"
expected = "outer1\ninner1\ninner2\nouter2"
assert str(outer) == expected

View File

@@ -0,0 +1,39 @@
from peakrdl_busdecoder.body import CombinationalBody, IfBody
class TestCombinationalBody:
"""Test the CombinationalBody class."""
def test_simple_combinational_block(self) -> None:
"""Test simple combinational block."""
body = CombinationalBody()
body += "assign1 = value1;"
body += "assign2 = value2;"
result = str(body)
assert "always_comb" in result
assert "begin" in result
assert "assign1 = value1;" in result
assert "assign2 = value2;" in result
assert "end" in result
def test_empty_combinational_block(self) -> None:
"""Test empty combinational block."""
body = CombinationalBody()
result = str(body)
assert "always_comb" in result
assert "begin" in result
assert "end" in result
def test_combinational_with_if_statement(self) -> None:
"""Test combinational block with if statement."""
cb = CombinationalBody()
ifb = IfBody()
with ifb.cm("condition") as b:
b += "assignment = value;"
cb += ifb
result = str(cb)
assert "always_comb" in result
assert "if (condition)" in result
assert "assignment = value;" in result

View File

@@ -0,0 +1,46 @@
from peakrdl_busdecoder.body import ForLoopBody
class TestForLoopBody:
"""Test the ForLoopBody class."""
def test_genvar_for_loop(self) -> None:
"""Test genvar-style for loop."""
body = ForLoopBody("genvar", "i", 4)
body += "statement1;"
body += "statement2;"
result = str(body)
assert "for (genvar i = 0; i < 4; i++)" in result
assert "statement1;" in result
assert "statement2;" in result
assert "end" in result
def test_int_for_loop(self) -> None:
"""Test int-style for loop."""
body = ForLoopBody("int", "j", 8)
body += "assignment = value;"
result = str(body)
assert "for (int j = 0; j < 8; j++)" in result
assert "assignment = value;" in result
assert "end" in result
def test_empty_for_loop(self) -> None:
"""Test empty for loop."""
body = ForLoopBody("genvar", "k", 2)
result = str(body)
# Empty for loop should still have structure
assert "for (genvar k = 0; k < 2; k++)" in result
def test_nested_for_loops(self) -> None:
"""Test nested for loops."""
outer = ForLoopBody("genvar", "i", 3)
inner = ForLoopBody("genvar", "j", 2)
inner += "nested_statement;"
outer += inner
result = str(outer)
assert "for (genvar i = 0; i < 3; i++)" in result
assert "for (genvar j = 0; j < 2; j++)" in result
assert "nested_statement;" in result

View File

@@ -0,0 +1,85 @@
from peakrdl_busdecoder.body import IfBody
class TestIfBody:
"""Test the IfBody class."""
def test_simple_if(self):
"""Test simple if statement."""
body = IfBody()
with body.cm("condition1") as b:
b += "statement1;"
result = str(body)
assert "if (condition1)" in result
assert "statement1;" in result
assert "end" in result
def test_if_else(self):
"""Test if-else statement."""
body = IfBody()
with body.cm("condition1") as b:
b += "if_statement;"
with body.cm(None) as b: # None for else
b += "else_statement;"
result = str(body)
assert "if (condition1)" in result
assert "if_statement;" in result
assert "else" in result
assert "else_statement;" in result
def test_if_elif_else(self):
"""Test if-elif-else chain."""
body = IfBody()
with body.cm("condition1") as b:
b += "statement1;"
with body.cm("condition2") as b:
b += "statement2;"
with body.cm(None) as b: # None for else
b += "statement3;"
result = str(body)
assert "if (condition1)" in result
assert "statement1;" in result
assert "else if (condition2)" in result
assert "statement2;" in result
assert "else" in result
assert "statement3;" in result
def test_multiple_elif(self):
"""Test multiple elif statements."""
body = IfBody()
with body.cm("cond1") as b:
b += "stmt1;"
with body.cm("cond2") as b:
b += "stmt2;"
with body.cm("cond3") as b:
b += "stmt3;"
result = str(body)
assert "if (cond1)" in result
assert "else if (cond2)" in result
assert "else if (cond3)" in result
def test_empty_if_branches(self):
"""Test if statement with empty branches."""
body = IfBody()
with body.cm("condition"):
pass
result = str(body)
assert "if (condition)" in result
def test_nested_if(self):
"""Test nested if statements."""
outer = IfBody()
with outer.cm("outer_cond") as outer_body:
inner = IfBody()
with inner.cm("inner_cond") as inner_body:
inner_body += "nested_statement;"
outer_body += inner
result = str(outer)
assert "if (outer_cond)" in result
assert "if (inner_cond)" in result
assert "nested_statement;" in result

View File

@@ -0,0 +1,59 @@
from peakrdl_busdecoder.body import StructBody
class TestStructBody:
"""Test the StructBody class."""
def test_simple_struct(self) -> None:
"""Test simple struct definition."""
body = StructBody("my_struct_t", packed=True, typedef=True)
body += "logic [7:0] field1;"
body += "logic field2;"
result = str(body)
assert "typedef struct packed" in result
assert "my_struct_t" in result
assert "logic [7:0] field1;" in result
assert "logic field2;" in result
def test_unpacked_struct(self) -> None:
"""Test unpacked struct definition."""
body = StructBody("unpacked_t", packed=False, typedef=True)
body += "int field1;"
result = str(body)
assert "typedef struct" in result
assert "packed" not in result or "typedef struct {" in result
assert "unpacked_t" in result
def test_struct_without_typedef(self) -> None:
"""Test struct without typedef."""
body = StructBody("my_struct", packed=True, typedef=False)
body += "logic field;"
result = str(body)
# When typedef=False, packed is not used
assert "struct {" in result
assert "typedef" not in result
assert "my_struct" in result
def test_empty_struct(self) -> None:
"""Test empty struct."""
body = StructBody("empty_t", packed=True, typedef=True)
result = str(body)
assert "typedef struct packed" in result
assert "empty_t" in result
def test_nested_struct(self) -> None:
"""Test struct with nested struct."""
outer = StructBody("outer_t", packed=True, typedef=True)
inner = StructBody("inner_t", packed=True, typedef=True)
inner += "logic field1;"
outer += "logic field2;"
outer += str(inner) # Include inner struct as a string
result = str(outer)
assert "outer_t" in result
assert "field2;" in result
# Inner struct should appear in the string
assert "inner_t" in result

0
tests/cocotb/__init__.py Normal file
View File

View File

View File

View File

@@ -0,0 +1,123 @@
"""APB3 smoke tests for generated multi-register design."""
import cocotb
from cocotb.triggers import Timer
WRITE_ADDR = 0x0
READ_ADDR = 0x8
WRITE_DATA = 0xCAFEBABE
READ_DATA = 0x0BAD_F00D
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_apb3_read_write_paths(dut):
"""Exercise APB3 slave interface and observe master fanout."""
s_apb = _apb3_slave(dut)
masters = {
"reg1": _apb3_master(dut, "m_apb_reg1"),
"reg2": _apb3_master(dut, "m_apb_reg2"),
"reg3": _apb3_master(dut, "m_apb_reg3"),
}
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 masters.values():
master.PRDATA.value = 0
master.PREADY.value = 0
master.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to reg1
masters["reg1"].PREADY.value = 1
s_apb.PADDR.value = WRITE_ADDR
s_apb.PWDATA.value = WRITE_DATA
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(masters["reg1"].PSEL.value) == 1, "reg1 should be selected for write"
assert int(masters["reg1"].PWRITE.value) == 1, "Write should propagate to master"
assert int(masters["reg1"].PADDR.value) == WRITE_ADDR, "Address should reach selected master"
assert int(masters["reg1"].PWDATA.value) == WRITE_DATA, "Write data should fan out"
for name, master in masters.items():
if name != "reg1":
assert int(master.PSEL.value) == 0, f"{name} must idle during reg1 write"
assert int(s_apb.PREADY.value) == 1, "Ready must reflect selected master"
assert int(s_apb.PSLVERR.value) == 0, "Write should not signal error"
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
masters["reg1"].PREADY.value = 0
await Timer(1, units="ns")
# Read from reg3
masters["reg3"].PRDATA.value = READ_DATA
masters["reg3"].PREADY.value = 1
masters["reg3"].PSLVERR.value = 0
s_apb.PADDR.value = READ_ADDR
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
s_apb.PWRITE.value = 0
await Timer(1, units="ns")
assert int(masters["reg3"].PSEL.value) == 1, "reg3 should be selected for read"
assert int(masters["reg3"].PWRITE.value) == 0, "Read should clear write"
assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Address should reach read target"
for name, master in masters.items():
if name != "reg3":
assert int(master.PSEL.value) == 0, f"{name} must idle during reg3 read"
assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return to slave"
assert int(s_apb.PREADY.value) == 1, "Read should acknowledge"
assert int(s_apb.PSLVERR.value) == 0, "Read should not signal error"
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
masters["reg3"].PREADY.value = 0
await Timer(1, units="ns")

View File

@@ -0,0 +1,50 @@
"""Pytest wrapper launching the APB3 cocotb smoke test."""
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_smoke(tmp_path: Path) -> None:
"""Compile the APB3 design and execute the cocotb smoke test."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"),
"multi_reg",
tmp_path,
cpuif_cls=APB3CpuifFlat,
)
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_register_access",
build_dir=build_dir,
log_file=str(tmp_path / "sim.log"),
)

View File

View File

View File

@@ -0,0 +1,138 @@
"""APB4 smoke tests using generated multi-register design."""
import cocotb
from cocotb.triggers import Timer
WRITE_ADDR = 0x4
READ_ADDR = 0x8
WRITE_DATA = 0x1234_5678
READ_DATA = 0x89AB_CDEF
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_apb4_read_write_paths(dut):
"""Drive APB4 slave signals and observe master activity."""
s_apb = _apb4_slave(dut)
masters = {
"reg1": _apb4_master(dut, "m_apb_reg1"),
"reg2": _apb4_master(dut, "m_apb_reg2"),
"reg3": _apb4_master(dut, "m_apb_reg3"),
}
# 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 masters.values():
master.PRDATA.value = 0
master.PREADY.value = 0
master.PSLVERR.value = 0
await Timer(1, units="ns")
# ------------------------------------------------------------------
# Write transfer to reg2
# ------------------------------------------------------------------
masters["reg2"].PREADY.value = 1
s_apb.PADDR.value = WRITE_ADDR
s_apb.PWDATA.value = WRITE_DATA
s_apb.PSTRB.value = 0xF
s_apb.PPROT.value = 0
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(masters["reg2"].PSEL.value) == 1, "reg2 must be selected for write"
assert int(masters["reg2"].PWRITE.value) == 1, "Write strobes should propagate"
assert int(masters["reg2"].PADDR.value) == WRITE_ADDR, "Address should fan out"
assert int(masters["reg2"].PWDATA.value) == WRITE_DATA, "Write data should fan out"
for name, master in masters.items():
if name != "reg2":
assert int(master.PSEL.value) == 0, f"{name} should remain idle on write"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror selected master"
assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful write"
# Return to idle
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
masters["reg2"].PREADY.value = 0
await Timer(1, units="ns")
# ------------------------------------------------------------------
# Read transfer from reg3
# ------------------------------------------------------------------
masters["reg3"].PRDATA.value = READ_DATA
masters["reg3"].PREADY.value = 1
masters["reg3"].PSLVERR.value = 0
s_apb.PADDR.value = READ_ADDR
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
s_apb.PWRITE.value = 0
await Timer(1, units="ns")
assert int(masters["reg3"].PSEL.value) == 1, "reg3 must be selected for read"
assert int(masters["reg3"].PWRITE.value) == 0, "Read should deassert write"
assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Read address should propagate"
for name, master in masters.items():
if name != "reg3":
assert int(master.PSEL.value) == 0, f"{name} should remain idle on read"
assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return from master"
assert int(s_apb.PREADY.value) == 1, "Ready must follow selected master"
assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful read"
# Back to idle
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
masters["reg3"].PREADY.value = 0
await Timer(1, units="ns")

View File

@@ -0,0 +1,50 @@
"""Pytest wrapper launching the APB4 cocotb smoke test."""
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_smoke(tmp_path: Path) -> None:
"""Compile the APB4 design and execute the cocotb smoke test."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"),
"multi_reg",
tmp_path,
cpuif_cls=APB4CpuifFlat,
)
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,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_register_access",
build_dir=build_dir,
log_file=str(tmp_path / "sim.log"),
)

View File

View File

View File

@@ -0,0 +1,161 @@
"""AXI4-Lite smoke test ensuring address decode fanout works."""
import cocotb
from cocotb.triggers import Timer
WRITE_ADDR = 0x4
READ_ADDR = 0x8
WRITE_DATA = 0x1357_9BDF
READ_DATA = 0x2468_ACED
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_axi4lite_read_write_paths(dut):
"""Drive AXI4-Lite slave channels and validate master side wiring."""
s_axil = _axil_slave(dut)
masters = {
"reg1": _axil_master(dut, "m_axil_reg1"),
"reg2": _axil_master(dut, "m_axil_reg2"),
"reg3": _axil_master(dut, "m_axil_reg3"),
}
# 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 masters.values():
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 transaction targeting reg2
# --------------------------------------------------------------
s_axil.AWADDR.value = WRITE_ADDR
s_axil.AWPROT.value = 0
s_axil.AWVALID.value = 1
s_axil.WDATA.value = WRITE_DATA
s_axil.WSTRB.value = 0xF
s_axil.WVALID.value = 1
s_axil.BREADY.value = 1
await Timer(1, units="ns")
assert int(masters["reg2"].AWVALID.value) == 1, "reg2 AWVALID should follow slave"
assert int(masters["reg2"].WVALID.value) == 1, "reg2 WVALID should follow slave"
assert int(masters["reg2"].AWADDR.value) == WRITE_ADDR, "AWADDR should fan out"
assert int(masters["reg2"].WDATA.value) == WRITE_DATA, "WDATA should fan out"
assert int(masters["reg2"].WSTRB.value) == 0xF, "WSTRB should propagate"
for name, master in masters.items():
if name != "reg2":
assert int(master.AWVALID.value) == 0, f"{name} AWVALID should stay low"
assert int(master.WVALID.value) == 0, f"{name} WVALID should stay low"
# Release write channel
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
s_axil.BREADY.value = 0
await Timer(1, units="ns")
# --------------------------------------------------------------
# Read transaction targeting reg3
# --------------------------------------------------------------
masters["reg3"].RVALID.value = 1
masters["reg3"].RDATA.value = READ_DATA
masters["reg3"].RRESP.value = 0
s_axil.ARADDR.value = READ_ADDR
s_axil.ARPROT.value = 0
s_axil.ARVALID.value = 1
s_axil.RREADY.value = 1
await Timer(1, units="ns")
assert int(masters["reg3"].ARVALID.value) == 1, "reg3 ARVALID should follow slave"
assert int(masters["reg3"].ARADDR.value) == READ_ADDR, "ARADDR should fan out"
for name, master in masters.items():
if name != "reg3":
assert int(master.ARVALID.value) == 0, f"{name} ARVALID should stay low"
assert int(s_axil.RVALID.value) == 1, "Slave should raise RVALID when master responds"
assert int(s_axil.RDATA.value) == READ_DATA, "Read data should return to slave"
assert int(s_axil.RRESP.value) == 0, "No error expected for read"
# Return to idle
s_axil.ARVALID.value = 0
s_axil.RREADY.value = 0
masters["reg3"].RVALID.value = 0
await Timer(1, units="ns")

View File

@@ -0,0 +1,50 @@
"""Pytest wrapper launching the AXI4-Lite cocotb smoke test."""
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_smoke(tmp_path: Path) -> None:
"""Compile the AXI4-Lite design and execute the cocotb smoke test."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"),
"multi_reg",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
)
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_register_access",
build_dir=build_dir,
log_file=str(tmp_path / "sim.log"),
)

View File

@@ -0,0 +1,3 @@
from pathlib import Path
rdls = map(Path, ["simple.rdl", "multiple_reg.rdl"])

View File

@@ -0,0 +1,22 @@
addrmap multi_reg {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} reg1 @ 0x0;
reg {
field {
sw=r;
hw=w;
} status[15:0];
} reg2 @ 0x4;
reg {
field {
sw=rw;
hw=r;
} control[7:0];
} reg3 @ 0x8;
};

View File

@@ -0,0 +1,8 @@
addrmap simple_test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} test_reg @ 0x0;
};

67
tests/cocotb_lib/utils.py Normal file
View File

@@ -0,0 +1,67 @@
"""Common utilities for cocotb testbenches."""
from pathlib import Path
from typing import Any
from systemrdl import RDLCompiler
from peakrdl_busdecoder.cpuif.base_cpuif import BaseCpuif
from peakrdl_busdecoder.exporter import BusDecoderExporter
def compile_rdl_and_export(
rdl_source: str, top_name: str, output_dir: Path, cpuif_cls: type[BaseCpuif], **kwargs: Any
) -> tuple[Path, Path]:
"""
Compile RDL source and export to SystemVerilog.
Args:
rdl_source: SystemRDL source code path
top_name: Name of the top-level addrmap
output_dir: Directory to write generated files
cpuif_cls: CPU interface class to use
**kwargs: Additional arguments to pass to exporter
Returns:
Tuple of (module_path, package_path) for generated files
"""
# Compile RDL source
compiler = RDLCompiler()
compiler.compile_file(rdl_source)
top = compiler.elaborate(top_name)
# Export to SystemVerilog
exporter = BusDecoderExporter()
exporter.export(top, str(output_dir), cpuif_cls=cpuif_cls, **kwargs)
# Return paths to generated files
module_name = kwargs.get("module_name", top_name)
package_name = kwargs.get("package_name", f"{top_name}_pkg")
module_path = Path(output_dir) / f"{module_name}.sv"
package_path = Path(output_dir) / f"{package_name}.sv"
return module_path, package_path
def get_verilog_sources(module_path: Path, package_path: Path, intf_files: list[Path]) -> list[str]:
"""
Get list of Verilog source files needed for simulation.
Args:
module_path: Path to the generated module file
package_path: Path to the generated package file
intf_files: List of paths to interface definition files
Returns:
List of source file paths as strings
"""
sources = []
# Add interface files first
sources.extend([str(f) for f in intf_files])
# Add package file
sources.append(str(package_path))
# Add module file
sources.append(str(module_path))
return sources

60
tests/conftest.py Normal file
View File

@@ -0,0 +1,60 @@
"""Pytest fixtures for unit tests."""
from __future__ import annotations
collect_ignore_glob = ["cocotb/*/smoke/test_register_access.py"]
import os
from collections.abc import Callable
from pathlib import Path
from tempfile import NamedTemporaryFile
import pytest
from systemrdl import RDLCompileError, RDLCompiler # type:ignore
from systemrdl.node import AddrmapNode
_SHIM_DIR = Path(__file__).resolve().parents[1] / "tools" / "shims"
os.environ["PATH"] = f"{_SHIM_DIR}{os.pathsep}{os.environ.get('PATH', '')}"
@pytest.fixture
def compile_rdl(tmp_path: Path) -> Callable[..., AddrmapNode]:
"""Compile inline SystemRDL source and return the elaborated root node.
Parameters
----------
tmp_path:
Temporary directory provided by pytest.
"""
def _compile(
source: str,
*,
top: str | None = None,
defines: dict[str, str] | None = None,
include_paths: list[Path | str] | None = None,
) -> AddrmapNode:
compiler = RDLCompiler()
# Use delete=False to keep the file around after closing
with NamedTemporaryFile("w", suffix=".rdl", dir=tmp_path, delete=False) as tmp_file:
tmp_file.write(source)
tmp_file.flush()
try:
compiler.compile_file(
tmp_file.name,
incl_search_paths=(list(map(str, include_paths)) if include_paths else None),
defines=defines,
)
if top is not None:
root = compiler.elaborate(top) # type:ignore
return root.top
root = compiler.elaborate() # type:ignore
return root.top
except RDLCompileError:
# Print error messages if available
if hasattr(compiler, "print_messages"):
compiler.print_messages() # type:ignore
raise
return _compile

View File

View File

@@ -1,20 +1,16 @@
"""Integration tests for the BusDecoderExporter."""
from __future__ import annotations
import os
from collections.abc import Callable
from pathlib import Path
import pytest
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
from peakrdl_busdecoder.exporter import BusDecoderExporter
class TestBusDecoderExporter:
"""Test the top-level BusDecoderExporter."""
def test_simple_register_export(self, compile_rdl, tmp_path):
def test_simple_register_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
"""Test exporting a simple register."""
rdl_source = """
addrmap simple_reg {
@@ -47,7 +43,7 @@ class TestBusDecoderExporter:
package_content = package_file.read_text()
assert "package simple_reg_pkg" in package_content
def test_register_array_export(self, compile_rdl, tmp_path):
def test_register_array_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
"""Test exporting a register array."""
rdl_source = """
addrmap reg_array {
@@ -73,7 +69,7 @@ class TestBusDecoderExporter:
assert "module reg_array" in module_content
assert "my_regs" in module_content
def test_nested_addrmap_export(self, compile_rdl, tmp_path):
def test_nested_addrmap_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
"""Test exporting nested addrmaps."""
rdl_source = """
addrmap inner_block {
@@ -104,7 +100,7 @@ class TestBusDecoderExporter:
assert "inner" in module_content
assert "inner_reg" in module_content
def test_custom_module_name(self, compile_rdl, tmp_path):
def test_custom_module_name(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
"""Test exporting with custom module name."""
rdl_source = """
addrmap my_addrmap {
@@ -132,7 +128,7 @@ class TestBusDecoderExporter:
module_content = module_file.read_text()
assert "module custom_module" in module_content
def test_custom_package_name(self, compile_rdl, tmp_path):
def test_custom_package_name(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
"""Test exporting with custom package name."""
rdl_source = """
addrmap my_addrmap {
@@ -157,7 +153,7 @@ class TestBusDecoderExporter:
package_content = package_file.read_text()
assert "package custom_pkg" in package_content
def test_multiple_registers(self, compile_rdl, tmp_path):
def test_multiple_registers(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
"""Test exporting multiple registers."""
rdl_source = """
addrmap multi_reg {
@@ -198,61 +194,89 @@ class TestBusDecoderExporter:
assert "reg2" in module_content
assert "reg3" in module_content
class TestAPB4Interface:
"""Test APB4 CPU interface generation."""
def test_apb4_port_declaration(self, compile_rdl, tmp_path):
"""Test that APB4 interface ports are generated."""
def test_master_address_widths_export(
self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path
) -> None:
"""Test exporting master address width parameters for child addrmaps."""
rdl_source = """
addrmap apb_test {
addrmap child1 {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="apb_test")
exporter = BusDecoderExporter()
output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif)
module_file = tmp_path / "apb_test.sv"
module_content = module_file.read_text()
# Check for APB4 signals
assert "PSEL" in module_content or "psel" in module_content
assert "PENABLE" in module_content or "penable" in module_content
assert "PWRITE" in module_content or "pwrite" in module_content
assert "PADDR" in module_content or "paddr" in module_content
assert "PWDATA" in module_content or "pwdata" in module_content
assert "PRDATA" in module_content or "prdata" in module_content
assert "PREADY" in module_content or "pready" in module_content
def test_apb4_read_write_logic(self, compile_rdl, tmp_path):
"""Test that APB4 read/write logic is generated."""
rdl_source = """
addrmap apb_rw {
} reg1 @ 0x0;
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
} reg2 @ 0x4;
};
addrmap child2 {
reg {
field {
sw=rw;
hw=r;
} data[15:0];
} reg2 @ 0x0;
};
addrmap parent {
external child1 c1 @ 0x0000;
external child2 c2 @ 0x1000;
};
"""
top = compile_rdl(rdl_source, top="apb_rw")
top = compile_rdl(rdl_source, top="parent")
exporter = BusDecoderExporter()
output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif)
module_file = tmp_path / "apb_rw.sv"
module_content = module_file.read_text()
package_file = tmp_path / "parent_pkg.sv"
assert package_file.exists()
# Basic sanity checks for logic generation
assert "always" in module_content or "assign" in module_content
assert "my_reg" in module_content
package_content = package_file.read_text()
assert "package parent_pkg" in package_content
# Check for master address width parameters
assert "localparam PARENT_C1_ADDR_WIDTH = 3" in package_content
assert "localparam PARENT_C2_ADDR_WIDTH = 2" in package_content
def test_master_address_widths_with_arrays(
self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path
) -> None:
"""Test exporting master address width parameters for arrayed child addrmaps."""
rdl_source = """
addrmap child {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} reg1 @ 0x0;
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} reg2 @ 0x4;
};
addrmap parent {
external child children[4] @ 0x0 += 0x100;
};
"""
top = compile_rdl(rdl_source, top="parent")
exporter = BusDecoderExporter()
output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif)
package_file = tmp_path / "parent_pkg.sv"
assert package_file.exists()
package_content = package_file.read_text()
assert "package parent_pkg" in package_content
# Check for master address width parameter - array should have a single parameter
assert "localparam PARENT_CHILDREN_ADDR_WIDTH = 3" in package_content

View File

View File

@@ -0,0 +1,96 @@
from collections.abc import Callable
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder.decode_logic_gen import DecodeLogicFlavor, DecodeLogicGenerator
from peakrdl_busdecoder.design_state import DesignState
class TestDecodeLogicGenerator:
"""Test the DecodeLogicGenerator."""
def test_decode_logic_read(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test decode logic generation for read operations."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ)
# Basic sanity check - it should initialize
assert gen is not None
assert gen._flavor == DecodeLogicFlavor.READ # type: ignore
def test_decode_logic_write(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test decode logic generation for write operations."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE)
assert gen is not None
assert gen._flavor == DecodeLogicFlavor.WRITE # type: ignore
def test_cpuif_addr_predicate(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test address predicate generation."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x100;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ)
# Get the register node
reg_node = None
for child in top.children():
if child.inst_name == "my_reg":
reg_node = child
break
assert reg_node is not None
predicates = gen.cpuif_addr_predicate(reg_node)
# Should return a list of conditions
assert isinstance(predicates, list)
assert len(predicates) > 0
# Should check address bounds
for pred in predicates:
assert "cpuif_rd_addr" in pred or ">=" in pred or "<" in pred
def test_decode_logic_flavor_enum(self) -> None:
"""Test DecodeLogicFlavor enum values."""
assert DecodeLogicFlavor.READ.value == "rd"
assert DecodeLogicFlavor.WRITE.value == "wr"
assert DecodeLogicFlavor.READ.cpuif_address == "cpuif_rd_addr"
assert DecodeLogicFlavor.WRITE.cpuif_address == "cpuif_wr_addr"
assert DecodeLogicFlavor.READ.cpuif_select == "cpuif_rd_sel"
assert DecodeLogicFlavor.WRITE.cpuif_select == "cpuif_wr_sel"

View File

@@ -0,0 +1,125 @@
from collections.abc import Callable
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder.design_state import DesignState
class TestDesignState:
"""Test the DesignState class."""
def test_design_state_basic(self, compile_rdl:Callable[..., AddrmapNode])->None:
"""Test basic DesignState initialization."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
assert ds.top_node == top
assert ds.module_name == "test"
assert ds.package_name == "test_pkg"
assert ds.cpuif_data_width == 32 # Should infer from 32-bit field
assert ds.addr_width > 0
def test_design_state_custom_module_name(self, compile_rdl:Callable[..., AddrmapNode])->None:
"""Test DesignState with custom module name."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"module_name": "custom_module"})
assert ds.module_name == "custom_module"
assert ds.package_name == "custom_module_pkg"
def test_design_state_custom_package_name(self, compile_rdl:Callable[..., AddrmapNode])->None:
"""Test DesignState with custom package name."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"package_name": "custom_pkg"})
assert ds.package_name == "custom_pkg"
def test_design_state_custom_address_width(self, compile_rdl:Callable[..., AddrmapNode])->None:
"""Test DesignState with custom address width."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"address_width": 16})
assert ds.addr_width == 16
def test_design_state_unroll_arrays(self, compile_rdl:Callable[..., AddrmapNode])->None:
"""Test DesignState with cpuif_unroll option."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_regs[4] @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"cpuif_unroll": True})
assert ds.cpuif_unroll is True
def test_design_state_64bit_registers(self, compile_rdl:Callable[..., AddrmapNode])->None:
"""Test DesignState with wider data width."""
rdl_source = """
addrmap test {
reg {
regwidth = 32;
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
# Should infer 32-bit data width from field
assert ds.cpuif_data_width == 32

View File

@@ -0,0 +1,98 @@
from collections.abc import Callable
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder.design_state import DesignState
from peakrdl_busdecoder.struct_gen import StructGenerator
class TestStructGenerator:
"""Test the StructGenerator."""
def test_simple_struct_generation(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test struct generation for simple register."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = StructGenerator(ds)
# Should generate struct definition
assert gen is not None
result = str(gen)
# Should contain struct declaration
assert "struct" in result or "typedef" in result
def test_nested_struct_generation(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test struct generation for nested addrmaps."""
rdl_source = """
addrmap inner {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg @ 0x0;
};
addrmap outer {
inner my_inner @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="outer")
ds = DesignState(top, {})
gen = StructGenerator(ds)
# Walk the tree to generate structs
from systemrdl.walker import RDLWalker
walker = RDLWalker()
walker.walk(top, gen, skip_top=True)
result = str(gen)
# Should contain struct declaration
assert "struct" in result or "typedef" in result
# The struct should reference the inner component
assert "my_inner" in result
def test_array_struct_generation(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test struct generation for register arrays."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_regs[4] @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = StructGenerator(ds)
# Walk the tree to generate structs
from systemrdl.walker import RDLWalker
walker = RDLWalker()
walker.walk(top, gen, skip_top=True)
result = str(gen)
# Should contain array notation
assert "[" in result and "]" in result
# Should reference the register
assert "my_regs" in result

View File

@@ -1,3 +0,0 @@
[pytest]
testpaths = unit
python_files = test_*.py testcase.py

View File

@@ -1 +0,0 @@
"""Unit test package for PeakRDL BusDecoder."""

View File

@@ -1,56 +1,75 @@
"""Pytest fixtures for unit tests."""
from __future__ import annotations
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Iterable, Mapping, Optional
from collections.abc import Callable
import pytest
from systemrdl import RDLCompileError, RDLCompiler
from systemrdl.node import AddrmapNode
@pytest.fixture
def compile_rdl(tmp_path: Path):
"""Compile inline SystemRDL source and return the elaborated root node.
def external_nested_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode:
"""Create an RDL design with external nested addressable components.
Parameters
----------
tmp_path:
Temporary directory provided by pytest.
This tests the scenario where an addrmap contains external children
that themselves have external addressable children.
The decoder should only generate select signals for the top-level
external children, not their internal structure.
"""
rdl_source = """
mem queue_t {
name = "Queue";
mementries = 1024;
memwidth = 64;
};
def _compile(
source: str,
*,
top: Optional[str] = None,
defines: Optional[Mapping[str, object]] = None,
include_paths: Optional[Iterable[Path | str]] = None,
):
compiler = RDLCompiler()
addrmap port_t {
name = "Port";
desc = "";
for key, value in (defines or {}).items():
compiler.define(key, value)
external queue_t common[3] @ 0x0 += 0x2000;
external queue_t response @ 0x6000;
};
for include_path in include_paths or ():
compiler.add_include_path(str(include_path))
addrmap buffer_t {
name = "Buffer";
desc = "";
# Use delete=False to keep the file around after closing
with NamedTemporaryFile("w", suffix=".rdl", dir=tmp_path, delete=False) as tmp_file:
tmp_file.write(source)
tmp_file.flush()
port_t multicast @ 0x0;
port_t port [16] @ 0x8000 += 0x8000;
};
"""
return compile_rdl(rdl_source, top="buffer_t")
try:
compiler.compile_file(tmp_file.name)
if top is not None:
root = compiler.elaborate(top)
return root.top
root = compiler.elaborate()
return root.top
except RDLCompileError:
# Print error messages if available
if hasattr(compiler, "print_messages"):
compiler.print_messages()
raise
return _compile
@pytest.fixture
def nested_addrmap_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode:
"""Create an RDL design with nested non-external addrmaps for testing depth control."""
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;
};
"""
return compile_rdl(rdl_source, top="level0")

View File

@@ -1,285 +0,0 @@
"""Tests for body classes used in code generation."""
from __future__ import annotations
import pytest
from peakrdl_busdecoder.body import (
Body,
CombinationalBody,
ForLoopBody,
IfBody,
StructBody,
)
class TestBody:
"""Test the base Body class."""
def test_empty_body(self):
"""Test empty body returns empty string."""
body = Body()
assert str(body) == ""
assert not body # Should be falsy when empty
def test_add_single_line(self):
"""Test adding a single line to body."""
body = Body()
body += "line1"
assert str(body) == "line1"
assert body # Should be truthy when not empty
def test_add_multiple_lines(self):
"""Test adding multiple lines to body."""
body = Body()
body += "line1"
body += "line2"
body += "line3"
expected = "line1\nline2\nline3"
assert str(body) == expected
def test_add_returns_self(self):
"""Test that add operation returns self for chaining."""
body = Body()
body += "line1"
body += "line2"
# Chaining works because += returns self
assert len(body.lines) == 2
def test_add_nested_body(self):
"""Test adding another body as a line."""
outer = Body()
inner = Body()
inner += "inner1"
inner += "inner2"
outer += "outer1"
outer += inner
outer += "outer2"
expected = "outer1\ninner1\ninner2\nouter2"
assert str(outer) == expected
class TestForLoopBody:
"""Test the ForLoopBody class."""
def test_genvar_for_loop(self):
"""Test genvar-style for loop."""
body = ForLoopBody("genvar", "i", 4)
body += "statement1;"
body += "statement2;"
result = str(body)
assert "for (genvar i = 0; i < 4; i++)" in result
assert "statement1;" in result
assert "statement2;" in result
assert "end" in result
def test_int_for_loop(self):
"""Test int-style for loop."""
body = ForLoopBody("int", "j", 8)
body += "assignment = value;"
result = str(body)
assert "for (int j = 0; j < 8; j++)" in result
assert "assignment = value;" in result
assert "end" in result
def test_empty_for_loop(self):
"""Test empty for loop."""
body = ForLoopBody("genvar", "k", 2)
result = str(body)
# Empty for loop should still have structure
assert "for (genvar k = 0; k < 2; k++)" in result
def test_nested_for_loops(self):
"""Test nested for loops."""
outer = ForLoopBody("genvar", "i", 3)
inner = ForLoopBody("genvar", "j", 2)
inner += "nested_statement;"
outer += inner
result = str(outer)
assert "for (genvar i = 0; i < 3; i++)" in result
assert "for (genvar j = 0; j < 2; j++)" in result
assert "nested_statement;" in result
class TestIfBody:
"""Test the IfBody class."""
def test_simple_if(self):
"""Test simple if statement."""
body = IfBody()
with body.cm("condition1") as b:
b += "statement1;"
result = str(body)
assert "if (condition1)" in result
assert "statement1;" in result
assert "end" in result
def test_if_else(self):
"""Test if-else statement."""
body = IfBody()
with body.cm("condition1") as b:
b += "if_statement;"
with body.cm(None) as b: # None for else
b += "else_statement;"
result = str(body)
assert "if (condition1)" in result
assert "if_statement;" in result
assert "else" in result
assert "else_statement;" in result
def test_if_elif_else(self):
"""Test if-elif-else chain."""
body = IfBody()
with body.cm("condition1") as b:
b += "statement1;"
with body.cm("condition2") as b:
b += "statement2;"
with body.cm(None) as b: # None for else
b += "statement3;"
result = str(body)
assert "if (condition1)" in result
assert "statement1;" in result
assert "else if (condition2)" in result
assert "statement2;" in result
assert "else" in result
assert "statement3;" in result
def test_multiple_elif(self):
"""Test multiple elif statements."""
body = IfBody()
with body.cm("cond1") as b:
b += "stmt1;"
with body.cm("cond2") as b:
b += "stmt2;"
with body.cm("cond3") as b:
b += "stmt3;"
result = str(body)
assert "if (cond1)" in result
assert "else if (cond2)" in result
assert "else if (cond3)" in result
def test_empty_if_branches(self):
"""Test if statement with empty branches."""
body = IfBody()
with body.cm("condition"):
pass
result = str(body)
assert "if (condition)" in result
def test_nested_if(self):
"""Test nested if statements."""
outer = IfBody()
with outer.cm("outer_cond") as outer_body:
inner = IfBody()
with inner.cm("inner_cond") as inner_body:
inner_body += "nested_statement;"
outer_body += inner
result = str(outer)
assert "if (outer_cond)" in result
assert "if (inner_cond)" in result
assert "nested_statement;" in result
class TestCombinationalBody:
"""Test the CombinationalBody class."""
def test_simple_combinational_block(self):
"""Test simple combinational block."""
body = CombinationalBody()
body += "assign1 = value1;"
body += "assign2 = value2;"
result = str(body)
assert "always_comb" in result
assert "begin" in result
assert "assign1 = value1;" in result
assert "assign2 = value2;" in result
assert "end" in result
def test_empty_combinational_block(self):
"""Test empty combinational block."""
body = CombinationalBody()
result = str(body)
assert "always_comb" in result
assert "begin" in result
assert "end" in result
def test_combinational_with_if_statement(self):
"""Test combinational block with if statement."""
cb = CombinationalBody()
ifb = IfBody()
with ifb.cm("condition") as b:
b += "assignment = value;"
cb += ifb
result = str(cb)
assert "always_comb" in result
assert "if (condition)" in result
assert "assignment = value;" in result
class TestStructBody:
"""Test the StructBody class."""
def test_simple_struct(self):
"""Test simple struct definition."""
body = StructBody("my_struct_t", packed=True, typedef=True)
body += "logic [7:0] field1;"
body += "logic field2;"
result = str(body)
assert "typedef struct packed" in result
assert "my_struct_t" in result
assert "logic [7:0] field1;" in result
assert "logic field2;" in result
def test_unpacked_struct(self):
"""Test unpacked struct definition."""
body = StructBody("unpacked_t", packed=False, typedef=True)
body += "int field1;"
result = str(body)
assert "typedef struct" in result
assert "packed" not in result or "typedef struct {" in result
assert "unpacked_t" in result
def test_struct_without_typedef(self):
"""Test struct without typedef."""
body = StructBody("my_struct", packed=True, typedef=False)
body += "logic field;"
result = str(body)
# When typedef=False, packed is not used
assert "struct {" in result
assert "typedef" not in result
assert "my_struct" in result
def test_empty_struct(self):
"""Test empty struct."""
body = StructBody("empty_t", packed=True, typedef=True)
result = str(body)
assert "typedef struct packed" in result
assert "empty_t" in result
def test_nested_struct(self):
"""Test struct with nested struct."""
outer = StructBody("outer_t", packed=True, typedef=True)
inner = StructBody("inner_t", packed=True, typedef=True)
inner += "logic field1;"
outer += "logic field2;"
outer += str(inner) # Include inner struct as a string
result = str(outer)
assert "outer_t" in result
assert "field2;" in result
# Inner struct should appear in the string
assert "inner_t" in result

View File

@@ -0,0 +1,139 @@
"""Test handling of external nested addressable components."""
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_external_nested_components_generate_correct_decoder(external_nested_rdl: AddrmapNode) -> None:
"""Test that external nested components generate correct decoder logic.
The decoder should:
- Generate select signals for multicast and port[16]
- NOT generate select signals for multicast.common[] or multicast.response
- NOT generate invalid paths like multicast.common[i0]
"""
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(
external_nested_rdl,
tmpdir,
cpuif_cls=APB4Cpuif,
)
# Read the generated module
module_file = Path(tmpdir) / "buffer_t.sv"
content = module_file.read_text()
# Should have correct select signals
assert "cpuif_wr_sel.multicast = 1'b1;" in content
assert "cpuif_wr_sel.port[i0] = 1'b1;" in content
# Should NOT have invalid nested paths
assert "cpuif_wr_sel.multicast.common" not in content
assert "cpuif_wr_sel.multicast.response" not in content
assert "cpuif_rd_sel.multicast.common" not in content
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 "logic multicast;" in content
assert "logic [15:0]port;" in content
def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None:
"""Test that external nested components generate correct interface ports.
The module should have:
- One master interface for multicast
- Array of 16 master interfaces for port[]
- NO interfaces for internal components like common[] or response
"""
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(
external_nested_rdl,
tmpdir,
cpuif_cls=APB4Cpuif,
)
# Read the generated module
module_file = Path(tmpdir) / "buffer_t.sv"
content = module_file.read_text()
# Should have master interfaces for top-level external children
assert "m_apb_multicast" in content
assert "m_apb_port [16]" in content or "m_apb_port[16]" in content
# Should NOT have interfaces for nested external children
assert "m_apb_multicast_common" not in content
assert "m_apb_multicast_response" not in content
assert "m_apb_common" not in content
assert "m_apb_response" not in content
def test_non_external_nested_components_are_descended(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that non-external nested components are still descended into.
This is a regression test to ensure we didn't break normal nested
component handling.
"""
rdl_source = """
addrmap inner_block {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg @ 0x0;
};
addrmap outer_block {
inner_block inner @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="outer_block")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "outer_block.sv"
content = module_file.read_text()
# Should descend into inner and reference inner_reg
assert "inner" in content
assert "inner_reg" in content
def test_max_decode_depth_parameter_exists(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that max_decode_depth parameter can be set."""
rdl_source = """
addrmap simple {
reg {
field { sw=rw; hw=r; } data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="simple")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
# Should not raise an exception
exporter.export(
top,
tmpdir,
cpuif_cls=APB4Cpuif,
max_decode_depth=2,
)
# Verify output was generated
module_file = Path(tmpdir) / "simple.sv"
assert module_file.exists()

View File

@@ -1,312 +0,0 @@
"""Tests for code generation classes."""
from __future__ import annotations
import pytest
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
from peakrdl_busdecoder.decode_logic_gen import DecodeLogicFlavor, DecodeLogicGenerator
from peakrdl_busdecoder.design_state import DesignState
from peakrdl_busdecoder.exporter import BusDecoderExporter
from peakrdl_busdecoder.struct_gen import StructGenerator
class TestDecodeLogicGenerator:
"""Test the DecodeLogicGenerator."""
def test_decode_logic_read(self, compile_rdl):
"""Test decode logic generation for read operations."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ)
# Basic sanity check - it should initialize
assert gen is not None
assert gen._flavor == DecodeLogicFlavor.READ
def test_decode_logic_write(self, compile_rdl):
"""Test decode logic generation for write operations."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE)
assert gen is not None
assert gen._flavor == DecodeLogicFlavor.WRITE
def test_cpuif_addr_predicate(self, compile_rdl):
"""Test address predicate generation."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x100;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ)
# Get the register node
reg_node = None
for child in top.children():
if child.inst_name == "my_reg":
reg_node = child
break
assert reg_node is not None
predicates = gen.cpuif_addr_predicate(reg_node)
# Should return a list of conditions
assert isinstance(predicates, list)
assert len(predicates) > 0
# Should check address bounds
for pred in predicates:
assert "cpuif_rd_addr" in pred or ">=" in pred or "<" in pred
def test_decode_logic_flavor_enum(self):
"""Test DecodeLogicFlavor enum values."""
assert DecodeLogicFlavor.READ.value == "rd"
assert DecodeLogicFlavor.WRITE.value == "wr"
assert DecodeLogicFlavor.READ.cpuif_address == "cpuif_rd_addr"
assert DecodeLogicFlavor.WRITE.cpuif_address == "cpuif_wr_addr"
assert DecodeLogicFlavor.READ.cpuif_select == "cpuif_rd_sel"
assert DecodeLogicFlavor.WRITE.cpuif_select == "cpuif_wr_sel"
class TestStructGenerator:
"""Test the StructGenerator."""
def test_simple_struct_generation(self, compile_rdl):
"""Test struct generation for simple register."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = StructGenerator(ds)
# Should generate struct definition
assert gen is not None
result = str(gen)
# Should contain struct declaration
assert "struct" in result or "typedef" in result
def test_nested_struct_generation(self, compile_rdl):
"""Test struct generation for nested addrmaps."""
rdl_source = """
addrmap inner {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg @ 0x0;
};
addrmap outer {
inner my_inner @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="outer")
ds = DesignState(top, {})
gen = StructGenerator(ds)
# Walk the tree to generate structs
from systemrdl.walker import RDLWalker
walker = RDLWalker()
walker.walk(top, gen, skip_top=True)
result = str(gen)
# Should contain struct declaration
assert "struct" in result or "typedef" in result
# The struct should reference the inner component
assert "my_inner" in result
def test_array_struct_generation(self, compile_rdl):
"""Test struct generation for register arrays."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_regs[4] @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
gen = StructGenerator(ds)
# Walk the tree to generate structs
from systemrdl.walker import RDLWalker
walker = RDLWalker()
walker.walk(top, gen, skip_top=True)
result = str(gen)
# Should contain array notation
assert "[" in result and "]" in result
# Should reference the register
assert "my_regs" in result
class TestDesignState:
"""Test the DesignState class."""
def test_design_state_basic(self, compile_rdl):
"""Test basic DesignState initialization."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
assert ds.top_node == top
assert ds.module_name == "test"
assert ds.package_name == "test_pkg"
assert ds.cpuif_data_width == 32 # Should infer from 32-bit field
assert ds.addr_width > 0
def test_design_state_custom_module_name(self, compile_rdl):
"""Test DesignState with custom module name."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"module_name": "custom_module"})
assert ds.module_name == "custom_module"
assert ds.package_name == "custom_module_pkg"
def test_design_state_custom_package_name(self, compile_rdl):
"""Test DesignState with custom package name."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"package_name": "custom_pkg"})
assert ds.package_name == "custom_pkg"
def test_design_state_custom_address_width(self, compile_rdl):
"""Test DesignState with custom address width."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"address_width": 16})
assert ds.addr_width == 16
def test_design_state_unroll_arrays(self, compile_rdl):
"""Test DesignState with cpuif_unroll option."""
rdl_source = """
addrmap test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_regs[4] @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {"cpuif_unroll": True})
assert ds.cpuif_unroll is True
def test_design_state_64bit_registers(self, compile_rdl):
"""Test DesignState with wider data width."""
rdl_source = """
addrmap test {
reg {
regwidth = 32;
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test")
ds = DesignState(top, {})
# Should infer 32-bit data width from field
assert ds.cpuif_data_width == 32

0
tests/unroll/__init__.py Normal file
View File

40
tests/unroll/conftest.py Normal file
View File

@@ -0,0 +1,40 @@
from collections.abc import Callable
import pytest
from systemrdl.node import AddrmapNode
@pytest.fixture
def sample_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode:
"""Create a simple RDL design with an array."""
rdl_source = """
addrmap top {
reg my_reg {
field {
sw=rw;
hw=r;
} data[31:0];
};
my_reg regs[4] @ 0x0 += 0x4;
};
"""
return compile_rdl(rdl_source)
@pytest.fixture
def multidim_array_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode:
"""Create an RDL design with a multi-dimensional array."""
rdl_source = """
addrmap top {
reg my_reg {
field {
sw=rw;
hw=r;
} data[31:0];
};
my_reg matrix[2][3] @ 0x0 += 0x4;
};
"""
return compile_rdl(rdl_source)

View File

@@ -1,34 +1,14 @@
"""Test the --unroll CLI argument functionality."""
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
@pytest.fixture
def sample_rdl(compile_rdl):
"""Create a simple RDL design with an array."""
rdl_source = """
addrmap top {
reg my_reg {
field {
sw=rw;
hw=r;
} data[31:0];
};
my_reg regs[4] @ 0x0 += 0x4;
};
"""
return compile_rdl(rdl_source)
def test_unroll_disabled_creates_array_interface(sample_rdl):
def test_unroll_disabled_creates_array_interface(sample_rdl: AddrmapNode) -> None:
"""Test that with unroll=False, array nodes are kept as arrays."""
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
@@ -38,17 +18,17 @@ def test_unroll_disabled_creates_array_interface(sample_rdl):
cpuif_cls=APB4Cpuif,
cpuif_unroll=False,
)
# Read the generated module
module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text()
# Should have a single array interface with [4] dimension
assert "m_apb_regs [4]" in content
# Should have a parameter for array size
assert "N_REGSS = 4" in content
# Should NOT have individual indexed interfaces
assert "m_apb_regs_0" not in content
assert "m_apb_regs_1" not in content
@@ -56,7 +36,7 @@ def test_unroll_disabled_creates_array_interface(sample_rdl):
assert "m_apb_regs_3" not in content
def test_unroll_enabled_creates_individual_interfaces(sample_rdl):
def test_unroll_enabled_creates_individual_interfaces(sample_rdl: AddrmapNode) -> None:
"""Test that with unroll=True, array elements are unrolled into separate instances."""
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
@@ -66,31 +46,31 @@ def test_unroll_enabled_creates_individual_interfaces(sample_rdl):
cpuif_cls=APB4Cpuif,
cpuif_unroll=True,
)
# Read the generated module
module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text()
# Should have individual interfaces without array dimensions
assert "m_apb_regs_0," in content or "m_apb_regs_0\n" in content
assert "m_apb_regs_1," in content or "m_apb_regs_1\n" in content
assert "m_apb_regs_2," in content or "m_apb_regs_2\n" in content
assert "m_apb_regs_3" in content
# Should NOT have array interface
assert "m_apb_regs [4]" not in content
# Should NOT have individual interfaces with array dimensions (the bug we're fixing)
assert "m_apb_regs_0 [4]" not in content
assert "m_apb_regs_1 [4]" not in content
assert "m_apb_regs_2 [4]" not in content
assert "m_apb_regs_3 [4]" not in content
# Should NOT have array size parameter when unrolled
assert "N_REGSS" not in content
def test_unroll_with_apb3(sample_rdl):
def test_unroll_with_apb3(sample_rdl: AddrmapNode) -> None:
"""Test that unroll works correctly with APB3 interface."""
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
@@ -100,40 +80,22 @@ def test_unroll_with_apb3(sample_rdl):
cpuif_cls=APB3Cpuif,
cpuif_unroll=True,
)
# Read the generated module
module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text()
# Should have individual APB3 interfaces
assert "m_apb_regs_0," in content or "m_apb_regs_0\n" in content
assert "m_apb_regs_1," in content or "m_apb_regs_1\n" in content
assert "m_apb_regs_2," in content or "m_apb_regs_2\n" in content
assert "m_apb_regs_3" in content
# Should NOT have array dimensions on unrolled interfaces
assert "m_apb_regs_0 [4]" not in content
@pytest.fixture
def multidim_array_rdl(compile_rdl):
"""Create an RDL design with a multi-dimensional array."""
rdl_source = """
addrmap top {
reg my_reg {
field {
sw=rw;
hw=r;
} data[31:0];
};
my_reg matrix[2][3] @ 0x0 += 0x4;
};
"""
return compile_rdl(rdl_source)
def test_unroll_multidimensional_array(multidim_array_rdl):
def test_unroll_multidimensional_array(multidim_array_rdl: AddrmapNode) -> None:
"""Test that unroll works correctly with multi-dimensional arrays."""
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
@@ -143,11 +105,11 @@ def test_unroll_multidimensional_array(multidim_array_rdl):
cpuif_cls=APB4Cpuif,
cpuif_unroll=True,
)
# Read the generated module
module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text()
# Should have individual interfaces for each element in the 2x3 array
# Format should be m_apb_matrix_0_0, m_apb_matrix_0_1, ..., m_apb_matrix_1_2
assert "m_apb_matrix_0_0" in content
@@ -156,7 +118,7 @@ def test_unroll_multidimensional_array(multidim_array_rdl):
assert "m_apb_matrix_1_0" in content
assert "m_apb_matrix_1_1" in content
assert "m_apb_matrix_1_2" in content
# Should NOT have array dimensions on any of the unrolled interfaces
for i in range(2):
for j in range(3):

0
tests/utils/__init__.py Normal file
View File

View File

@@ -1,96 +1,14 @@
"""Tests for utility functions."""
from collections.abc import Callable
from __future__ import annotations
import pytest
from systemrdl import RDLCompiler
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder.utils import clog2, get_indexed_path, is_pow2, roundup_pow2
class TestMathUtils:
"""Test mathematical utility functions."""
def test_clog2_basic(self):
"""Test clog2 function with basic values."""
assert clog2(1) == 0
assert clog2(2) == 1
assert clog2(3) == 2
assert clog2(4) == 2
assert clog2(5) == 3
assert clog2(8) == 3
assert clog2(9) == 4
assert clog2(16) == 4
assert clog2(17) == 5
assert clog2(32) == 5
assert clog2(33) == 6
assert clog2(64) == 6
assert clog2(128) == 7
assert clog2(256) == 8
assert clog2(1024) == 10
def test_is_pow2_true_cases(self):
"""Test is_pow2 returns True for powers of 2."""
assert is_pow2(1) is True
assert is_pow2(2) is True
assert is_pow2(4) is True
assert is_pow2(8) is True
assert is_pow2(16) is True
assert is_pow2(32) is True
assert is_pow2(64) is True
assert is_pow2(128) is True
assert is_pow2(256) is True
assert is_pow2(512) is True
assert is_pow2(1024) is True
def test_is_pow2_false_cases(self):
"""Test is_pow2 returns False for non-powers of 2."""
assert is_pow2(0) is False
assert is_pow2(3) is False
assert is_pow2(5) is False
assert is_pow2(6) is False
assert is_pow2(7) is False
assert is_pow2(9) is False
assert is_pow2(10) is False
assert is_pow2(15) is False
assert is_pow2(17) is False
assert is_pow2(100) is False
assert is_pow2(255) is False
assert is_pow2(1000) is False
def test_roundup_pow2_already_power_of_2(self):
"""Test roundup_pow2 with values that are already powers of 2."""
assert roundup_pow2(1) == 1
assert roundup_pow2(2) == 2
assert roundup_pow2(4) == 4
assert roundup_pow2(8) == 8
assert roundup_pow2(16) == 16
assert roundup_pow2(32) == 32
assert roundup_pow2(64) == 64
assert roundup_pow2(128) == 128
assert roundup_pow2(256) == 256
def test_roundup_pow2_non_power_of_2(self):
"""Test roundup_pow2 with values that are not powers of 2."""
assert roundup_pow2(3) == 4
assert roundup_pow2(5) == 8
assert roundup_pow2(6) == 8
assert roundup_pow2(7) == 8
assert roundup_pow2(9) == 16
assert roundup_pow2(15) == 16
assert roundup_pow2(17) == 32
assert roundup_pow2(31) == 32
assert roundup_pow2(33) == 64
assert roundup_pow2(100) == 128
assert roundup_pow2(255) == 256
assert roundup_pow2(257) == 512
from peakrdl_busdecoder.utils import get_indexed_path
class TestGetIndexedPath:
"""Test get_indexed_path function."""
def test_simple_path(self, compile_rdl):
def test_simple_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test simple path without arrays."""
rdl_source = """
addrmap my_addrmap {
@@ -111,7 +29,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node)
assert path == "my_reg"
def test_nested_path(self, compile_rdl):
def test_nested_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test nested path without arrays."""
rdl_source = """
addrmap inner_map {
@@ -143,7 +61,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node)
assert path == "inner.my_reg"
def test_array_path(self, compile_rdl):
def test_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test path with array indices."""
rdl_source = """
addrmap my_addrmap {
@@ -163,7 +81,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node)
assert path == "my_reg[i0]"
def test_multidimensional_array_path(self, compile_rdl):
def test_multidimensional_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test path with multidimensional arrays."""
rdl_source = """
addrmap my_addrmap {
@@ -183,7 +101,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node)
assert path == "my_reg[i0][i1]"
def test_nested_array_path(self, compile_rdl):
def test_nested_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test path with nested arrays."""
rdl_source = """
addrmap inner_map {
@@ -215,7 +133,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node)
assert path == "inner[i0].my_reg[i1]"
def test_custom_indexer(self, compile_rdl):
def test_custom_indexer(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test path with custom indexer name."""
rdl_source = """
addrmap my_addrmap {
@@ -235,7 +153,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node, indexer="idx")
assert path == "my_reg[idx0]"
def test_skip_kw_filter(self, compile_rdl):
def test_skip_kw_filter(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test path with keyword filtering skipped."""
rdl_source = """
addrmap my_addrmap {

View File

@@ -0,0 +1,79 @@
from peakrdl_busdecoder.utils import clog2, is_pow2, roundup_pow2
class TestMathUtils:
"""Test mathematical utility functions."""
def test_clog2_basic(self) -> None:
"""Test clog2 function with basic values."""
assert clog2(1) == 0
assert clog2(2) == 1
assert clog2(3) == 2
assert clog2(4) == 2
assert clog2(5) == 3
assert clog2(8) == 3
assert clog2(9) == 4
assert clog2(16) == 4
assert clog2(17) == 5
assert clog2(32) == 5
assert clog2(33) == 6
assert clog2(64) == 6
assert clog2(128) == 7
assert clog2(256) == 8
assert clog2(1024) == 10
def test_is_pow2_true_cases(self) -> None:
"""Test is_pow2 returns True for powers of 2."""
assert is_pow2(1) is True
assert is_pow2(2) is True
assert is_pow2(4) is True
assert is_pow2(8) is True
assert is_pow2(16) is True
assert is_pow2(32) is True
assert is_pow2(64) is True
assert is_pow2(128) is True
assert is_pow2(256) is True
assert is_pow2(512) is True
assert is_pow2(1024) is True
def test_is_pow2_false_cases(self) -> None:
"""Test is_pow2 returns False for non-powers of 2."""
assert is_pow2(0) is False
assert is_pow2(3) is False
assert is_pow2(5) is False
assert is_pow2(6) is False
assert is_pow2(7) is False
assert is_pow2(9) is False
assert is_pow2(10) is False
assert is_pow2(15) is False
assert is_pow2(17) is False
assert is_pow2(100) is False
assert is_pow2(255) is False
assert is_pow2(1000) is False
def test_roundup_pow2_already_power_of_2(self) -> None:
"""Test roundup_pow2 with values that are already powers of 2."""
assert roundup_pow2(1) == 1
assert roundup_pow2(2) == 2
assert roundup_pow2(4) == 4
assert roundup_pow2(8) == 8
assert roundup_pow2(16) == 16
assert roundup_pow2(32) == 32
assert roundup_pow2(64) == 64
assert roundup_pow2(128) == 128
assert roundup_pow2(256) == 256
def test_roundup_pow2_non_power_of_2(self) -> None:
"""Test roundup_pow2 with values that are not powers of 2."""
assert roundup_pow2(3) == 4
assert roundup_pow2(5) == 8
assert roundup_pow2(6) == 8
assert roundup_pow2(7) == 8
assert roundup_pow2(9) == 16
assert roundup_pow2(15) == 16
assert roundup_pow2(17) == 32
assert roundup_pow2(31) == 32
assert roundup_pow2(33) == 64
assert roundup_pow2(100) == 128
assert roundup_pow2(255) == 256
assert roundup_pow2(257) == 512

62
tools/shims/xargs Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Minimal xargs replacement for environments where /usr/bin/xargs is unavailable.
Supports the subset of functionality exercised by Verilator's generated makefiles:
optional -0 (NUL-delimited input) and -t (echo command) flags, followed by a command
invocation constructed from stdin tokens.
"""
from __future__ import annotations
import os
import subprocess
import sys
def main() -> int:
args = sys.argv[1:]
show_cmd = False
null_delimited = False
while args and args[0].startswith("-") and args[0] != "-":
opt = args.pop(0)
if opt == "-0":
null_delimited = True
elif opt == "-t":
show_cmd = True
else:
sys.stderr.write(f"xargs shim: unsupported option {opt}\n")
return 1
if not args:
args = ["echo"]
data = sys.stdin.buffer.read()
if not data.strip():
return 0
if null_delimited:
items = [chunk.decode() for chunk in data.split(b"\0") if chunk]
else:
items = data.decode().split()
if not items:
return 0
cmd = args + items
if show_cmd:
print(" ".join(cmd))
try:
subprocess.check_call(cmd)
except FileNotFoundError:
return 127
except subprocess.CalledProcessError as exc:
return exc.returncode
return 0
if __name__ == "__main__":
raise SystemExit(main())

1321
uv.lock generated

File diff suppressed because it is too large Load Diff