Refactor tests (better grouping + cocotb support) (#15)
* initial refactor * fix cocotb tests * fix typecheck * install verilator
This commit is contained in:
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -25,6 +25,12 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install Verilator
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y verilator
|
||||
verilator --version
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --group test
|
||||
|
||||
@@ -2,6 +2,10 @@ interface apb3_intf #(
|
||||
parameter DATA_WIDTH = 32,
|
||||
parameter ADDR_WIDTH = 32
|
||||
);
|
||||
// Clocking
|
||||
logic PCLK;
|
||||
logic PRESETn;
|
||||
|
||||
// Command
|
||||
logic PSEL;
|
||||
logic PENABLE;
|
||||
@@ -15,6 +19,9 @@ interface apb3_intf #(
|
||||
logic PSLVERR;
|
||||
|
||||
modport master (
|
||||
input PCLK,
|
||||
input PRESETn,
|
||||
|
||||
output PSEL,
|
||||
output PENABLE,
|
||||
output PWRITE,
|
||||
@@ -27,6 +34,9 @@ interface apb3_intf #(
|
||||
);
|
||||
|
||||
modport slave (
|
||||
input PCLK,
|
||||
input PRESETn,
|
||||
|
||||
input PSEL,
|
||||
input PENABLE,
|
||||
input PWRITE,
|
||||
|
||||
@@ -2,6 +2,10 @@ interface apb4_intf #(
|
||||
parameter DATA_WIDTH = 32,
|
||||
parameter ADDR_WIDTH = 32
|
||||
);
|
||||
// Clocking
|
||||
logic PCLK;
|
||||
logic PRESETn;
|
||||
|
||||
// Command
|
||||
logic PSEL;
|
||||
logic PENABLE;
|
||||
@@ -17,6 +21,9 @@ interface apb4_intf #(
|
||||
logic PSLVERR;
|
||||
|
||||
modport master (
|
||||
input PCLK,
|
||||
input PRESETn,
|
||||
|
||||
output PSEL,
|
||||
output PENABLE,
|
||||
output PWRITE,
|
||||
@@ -31,6 +38,9 @@ interface apb4_intf #(
|
||||
);
|
||||
|
||||
modport slave (
|
||||
input PCLK,
|
||||
input PRESETn,
|
||||
|
||||
input PSEL,
|
||||
input PENABLE,
|
||||
input PWRITE,
|
||||
|
||||
@@ -2,6 +2,9 @@ interface axi4lite_intf #(
|
||||
parameter DATA_WIDTH = 32,
|
||||
parameter ADDR_WIDTH = 32
|
||||
);
|
||||
logic ACLK;
|
||||
logic ARESETn;
|
||||
|
||||
logic AWREADY;
|
||||
logic AWVALID;
|
||||
logic [ADDR_WIDTH-1:0] AWADDR;
|
||||
@@ -27,6 +30,9 @@ interface axi4lite_intf #(
|
||||
logic [1:0] RRESP;
|
||||
|
||||
modport master (
|
||||
input ACLK,
|
||||
input ARESETn,
|
||||
|
||||
input AWREADY,
|
||||
output AWVALID,
|
||||
output AWADDR,
|
||||
@@ -53,15 +59,18 @@ interface axi4lite_intf #(
|
||||
);
|
||||
|
||||
modport slave (
|
||||
input ACLK,
|
||||
input ARESETn,
|
||||
|
||||
output AWREADY,
|
||||
// input AWVALID,
|
||||
// input AWADDR,
|
||||
input AWVALID,
|
||||
input AWADDR,
|
||||
input AWPROT,
|
||||
|
||||
output WREADY,
|
||||
// input WVALID,
|
||||
// input WDATA,
|
||||
// input WSTRB,
|
||||
input WVALID,
|
||||
input WDATA,
|
||||
input WSTRB,
|
||||
|
||||
input BREADY,
|
||||
output BVALID,
|
||||
@@ -73,8 +82,8 @@ interface axi4lite_intf #(
|
||||
input ARPROT,
|
||||
|
||||
input RREADY,
|
||||
// output RVALID,
|
||||
// output RDATA,
|
||||
// output RRESP
|
||||
output RVALID,
|
||||
output RDATA,
|
||||
output RRESP
|
||||
);
|
||||
endinterface
|
||||
|
||||
@@ -12,14 +12,14 @@ class APB3CpuifFlat(BaseCpuif):
|
||||
return [
|
||||
f"input logic {self.signal('PCLK', child)}",
|
||||
f"input logic {self.signal('PRESETn', child)}",
|
||||
f"input logic {self.signal('PSELx', child)}",
|
||||
f"input logic {self.signal('PENABLE', child)}",
|
||||
f"input logic {self.signal('PWRITE', child)}",
|
||||
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
|
||||
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
|
||||
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
|
||||
f"output logic {self.signal('PREADY', child)}",
|
||||
f"output logic {self.signal('PSLVERR', child)}",
|
||||
f"output logic {self.signal('PSELx', child)}",
|
||||
f"output logic {self.signal('PENABLE', child)}",
|
||||
f"output logic {self.signal('PWRITE', child)}",
|
||||
f"output logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
|
||||
f"output logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
|
||||
f"input logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
|
||||
f"input logic {self.signal('PREADY', child)}",
|
||||
f"input logic {self.signal('PSLVERR', child)}",
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -48,9 +48,10 @@ class APB3CpuifFlat(BaseCpuif):
|
||||
node: AddressableNode | None = None,
|
||||
idx: str | int | None = None,
|
||||
) -> str:
|
||||
mapped_signal = "PSELx" if signal == "PSEL" else signal
|
||||
if node is None:
|
||||
# Node is none, so this is a slave signal
|
||||
return f"s_apb_{signal}"
|
||||
return f"s_apb_{mapped_signal}"
|
||||
|
||||
# Master signal
|
||||
base = f"m_apb_{node.inst_name}"
|
||||
@@ -58,12 +59,12 @@ class APB3CpuifFlat(BaseCpuif):
|
||||
# Not an array or an unrolled element
|
||||
if node.current_idx is not None:
|
||||
# This is a specific instance of an unrolled array
|
||||
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
|
||||
return f"{base}_{signal}"
|
||||
return f"{base}_{mapped_signal}_{'_'.join(map(str, node.current_idx))}"
|
||||
return f"{base}_{mapped_signal}"
|
||||
# Is an array
|
||||
if idx is not None:
|
||||
return f"{base}_{signal}[{idx}]"
|
||||
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
|
||||
return f"{base}_{mapped_signal}[{idx}]"
|
||||
return f"{base}_{mapped_signal}[N_{node.inst_name.upper()}S]"
|
||||
|
||||
def fanout(self, node: AddressableNode) -> str:
|
||||
fanout: dict[str, str] = {}
|
||||
|
||||
@@ -12,16 +12,16 @@ class APB4CpuifFlat(BaseCpuif):
|
||||
return [
|
||||
f"input logic {self.signal('PCLK', child)}",
|
||||
f"input logic {self.signal('PRESETn', child)}",
|
||||
f"input logic {self.signal('PSELx', child)}",
|
||||
f"input logic {self.signal('PENABLE', child)}",
|
||||
f"input logic {self.signal('PWRITE', child)}",
|
||||
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
|
||||
f"input logic [2:0] {self.signal('PPROT', child)}",
|
||||
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
|
||||
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",
|
||||
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
|
||||
f"output logic {self.signal('PREADY', child)}",
|
||||
f"output logic {self.signal('PSLVERR', child)}",
|
||||
f"output logic {self.signal('PSELx', child)}",
|
||||
f"output logic {self.signal('PENABLE', child)}",
|
||||
f"output logic {self.signal('PWRITE', child)}",
|
||||
f"output logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
|
||||
f"output logic [2:0] {self.signal('PPROT', child)}",
|
||||
f"output logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
|
||||
f"output logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",
|
||||
f"input logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
|
||||
f"input logic {self.signal('PREADY', child)}",
|
||||
f"input logic {self.signal('PSLVERR', child)}",
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -52,9 +52,10 @@ class APB4CpuifFlat(BaseCpuif):
|
||||
node: AddressableNode | None = None,
|
||||
idx: str | int | None = None,
|
||||
) -> str:
|
||||
mapped_signal = "PSELx" if signal == "PSEL" else signal
|
||||
if node is None:
|
||||
# Node is none, so this is a slave signal
|
||||
return f"s_apb_{signal}"
|
||||
return f"s_apb_{mapped_signal}"
|
||||
|
||||
# Master signal
|
||||
base = f"m_apb_{node.inst_name}"
|
||||
@@ -62,12 +63,12 @@ class APB4CpuifFlat(BaseCpuif):
|
||||
# Not an array or an unrolled element
|
||||
if node.current_idx is not None:
|
||||
# This is a specific instance of an unrolled array
|
||||
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
|
||||
return f"{base}_{signal}"
|
||||
return f"{base}_{mapped_signal}_{'_'.join(map(str, node.current_idx))}"
|
||||
return f"{base}_{mapped_signal}"
|
||||
# Is an array
|
||||
if idx is not None:
|
||||
return f"{base}_{signal}[{idx}]"
|
||||
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
|
||||
return f"{base}_{mapped_signal}[{idx}]"
|
||||
return f"{base}_{mapped_signal}[N_{node.inst_name.upper()}S]"
|
||||
|
||||
def fanout(self, node: AddressableNode) -> str:
|
||||
fanout: dict[str, str] = {}
|
||||
|
||||
@@ -10,7 +10,7 @@ class AXI4LiteCpuif(BaseCpuif):
|
||||
template_path = "axi4lite_tmpl.sv"
|
||||
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}"
|
||||
|
||||
# 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
|
||||
if self.check_is_array(child):
|
||||
assert child.array_dimensions is not None
|
||||
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
|
||||
return [f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"]
|
||||
|
||||
return base
|
||||
return [base]
|
||||
|
||||
@property
|
||||
def port_declaration(self) -> str:
|
||||
"""Returns the port declaration for the AXI4-Lite interface."""
|
||||
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
|
||||
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
|
||||
|
||||
master_ports: list[str] = []
|
||||
for child in self.addressable_children:
|
||||
master_ports.extend(self._port_declaration(child))
|
||||
|
||||
return ",\n".join(slave_ports + master_ports)
|
||||
|
||||
@overload
|
||||
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
|
||||
@overload
|
||||
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
|
||||
def signal(self, signal: str, node: AddressableNode, indexer: str | None = None) -> str: ...
|
||||
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
|
||||
if node is None or indexer is None:
|
||||
# Node is none, so this is a slave signal
|
||||
|
||||
@@ -2,94 +2,91 @@ from typing import overload
|
||||
|
||||
from systemrdl.node import AddressableNode
|
||||
|
||||
from ...utils import get_indexed_path
|
||||
from ..base_cpuif import BaseCpuif
|
||||
from .axi4_lite_cpuif import AXI4LiteCpuif
|
||||
|
||||
|
||||
class AXI4LiteCpuifFlat(BaseCpuif):
|
||||
class AXI4LiteCpuifFlat(AXI4LiteCpuif):
|
||||
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
|
||||
|
||||
template_path = "axi4lite_tmpl.sv"
|
||||
is_interface = True
|
||||
is_interface = False
|
||||
|
||||
def _port_declaration(self, child: AddressableNode) -> str:
|
||||
base = f"axi4lite_intf.master m_axil_{child.inst_name}"
|
||||
|
||||
# When unrolled, current_idx is set - append it to the name
|
||||
if child.current_idx is not None:
|
||||
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
|
||||
|
||||
# Only add array dimensions if this should be treated as an array
|
||||
if self.check_is_array(child):
|
||||
assert child.array_dimensions is not None
|
||||
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
|
||||
|
||||
return base
|
||||
def _port_declaration(self, child: AddressableNode) -> list[str]:
|
||||
return [
|
||||
f"input logic {self.signal('AWREADY', child)}",
|
||||
f"output logic {self.signal('AWVALID', child)}",
|
||||
f"output logic [{self.addr_width - 1}:0] {self.signal('AWADDR', child)}",
|
||||
f"output logic [2:0] {self.signal('AWPROT', child)}",
|
||||
f"input logic {self.signal('WREADY', child)}",
|
||||
f"output logic {self.signal('WVALID', child)}",
|
||||
f"output logic [{self.data_width - 1}:0] {self.signal('WDATA', child)}",
|
||||
f"output logic [{self.data_width_bytes - 1}:0] {self.signal('WSTRB', child)}",
|
||||
f"output logic {self.signal('BREADY', child)}",
|
||||
f"input logic {self.signal('BVALID', child)}",
|
||||
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
|
||||
def port_declaration(self) -> str:
|
||||
"""Returns the port declaration for the AXI4-Lite interface."""
|
||||
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
|
||||
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
|
||||
slave_ports: list[str] = [
|
||||
f"input logic {self.signal('ACLK')}",
|
||||
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)
|
||||
|
||||
@overload
|
||||
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
|
||||
|
||||
@overload
|
||||
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
|
||||
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
|
||||
if node is None or indexer is None:
|
||||
# Node is none, so this is a slave signal
|
||||
return f"s_axil.{signal}"
|
||||
def signal(self, signal: str, node: AddressableNode, indexer: str | None = None) -> str: ...
|
||||
|
||||
# Master signal
|
||||
return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
|
||||
|
||||
def fanout(self, node: AddressableNode) -> str:
|
||||
fanout: dict[str, 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] = {}
|
||||
def signal(
|
||||
self,
|
||||
signal: str,
|
||||
node: AddressableNode | None = None,
|
||||
indexer: str | None = None,
|
||||
) -> str:
|
||||
if node is None:
|
||||
fanin["cpuif_rd_ack"] = "'0"
|
||||
fanin["cpuif_rd_err"] = "'0"
|
||||
else:
|
||||
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
|
||||
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
|
||||
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
|
||||
return f"s_axil_{signal}"
|
||||
|
||||
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:
|
||||
fanin: dict[str, str] = {}
|
||||
if node is None:
|
||||
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())
|
||||
if indexer is None:
|
||||
return f"{base}[N_{node.inst_name.upper()}S]"
|
||||
return f"{base}[{indexer}]"
|
||||
|
||||
@@ -63,11 +63,16 @@ class DecodeLogicGenerator(BusDecoderListener):
|
||||
l_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})")
|
||||
u_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})")
|
||||
|
||||
# Generate Conditions
|
||||
return [
|
||||
f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})",
|
||||
f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})",
|
||||
]
|
||||
lower_expr = f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})"
|
||||
upper_expr = f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})"
|
||||
|
||||
predicates: list[str] = []
|
||||
# Avoid generating a redundant >= 0 comparison, which triggers Verilator warnings.
|
||||
if not (l_bound.value == 0 and len(l_bound_comp) == 1):
|
||||
predicates.append(lower_expr)
|
||||
predicates.append(upper_expr)
|
||||
|
||||
return predicates
|
||||
|
||||
def cpuif_prot_predicate(self, node: AddressableNode) -> list[str]:
|
||||
if self._flavor == DecodeLogicFlavor.READ:
|
||||
|
||||
@@ -44,7 +44,7 @@ module {{ds.module_name}}
|
||||
//--------------------------------------------------------------------------
|
||||
// Slave <-> Internal CPUIF <-> Master
|
||||
//--------------------------------------------------------------------------
|
||||
{{-cpuif.get_implementation()|indent(4)}}
|
||||
{{cpuif.get_implementation()|indent(4)}}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Write Address Decoder
|
||||
|
||||
@@ -72,16 +72,28 @@ Run examples to see generated code for different configurations:
|
||||
python tests/cocotb/examples.py
|
||||
```
|
||||
|
||||
#### Simulation layout
|
||||
|
||||
Simulation-oriented tests are grouped by CPU interface under
|
||||
`tests/cocotb/<cpuif>/<group>/`. For example, the APB4 smoke test lives in
|
||||
`tests/cocotb/apb4/smoke/` alongside its pytest runner module. Each runner
|
||||
compiles the appropriate SystemRDL design, adds the interface wrapper from
|
||||
`hdl-src/`, and invokes cocotb via Verilator.
|
||||
|
||||
#### Full simulation tests (requires simulator)
|
||||
|
||||
To run the full cocotb simulation tests:
|
||||
To execute the smoke tests for every supported interface:
|
||||
|
||||
```bash
|
||||
# Run all cocotb simulation tests
|
||||
pytest tests/cocotb/testbenches/test_*_runner.py -v
|
||||
pytest tests/cocotb/*/smoke/test_runner.py -v
|
||||
```
|
||||
|
||||
# Run specific interface tests
|
||||
pytest tests/cocotb/testbenches/test_apb4_runner.py -v
|
||||
To target a single interface, point pytest at that runner module:
|
||||
|
||||
```bash
|
||||
pytest tests/cocotb/apb3/smoke/test_runner.py -v
|
||||
pytest tests/cocotb/apb4/smoke/test_runner.py -v
|
||||
pytest tests/cocotb/axi4lite/smoke/test_runner.py -v
|
||||
```
|
||||
|
||||
For more information about cocotb tests, see [`tests/cocotb/README.md`](cocotb/README.md).
|
||||
|
||||
0
tests/body/__init__.py
Normal file
0
tests/body/__init__.py
Normal file
50
tests/body/test_body.py
Normal file
50
tests/body/test_body.py
Normal 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
|
||||
|
||||
|
||||
39
tests/body/test_combinational_body.py
Normal file
39
tests/body/test_combinational_body.py
Normal 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
|
||||
46
tests/body/test_for_loop_body.py
Normal file
46
tests/body/test_for_loop_body.py
Normal 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
|
||||
85
tests/body/test_if_body.py
Normal file
85
tests/body/test_if_body.py
Normal 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
|
||||
|
||||
59
tests/body/test_struct_body.py
Normal file
59
tests/body/test_struct_body.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
"""Cocotb tests package."""
|
||||
|
||||
0
tests/cocotb/apb3/__init__.py
Normal file
0
tests/cocotb/apb3/__init__.py
Normal file
0
tests/cocotb/apb3/smoke/__init__.py
Normal file
0
tests/cocotb/apb3/smoke/__init__.py
Normal file
123
tests/cocotb/apb3/smoke/test_register_access.py
Normal file
123
tests/cocotb/apb3/smoke/test_register_access.py
Normal 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")
|
||||
50
tests/cocotb/apb3/smoke/test_runner.py
Normal file
50
tests/cocotb/apb3/smoke/test_runner.py
Normal 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"),
|
||||
)
|
||||
0
tests/cocotb/apb4/__init__.py
Normal file
0
tests/cocotb/apb4/__init__.py
Normal file
0
tests/cocotb/apb4/smoke/__init__.py
Normal file
0
tests/cocotb/apb4/smoke/__init__.py
Normal file
138
tests/cocotb/apb4/smoke/test_register_access.py
Normal file
138
tests/cocotb/apb4/smoke/test_register_access.py
Normal 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")
|
||||
50
tests/cocotb/apb4/smoke/test_runner.py
Normal file
50
tests/cocotb/apb4/smoke/test_runner.py
Normal 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"),
|
||||
)
|
||||
0
tests/cocotb/axi4lite/__init__.py
Normal file
0
tests/cocotb/axi4lite/__init__.py
Normal file
0
tests/cocotb/axi4lite/smoke/__init__.py
Normal file
0
tests/cocotb/axi4lite/smoke/__init__.py
Normal file
161
tests/cocotb/axi4lite/smoke/test_register_access.py
Normal file
161
tests/cocotb/axi4lite/smoke/test_register_access.py
Normal 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")
|
||||
50
tests/cocotb/axi4lite/smoke/test_runner.py
Normal file
50
tests/cocotb/axi4lite/smoke/test_runner.py
Normal 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"),
|
||||
)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -1 +0,0 @@
|
||||
"""Cocotb testbenches package."""
|
||||
@@ -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!")
|
||||
@@ -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!")
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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!")
|
||||
@@ -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"])
|
||||
3
tests/cocotb_lib/__init__.py
Normal file
3
tests/cocotb_lib/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
rdls = map(Path, ["simple.rdl", "multiple_reg.rdl"])
|
||||
22
tests/cocotb_lib/multiple_reg.rdl
Normal file
22
tests/cocotb_lib/multiple_reg.rdl
Normal 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;
|
||||
};
|
||||
8
tests/cocotb_lib/simple.rdl
Normal file
8
tests/cocotb_lib/simple.rdl
Normal file
@@ -0,0 +1,8 @@
|
||||
addrmap simple_test {
|
||||
reg {
|
||||
field {
|
||||
sw=rw;
|
||||
hw=r;
|
||||
} data[31:0];
|
||||
} test_reg @ 0x0;
|
||||
};
|
||||
@@ -1,24 +1,22 @@
|
||||
"""Common utilities for cocotb testbenches."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any
|
||||
|
||||
from systemrdl import RDLCompiler
|
||||
|
||||
from peakrdl_busdecoder.cpuif.base_cpuif import BaseCpuif
|
||||
from peakrdl_busdecoder.exporter import BusDecoderExporter
|
||||
|
||||
|
||||
def compile_rdl_and_export(
|
||||
rdl_source: str, top_name: str, output_dir: str, cpuif_cls: Any, **kwargs: Any
|
||||
rdl_source: str, top_name: str, output_dir: Path, cpuif_cls: type[BaseCpuif], **kwargs: Any
|
||||
) -> tuple[Path, Path]:
|
||||
"""
|
||||
Compile RDL source and export to SystemVerilog.
|
||||
|
||||
Args:
|
||||
rdl_source: SystemRDL source code as a string
|
||||
rdl_source: SystemRDL source code path
|
||||
top_name: Name of the top-level addrmap
|
||||
output_dir: Directory to write generated files
|
||||
cpuif_cls: CPU interface class to use
|
||||
@@ -30,23 +28,12 @@ def compile_rdl_and_export(
|
||||
# Compile RDL source
|
||||
compiler = RDLCompiler()
|
||||
|
||||
# Write source to temporary file
|
||||
with NamedTemporaryFile("w", suffix=".rdl", dir=output_dir, delete=False) as tmp_file:
|
||||
tmp_file.write(rdl_source)
|
||||
tmp_file.flush()
|
||||
tmp_path = tmp_file.name
|
||||
|
||||
try:
|
||||
compiler.compile_file(tmp_path)
|
||||
compiler.compile_file(rdl_source)
|
||||
top = compiler.elaborate(top_name)
|
||||
|
||||
# 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)
|
||||
exporter.export(top, str(output_dir), cpuif_cls=cpuif_cls, **kwargs)
|
||||
|
||||
# Return paths to generated files
|
||||
module_name = kwargs.get("module_name", top_name)
|
||||
60
tests/conftest.py
Normal file
60
tests/conftest.py
Normal 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
|
||||
0
tests/exporter/__init__.py
Normal file
0
tests/exporter/__init__.py
Normal file
@@ -1,19 +1,16 @@
|
||||
"""Integration tests for the BusDecoderExporter."""
|
||||
|
||||
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from systemrdl.node import AddrmapNode
|
||||
|
||||
from peakrdl_busdecoder import BusDecoderExporter
|
||||
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
|
||||
from peakrdl_busdecoder.exporter import BusDecoderExporter
|
||||
|
||||
|
||||
class TestBusDecoderExporter:
|
||||
"""Test the top-level BusDecoderExporter."""
|
||||
|
||||
def test_simple_register_export(self, compile_rdl, tmp_path):
|
||||
def test_simple_register_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
|
||||
"""Test exporting a simple register."""
|
||||
rdl_source = """
|
||||
addrmap simple_reg {
|
||||
@@ -46,7 +43,7 @@ class TestBusDecoderExporter:
|
||||
package_content = package_file.read_text()
|
||||
assert "package simple_reg_pkg" in package_content
|
||||
|
||||
def test_register_array_export(self, compile_rdl, tmp_path):
|
||||
def test_register_array_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
|
||||
"""Test exporting a register array."""
|
||||
rdl_source = """
|
||||
addrmap reg_array {
|
||||
@@ -72,7 +69,7 @@ class TestBusDecoderExporter:
|
||||
assert "module reg_array" in module_content
|
||||
assert "my_regs" in module_content
|
||||
|
||||
def test_nested_addrmap_export(self, compile_rdl, tmp_path):
|
||||
def test_nested_addrmap_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
|
||||
"""Test exporting nested addrmaps."""
|
||||
rdl_source = """
|
||||
addrmap inner_block {
|
||||
@@ -103,7 +100,7 @@ class TestBusDecoderExporter:
|
||||
assert "inner" in module_content
|
||||
assert "inner_reg" in module_content
|
||||
|
||||
def test_custom_module_name(self, compile_rdl, tmp_path):
|
||||
def test_custom_module_name(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
|
||||
"""Test exporting with custom module name."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
@@ -131,7 +128,7 @@ class TestBusDecoderExporter:
|
||||
module_content = module_file.read_text()
|
||||
assert "module custom_module" in module_content
|
||||
|
||||
def test_custom_package_name(self, compile_rdl, tmp_path):
|
||||
def test_custom_package_name(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
|
||||
"""Test exporting with custom package name."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
@@ -156,7 +153,7 @@ class TestBusDecoderExporter:
|
||||
package_content = package_file.read_text()
|
||||
assert "package custom_pkg" in package_content
|
||||
|
||||
def test_multiple_registers(self, compile_rdl, tmp_path):
|
||||
def test_multiple_registers(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None:
|
||||
"""Test exporting multiple registers."""
|
||||
rdl_source = """
|
||||
addrmap multi_reg {
|
||||
@@ -196,134 +193,3 @@ class TestBusDecoderExporter:
|
||||
assert "reg1" in module_content
|
||||
assert "reg2" in module_content
|
||||
assert "reg3" in module_content
|
||||
|
||||
|
||||
class TestAPB4Interface:
|
||||
"""Test APB4 CPU interface generation."""
|
||||
|
||||
def test_apb4_port_declaration(self, compile_rdl, tmp_path):
|
||||
"""Test that APB4 interface ports are generated."""
|
||||
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"
|
||||
0
tests/generator/__init__.py
Normal file
0
tests/generator/__init__.py
Normal file
96
tests/generator/test_decode_logic_generator.py
Normal file
96
tests/generator/test_decode_logic_generator.py
Normal 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"
|
||||
125
tests/generator/test_design_state.py
Normal file
125
tests/generator/test_design_state.py
Normal 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
|
||||
98
tests/generator/test_struct_generator.py
Normal file
98
tests/generator/test_struct_generator.py
Normal 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
|
||||
@@ -1,3 +1,5 @@
|
||||
[pytest]
|
||||
testpaths = unit
|
||||
python_files = test_*.py testcase.py
|
||||
markers =
|
||||
simulation: marks hardware simulation tests requiring cocotb/verilator
|
||||
verilator: marks tests that invoke the Verilator toolchain
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Unit test package for PeakRDL BusDecoder."""
|
||||
@@ -5,52 +5,71 @@
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Iterable, Mapping, Optional
|
||||
from collections.abc import Callable
|
||||
|
||||
import pytest
|
||||
from systemrdl import RDLCompileError, RDLCompiler
|
||||
from systemrdl.node import AddrmapNode
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def compile_rdl(tmp_path: Path):
|
||||
"""Compile inline SystemRDL source and return the elaborated root node.
|
||||
def external_nested_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode:
|
||||
"""Create an RDL design with external nested addressable components.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tmp_path:
|
||||
Temporary directory provided by pytest.
|
||||
This tests the scenario where an addrmap contains external children
|
||||
that themselves have external addressable children.
|
||||
The decoder should only generate select signals for the top-level
|
||||
external children, not their internal structure.
|
||||
"""
|
||||
rdl_source = """
|
||||
mem queue_t {
|
||||
name = "Queue";
|
||||
mementries = 1024;
|
||||
memwidth = 64;
|
||||
};
|
||||
|
||||
def _compile(
|
||||
source: str,
|
||||
*,
|
||||
top: Optional[str] = None,
|
||||
defines: Optional[Mapping[str, object]] = None,
|
||||
include_paths: Optional[Iterable[Path | str]] = None,
|
||||
):
|
||||
compiler = RDLCompiler()
|
||||
addrmap port_t {
|
||||
name = "Port";
|
||||
desc = "";
|
||||
|
||||
for key, value in (defines or {}).items():
|
||||
compiler.define(key, value)
|
||||
external queue_t common[3] @ 0x0 += 0x2000;
|
||||
external queue_t response @ 0x6000;
|
||||
};
|
||||
|
||||
for include_path in include_paths or ():
|
||||
compiler.add_include_path(str(include_path))
|
||||
addrmap buffer_t {
|
||||
name = "Buffer";
|
||||
desc = "";
|
||||
|
||||
# Use delete=False to keep the file around after closing
|
||||
with NamedTemporaryFile("w", suffix=".rdl", dir=tmp_path, delete=False) as tmp_file:
|
||||
tmp_file.write(source)
|
||||
tmp_file.flush()
|
||||
port_t multicast @ 0x0;
|
||||
port_t port [16] @ 0x8000 += 0x8000;
|
||||
};
|
||||
"""
|
||||
return compile_rdl(rdl_source, top="buffer_t")
|
||||
|
||||
try:
|
||||
compiler.compile_file(tmp_file.name)
|
||||
if top is not None:
|
||||
root = compiler.elaborate(top)
|
||||
return root.top
|
||||
root = compiler.elaborate()
|
||||
return root.top
|
||||
except RDLCompileError:
|
||||
# Print error messages if available
|
||||
if hasattr(compiler, "print_messages"):
|
||||
compiler.print_messages()
|
||||
raise
|
||||
|
||||
return _compile
|
||||
@pytest.fixture
|
||||
def nested_addrmap_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode:
|
||||
"""Create an RDL design with nested non-external addrmaps for testing depth control."""
|
||||
rdl_source = """
|
||||
addrmap level2 {
|
||||
reg {
|
||||
field { sw=rw; hw=r; } data2[31:0];
|
||||
} reg2 @ 0x0;
|
||||
|
||||
reg {
|
||||
field { sw=rw; hw=r; } data2b[31:0];
|
||||
} reg2b @ 0x4;
|
||||
};
|
||||
|
||||
addrmap level1 {
|
||||
reg {
|
||||
field { sw=rw; hw=r; } data1[31:0];
|
||||
} reg1 @ 0x0;
|
||||
|
||||
level2 inner2 @ 0x10;
|
||||
};
|
||||
|
||||
addrmap level0 {
|
||||
level1 inner1 @ 0x0;
|
||||
};
|
||||
"""
|
||||
return compile_rdl(rdl_source, top="level0")
|
||||
|
||||
@@ -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
|
||||
@@ -1,79 +1,16 @@
|
||||
"""Test handling of external nested addressable components."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
from systemrdl.node import AddrmapNode
|
||||
|
||||
from peakrdl_busdecoder import BusDecoderExporter
|
||||
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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):
|
||||
def test_external_nested_components_generate_correct_decoder(external_nested_rdl: AddrmapNode) -> None:
|
||||
"""Test that external nested components generate correct decoder logic.
|
||||
|
||||
The decoder should:
|
||||
@@ -109,7 +46,7 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
rdl_source = """
|
||||
addrmap simple {
|
||||
|
||||
@@ -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
0
tests/unroll/__init__.py
Normal file
40
tests/unroll/conftest.py
Normal file
40
tests/unroll/conftest.py
Normal 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)
|
||||
@@ -1,34 +1,14 @@
|
||||
"""Test the --unroll CLI argument functionality."""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
from systemrdl.node import AddrmapNode
|
||||
|
||||
from peakrdl_busdecoder import BusDecoderExporter
|
||||
from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif
|
||||
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_rdl(compile_rdl):
|
||||
"""Create a simple RDL design with an array."""
|
||||
rdl_source = """
|
||||
addrmap top {
|
||||
reg my_reg {
|
||||
field {
|
||||
sw=rw;
|
||||
hw=r;
|
||||
} data[31:0];
|
||||
};
|
||||
|
||||
my_reg regs[4] @ 0x0 += 0x4;
|
||||
};
|
||||
"""
|
||||
return compile_rdl(rdl_source)
|
||||
|
||||
|
||||
def test_unroll_disabled_creates_array_interface(sample_rdl):
|
||||
def test_unroll_disabled_creates_array_interface(sample_rdl: AddrmapNode) -> None:
|
||||
"""Test that with unroll=False, array nodes are kept as arrays."""
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
exporter = BusDecoderExporter()
|
||||
@@ -56,7 +36,7 @@ def test_unroll_disabled_creates_array_interface(sample_rdl):
|
||||
assert "m_apb_regs_3" not in content
|
||||
|
||||
|
||||
def test_unroll_enabled_creates_individual_interfaces(sample_rdl):
|
||||
def test_unroll_enabled_creates_individual_interfaces(sample_rdl: AddrmapNode) -> None:
|
||||
"""Test that with unroll=True, array elements are unrolled into separate instances."""
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
exporter = BusDecoderExporter()
|
||||
@@ -90,7 +70,7 @@ def test_unroll_enabled_creates_individual_interfaces(sample_rdl):
|
||||
assert "N_REGSS" not in content
|
||||
|
||||
|
||||
def test_unroll_with_apb3(sample_rdl):
|
||||
def test_unroll_with_apb3(sample_rdl: AddrmapNode) -> None:
|
||||
"""Test that unroll works correctly with APB3 interface."""
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
exporter = BusDecoderExporter()
|
||||
@@ -115,25 +95,7 @@ def test_unroll_with_apb3(sample_rdl):
|
||||
assert "m_apb_regs_0 [4]" not in content
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multidim_array_rdl(compile_rdl):
|
||||
"""Create an RDL design with a multi-dimensional array."""
|
||||
rdl_source = """
|
||||
addrmap top {
|
||||
reg my_reg {
|
||||
field {
|
||||
sw=rw;
|
||||
hw=r;
|
||||
} data[31:0];
|
||||
};
|
||||
|
||||
my_reg matrix[2][3] @ 0x0 += 0x4;
|
||||
};
|
||||
"""
|
||||
return compile_rdl(rdl_source)
|
||||
|
||||
|
||||
def test_unroll_multidimensional_array(multidim_array_rdl):
|
||||
def test_unroll_multidimensional_array(multidim_array_rdl: AddrmapNode) -> None:
|
||||
"""Test that unroll works correctly with multi-dimensional arrays."""
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
exporter = BusDecoderExporter()
|
||||
0
tests/utils/__init__.py
Normal file
0
tests/utils/__init__.py
Normal 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 peakrdl_busdecoder.utils import clog2, get_indexed_path, is_pow2, roundup_pow2
|
||||
|
||||
|
||||
class TestMathUtils:
|
||||
"""Test mathematical utility functions."""
|
||||
|
||||
def test_clog2_basic(self):
|
||||
"""Test clog2 function with basic values."""
|
||||
assert clog2(1) == 0
|
||||
assert clog2(2) == 1
|
||||
assert clog2(3) == 2
|
||||
assert clog2(4) == 2
|
||||
assert clog2(5) == 3
|
||||
assert clog2(8) == 3
|
||||
assert clog2(9) == 4
|
||||
assert clog2(16) == 4
|
||||
assert clog2(17) == 5
|
||||
assert clog2(32) == 5
|
||||
assert clog2(33) == 6
|
||||
assert clog2(64) == 6
|
||||
assert clog2(128) == 7
|
||||
assert clog2(256) == 8
|
||||
assert clog2(1024) == 10
|
||||
|
||||
def test_is_pow2_true_cases(self):
|
||||
"""Test is_pow2 returns True for powers of 2."""
|
||||
assert is_pow2(1) is True
|
||||
assert is_pow2(2) is True
|
||||
assert is_pow2(4) is True
|
||||
assert is_pow2(8) is True
|
||||
assert is_pow2(16) is True
|
||||
assert is_pow2(32) is True
|
||||
assert is_pow2(64) is True
|
||||
assert is_pow2(128) is True
|
||||
assert is_pow2(256) is True
|
||||
assert is_pow2(512) is True
|
||||
assert is_pow2(1024) is True
|
||||
|
||||
def test_is_pow2_false_cases(self):
|
||||
"""Test is_pow2 returns False for non-powers of 2."""
|
||||
assert is_pow2(0) is False
|
||||
assert is_pow2(3) is False
|
||||
assert is_pow2(5) is False
|
||||
assert is_pow2(6) is False
|
||||
assert is_pow2(7) is False
|
||||
assert is_pow2(9) is False
|
||||
assert is_pow2(10) is False
|
||||
assert is_pow2(15) is False
|
||||
assert is_pow2(17) is False
|
||||
assert is_pow2(100) is False
|
||||
assert is_pow2(255) is False
|
||||
assert is_pow2(1000) is False
|
||||
|
||||
def test_roundup_pow2_already_power_of_2(self):
|
||||
"""Test roundup_pow2 with values that are already powers of 2."""
|
||||
assert roundup_pow2(1) == 1
|
||||
assert roundup_pow2(2) == 2
|
||||
assert roundup_pow2(4) == 4
|
||||
assert roundup_pow2(8) == 8
|
||||
assert roundup_pow2(16) == 16
|
||||
assert roundup_pow2(32) == 32
|
||||
assert roundup_pow2(64) == 64
|
||||
assert roundup_pow2(128) == 128
|
||||
assert roundup_pow2(256) == 256
|
||||
|
||||
def test_roundup_pow2_non_power_of_2(self):
|
||||
"""Test roundup_pow2 with values that are not powers of 2."""
|
||||
assert roundup_pow2(3) == 4
|
||||
assert roundup_pow2(5) == 8
|
||||
assert roundup_pow2(6) == 8
|
||||
assert roundup_pow2(7) == 8
|
||||
assert roundup_pow2(9) == 16
|
||||
assert roundup_pow2(15) == 16
|
||||
assert roundup_pow2(17) == 32
|
||||
assert roundup_pow2(31) == 32
|
||||
assert roundup_pow2(33) == 64
|
||||
assert roundup_pow2(100) == 128
|
||||
assert roundup_pow2(255) == 256
|
||||
assert roundup_pow2(257) == 512
|
||||
from peakrdl_busdecoder.utils import get_indexed_path
|
||||
|
||||
|
||||
class TestGetIndexedPath:
|
||||
"""Test get_indexed_path function."""
|
||||
|
||||
def test_simple_path(self, compile_rdl):
|
||||
def test_simple_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test simple path without arrays."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
@@ -111,7 +29,7 @@ class TestGetIndexedPath:
|
||||
path = get_indexed_path(top, reg_node)
|
||||
assert path == "my_reg"
|
||||
|
||||
def test_nested_path(self, compile_rdl):
|
||||
def test_nested_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test nested path without arrays."""
|
||||
rdl_source = """
|
||||
addrmap inner_map {
|
||||
@@ -143,7 +61,7 @@ class TestGetIndexedPath:
|
||||
path = get_indexed_path(top, reg_node)
|
||||
assert path == "inner.my_reg"
|
||||
|
||||
def test_array_path(self, compile_rdl):
|
||||
def test_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test path with array indices."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
@@ -163,7 +81,7 @@ class TestGetIndexedPath:
|
||||
path = get_indexed_path(top, reg_node)
|
||||
assert path == "my_reg[i0]"
|
||||
|
||||
def test_multidimensional_array_path(self, compile_rdl):
|
||||
def test_multidimensional_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test path with multidimensional arrays."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
@@ -183,7 +101,7 @@ class TestGetIndexedPath:
|
||||
path = get_indexed_path(top, reg_node)
|
||||
assert path == "my_reg[i0][i1]"
|
||||
|
||||
def test_nested_array_path(self, compile_rdl):
|
||||
def test_nested_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test path with nested arrays."""
|
||||
rdl_source = """
|
||||
addrmap inner_map {
|
||||
@@ -215,7 +133,7 @@ class TestGetIndexedPath:
|
||||
path = get_indexed_path(top, reg_node)
|
||||
assert path == "inner[i0].my_reg[i1]"
|
||||
|
||||
def test_custom_indexer(self, compile_rdl):
|
||||
def test_custom_indexer(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test path with custom indexer name."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
@@ -235,7 +153,7 @@ class TestGetIndexedPath:
|
||||
path = get_indexed_path(top, reg_node, indexer="idx")
|
||||
assert path == "my_reg[idx0]"
|
||||
|
||||
def test_skip_kw_filter(self, compile_rdl):
|
||||
def test_skip_kw_filter(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
|
||||
"""Test path with keyword filtering skipped."""
|
||||
rdl_source = """
|
||||
addrmap my_addrmap {
|
||||
79
tests/utils/test_math_utils.py
Normal file
79
tests/utils/test_math_utils.py
Normal 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
62
tools/shims/xargs
Executable 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())
|
||||
Reference in New Issue
Block a user