Add cocotb testbench for validating generated bus decoder RTL across APB3, APB4, and AXI4-Lite interfaces (#9)
* Initial plan * Add cocotb test infrastructure and testbenches for APB3, APB4, and AXI4-Lite Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Add integration tests, examples, and documentation for cocotb testbenches Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Address code review feedback: use relative imports and update installation docs Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Add implementation summary document Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Merge cocotb dependencies into test group Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Add optional cocotb simulation workflow with Icarus Verilog Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
This commit is contained in:
5
tests/cocotb/common/__init__.py
Normal file
5
tests/cocotb/common/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Common cocotb utilities package."""
|
||||
|
||||
from .utils import compile_rdl_and_export, get_verilog_sources
|
||||
|
||||
__all__ = ["compile_rdl_and_export", "get_verilog_sources"]
|
||||
123
tests/cocotb/common/apb4_master.py
Normal file
123
tests/cocotb/common/apb4_master.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""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
|
||||
80
tests/cocotb/common/utils.py
Normal file
80
tests/cocotb/common/utils.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""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.exporter import BusDecoderExporter
|
||||
|
||||
|
||||
def compile_rdl_and_export(
|
||||
rdl_source: str, top_name: str, output_dir: str, cpuif_cls: Any, **kwargs: Any
|
||||
) -> tuple[Path, Path]:
|
||||
"""
|
||||
Compile RDL source and export to SystemVerilog.
|
||||
|
||||
Args:
|
||||
rdl_source: SystemRDL source code as a string
|
||||
top_name: Name of the top-level addrmap
|
||||
output_dir: Directory to write generated files
|
||||
cpuif_cls: CPU interface class to use
|
||||
**kwargs: Additional arguments to pass to exporter
|
||||
|
||||
Returns:
|
||||
Tuple of (module_path, package_path) for generated files
|
||||
"""
|
||||
# Compile RDL source
|
||||
compiler = RDLCompiler()
|
||||
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# Return paths to generated files
|
||||
module_name = kwargs.get("module_name", top_name)
|
||||
package_name = kwargs.get("package_name", f"{top_name}_pkg")
|
||||
|
||||
module_path = Path(output_dir) / f"{module_name}.sv"
|
||||
package_path = Path(output_dir) / f"{package_name}.sv"
|
||||
|
||||
return module_path, package_path
|
||||
|
||||
|
||||
def get_verilog_sources(module_path: Path, package_path: Path, intf_files: list[Path]) -> list[str]:
|
||||
"""
|
||||
Get list of Verilog source files needed for simulation.
|
||||
|
||||
Args:
|
||||
module_path: Path to the generated module file
|
||||
package_path: Path to the generated package file
|
||||
intf_files: List of paths to interface definition files
|
||||
|
||||
Returns:
|
||||
List of source file paths as strings
|
||||
"""
|
||||
sources = []
|
||||
# Add interface files first
|
||||
sources.extend([str(f) for f in intf_files])
|
||||
# Add package file
|
||||
sources.append(str(package_path))
|
||||
# Add module file
|
||||
sources.append(str(module_path))
|
||||
return sources
|
||||
Reference in New Issue
Block a user