Refactor tests (better grouping + cocotb support) (#15)

* initial refactor

* fix cocotb tests

* fix typecheck

* install verilator
This commit is contained in:
Arnav Sacheti
2025-10-26 17:56:35 -07:00
committed by GitHub
parent 93276ff616
commit b1f1bf983a
66 changed files with 1734 additions and 2963 deletions

View File

@@ -24,6 +24,12 @@ jobs:
uses: astral-sh/setup-uv@v3 uses: astral-sh/setup-uv@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Verilator
run: |
sudo apt-get update
sudo apt-get install -y verilator
verilator --version
- name: Install dependencies - name: Install dependencies
run: | run: |

View File

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

View File

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

View File

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

View File

@@ -12,14 +12,14 @@ class APB3CpuifFlat(BaseCpuif):
return [ return [
f"input logic {self.signal('PCLK', child)}", f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}", f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}", f"output logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}", f"output logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}", f"output logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}", f"output 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('PWDATA', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}", f"input logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}", f"input logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}", f"input logic {self.signal('PSLVERR', child)}",
] ]
@property @property
@@ -48,9 +48,10 @@ class APB3CpuifFlat(BaseCpuif):
node: AddressableNode | None = None, node: AddressableNode | None = None,
idx: str | int | None = None, idx: str | int | None = None,
) -> str: ) -> str:
mapped_signal = "PSELx" if signal == "PSEL" else signal
if node is None: if node is None:
# Node is none, so this is a slave signal # Node is none, so this is a slave signal
return f"s_apb_{signal}" return f"s_apb_{mapped_signal}"
# Master signal # Master signal
base = f"m_apb_{node.inst_name}" base = f"m_apb_{node.inst_name}"
@@ -58,12 +59,12 @@ class APB3CpuifFlat(BaseCpuif):
# Not an array or an unrolled element # Not an array or an unrolled element
if node.current_idx is not None: if node.current_idx is not None:
# This is a specific instance of an unrolled array # This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}" return f"{base}_{mapped_signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}" return f"{base}_{mapped_signal}"
# Is an array # Is an array
if idx is not None: if idx is not None:
return f"{base}_{signal}[{idx}]" return f"{base}_{mapped_signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]" return f"{base}_{mapped_signal}[N_{node.inst_name.upper()}S]"
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}

View File

@@ -12,16 +12,16 @@ class APB4CpuifFlat(BaseCpuif):
return [ return [
f"input logic {self.signal('PCLK', child)}", f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}", f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}", f"output logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}", f"output logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}", f"output logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}", f"output logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [2:0] {self.signal('PPROT', child)}", f"output logic [2:0] {self.signal('PPROT', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}", f"output 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 // 8 - 1}:0] {self.signal('PSTRB', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}", f"input logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}", f"input logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}", f"input logic {self.signal('PSLVERR', child)}",
] ]
@property @property
@@ -52,9 +52,10 @@ class APB4CpuifFlat(BaseCpuif):
node: AddressableNode | None = None, node: AddressableNode | None = None,
idx: str | int | None = None, idx: str | int | None = None,
) -> str: ) -> str:
mapped_signal = "PSELx" if signal == "PSEL" else signal
if node is None: if node is None:
# Node is none, so this is a slave signal # Node is none, so this is a slave signal
return f"s_apb_{signal}" return f"s_apb_{mapped_signal}"
# Master signal # Master signal
base = f"m_apb_{node.inst_name}" base = f"m_apb_{node.inst_name}"
@@ -62,12 +63,12 @@ class APB4CpuifFlat(BaseCpuif):
# Not an array or an unrolled element # Not an array or an unrolled element
if node.current_idx is not None: if node.current_idx is not None:
# This is a specific instance of an unrolled array # This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}" return f"{base}_{mapped_signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}" return f"{base}_{mapped_signal}"
# Is an array # Is an array
if idx is not None: if idx is not None:
return f"{base}_{signal}[{idx}]" return f"{base}_{mapped_signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]" return f"{base}_{mapped_signal}[N_{node.inst_name.upper()}S]"
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}

View File

@@ -10,7 +10,7 @@ class AXI4LiteCpuif(BaseCpuif):
template_path = "axi4lite_tmpl.sv" template_path = "axi4lite_tmpl.sv"
is_interface = True is_interface = True
def _port_declaration(self, child: AddressableNode) -> str: def _port_declaration(self, child: AddressableNode) -> list[str]:
base = f"axi4lite_intf.master m_axil_{child.inst_name}" base = f"axi4lite_intf.master m_axil_{child.inst_name}"
# When unrolled, current_idx is set - append it to the name # When unrolled, current_idx is set - append it to the name
@@ -20,22 +20,25 @@ class AXI4LiteCpuif(BaseCpuif):
# Only add array dimensions if this should be treated as an array # Only add array dimensions if this should be treated as an array
if self.check_is_array(child): if self.check_is_array(child):
assert child.array_dimensions is not None assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}" return [f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"]
return base return [base]
@property @property
def port_declaration(self) -> str: def port_declaration(self) -> str:
"""Returns the port declaration for the AXI4-Lite interface.""" """Returns the port declaration for the AXI4-Lite interface."""
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"] slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
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 ",\n".join(slave_ports + master_ports)
@overload @overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ... def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload @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: def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None: if node is None or indexer is None:
# Node is none, so this is a slave signal # Node is none, so this is a slave signal

View File

@@ -2,94 +2,91 @@ from typing import overload
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from ...utils import get_indexed_path from .axi4_lite_cpuif import AXI4LiteCpuif
from ..base_cpuif import BaseCpuif
class AXI4LiteCpuifFlat(BaseCpuif): class AXI4LiteCpuifFlat(AXI4LiteCpuif):
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
template_path = "axi4lite_tmpl.sv" template_path = "axi4lite_tmpl.sv"
is_interface = True is_interface = False
def _port_declaration(self, child: AddressableNode) -> str: def _port_declaration(self, child: AddressableNode) -> list[str]:
base = f"axi4lite_intf.master m_axil_{child.inst_name}" return [
f"input logic {self.signal('AWREADY', child)}",
# When unrolled, current_idx is set - append it to the name f"output logic {self.signal('AWVALID', child)}",
if child.current_idx is not None: f"output logic [{self.addr_width - 1}:0] {self.signal('AWADDR', child)}",
base = f"{base}_{'_'.join(map(str, child.current_idx))}" f"output logic [2:0] {self.signal('AWPROT', child)}",
f"input logic {self.signal('WREADY', child)}",
# Only add array dimensions if this should be treated as an array f"output logic {self.signal('WVALID', child)}",
if self.check_is_array(child): f"output logic [{self.data_width - 1}:0] {self.signal('WDATA', child)}",
assert child.array_dimensions is not None f"output logic [{self.data_width_bytes - 1}:0] {self.signal('WSTRB', child)}",
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}" f"output logic {self.signal('BREADY', child)}",
f"input logic {self.signal('BVALID', child)}",
return base f"input logic [1:0] {self.signal('BRESP', child)}",
f"input logic {self.signal('ARREADY', child)}",
f"output logic {self.signal('ARVALID', child)}",
f"output logic [{self.addr_width - 1}:0] {self.signal('ARADDR', child)}",
f"output logic [2:0] {self.signal('ARPROT', child)}",
f"output logic {self.signal('RREADY', child)}",
f"input logic {self.signal('RVALID', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('RDATA', child)}",
f"input logic [1:0] {self.signal('RRESP', child)}",
]
@property @property
def port_declaration(self) -> str: def port_declaration(self) -> str:
"""Returns the port declaration for the AXI4-Lite interface.""" slave_ports: list[str] = [
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"] f"input logic {self.signal('ACLK')}",
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children)) f"input logic {self.signal('ARESETn')}",
f"output logic {self.signal('AWREADY')}",
f"input logic {self.signal('AWVALID')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('AWADDR')}",
f"input logic [2:0] {self.signal('AWPROT')}",
f"output logic {self.signal('WREADY')}",
f"input logic {self.signal('WVALID')}",
f"input logic [{self.data_width - 1}:0] {self.signal('WDATA')}",
f"input logic [{self.data_width_bytes - 1}:0] {self.signal('WSTRB')}",
f"input logic {self.signal('BREADY')}",
f"output logic {self.signal('BVALID')}",
f"output logic [1:0] {self.signal('BRESP')}",
f"output logic {self.signal('ARREADY')}",
f"input logic {self.signal('ARVALID')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('ARADDR')}",
f"input logic [2:0] {self.signal('ARPROT')}",
f"input logic {self.signal('RREADY')}",
f"output logic {self.signal('RVALID')}",
f"output logic [{self.data_width - 1}:0] {self.signal('RDATA')}",
f"output logic [1:0] {self.signal('RRESP')}",
]
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 ",\n".join(slave_ports + master_ports)
@overload @overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ... def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload @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 def signal(
return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}" self,
signal: str,
def fanout(self, node: AddressableNode) -> str: node: AddressableNode | None = None,
fanout: dict[str, str] = {} indexer: str | None = None,
) -> str:
wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
rd_sel = f"cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
# Write address channel
fanout[self.signal("AWVALID", node, "gi")] = wr_sel
fanout[self.signal("AWADDR", node, "gi")] = self.signal("AWADDR")
fanout[self.signal("AWPROT", node, "gi")] = self.signal("AWPROT")
# Write data channel
fanout[self.signal("WVALID", node, "gi")] = wr_sel
fanout[self.signal("WDATA", node, "gi")] = "cpuif_wr_data"
fanout[self.signal("WSTRB", node, "gi")] = "cpuif_wr_byte_en"
# Write response channel (master -> slave)
fanout[self.signal("BREADY", node, "gi")] = self.signal("BREADY")
# Read address channel
fanout[self.signal("ARVALID", node, "gi")] = rd_sel
fanout[self.signal("ARADDR", node, "gi")] = self.signal("ARADDR")
fanout[self.signal("ARPROT", node, "gi")] = self.signal("ARPROT")
# Read data channel (master -> slave)
fanout[self.signal("RREADY", node, "gi")] = self.signal("RREADY")
return "\n".join(f"assign {lhs} = {rhs};" for lhs, rhs in fanout.items())
def fanin(self, node: AddressableNode | None = None) -> str:
fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" return f"s_axil_{signal}"
fanin["cpuif_rd_err"] = "'0"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items()) base = f"m_axil_{node.inst_name}_{signal}"
if not self.check_is_array(node):
if node.current_idx is not None:
return f"{base}_{'_'.join(map(str, node.current_idx))}"
return base
def readback(self, node: AddressableNode | None = None) -> str: if indexer is None:
fanin: dict[str, str] = {} return f"{base}[N_{node.inst_name.upper()}S]"
if node is None: return f"{base}[{indexer}]"
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())

View File

@@ -63,11 +63,16 @@ class DecodeLogicGenerator(BusDecoderListener):
l_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})") 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)})") u_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})")
# Generate Conditions lower_expr = f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})"
return [ upper_expr = f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})"
f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})",
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]: def cpuif_prot_predicate(self, node: AddressableNode) -> list[str]:
if self._flavor == DecodeLogicFlavor.READ: if self._flavor == DecodeLogicFlavor.READ:

View File

@@ -44,7 +44,7 @@ module {{ds.module_name}}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Slave <-> Internal CPUIF <-> Master // Slave <-> Internal CPUIF <-> Master
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{-cpuif.get_implementation()|indent(4)}} {{cpuif.get_implementation()|indent(4)}}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Write Address Decoder // Write Address Decoder

View File

@@ -72,16 +72,28 @@ Run examples to see generated code for different configurations:
python tests/cocotb/examples.py 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) #### Full simulation tests (requires simulator)
To run the full cocotb simulation tests: To execute the smoke tests for every supported interface:
```bash ```bash
# Run all cocotb simulation tests pytest tests/cocotb/*/smoke/test_runner.py -v
pytest tests/cocotb/testbenches/test_*_runner.py -v ```
# Run specific interface tests To target a single interface, point pytest at that runner module:
pytest tests/cocotb/testbenches/test_apb4_runner.py -v
```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). 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

View File

@@ -1,142 +0,0 @@
# Cocotb Testbench Implementation Summary
## Overview
This implementation adds comprehensive cocotb-based testbenches for validating generated SystemVerilog bus decoder RTL across multiple CPU interface types (APB3, APB4, and AXI4-Lite).
## Files Added
### Test Infrastructure
- **pyproject.toml** - Added cocotb-test dependency group
- **tests/cocotb/common/utils.py** - Utilities for RDL compilation and code generation
- **tests/cocotb/common/apb4_master.py** - APB4 Bus Functional Model
- **tests/cocotb/Makefile.common** - Makefile template for cocotb simulations
### Testbenches
- **tests/cocotb/testbenches/test_apb4_decoder.py** - APB4 interface tests (3 test cases)
- test_simple_read_write: Basic read/write operations
- test_multiple_registers: Multiple register access
- test_byte_strobe: Byte strobe functionality
- **tests/cocotb/testbenches/test_apb3_decoder.py** - APB3 interface tests (2 test cases)
- test_simple_read_write: Basic read/write operations
- test_multiple_registers: Multiple register access
- **tests/cocotb/testbenches/test_axi4lite_decoder.py** - AXI4-Lite interface tests (3 test cases)
- test_simple_read_write: Basic read/write operations
- test_multiple_registers: Multiple register access
- test_byte_strobe: Byte strobe functionality
- **tests/cocotb/testbenches/test_apb4_runner.py** - Pytest wrapper for running APB4 tests
- **tests/cocotb/testbenches/test_integration.py** - Integration tests (9 test cases)
- Tests code generation for all three interfaces
- Tests utility functions
- Validates generated code structure
### Documentation & Examples
- **tests/cocotb/README.md** - Comprehensive cocotb test documentation
- **tests/cocotb/examples.py** - Example script demonstrating code generation
- **tests/README.md** - Updated with cocotb test instructions
## Features
### Bus Functional Models (BFMs)
Each CPU interface has both master and slave BFMs:
- **APB4Master/APB4SlaveResponder**: Full APB4 protocol with PSTRB support
- **APB3Master/APB3SlaveResponder**: APB3 protocol without PSTRB
- **AXI4LiteMaster/AXI4LiteSlaveResponder**: Full AXI4-Lite protocol with separate channels
### Test Coverage
1. **Simple read/write operations**: Verify basic decoder functionality
2. **Multiple registers**: Test address decoding for multiple targets
3. **Register arrays**: Validate array handling
4. **Byte strobes**: Test partial word writes (APB4, AXI4-Lite)
5. **Nested address maps**: Validate hierarchical structures
### Code Generation Tests
The integration tests validate:
- Code generation for all three CPU interfaces
- Custom module/package naming
- Register arrays
- Nested address maps
- Generated code structure and content
## How to Run
### Integration Tests (No Simulator Required)
```bash
pytest tests/cocotb/testbenches/test_integration.py -v
```
### Example Script
```bash
python -m tests.cocotb.examples
```
### Full Simulation Tests (Requires Simulator)
```bash
# Install simulator first
apt-get install iverilog # or verilator
# Install cocotb
uv sync --group cocotb-test
# Run tests (when simulator available)
pytest tests/cocotb/testbenches/test_*_runner.py -v
```
## Test Results
### ✅ Integration Tests: 9/9 Passing
- test_apb4_simple_register
- test_apb3_multiple_registers
- test_axi4lite_nested_addrmap
- test_register_array
- test_get_verilog_sources
- test_compile_rdl_and_export_with_custom_names
- test_cpuif_generation[APB3Cpuif-apb3_intf]
- test_cpuif_generation[APB4Cpuif-apb4_intf]
- test_cpuif_generation[AXI4LiteCpuif-axi4lite_intf]
### ✅ Existing Unit Tests: 56/56 Passing
- No regressions introduced
- 4 pre-existing failures in test_unroll.py remain unchanged
### ✅ Security Checks
- No vulnerabilities found in new dependencies (cocotb, cocotb-bus)
- CodeQL analysis: 0 alerts
## Design Decisions
1. **Separate integration tests**: Created tests that run without a simulator for CI/CD friendliness
2. **Relative imports**: Used proper Python package structure instead of sys.path manipulation
3. **Multiple CPU interfaces**: Comprehensive coverage of all supported interfaces
4. **BFM architecture**: Reusable Bus Functional Models for each protocol
5. **Example script**: Provides easy-to-run demonstrations of code generation
## Future Enhancements
1. **CI Integration**: Add optional CI job with simulator for full cocotb tests
2. **More test scenarios**: Coverage tests, error handling, corner cases
3. **Avalon MM support**: Add testbenches for Avalon Memory-Mapped interface
4. **Waveform verification**: Automated protocol compliance checking
5. **Performance tests**: Bus utilization and throughput testing
## Dependencies
### Required
- peakrdl-busdecoder (existing)
- systemrdl-compiler (existing)
### Optional (for simulation)
- cocotb >= 1.8.0
- cocotb-bus >= 0.2.1
- iverilog or verilator (HDL simulator)
## Notes
- Integration tests run in CI without requiring a simulator
- Full simulation tests are marked with pytest.skip when simulator not available
- All code follows project conventions (ruff formatting, type hints)
- Documentation includes both uv (project standard) and pip alternatives

View File

@@ -1,39 +0,0 @@
# Makefile for cocotb simulation
# This makefile can be used to run cocotb tests with Icarus Verilog or other simulators
# Defaults
SIM ?= icarus
TOPLEVEL_LANG ?= verilog
# Project paths
REPO_ROOT := $(shell git rev-parse --show-toplevel)
HDL_SRC_DIR := $(REPO_ROOT)/hdl-src
TEST_DIR := $(REPO_ROOT)/tests/cocotb
COMMON_DIR := $(TEST_DIR)/common
# Python paths
export PYTHONPATH := $(REPO_ROOT):$(TEST_DIR):$(PYTHONPATH)
# Simulator options
COMPILE_ARGS += -g2012 # SystemVerilog 2012
EXTRA_ARGS += --trace --trace-structs
# Common sources (interfaces)
VERILOG_SOURCES += $(HDL_SRC_DIR)/apb4_intf.sv
VERILOG_SOURCES += $(HDL_SRC_DIR)/apb3_intf.sv
VERILOG_SOURCES += $(HDL_SRC_DIR)/axi4lite_intf.sv
# Test-specific configuration
# These should be overridden by test-specific makefiles
# Example usage:
# To run APB4 test:
# make -f Makefile.apb4 test_simple
#
# To run all tests:
# make -f Makefile.apb4 test
# make -f Makefile.apb3 test
# make -f Makefile.axi4lite test
# Include cocotb's make rules to build/run simulation
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

@@ -1,53 +0,0 @@
# Cocotb Integration Tests
This directory contains cocotb-based integration tests that verify the functionality
of generated bus decoder RTL for different CPU interfaces.
## Overview
These tests:
1. Generate SystemVerilog decoder modules from SystemRDL specifications
2. Simulate the generated RTL using cocotb
3. Verify read/write operations work correctly for different bus protocols
## Supported CPU Interfaces
- APB3 (AMBA APB3)
- APB4 (AMBA APB4 with strobe support)
- AXI4-Lite (AMBA AXI4-Lite)
## Running the Tests
### Install Dependencies
```bash
# Using uv (recommended)
uv sync --group test
# Or using pip
pip install -e .[test]
```
### Run All Cocotb Tests
```bash
pytest tests/cocotb/testbenches/
```
### Run Specific Interface Tests
```bash
# Test APB4 interface
pytest tests/cocotb/testbenches/test_apb4_decoder.py
# Test APB3 interface
pytest tests/cocotb/testbenches/test_apb3_decoder.py
# Test AXI4-Lite interface
pytest tests/cocotb/testbenches/test_axi4lite_decoder.py
```
## Test Structure
- `common/`: Shared utilities and base classes for cocotb tests
- `testbenches/`: Individual testbenches for each CPU interface

View File

@@ -1 +0,0 @@
"""Cocotb tests package."""

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}_PSELx")
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}_PSELx")
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}_PSELx")
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}_PSELx")
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

@@ -1,5 +0,0 @@
"""Common cocotb utilities package."""
from .utils import compile_rdl_and_export, get_verilog_sources
__all__ = ["compile_rdl_and_export", "get_verilog_sources"]

View File

@@ -1,123 +0,0 @@
"""APB4 Master Bus Functional Model for cocotb."""
import cocotb
from cocotb.triggers import RisingEdge, Timer
class APB4Master:
"""APB4 Master Bus Functional Model."""
def __init__(self, dut, name, clock):
"""
Initialize APB4 Master.
Args:
dut: The device under test
name: Signal name prefix (e.g., 's_apb')
clock: Clock signal to use for synchronization
"""
self.dut = dut
self.clock = clock
self.name = name
# Get signals
self.psel = getattr(dut, f"{name}_PSEL")
self.penable = getattr(dut, f"{name}_PENABLE")
self.pwrite = getattr(dut, f"{name}_PWRITE")
self.paddr = getattr(dut, f"{name}_PADDR")
self.pwdata = getattr(dut, f"{name}_PWDATA")
self.pstrb = getattr(dut, f"{name}_PSTRB")
self.pprot = getattr(dut, f"{name}_PPROT")
self.prdata = getattr(dut, f"{name}_PRDATA")
self.pready = getattr(dut, f"{name}_PREADY")
self.pslverr = getattr(dut, f"{name}_PSLVERR")
def reset(self):
"""Reset the bus to idle state."""
self.psel.value = 0
self.penable.value = 0
self.pwrite.value = 0
self.paddr.value = 0
self.pwdata.value = 0
self.pstrb.value = 0
self.pprot.value = 0
async def write(self, addr, data, strb=None):
"""
Perform APB4 write transaction.
Args:
addr: Address to write to
data: Data to write
strb: Byte strobe mask (default: all bytes enabled)
Returns:
True if write succeeded, False if error
"""
# Calculate strobe if not provided
if strb is None:
data_width_bytes = len(self.pwdata) // 8
strb = (1 << data_width_bytes) - 1
# Setup phase
await RisingEdge(self.clock)
self.psel.value = 1
self.penable.value = 0
self.pwrite.value = 1
self.paddr.value = addr
self.pwdata.value = data
self.pstrb.value = strb
self.pprot.value = 0
# Access phase
await RisingEdge(self.clock)
self.penable.value = 1
# Wait for ready
while True:
await RisingEdge(self.clock)
if self.pready.value == 1:
error = self.pslverr.value == 1
break
# Return to idle
self.psel.value = 0
self.penable.value = 0
return not error
async def read(self, addr):
"""
Perform APB4 read transaction.
Args:
addr: Address to read from
Returns:
Tuple of (data, error) where error is True if read failed
"""
# Setup phase
await RisingEdge(self.clock)
self.psel.value = 1
self.penable.value = 0
self.pwrite.value = 0
self.paddr.value = addr
self.pprot.value = 0
# Access phase
await RisingEdge(self.clock)
self.penable.value = 1
# Wait for ready
while True:
await RisingEdge(self.clock)
if self.pready.value == 1:
data = self.prdata.value.integer
error = self.pslverr.value == 1
break
# Return to idle
self.psel.value = 0
self.penable.value = 0
return data, error

View File

@@ -1,221 +0,0 @@
#!/usr/bin/env python3
"""
Example script showing how to generate and test bus decoders.
This script demonstrates:
1. Compiling RDL specifications
2. Generating SystemVerilog decoders for different CPU interfaces
3. Validating the generated code (syntax check only, no simulation)
To run actual cocotb simulations, you need:
- Icarus Verilog, Verilator, or other HDL simulator
- cocotb and cocotb-bus Python packages
"""
import sys
import tempfile
from pathlib import Path
from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif
from .common.utils import compile_rdl_and_export
def example_apb4_simple_register():
"""Generate APB4 decoder for a simple register."""
print("\n" + "=" * 70)
print("Example 1: APB4 Decoder with Simple Register")
print("=" * 70)
rdl_source = """
addrmap simple_test {
name = "Simple Register Test";
desc = "A simple register for testing";
reg {
name = "Test Register";
desc = "32-bit test register";
field {
sw=rw;
hw=r;
desc = "Data field";
} data[31:0];
} test_reg @ 0x0;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
print(f"\nGenerating SystemVerilog in: {tmpdir}")
module_path, package_path = compile_rdl_and_export(
rdl_source, "simple_test", tmpdir, APB4Cpuif
)
print(f"✓ Generated module: {module_path.name}")
print(f"✓ Generated package: {package_path.name}")
# Show snippet of generated code
with open(module_path) as f:
lines = f.readlines()[:20]
print("\n--- Generated Module (first 20 lines) ---")
for line in lines:
print(line, end="")
def example_apb3_multiple_registers():
"""Generate APB3 decoder for multiple registers."""
print("\n" + "=" * 70)
print("Example 2: APB3 Decoder with Multiple Registers")
print("=" * 70)
rdl_source = """
addrmap multi_reg {
name = "Multiple Register Block";
reg {
name = "Control Register";
field { sw=rw; hw=r; } data[31:0];
} ctrl @ 0x0;
reg {
name = "Status Register";
field { sw=r; hw=w; } status[15:0];
} status @ 0x4;
reg {
name = "Data Register";
field { sw=rw; hw=r; } data[31:0];
} data @ 0x8;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
print(f"\nGenerating SystemVerilog in: {tmpdir}")
module_path, package_path = compile_rdl_and_export(
rdl_source, "multi_reg", tmpdir, APB3Cpuif
)
print(f"✓ Generated module: {module_path.name}")
print(f"✓ Generated package: {package_path.name}")
# Count registers in generated code
with open(module_path) as f:
content = f.read()
print(f"\n✓ Found 'ctrl' in generated code: {'ctrl' in content}")
print(f"✓ Found 'status' in generated code: {'status' in content}")
print(f"✓ Found 'data' in generated code: {'data' in content}")
def example_axi4lite_nested_addrmap():
"""Generate AXI4-Lite decoder for nested address map."""
print("\n" + "=" * 70)
print("Example 3: AXI4-Lite Decoder with Nested Address Map")
print("=" * 70)
rdl_source = """
addrmap inner_block {
name = "Inner Block";
reg {
field { sw=rw; hw=r; } data[31:0];
} inner_reg @ 0x0;
};
addrmap outer_block {
name = "Outer Block";
inner_block inner @ 0x0;
reg {
field { sw=rw; hw=r; } outer_data[31:0];
} outer_reg @ 0x100;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
print(f"\nGenerating SystemVerilog in: {tmpdir}")
module_path, package_path = compile_rdl_and_export(
rdl_source, "outer_block", tmpdir, AXI4LiteCpuif
)
print(f"✓ Generated module: {module_path.name}")
print(f"✓ Generated package: {package_path.name}")
# Check for nested structure
with open(module_path) as f:
content = f.read()
print(f"\n✓ Found 'inner' in generated code: {'inner' in content}")
print(f"✓ Found 'outer_reg' in generated code: {'outer_reg' in content}")
def example_register_array():
"""Generate decoder with register arrays."""
print("\n" + "=" * 70)
print("Example 4: Decoder with Register Arrays")
print("=" * 70)
rdl_source = """
addrmap array_test {
name = "Register Array Test";
reg {
field { sw=rw; hw=r; } data[31:0];
} regs[8] @ 0x0 += 0x4;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
print(f"\nGenerating SystemVerilog in: {tmpdir}")
module_path, package_path = compile_rdl_and_export(
rdl_source, "array_test", tmpdir, APB4Cpuif
)
print(f"✓ Generated module: {module_path.name}")
print(f"✓ Generated package: {package_path.name}")
with open(module_path) as f:
content = f.read()
print(f"\n✓ Found 'regs' in generated code: {'regs' in content}")
def main():
"""Run all examples."""
print("\n")
print("*" * 70)
print("*" + " " * 68 + "*")
print("*" + " PeakRDL-BusDecoder: Code Generation Examples".center(68) + "*")
print("*" + " " * 68 + "*")
print("*" * 70)
try:
example_apb4_simple_register()
example_apb3_multiple_registers()
example_axi4lite_nested_addrmap()
example_register_array()
print("\n" + "=" * 70)
print("All examples completed successfully!")
print("=" * 70)
print(
"""
To run actual simulations with cocotb:
1. Install simulator: apt-get install iverilog (or verilator)
2. Install cocotb: pip install cocotb cocotb-bus
3. Run tests: pytest tests/cocotb/testbenches/
For more information, see: tests/cocotb/README.md
"""
)
except Exception as e:
print(f"\n✗ Error: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1 +0,0 @@
"""Cocotb testbenches package."""

View File

@@ -1,184 +0,0 @@
"""Cocotb tests for APB3 bus decoder."""
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
class APB3Master:
"""APB3 Master Bus Functional Model (no PSTRB support)."""
def __init__(self, dut, name, clock):
self.dut = dut
self.clock = clock
self.name = name
self.psel = getattr(dut, f"{name}_PSEL")
self.penable = getattr(dut, f"{name}_PENABLE")
self.pwrite = getattr(dut, f"{name}_PWRITE")
self.paddr = getattr(dut, f"{name}_PADDR")
self.pwdata = getattr(dut, f"{name}_PWDATA")
self.prdata = getattr(dut, f"{name}_PRDATA")
self.pready = getattr(dut, f"{name}_PREADY")
self.pslverr = getattr(dut, f"{name}_PSLVERR")
def reset(self):
"""Reset the bus to idle state."""
self.psel.value = 0
self.penable.value = 0
self.pwrite.value = 0
self.paddr.value = 0
self.pwdata.value = 0
async def write(self, addr, data):
"""Perform APB3 write transaction."""
await RisingEdge(self.clock)
self.psel.value = 1
self.penable.value = 0
self.pwrite.value = 1
self.paddr.value = addr
self.pwdata.value = data
await RisingEdge(self.clock)
self.penable.value = 1
while True:
await RisingEdge(self.clock)
if self.pready.value == 1:
error = self.pslverr.value == 1
break
self.psel.value = 0
self.penable.value = 0
return not error
async def read(self, addr):
"""Perform APB3 read transaction."""
await RisingEdge(self.clock)
self.psel.value = 1
self.penable.value = 0
self.pwrite.value = 0
self.paddr.value = addr
await RisingEdge(self.clock)
self.penable.value = 1
while True:
await RisingEdge(self.clock)
if self.pready.value == 1:
data = self.prdata.value.integer
error = self.pslverr.value == 1
break
self.psel.value = 0
self.penable.value = 0
return data, error
class APB3SlaveResponder:
"""Simple APB3 Slave responder."""
def __init__(self, dut, name, clock):
self.dut = dut
self.clock = clock
self.name = name
self.psel = getattr(dut, f"{name}_PSEL")
self.penable = getattr(dut, f"{name}_PENABLE")
self.pwrite = getattr(dut, f"{name}_PWRITE")
self.paddr = getattr(dut, f"{name}_PADDR")
self.pwdata = getattr(dut, f"{name}_PWDATA")
self.prdata = getattr(dut, f"{name}_PRDATA")
self.pready = getattr(dut, f"{name}_PREADY")
self.pslverr = getattr(dut, f"{name}_PSLVERR")
self.storage = {}
async def run(self):
"""Run the slave responder."""
while True:
await RisingEdge(self.clock)
if self.psel.value == 1 and self.penable.value == 1:
addr = self.paddr.value.integer
if self.pwrite.value == 1:
data = self.pwdata.value.integer
self.storage[addr] = data
self.pready.value = 1
self.pslverr.value = 0
else:
data = self.storage.get(addr, 0)
self.prdata.value = data
self.pready.value = 1
self.pslverr.value = 0
else:
self.pready.value = 0
self.pslverr.value = 0
@cocotb.test()
async def test_simple_read_write(dut):
"""Test simple read and write operations on APB3."""
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
master = APB3Master(dut, "s_apb", dut.clk)
slave = APB3SlaveResponder(dut, "m_apb_test_reg", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
cocotb.start_soon(slave.run())
for _ in range(5):
await RisingEdge(dut.clk)
# Write test
dut._log.info("Writing 0xABCD1234 to address 0x0")
success = await master.write(0x0, 0xABCD1234)
assert success, "Write operation failed"
# Read test
dut._log.info("Reading from address 0x0")
data, error = await master.read(0x0)
assert not error, "Read operation returned error"
assert data == 0xABCD1234, f"Read data mismatch: expected 0xABCD1234, got 0x{data:08X}"
dut._log.info("Test passed!")
@cocotb.test()
async def test_multiple_registers(dut):
"""Test operations on multiple registers with APB3."""
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
master = APB3Master(dut, "s_apb", dut.clk)
slave1 = APB3SlaveResponder(dut, "m_apb_reg1", dut.clk)
slave2 = APB3SlaveResponder(dut, "m_apb_reg2", dut.clk)
slave3 = APB3SlaveResponder(dut, "m_apb_reg3", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
cocotb.start_soon(slave1.run())
cocotb.start_soon(slave2.run())
cocotb.start_soon(slave3.run())
for _ in range(5):
await RisingEdge(dut.clk)
# Test each register
test_data = [0x11111111, 0x22222222, 0x33333333]
for i, data in enumerate(test_data):
addr = i * 4
dut._log.info(f"Writing 0x{data:08X} to address 0x{addr:X}")
success = await master.write(addr, data)
assert success, f"Write to address 0x{addr:X} failed"
dut._log.info(f"Reading from address 0x{addr:X}")
read_data, error = await master.read(addr)
assert not error, f"Read from address 0x{addr:X} returned error"
assert read_data == data, f"Data mismatch at 0x{addr:X}: expected 0x{data:08X}, got 0x{read_data:08X}"
dut._log.info("Test passed!")

View File

@@ -1,264 +0,0 @@
"""Cocotb tests for APB4 bus decoder."""
import os
import tempfile
from pathlib import Path
import cocotb
import pytest
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
# Import the common test utilities
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from common.utils import compile_rdl_and_export
# APB4 Master BFM
class APB4Master:
"""APB4 Master Bus Functional Model."""
def __init__(self, dut, name, clock):
self.dut = dut
self.clock = clock
self.name = name
self.psel = getattr(dut, f"{name}_PSEL")
self.penable = getattr(dut, f"{name}_PENABLE")
self.pwrite = getattr(dut, f"{name}_PWRITE")
self.paddr = getattr(dut, f"{name}_PADDR")
self.pwdata = getattr(dut, f"{name}_PWDATA")
self.pstrb = getattr(dut, f"{name}_PSTRB")
self.pprot = getattr(dut, f"{name}_PPROT")
self.prdata = getattr(dut, f"{name}_PRDATA")
self.pready = getattr(dut, f"{name}_PREADY")
self.pslverr = getattr(dut, f"{name}_PSLVERR")
def reset(self):
"""Reset the bus to idle state."""
self.psel.value = 0
self.penable.value = 0
self.pwrite.value = 0
self.paddr.value = 0
self.pwdata.value = 0
self.pstrb.value = 0
self.pprot.value = 0
async def write(self, addr, data, strb=None):
"""Perform APB4 write transaction."""
if strb is None:
strb = 0xF
await RisingEdge(self.clock)
self.psel.value = 1
self.penable.value = 0
self.pwrite.value = 1
self.paddr.value = addr
self.pwdata.value = data
self.pstrb.value = strb
self.pprot.value = 0
await RisingEdge(self.clock)
self.penable.value = 1
while True:
await RisingEdge(self.clock)
if self.pready.value == 1:
error = self.pslverr.value == 1
break
self.psel.value = 0
self.penable.value = 0
return not error
async def read(self, addr):
"""Perform APB4 read transaction."""
await RisingEdge(self.clock)
self.psel.value = 1
self.penable.value = 0
self.pwrite.value = 0
self.paddr.value = addr
self.pprot.value = 0
await RisingEdge(self.clock)
self.penable.value = 1
while True:
await RisingEdge(self.clock)
if self.pready.value == 1:
data = self.prdata.value.integer
error = self.pslverr.value == 1
break
self.psel.value = 0
self.penable.value = 0
return data, error
# APB4 Slave responder
class APB4SlaveResponder:
"""Simple APB4 Slave responder that acknowledges all transactions."""
def __init__(self, dut, name, clock):
self.dut = dut
self.clock = clock
self.name = name
self.psel = getattr(dut, f"{name}_PSEL")
self.penable = getattr(dut, f"{name}_PENABLE")
self.pwrite = getattr(dut, f"{name}_PWRITE")
self.paddr = getattr(dut, f"{name}_PADDR")
self.pwdata = getattr(dut, f"{name}_PWDATA")
self.pstrb = getattr(dut, f"{name}_PSTRB")
self.prdata = getattr(dut, f"{name}_PRDATA")
self.pready = getattr(dut, f"{name}_PREADY")
self.pslverr = getattr(dut, f"{name}_PSLVERR")
# Storage for register values
self.storage = {}
async def run(self):
"""Run the slave responder."""
while True:
await RisingEdge(self.clock)
if self.psel.value == 1 and self.penable.value == 1:
addr = self.paddr.value.integer
if self.pwrite.value == 1:
# Write operation
data = self.pwdata.value.integer
self.storage[addr] = data
self.pready.value = 1
self.pslverr.value = 0
else:
# Read operation
data = self.storage.get(addr, 0)
self.prdata.value = data
self.pready.value = 1
self.pslverr.value = 0
else:
self.pready.value = 0
self.pslverr.value = 0
@cocotb.test()
async def test_simple_read_write(dut):
"""Test simple read and write operations."""
# Start clock
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
# Create master and slave
master = APB4Master(dut, "s_apb", dut.clk)
slave = APB4SlaveResponder(dut, "m_apb_test_reg", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
# Start slave responder
cocotb.start_soon(slave.run())
# Wait a few cycles
for _ in range(5):
await RisingEdge(dut.clk)
# Write test
dut._log.info("Writing 0xDEADBEEF to address 0x0")
success = await master.write(0x0, 0xDEADBEEF)
assert success, "Write operation failed"
# Read test
dut._log.info("Reading from address 0x0")
data, error = await master.read(0x0)
assert not error, "Read operation returned error"
assert data == 0xDEADBEEF, f"Read data mismatch: expected 0xDEADBEEF, got 0x{data:08X}"
dut._log.info("Test passed!")
@cocotb.test()
async def test_multiple_registers(dut):
"""Test operations on multiple registers."""
# Start clock
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
# Create master and slaves
master = APB4Master(dut, "s_apb", dut.clk)
slave1 = APB4SlaveResponder(dut, "m_apb_reg1", dut.clk)
slave2 = APB4SlaveResponder(dut, "m_apb_reg2", dut.clk)
slave3 = APB4SlaveResponder(dut, "m_apb_reg3", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
# Start slave responders
cocotb.start_soon(slave1.run())
cocotb.start_soon(slave2.run())
cocotb.start_soon(slave3.run())
# Wait a few cycles
for _ in range(5):
await RisingEdge(dut.clk)
# Test each register
test_data = [0x12345678, 0xABCDEF00, 0xCAFEBABE]
for i, data in enumerate(test_data):
addr = i * 4
dut._log.info(f"Writing 0x{data:08X} to address 0x{addr:X}")
success = await master.write(addr, data)
assert success, f"Write to address 0x{addr:X} failed"
dut._log.info(f"Reading from address 0x{addr:X}")
read_data, error = await master.read(addr)
assert not error, f"Read from address 0x{addr:X} returned error"
assert read_data == data, f"Data mismatch at 0x{addr:X}: expected 0x{data:08X}, got 0x{read_data:08X}"
dut._log.info("Test passed!")
@cocotb.test()
async def test_byte_strobe(dut):
"""Test byte strobe functionality."""
# Start clock
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
# Create master and slave
master = APB4Master(dut, "s_apb", dut.clk)
slave = APB4SlaveResponder(dut, "m_apb_test_reg", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
# Start slave responder
cocotb.start_soon(slave.run())
# Wait a few cycles
for _ in range(5):
await RisingEdge(dut.clk)
# Write full word
await master.write(0x0, 0x12345678, strb=0xF)
# Read back
data, error = await master.read(0x0)
assert not error
assert data == 0x12345678
# Write only lower byte
await master.write(0x0, 0x000000AB, strb=0x1)
data, error = await master.read(0x0)
assert not error
assert (data & 0xFF) == 0xAB
dut._log.info("Test passed!")

View File

@@ -1,260 +0,0 @@
"""Pytest test runner for APB4 cocotb tests."""
import os
import shutil
import tempfile
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
# Import the common test utilities
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from common.utils import compile_rdl_and_export
# Check if Icarus Verilog is available
def is_simulator_available():
"""Check if Icarus Verilog simulator is available."""
return shutil.which("iverilog") is not None
# Skip tests if simulator is not available
skip_if_no_simulator = pytest.mark.skipif(
not is_simulator_available(),
reason="Requires Icarus Verilog or other simulator to be installed"
)
def generate_testbench_wrapper(top_name, slave_ports, tmpdir_path):
"""
Generate a testbench wrapper that exposes interface signals.
Args:
top_name: Name of the top-level module
slave_ports: List of slave port names
tmpdir_path: Path to temporary directory
Returns:
Path to generated testbench file
"""
tb_path = tmpdir_path / f"tb_{top_name}.sv"
with open(tb_path, "w") as f:
f.write(f"""
module tb_{top_name} (
input logic clk,
input logic rst
);
// Instantiate APB4 interfaces
apb4_intf #(
.DATA_WIDTH(32),
.ADDR_WIDTH(32)
) s_apb ();
""")
# Create interface instances for each slave port
for port in slave_ports:
f.write(f"""
apb4_intf #(
.DATA_WIDTH(32),
.ADDR_WIDTH(32)
) {port} ();
""")
# Wire master signals
f.write("""
// Wire master signals from interface to top level for cocotb access
logic s_apb_PSEL;
logic s_apb_PENABLE;
logic s_apb_PWRITE;
logic [31:0] s_apb_PADDR;
logic [31:0] s_apb_PWDATA;
logic [3:0] s_apb_PSTRB;
logic [2:0] s_apb_PPROT;
logic [31:0] s_apb_PRDATA;
logic s_apb_PREADY;
logic s_apb_PSLVERR;
assign s_apb.PSEL = s_apb_PSEL;
assign s_apb.PENABLE = s_apb_PENABLE;
assign s_apb.PWRITE = s_apb_PWRITE;
assign s_apb.PADDR = s_apb_PADDR;
assign s_apb.PWDATA = s_apb_PWDATA;
assign s_apb.PSTRB = s_apb_PSTRB;
assign s_apb.PPROT = s_apb_PPROT;
assign s_apb_PRDATA = s_apb.PRDATA;
assign s_apb_PREADY = s_apb.PREADY;
assign s_apb_PSLVERR = s_apb.PSLVERR;
""")
# Wire slave signals
for port in slave_ports:
f.write(f"""
logic {port}_PSEL;
logic {port}_PENABLE;
logic {port}_PWRITE;
logic [31:0] {port}_PADDR;
logic [31:0] {port}_PWDATA;
logic [3:0] {port}_PSTRB;
logic [31:0] {port}_PRDATA;
logic {port}_PREADY;
logic {port}_PSLVERR;
assign {port}_PSEL = {port}.PSEL;
assign {port}_PENABLE = {port}.PENABLE;
assign {port}_PWRITE = {port}.PWRITE;
assign {port}_PADDR = {port}.PADDR;
assign {port}_PWDATA = {port}.PWDATA;
assign {port}_PSTRB = {port}.PSTRB;
assign {port}.PRDATA = {port}_PRDATA;
assign {port}.PREADY = {port}_PREADY;
assign {port}.PSLVERR = {port}_PSLVERR;
""")
# Instantiate DUT
f.write(f"""
// Instantiate DUT
{top_name} dut (
.s_apb(s_apb)""")
for port in slave_ports:
f.write(f",\n .{port}({port})")
f.write("""
);
// Dump waves
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb_{top_name});
end
endmodule
""".format(top_name=top_name))
return tb_path
@skip_if_no_simulator
def test_apb4_simple_register():
"""Test APB4 decoder with a simple register."""
rdl_source = """
addrmap simple_test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} test_reg @ 0x0;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
# Compile RDL and export SystemVerilog
module_path, package_path = compile_rdl_and_export(
rdl_source, "simple_test", str(tmpdir_path), APB4Cpuif
)
# Generate testbench wrapper
tb_path = generate_testbench_wrapper(
"simple_test", ["m_apb_test_reg"], tmpdir_path
)
# Get HDL source directory
hdl_src_dir = Path(__file__).parent.parent.parent.parent / "hdl-src"
# Run simulation using cocotb.runner
from cocotb.runner import get_runner
runner = get_runner("icarus")
runner.build(
verilog_sources=[
str(hdl_src_dir / "apb4_intf.sv"),
str(package_path),
str(module_path),
str(tb_path),
],
hdl_toplevel="tb_simple_test",
always=True,
build_dir=str(tmpdir_path / "sim_build"),
)
runner.test(
hdl_toplevel="tb_simple_test",
test_module="test_apb4_decoder",
build_dir=str(tmpdir_path / "sim_build"),
)
@skip_if_no_simulator
def test_apb4_multiple_registers():
"""Test APB4 decoder with multiple registers."""
rdl_source = """
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;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
# Compile RDL and export SystemVerilog
module_path, package_path = compile_rdl_and_export(
rdl_source, "multi_reg", str(tmpdir_path), APB4Cpuif
)
# Generate testbench wrapper
tb_path = generate_testbench_wrapper(
"multi_reg", ["m_apb_reg1", "m_apb_reg2", "m_apb_reg3"], tmpdir_path
)
# Get HDL source directory
hdl_src_dir = Path(__file__).parent.parent.parent.parent / "hdl-src"
# Run simulation
from cocotb.runner import get_runner
runner = get_runner("icarus")
runner.build(
verilog_sources=[
str(hdl_src_dir / "apb4_intf.sv"),
str(package_path),
str(module_path),
str(tb_path),
],
hdl_toplevel="tb_multi_reg",
always=True,
build_dir=str(tmpdir_path / "sim_build"),
)
runner.test(
hdl_toplevel="tb_multi_reg",
test_module="test_apb4_decoder",
test_args=["--test-case=test_multiple_registers"],
build_dir=str(tmpdir_path / "sim_build"),
)

View File

@@ -1,311 +0,0 @@
"""Cocotb tests for AXI4-Lite bus decoder."""
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
class AXI4LiteMaster:
"""AXI4-Lite Master Bus Functional Model."""
def __init__(self, dut, name, clock):
self.dut = dut
self.clock = clock
self.name = name
# Write address channel
self.awvalid = getattr(dut, f"{name}_AWVALID")
self.awready = getattr(dut, f"{name}_AWREADY")
self.awaddr = getattr(dut, f"{name}_AWADDR")
self.awprot = getattr(dut, f"{name}_AWPROT")
# Write data channel
self.wvalid = getattr(dut, f"{name}_WVALID")
self.wready = getattr(dut, f"{name}_WREADY")
self.wdata = getattr(dut, f"{name}_WDATA")
self.wstrb = getattr(dut, f"{name}_WSTRB")
# Write response channel
self.bvalid = getattr(dut, f"{name}_BVALID")
self.bready = getattr(dut, f"{name}_BREADY")
self.bresp = getattr(dut, f"{name}_BRESP")
# Read address channel
self.arvalid = getattr(dut, f"{name}_ARVALID")
self.arready = getattr(dut, f"{name}_ARREADY")
self.araddr = getattr(dut, f"{name}_ARADDR")
self.arprot = getattr(dut, f"{name}_ARPROT")
# Read data channel
self.rvalid = getattr(dut, f"{name}_RVALID")
self.rready = getattr(dut, f"{name}_RREADY")
self.rdata = getattr(dut, f"{name}_RDATA")
self.rresp = getattr(dut, f"{name}_RRESP")
def reset(self):
"""Reset the bus to idle state."""
self.awvalid.value = 0
self.awaddr.value = 0
self.awprot.value = 0
self.wvalid.value = 0
self.wdata.value = 0
self.wstrb.value = 0
self.bready.value = 1
self.arvalid.value = 0
self.araddr.value = 0
self.arprot.value = 0
self.rready.value = 1
async def write(self, addr, data, strb=None):
"""Perform AXI4-Lite write transaction."""
if strb is None:
strb = 0xF
# Write address phase
await RisingEdge(self.clock)
self.awvalid.value = 1
self.awaddr.value = addr
self.awprot.value = 0
# Write data phase
self.wvalid.value = 1
self.wdata.value = data
self.wstrb.value = strb
# Wait for address accept
while True:
await RisingEdge(self.clock)
if self.awready.value == 1:
self.awvalid.value = 0
break
# Wait for data accept
while self.wready.value != 1:
await RisingEdge(self.clock)
self.wvalid.value = 0
# Wait for write response
self.bready.value = 1
while self.bvalid.value != 1:
await RisingEdge(self.clock)
error = self.bresp.value != 0
await RisingEdge(self.clock)
return not error
async def read(self, addr):
"""Perform AXI4-Lite read transaction."""
# Read address phase
await RisingEdge(self.clock)
self.arvalid.value = 1
self.araddr.value = addr
self.arprot.value = 0
# Wait for address accept
while True:
await RisingEdge(self.clock)
if self.arready.value == 1:
self.arvalid.value = 0
break
# Wait for read data
self.rready.value = 1
while self.rvalid.value != 1:
await RisingEdge(self.clock)
data = self.rdata.value.integer
error = self.rresp.value != 0
await RisingEdge(self.clock)
return data, error
class AXI4LiteSlaveResponder:
"""Simple AXI4-Lite Slave responder."""
def __init__(self, dut, name, clock):
self.dut = dut
self.clock = clock
self.name = name
# Get all signals
self.awvalid = getattr(dut, f"{name}_AWVALID")
self.awready = getattr(dut, f"{name}_AWREADY")
self.awaddr = getattr(dut, f"{name}_AWADDR")
self.wvalid = getattr(dut, f"{name}_WVALID")
self.wready = getattr(dut, f"{name}_WREADY")
self.wdata = getattr(dut, f"{name}_WDATA")
self.wstrb = getattr(dut, f"{name}_WSTRB")
self.bvalid = getattr(dut, f"{name}_BVALID")
self.bready = getattr(dut, f"{name}_BREADY")
self.bresp = getattr(dut, f"{name}_BRESP")
self.arvalid = getattr(dut, f"{name}_ARVALID")
self.arready = getattr(dut, f"{name}_ARREADY")
self.araddr = getattr(dut, f"{name}_ARADDR")
self.rvalid = getattr(dut, f"{name}_RVALID")
self.rready = getattr(dut, f"{name}_RREADY")
self.rdata = getattr(dut, f"{name}_RDATA")
self.rresp = getattr(dut, f"{name}_RRESP")
self.storage = {}
self.write_pending = False
self.pending_addr = 0
self.pending_data = 0
async def run(self):
"""Run the slave responder."""
while True:
await RisingEdge(self.clock)
# Handle write address channel
if self.awvalid.value == 1 and not self.write_pending:
self.awready.value = 1
self.pending_addr = self.awaddr.value.integer
self.write_pending = True
else:
self.awready.value = 0
# Handle write data channel
if self.wvalid.value == 1 and self.write_pending:
self.wready.value = 1
self.pending_data = self.wdata.value.integer
self.storage[self.pending_addr] = self.pending_data
# Send write response
self.bvalid.value = 1
self.bresp.value = 0
self.write_pending = False
else:
self.wready.value = 0
if self.bvalid.value == 1 and self.bready.value == 1:
self.bvalid.value = 0
# Handle read address channel
if self.arvalid.value == 1:
self.arready.value = 1
addr = self.araddr.value.integer
data = self.storage.get(addr, 0)
self.rdata.value = data
self.rvalid.value = 1
self.rresp.value = 0
else:
self.arready.value = 0
if self.rvalid.value == 1 and self.rready.value == 1:
self.rvalid.value = 0
@cocotb.test()
async def test_simple_read_write(dut):
"""Test simple read and write operations on AXI4-Lite."""
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
master = AXI4LiteMaster(dut, "s_axi", dut.clk)
slave = AXI4LiteSlaveResponder(dut, "m_axi_test_reg", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
cocotb.start_soon(slave.run())
for _ in range(5):
await RisingEdge(dut.clk)
# Write test
dut._log.info("Writing 0xFEEDFACE to address 0x0")
success = await master.write(0x0, 0xFEEDFACE)
assert success, "Write operation failed"
# Read test
dut._log.info("Reading from address 0x0")
data, error = await master.read(0x0)
assert not error, "Read operation returned error"
assert data == 0xFEEDFACE, f"Read data mismatch: expected 0xFEEDFACE, got 0x{data:08X}"
dut._log.info("Test passed!")
@cocotb.test()
async def test_multiple_registers(dut):
"""Test operations on multiple registers with AXI4-Lite."""
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
master = AXI4LiteMaster(dut, "s_axi", dut.clk)
slave1 = AXI4LiteSlaveResponder(dut, "m_axi_reg1", dut.clk)
slave2 = AXI4LiteSlaveResponder(dut, "m_axi_reg2", dut.clk)
slave3 = AXI4LiteSlaveResponder(dut, "m_axi_reg3", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
cocotb.start_soon(slave1.run())
cocotb.start_soon(slave2.run())
cocotb.start_soon(slave3.run())
for _ in range(5):
await RisingEdge(dut.clk)
# Test each register
test_data = [0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC]
for i, data in enumerate(test_data):
addr = i * 4
dut._log.info(f"Writing 0x{data:08X} to address 0x{addr:X}")
success = await master.write(addr, data)
assert success, f"Write to address 0x{addr:X} failed"
dut._log.info(f"Reading from address 0x{addr:X}")
read_data, error = await master.read(addr)
assert not error, f"Read from address 0x{addr:X} returned error"
assert read_data == data, f"Data mismatch at 0x{addr:X}: expected 0x{data:08X}, got 0x{read_data:08X}"
dut._log.info("Test passed!")
@cocotb.test()
async def test_byte_strobe(dut):
"""Test byte strobe functionality with AXI4-Lite."""
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
master = AXI4LiteMaster(dut, "s_axi", dut.clk)
slave = AXI4LiteSlaveResponder(dut, "m_axi_test_reg", dut.clk)
# Reset
dut.rst.value = 1
master.reset()
await Timer(100, units="ns")
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
cocotb.start_soon(slave.run())
for _ in range(5):
await RisingEdge(dut.clk)
# Write full word
await master.write(0x0, 0x12345678, strb=0xF)
# Read back
data, error = await master.read(0x0)
assert not error
assert data == 0x12345678
# Write only lower byte
await master.write(0x0, 0x000000CD, strb=0x1)
data, error = await master.read(0x0)
assert not error
assert (data & 0xFF) == 0xCD
dut._log.info("Test passed!")

View File

@@ -1,216 +0,0 @@
"""
Integration tests for cocotb testbench infrastructure.
These tests validate that the code generation and testbench setup works correctly
without requiring an actual HDL simulator. They check:
- RDL compilation and SystemVerilog generation
- Generated code contains expected elements
- Testbench utilities work correctly
"""
import tempfile
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif
from ..common.utils import compile_rdl_and_export, get_verilog_sources
class TestCodeGeneration:
"""Test code generation for different CPU interfaces."""
def test_apb4_simple_register(self):
"""Test APB4 code generation for simple register."""
rdl_source = """
addrmap simple_test {
reg {
field { sw=rw; hw=r; } data[31:0];
} test_reg @ 0x0;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
module_path, package_path = compile_rdl_and_export(
rdl_source, "simple_test", tmpdir, APB4Cpuif
)
# Verify files exist
assert module_path.exists()
assert package_path.exists()
# Verify module content
module_content = module_path.read_text()
assert "module simple_test" in module_content
assert "apb4_intf.slave s_apb" in module_content
assert "test_reg" in module_content
# Verify package content
package_content = package_path.read_text()
assert "package simple_test_pkg" in package_content
def test_apb3_multiple_registers(self):
"""Test APB3 code generation for multiple registers."""
rdl_source = """
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;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
module_path, package_path = compile_rdl_and_export(
rdl_source, "multi_reg", tmpdir, APB3Cpuif
)
assert module_path.exists()
assert package_path.exists()
module_content = module_path.read_text()
assert "module multi_reg" in module_content
assert "apb3_intf.slave s_apb" in module_content
assert "reg1" in module_content
assert "reg2" in module_content
assert "reg3" in module_content
def test_axi4lite_nested_addrmap(self):
"""Test AXI4-Lite code generation for nested address map."""
rdl_source = """
addrmap inner_block {
reg { field { sw=rw; hw=r; } data[31:0]; } inner_reg @ 0x0;
};
addrmap outer_block {
inner_block inner @ 0x0;
reg { field { sw=rw; hw=r; } outer_data[31:0]; } outer_reg @ 0x100;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
module_path, package_path = compile_rdl_and_export(
rdl_source, "outer_block", tmpdir, AXI4LiteCpuif
)
assert module_path.exists()
assert package_path.exists()
module_content = module_path.read_text()
assert "module outer_block" in module_content
assert "axi4lite_intf.slave s_axi" in module_content
assert "inner" in module_content
assert "outer_reg" in module_content
def test_register_array(self):
"""Test code generation with register arrays."""
rdl_source = """
addrmap array_test {
reg { field { sw=rw; hw=r; } data[31:0]; } regs[4] @ 0x0 += 0x4;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
module_path, package_path = compile_rdl_and_export(
rdl_source, "array_test", tmpdir, APB4Cpuif
)
assert module_path.exists()
assert package_path.exists()
module_content = module_path.read_text()
assert "module array_test" in module_content
assert "regs" in module_content
class TestUtilityFunctions:
"""Test utility functions for testbench setup."""
def test_get_verilog_sources(self):
"""Test that get_verilog_sources returns correct file list."""
hdl_src_dir = Path(__file__).parent.parent.parent.parent / "hdl-src"
module_path = Path("/tmp/test_module.sv")
package_path = Path("/tmp/test_pkg.sv")
intf_files = [
hdl_src_dir / "apb4_intf.sv",
hdl_src_dir / "apb3_intf.sv",
]
sources = get_verilog_sources(module_path, package_path, intf_files)
# Verify order: interfaces first, then package, then module
assert len(sources) == 4
assert str(intf_files[0]) in sources[0]
assert str(intf_files[1]) in sources[1]
assert str(package_path) in sources[2]
assert str(module_path) in sources[3]
def test_compile_rdl_and_export_with_custom_names(self):
"""Test code generation with custom module and package names."""
rdl_source = """
addrmap test_map {
reg { field { sw=rw; hw=r; } data[31:0]; } test_reg @ 0x0;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
module_path, package_path = compile_rdl_and_export(
rdl_source,
"test_map",
tmpdir,
APB4Cpuif,
module_name="custom_module",
package_name="custom_pkg",
)
# Verify custom names
assert module_path.name == "custom_module.sv"
assert package_path.name == "custom_pkg.sv"
# Verify content uses custom names
module_content = module_path.read_text()
assert "module custom_module" in module_content
package_content = package_path.read_text()
assert "package custom_pkg" in package_content
class TestMultipleCpuInterfaces:
"""Test that all CPU interfaces generate valid code."""
@pytest.mark.parametrize(
"cpuif_cls,intf_name",
[
(APB3Cpuif, "apb3_intf"),
(APB4Cpuif, "apb4_intf"),
(AXI4LiteCpuif, "axi4lite_intf"),
],
)
def test_cpuif_generation(self, cpuif_cls, intf_name):
"""Test code generation for each CPU interface type."""
rdl_source = """
addrmap test_block {
reg {
field { sw=rw; hw=r; } data[31:0];
} test_reg @ 0x0;
};
"""
with tempfile.TemporaryDirectory() as tmpdir:
module_path, package_path = compile_rdl_and_export(
rdl_source, "test_block", tmpdir, cpuif_cls
)
assert module_path.exists()
assert package_path.exists()
module_content = module_path.read_text()
assert "module test_block" in module_content
assert intf_name in module_content
if __name__ == "__main__":
pytest.main([__file__, "-v"])

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;
};

View File

@@ -1,24 +1,22 @@
"""Common utilities for cocotb testbenches.""" """Common utilities for cocotb testbenches."""
import os
import tempfile
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any from typing import Any
from systemrdl import RDLCompiler from systemrdl import RDLCompiler
from peakrdl_busdecoder.cpuif.base_cpuif import BaseCpuif
from peakrdl_busdecoder.exporter import BusDecoderExporter from peakrdl_busdecoder.exporter import BusDecoderExporter
def compile_rdl_and_export( def compile_rdl_and_export(
rdl_source: str, top_name: str, output_dir: str, cpuif_cls: Any, **kwargs: Any rdl_source: str, top_name: str, output_dir: Path, cpuif_cls: type[BaseCpuif], **kwargs: Any
) -> tuple[Path, Path]: ) -> tuple[Path, Path]:
""" """
Compile RDL source and export to SystemVerilog. Compile RDL source and export to SystemVerilog.
Args: Args:
rdl_source: SystemRDL source code as a string rdl_source: SystemRDL source code path
top_name: Name of the top-level addrmap top_name: Name of the top-level addrmap
output_dir: Directory to write generated files output_dir: Directory to write generated files
cpuif_cls: CPU interface class to use cpuif_cls: CPU interface class to use
@@ -30,23 +28,12 @@ def compile_rdl_and_export(
# Compile RDL source # Compile RDL source
compiler = RDLCompiler() compiler = RDLCompiler()
# Write source to temporary file compiler.compile_file(rdl_source)
with NamedTemporaryFile("w", suffix=".rdl", dir=output_dir, delete=False) as tmp_file: top = compiler.elaborate(top_name)
tmp_file.write(rdl_source)
tmp_file.flush()
tmp_path = tmp_file.name
try: # Export to SystemVerilog
compiler.compile_file(tmp_path) exporter = BusDecoderExporter()
top = compiler.elaborate(top_name) exporter.export(top, str(output_dir), cpuif_cls=cpuif_cls, **kwargs)
# Export to SystemVerilog
exporter = BusDecoderExporter()
exporter.export(top, output_dir, cpuif_cls=cpuif_cls, **kwargs)
finally:
# Clean up temporary RDL file
if os.path.exists(tmp_path):
os.unlink(tmp_path)
# Return paths to generated files # Return paths to generated files
module_name = kwargs.get("module_name", top_name) module_name = kwargs.get("module_name", top_name)

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,19 +1,16 @@
"""Integration tests for the BusDecoderExporter.""" from collections.abc import Callable
import os
from pathlib import Path 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.cpuif.apb4 import APB4Cpuif
from peakrdl_busdecoder.exporter import BusDecoderExporter
class TestBusDecoderExporter: class TestBusDecoderExporter:
"""Test the top-level BusDecoderExporter.""" """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.""" """Test exporting a simple register."""
rdl_source = """ rdl_source = """
addrmap simple_reg { addrmap simple_reg {
@@ -46,7 +43,7 @@ class TestBusDecoderExporter:
package_content = package_file.read_text() package_content = package_file.read_text()
assert "package simple_reg_pkg" in package_content 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.""" """Test exporting a register array."""
rdl_source = """ rdl_source = """
addrmap reg_array { addrmap reg_array {
@@ -72,7 +69,7 @@ class TestBusDecoderExporter:
assert "module reg_array" in module_content assert "module reg_array" in module_content
assert "my_regs" 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.""" """Test exporting nested addrmaps."""
rdl_source = """ rdl_source = """
addrmap inner_block { addrmap inner_block {
@@ -103,7 +100,7 @@ class TestBusDecoderExporter:
assert "inner" in module_content assert "inner" in module_content
assert "inner_reg" 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.""" """Test exporting with custom module name."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { addrmap my_addrmap {
@@ -131,7 +128,7 @@ class TestBusDecoderExporter:
module_content = module_file.read_text() module_content = module_file.read_text()
assert "module custom_module" in module_content 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.""" """Test exporting with custom package name."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { addrmap my_addrmap {
@@ -156,7 +153,7 @@ class TestBusDecoderExporter:
package_content = package_file.read_text() package_content = package_file.read_text()
assert "package custom_pkg" in package_content 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.""" """Test exporting multiple registers."""
rdl_source = """ rdl_source = """
addrmap multi_reg { addrmap multi_reg {
@@ -196,134 +193,3 @@ class TestBusDecoderExporter:
assert "reg1" in module_content assert "reg1" in module_content
assert "reg2" in module_content assert "reg2" in module_content
assert "reg3" 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."""
rdl_source = """
addrmap apb_test {
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 {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="apb_rw")
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()
# Basic sanity checks for logic generation
assert "always" in module_content or "assign" in module_content
assert "my_reg" in module_content
def test_nested_addrmap_with_array_stride(self, compile_rdl, tmp_path):
"""Test that nested addrmaps with arrays use correct stride values."""
rdl_source = """
addrmap inner_block {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg @ 0x0;
};
addrmap outer_block {
inner_block inner[4] @ 0x0 += 0x100;
reg {
field {
sw=rw;
hw=r;
} outer_data[31:0];
} outer_reg @ 0x400;
};
"""
top = compile_rdl(rdl_source, top="outer_block")
exporter = BusDecoderExporter()
output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif)
module_file = tmp_path / "outer_block.sv"
module_content = module_file.read_text()
# Check that the generated code uses the correct stride (0x100 = 256)
# not the array dimension (4)
# The decode logic should contain something like: i0)*11'h100 or i0)*256
assert "i0)*11'h100" in module_content or "i0)*'h100" in module_content, \
"Array stride should be 0x100 (256), not the dimension value (4)"
# Ensure it's NOT using the incorrect dimension value
assert (
"i0)*11'h4" not in module_content
and "i0)*4" not in module_content
), "Should not use array dimension (4) as stride"
def test_multidimensional_array_strides(self, compile_rdl, tmp_path):
"""Test that multidimensional arrays calculate correct strides for each dimension."""
rdl_source = """
addrmap test_block {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[2][3] @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="test_block")
exporter = BusDecoderExporter()
output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif)
module_file = tmp_path / "test_block.sv"
module_content = module_file.read_text()
# For a [2][3] array where each register is 4 bytes:
# i0 (leftmost/slowest) should have stride = 3 * 4 = 12 (0xc)
# i1 (rightmost/fastest) should have stride = 4 (0x4)
assert ("i0)*5'hc" in module_content or "i0)*12" in module_content), \
"i0 should use stride 12 (0xc) for [2][3] array"
assert ("i1)*5'h4" in module_content or "i1)*4" in module_content), \
"i1 should use stride 4 for [2][3] array"

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 +1,5 @@
[pytest] [pytest]
testpaths = unit
python_files = test_*.py testcase.py python_files = test_*.py testcase.py
markers =
simulation: marks hardware simulation tests requiring cocotb/verilator
verilator: marks tests that invoke the Verilator toolchain

View File

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

View File

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

@@ -1,79 +1,16 @@
"""Test handling of external nested addressable components.""" """Test handling of external nested addressable components."""
from collections.abc import Callable
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import pytest from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
@pytest.fixture def test_external_nested_components_generate_correct_decoder(external_nested_rdl: AddrmapNode) -> None:
def external_nested_rdl(compile_rdl):
"""Create an RDL design with external nested addressable components.
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;
};
addrmap port_t {
name = "Port";
desc = "";
external queue_t common[3] @ 0x0 += 0x2000;
external queue_t response @ 0x6000;
};
addrmap buffer_t {
name = "Buffer";
desc = "";
port_t multicast @ 0x0;
port_t port [16] @ 0x8000 += 0x8000;
};
"""
return compile_rdl(rdl_source, top="buffer_t")
@pytest.fixture
def nested_addrmap_rdl(compile_rdl):
"""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")
def test_external_nested_components_generate_correct_decoder(external_nested_rdl):
"""Test that external nested components generate correct decoder logic. """Test that external nested components generate correct decoder logic.
The decoder should: The decoder should:
@@ -109,7 +46,7 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
assert "logic [15:0]port;" in content assert "logic [15:0]port;" in content
def test_external_nested_components_generate_correct_interfaces(external_nested_rdl): def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None:
"""Test that external nested components generate correct interface ports. """Test that external nested components generate correct interface ports.
The module should have: The module should have:
@@ -140,7 +77,7 @@ def test_external_nested_components_generate_correct_interfaces(external_nested_
assert "m_apb_response" not in content assert "m_apb_response" not in content
def test_non_external_nested_components_are_descended(compile_rdl): def test_non_external_nested_components_are_descended(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that non-external nested components are still descended into. """Test that non-external nested components are still descended into.
This is a regression test to ensure we didn't break normal nested This is a regression test to ensure we didn't break normal nested
@@ -175,7 +112,7 @@ def test_non_external_nested_components_are_descended(compile_rdl):
assert "inner_reg" in content assert "inner_reg" in content
def test_max_decode_depth_parameter_exists(compile_rdl): def test_max_decode_depth_parameter_exists(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that max_decode_depth parameter can be set.""" """Test that max_decode_depth parameter can be set."""
rdl_source = """ rdl_source = """
addrmap simple { addrmap simple {

View File

@@ -1,312 +0,0 @@
"""Tests for code generation classes."""
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 pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import pytest from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
@pytest.fixture def test_unroll_disabled_creates_array_interface(sample_rdl: AddrmapNode) -> None:
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):
"""Test that with unroll=False, array nodes are kept as arrays.""" """Test that with unroll=False, array nodes are kept as arrays."""
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
@@ -38,17 +18,17 @@ def test_unroll_disabled_creates_array_interface(sample_rdl):
cpuif_cls=APB4Cpuif, cpuif_cls=APB4Cpuif,
cpuif_unroll=False, cpuif_unroll=False,
) )
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "top.sv" module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text() content = module_file.read_text()
# Should have a single array interface with [4] dimension # Should have a single array interface with [4] dimension
assert "m_apb_regs [4]" in content assert "m_apb_regs [4]" in content
# Should have a parameter for array size # Should have a parameter for array size
assert "N_REGSS = 4" in content assert "N_REGSS = 4" in content
# Should NOT have individual indexed interfaces # Should NOT have individual indexed interfaces
assert "m_apb_regs_0" not in content assert "m_apb_regs_0" not in content
assert "m_apb_regs_1" 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 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.""" """Test that with unroll=True, array elements are unrolled into separate instances."""
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
@@ -66,31 +46,31 @@ def test_unroll_enabled_creates_individual_interfaces(sample_rdl):
cpuif_cls=APB4Cpuif, cpuif_cls=APB4Cpuif,
cpuif_unroll=True, cpuif_unroll=True,
) )
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "top.sv" module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text() content = module_file.read_text()
# Should have individual interfaces without array dimensions # 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_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_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_2," in content or "m_apb_regs_2\n" in content
assert "m_apb_regs_3" in content assert "m_apb_regs_3" in content
# Should NOT have array interface # Should NOT have array interface
assert "m_apb_regs [4]" not in content assert "m_apb_regs [4]" not in content
# Should NOT have individual interfaces with array dimensions (the bug we're fixing) # 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_0 [4]" not in content
assert "m_apb_regs_1 [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_2 [4]" not in content
assert "m_apb_regs_3 [4]" not in content assert "m_apb_regs_3 [4]" not in content
# Should NOT have array size parameter when unrolled # Should NOT have array size parameter when unrolled
assert "N_REGSS" not in content 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.""" """Test that unroll works correctly with APB3 interface."""
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
@@ -100,40 +80,22 @@ def test_unroll_with_apb3(sample_rdl):
cpuif_cls=APB3Cpuif, cpuif_cls=APB3Cpuif,
cpuif_unroll=True, cpuif_unroll=True,
) )
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "top.sv" module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text() content = module_file.read_text()
# Should have individual APB3 interfaces # Should have individual APB3 interfaces
assert "m_apb_regs_0," in content or "m_apb_regs_0\n" in content 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_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_2," in content or "m_apb_regs_2\n" in content
assert "m_apb_regs_3" in content assert "m_apb_regs_3" in content
# Should NOT have array dimensions on unrolled interfaces # Should NOT have array dimensions on unrolled interfaces
assert "m_apb_regs_0 [4]" not in content assert "m_apb_regs_0 [4]" not in content
@pytest.fixture def test_unroll_multidimensional_array(multidim_array_rdl: AddrmapNode) -> None:
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):
"""Test that unroll works correctly with multi-dimensional arrays.""" """Test that unroll works correctly with multi-dimensional arrays."""
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
@@ -143,11 +105,11 @@ def test_unroll_multidimensional_array(multidim_array_rdl):
cpuif_cls=APB4Cpuif, cpuif_cls=APB4Cpuif,
cpuif_unroll=True, cpuif_unroll=True,
) )
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "top.sv" module_file = Path(tmpdir) / "top.sv"
content = module_file.read_text() content = module_file.read_text()
# Should have individual interfaces for each element in the 2x3 array # 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 # 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 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_0" in content
assert "m_apb_matrix_1_1" in content assert "m_apb_matrix_1_1" in content
assert "m_apb_matrix_1_2" in content assert "m_apb_matrix_1_2" in content
# Should NOT have array dimensions on any of the unrolled interfaces # Should NOT have array dimensions on any of the unrolled interfaces
for i in range(2): for i in range(2):
for j in range(3): 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
import pytest
from systemrdl import RDLCompiler
from systemrdl.node import AddrmapNode from systemrdl.node import AddrmapNode
from peakrdl_busdecoder.utils import clog2, get_indexed_path, is_pow2, roundup_pow2 from peakrdl_busdecoder.utils import get_indexed_path
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
class TestGetIndexedPath: class TestGetIndexedPath:
"""Test get_indexed_path function.""" """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.""" """Test simple path without arrays."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { addrmap my_addrmap {
@@ -111,7 +29,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node) path = get_indexed_path(top, reg_node)
assert path == "my_reg" 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.""" """Test nested path without arrays."""
rdl_source = """ rdl_source = """
addrmap inner_map { addrmap inner_map {
@@ -143,7 +61,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node) path = get_indexed_path(top, reg_node)
assert path == "inner.my_reg" 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.""" """Test path with array indices."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { addrmap my_addrmap {
@@ -163,7 +81,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node) path = get_indexed_path(top, reg_node)
assert path == "my_reg[i0]" 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.""" """Test path with multidimensional arrays."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { addrmap my_addrmap {
@@ -183,7 +101,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node) path = get_indexed_path(top, reg_node)
assert path == "my_reg[i0][i1]" 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.""" """Test path with nested arrays."""
rdl_source = """ rdl_source = """
addrmap inner_map { addrmap inner_map {
@@ -215,7 +133,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node) path = get_indexed_path(top, reg_node)
assert path == "inner[i0].my_reg[i1]" 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.""" """Test path with custom indexer name."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { addrmap my_addrmap {
@@ -235,7 +153,7 @@ class TestGetIndexedPath:
path = get_indexed_path(top, reg_node, indexer="idx") path = get_indexed_path(top, reg_node, indexer="idx")
assert path == "my_reg[idx0]" 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.""" """Test path with keyword filtering skipped."""
rdl_source = """ rdl_source = """
addrmap my_addrmap { 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())