Files
PeakRDL-BusDecoder/tests/cocotb/testbenches/test_apb4_decoder.py
Copilot 4dc61d24ca Add cocotb testbench for validating generated bus decoder RTL across APB3, APB4, and AXI4-Lite interfaces (#9)
* Initial plan

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

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

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

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

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

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

* Add implementation summary document

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

* Merge cocotb dependencies into test group

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

* Add optional cocotb simulation workflow with Icarus Verilog

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-23 23:46:51 -07:00

265 lines
7.8 KiB
Python

"""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!")