top of page

MicroZed Chronicles: Cocotb, AXI Streams and AXI-Lite Slaves

On my journey exploring how Cocotb works on a range of interfaces, this week I am going to look at how we can work with AXI Lite and AXI-Stream interfaces. For these interfaces, I decided to us the excellent Cocotb extensions provided by Alex Forencich. Alex has provided cocotb extensions ranging from AXI to I2C, Ethernet, and PCIe.

Internally, we have also been working on a range of VHDL procedures which will implement the AXI interface transactions when called upon. These are part of a larger internal project around design architecting. As such, I thought it would be good to try and test these interfaces with the Cocotb framework.

These AXI procedures focus on the use of records to group the signals and procedures that perform the AXI Lite or AXI access. Our aim was to be able to perform AXI accesses as close to a function call in our designs as possible. Of course, there are several libraries and packages behind the procedure call but such an approach should aid code readability and also accelerate development.

For example, the following procedure would be called to perform an AXI-Lite write and the address and data are provided as signals, the MOSI and MISO are the AXI input and output records, and the signal data_state stores the current state of the AXI interaction.

cl_axi_common.axil_pkg.m_tx_data(s_address, s_write_buffer, o_m_axil_mosi, i_m_axil_miso, s_axi4_wdata_state);  

To get started testing these procedures, I first installed the cocotb AXI extensions using the command

pip install cocotbext-axi

My test application received a read or write command over an AXI Stream link and then performed an AXI-Lite read or write as indicated by the AXI Stream command.

This means the Device Under Test (DUT) has the following interfaces:

• AXI-Lite Master – Access memory map

• AXI-Stream Slave – Receive commands

• AXI-Stream Master – Provide read responses

The protocol over AXI Stream is a simple one-byte header which indicates read or write along with a four-byte address, one byte length (in this case for AXI Lite always 1) and four bytes of data.

Ideally in my test bench, I want to drive the AXI-Stream slave, ensure the correct response from the AXI-Stream master, and connect the AXI-Lite master interface to a memory so that it can be used to read and write the data.

We can create the AXI-Stream sink and source using the following:

    axis_source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axi_s"), dut.i_clk, dut.i_aresetn)

    axis_sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axi_s"), dut.i_clk, dut.i_aresetn)

Note we need to include the packages.

from cocotbext.axi import AxiStreamSource, AxiStreamBus, AxiStreamSink

We need to provide the AXI signal prefix along with the clocks and resets to ensure cocotb can bind.

For the AXI Source, we can create a simple array which contains the data to be transferred over the stream.

Data = [0x09,0x00, 0x00, 0x00, 0x01,0x01,0x55,0xaa,0x12,0x34]

Transmitting this over the stream is achieved by calling the function method send.

 await axis_source.send(data)

While send is blocking, we might want to wait until the AXI Source is idle in which case we can use the method wait.

 await axis_source.wait()

If my DUT is working correctly, sending this frame should result in a write on the AXI-Lite bus. To ensure the AXI-Lite bus acts as we want, we are going to map it in cocotb to a RAM element. We can do this by binding the AXI-Lite master port to a RAM, and thanks to cocotb AXI extensions, we are able to bind the AXI-Lite master directly to an AXI-Lite RAM. All we need to do is provide the prefix, clock and reset along with the memory size.

AxiLiteRam(AxiLiteBus.from_prefix(dut, "m_axil"), dut.i_clk, dut.i_aresetn, size=2**16)

To use the AXI-Lite RAM we also need to import the following packages:

AxiLiteBus, AxiLiteRam

The complete cocotb test bench is below.

async def run_test(dut):
    PERIOD = 10
    global clk 
    crc_algorithm ='crc32-bzip2')
    cocotb.start_soon(Clock(dut.i_clk, PERIOD, units="ns").start())
    await reset_dut(dut.i_aresetn, 50)
    dut._log.debug("After reset")
    await Timer(20*PERIOD, units='ns')
    axis_source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axi_s"), dut.i_clk, dut.i_aresetn)
    axis_sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axi_s"), dut.i_clk, dut.i_aresetn)
    axi_master = AxiLiteRam(AxiLiteBus.from_prefix(dut, "m_axil"), dut.i_clk, dut.i_aresetn, size=2**16)
    data = [0x09,0x00, 0x00, 0x00, 0x01,0x01,0x55,0xaa,0x12,0x34] 
    await axis_source.send(data)
    await axis_source.wait()
    data = [0x05,0x00, 0x00, 0x00, 0x01,0x01] 
    await axis_source.send(data)
    await axis_source.wait()
    data = await axis_sink.recv()
    await Timer(20*PERIOD, units='ns')

Running this test bench shows the AXI-Stream data is input as we would expect.

I’m really happy with how the module-under-test worked, but also how easy cocotb makes working with AXI Lite and AXI Stream. We can use a similar approach to the AXI-Lite master with full AXI.

1 comment

1 Comment

Hi Adam, have you looked at performing code coverage with COCOTB? I'm guessing this may be more down to the simulator you use rather than COCOTB.

bottom of page