top of page

MicroZed Chronicles: Cocotb and AXI

A few weeks ago, we looked at the python-based cocotb framework. In that blog, we looked at a simple memory scrubber created around a XPM memory. The cocotb test bench accessed the memory using both regular accesses and injecting errors to test the scrubber.

This was a simple test structure and the interfaces where very simple being address, data, control signals as one would expect for a block memory.

As one would expect for a block memory, this was a straightforward test structure with simple address, data, and control signals interfaces.

Of course, one of the benefits of cocotb is the ability to easily work with more complex interfaces such as AXI. Using the bus functional models provided by cocotb, we can focus on the data we wish to read and write to DUT without having to create bus functional models.

If we want to work with a range of bus interfaces using cocotb we need to install the cocotb-bus package which contains support for AMBA (AXI), Avalon, XGMII, and OPB buses. There are also a range of community-created cocotb buses supported by cocotbext including the excellent range of AXI, I2C, PCIe, UART, and Ethernet created by Alex Forencich.

To get started demonstrating how we can use the AXI cocotb BFM, we are going to create a simple design in Vivado which will connect an AXI Bram controller to a BRAM. With this simple module, we can then manipulate contents in the BRAM over an AXI interface using cocotb.

The test bench will enable both the use of single-beat writes and burst interactions.

One of the key things we need to do is to make sure the Xilinx libraries are compiled for the simulator used. In this example, I again will be using ModelSim DE as the simulation engine for the cocotb script.

Within our test bench, the first thing we need to do along with the previous imports from cocotb is import the AXI4 Master. From this package, we can also import AXI4LiteMaster and AXI4 Slave. There are other functions defined which can help us monitor and interface with the AXI transactions such as protocol errors, response decoding, and burst configuration (fixed, incrementing or wrapping).

from cocotb_bus.drivers.amba import AXI4 Master

One of the key things we need to do in the test bench is define the AXI_PREFIX which will depend upon the RTL. For example, Vivado has generated the wrapper files for the IP integrator design using the prefix S_AXI_0 to all signals. Cocotb needs to know what this prefix is to be able to bind to the AXI interface.

S_AXI_0_araddr : in STD_LOGIC_VECTOR ( 11 downto 0 );


This test is going to be simple showing how we can perform a simple single-access write and read along with a burst write and read. Should the values written not agree with the values read back, then a test error will be raised. We need to import the TestFeature package to be able to raise test errors.

from cocotb.result import TestFailure

Just like with the previous cocotb file, I created a reset function and then a simple cocotb test which started the clock, bound the AXI Master to the DUT, and then performed a reset.

async def run_test(dut):
    global clk 
    cocotb.start_soon(Clock(dut.s_axi_aclk_0, PERIOD, units="ns").start())
    axim = AXI4Master(dut, AXI_PREFIX, dut.s_axi_aclk_0)  
    clk = dut.s_axi_aclk_0 

    await reset_dut(dut.s_axi_aresetn_0, 50)
    dut._log.debug("After reset")
    await Timer(20*PERIOD, units='ns')

Once this was completed, a single beat write read test was performed using the write function of the AXI4 Master class. To do this, I defined the address I wanted to write to and the data to be written. I used a random function to generate the data to be written.

    dut._log.debug("single beat")
    address = 0
    write_value = randrange(0, 2**32)
    await axim.write(address, write_value)

The read back of data is very similar and uses the read function and is provided the address only. The results provided from the read function are provided in an array. As a result, the read data will be in the first element.

We can then do a simple comparison between the read and write values. If they do not agree, I raise a test failure and report the values showing the difference.

    read_value = await
    read_value = read_value[0]
    if read_value != write_value:
        raise TestFailure("Read {:#x} from {:#x} but was expecting {:#x} ".format(read_value.integer, address, write_value))
    await Timer(20*PERIOD, units='ns')

Performing the burst access is also very straight forward. I use the random function to determine the size of the burst between 2 and 256, set the address to the target address and generate an array of write data values randomly.

    burst_length = (randrange(2, 256) )
    address = 0
    write_values =[randrange(0, 2**32) for i in range(burst_length)]
    await axim.write(address, write_values, size=4) #size is number of bytes

To read back and verify the write, we can use a similar approach as before to raise the error if the read and write values do not agree. We can use the length of the write values to define the burst access size required.

    read_values = await, len(write_values), size=4)
    for i in range(len(read_values)):
        if write_values[i] != read_values[i]:
            raise TestFailure("Read {:#x} from {:#x} but was expecting {:#x} " \
.format(read_value[i], address, write_value[i]))

This simple introduction to the cocotb-bus shows just how easily we can work with AXI interfaces on our designs. We can also use other interfaces such as I2C and UART etc. to ease verification in order to focus on testing the DUT and its corner cases to ensure it performs as expected.



bottom of page