From 8818dd7aac83e0dffe0223cad0b0674679c3efa6 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 17 Jun 2026 17:23:23 +0300 Subject: [PATCH] tests: add tb for axi4l_reg_map [WIP, 1 test broken] --- axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py | 164 +++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py diff --git a/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py b/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py new file mode 100644 index 0000000..8830cc7 --- /dev/null +++ b/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py @@ -0,0 +1,164 @@ +# SPDX-License-Identifier: MIT + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotbext.axi import AxiLiteBus, AxiLiteMaster + +OKAY = 0 +SLVERR = 2 + +REG0_CTRL = 0x00 +REG1_STATUS = 0x04 +REG2_CONFIG = 0x08 +REG_INVALID = 0x0C # N_REGS=3, so index 3 is bad + + +def _resp_code(resp): + """Return integer AXI response code from a cocotbext-axi response object.""" + if resp is None: + return OKAY + + value = getattr(resp, "resp", OKAY) + + # common guards + try: + return int(value) + except TypeError: + return int(value.integer) + + +def _read_data(resp): + return int.from_bytes(bytes(resp.data), "little") + + +class TB: + def __init__(self, dut): + self.dut = dut + self.axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "s_axil"), dut.clk, dut.rst) + + async def reset(self): + self.dut.rst.value = 1 + self.dut.busy_i.value = 0 + self.dut.error_code_i.value = 0 + + for _ in range(5): + await RisingEdge(self.dut.clk) + + self.dut.rst.value = 0 + + for _ in range(5): + await RisingEdge(self.dut.clk) + + # shortcuts sorta + async def write32(self, addr, value, expected_resp=OKAY): + resp = await self.axil.write(addr, value.to_bytes(4, "little")) + assert _resp_code(resp) == expected_resp, ( + f"write addr=0x{addr:08x}: expected resp {expected_resp}, got {_resp_code(resp)}" + ) + return resp + + async def read32(self, addr, expected_resp=OKAY): + resp = await self.axil.read(addr, 4) + assert _resp_code(resp) == expected_resp, ( + f"read addr=0x{addr:08x}: expected resp {expected_resp}, got {_resp_code(resp)}" + ) + return _read_data(resp) + + +@cocotb.test() +async def test_reset_and_basic_reads(dut): + """Check reset values and RO status path via reg_i.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + # REG0 reset: W1C sticky bit set. W1S reads as 0, RW reads from reg_o + assert await tb.read32(REG0_CTRL) == 0x00000004 + + # REG2 is a normal RW config register with non-zero reset value + assert await tb.read32(REG2_CONFIG) == 0x12345678 + + # Drive external status and read it through RO bits + dut.busy_i.value = 1 + dut.error_code_i.value = 0x5A + await RisingEdge(dut.clk) + + assert await tb.read32(REG1_STATUS) == 0x00005A01 + + +@cocotb.test() +async def test_rw_and_w1c_bits(dut): + """Check RW bit update and W1C sticky clear behavior.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + # Initial REG0: bit2 W1C is set, bit1 RW is clear, bit0 W1S reads as 0 + assert await tb.read32(REG0_CTRL) == 0x00000004 + + # Set RW bit1 while preserving W1C bit2 + await tb.write32(REG0_CTRL, 0x00000006) + assert await tb.read32(REG0_CTRL) == 0x00000006 + + # Write 1 to W1C bit2, keep RW bit1 set + await tb.write32(REG0_CTRL, 0x00000006) + assert await tb.read32(REG0_CTRL) == 0x00000002 + + # Write zero clears the RW bit because REG0 is a normal word write for RW fields + await tb.write32(REG0_CTRL, 0x00000000) + assert await tb.read32(REG0_CTRL) == 0x00000000 + + # Full RW register write/read + await tb.write32(REG2_CONFIG, 0xDEADBEEF) + assert await tb.read32(REG2_CONFIG) == 0xDEADBEEF + + +@cocotb.test() +async def test_w1s_seen_on_reg_o_probe(dut): + """Check that a W1S write creates a short-lived internal reg_o bit.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + seen = False + + async def monitor_w1s_bit(): + nonlocal seen + for _ in range(20): + await RisingEdge(dut.clk) + if int(dut.reg0_o.value) & 0x1: + seen = True + + mon = cocotb.start_soon(monitor_w1s_bit()) + await tb.write32(REG0_CTRL, 0x00000001) + await mon + + assert seen, "W1S bit was never observed on reg0_o[0]" + assert await tb.read32(REG0_CTRL) == 0x00000004 # W1S reads as 0, W1C reset bit remains set + + +@cocotb.test() +async def test_write_to_ro_returns_slverr(dut): + """Writing a RO bit should return SLVERR and should not change status.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + dut.busy_i.value = 1 + dut.error_code_i.value = 0x33 + await RisingEdge(dut.clk) + + await tb.write32(REG1_STATUS, 0x00000101, expected_resp=SLVERR) + assert await tb.read32(REG1_STATUS) == 0x00003301 + + +@cocotb.test() +async def test_invalid_address_returns_slverr(dut): + """N_REGS=3, so address 0x0C selects index 3 and must fail.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + await tb.write32(REG_INVALID, 0x11223344, expected_resp=SLVERR) + assert await tb.read32(REG_INVALID, expected_resp=SLVERR) == 0x00000000