alexforencich / cocotbext-axi

AXI interface modules for Cocotb
MIT License
209 stars 68 forks source link

AXI interface modules for Cocotb

Build Status codecov PyPI version Downloads

GitHub repository: https://github.com/alexforencich/cocotbext-axi

Introduction

AXI, AXI lite, and AXI stream simulation models for cocotb.

Installation

Installation from pip (release version, stable):

$ pip install cocotbext-axi

Installation from git (latest development version, potentially unstable):

$ pip install https://github.com/alexforencich/cocotbext-axi/archive/master.zip

Installation for active development:

$ git clone https://github.com/alexforencich/cocotbext-axi
$ pip install -e cocotbext-axi

Documentation and usage examples

See the tests directory, verilog-axi, and verilog-axis for complete testbenches using these modules.

AXI and AXI lite master

The AxiMaster and AxiLiteMaster classes implement AXI masters and are capable of generating read and write operations against AXI slaves. Requested operations will be split and aligned according to the AXI specification. The AxiMaster module is capable of generating narrow bursts, handling multiple in-flight operations, and handling reordering and interleaving in responses across different transaction IDs. AxiMaster and AxiLiteMaster and related objects all extend Region, so they can be attached to AddressSpace objects to handle memory operations in the specified region.

The AxiMaster is a wrapper around AxiMasterWrite and AxiMasterRead. Similarly, AxiLiteMaster is a wrapper around AxiLiteMasterWrite and AxiLiteMasterRead. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same.

To use these modules, import the one you need and connect it to the DUT:

from cocotbext.axi import AxiBus, AxiMaster

axi_master = AxiMaster(AxiBus.from_prefix(dut, "s_axi"), dut.clk, dut.rst)

The first argument to the constructor accepts an AxiBus or AxiLiteBus object, as appropriate. These objects are containers for the interface signals and include class methods to automate connections.

Once the module is instantiated, read and write operations can be initiated in a couple of different ways.

First, operations can be carried out with async blocking read(), write(), and their associated word-access wrappers. Multiple concurrent operations started from different coroutines are handled correctly, with results returned in the order that the operations complete. For example:

await axi_master.write(0x0000, b'test')
data = await axi_master.read(0x0000, 4)

Additional parameters can be specified to control sideband signals and burst settings. The transfer will be split into one or more bursts according to the AXI specification. All bursts generated from the same call to read() or write() will use the same ID, which will be automatically generated if not specified. read() and write() return namedtuple objects containing address, data or length, and resp. This is the preferred style, and this is the only style supported by the word-access wrappers.

Alternatively, operations can be initiated with non-blocking init_read() and init_write(). These functions return Event objects which are triggered when the operation completes, and the result can be retrieved from Event.data. For example:

write_op = axi_master.init_write(0x0000, b'test')
await write_op.wait()
resp = write_op.data
read_op = axi_master.init_read(0x0000, 4)
await read_op.wait()
resp = read_op.data

With this method, it is possible to start multiple concurrent operations from the same coroutine. It is also possible to use the events with Combine, First, and with_timeout.

AxiMaster and AxiLiteMaster constructor parameters

Additional parameters for AxiMaster

Methods

Additional optional arguments for AxiMaster

Additional optional arguments for AxiLiteMaster

AxiBus and AxiLiteBus objects

The AxiBus, AxiLiteBus, and related objects are containers for the interface signals. These hold instances of bus objects for the individual channels, which are currently extensions of cocotb_bus.bus.Bus. Class methods from_entity and from_prefix are provided to facilitate signal name matching. For AXI interfaces use AxiBus, AxiReadBus, or AxiWriteBus, as appropriate. For AXI lite interfaces, use AxiLiteBus, AxiLiteReadBus, or AxiLiteWriteBus, as appropriate.

AXI and AXI lite slave

The AxiSlave and AxiLiteSlave classes implement AXI slaves and are capable of completing read and write operations from upstream AXI masters. The AxiSlave module is capable of handling narrow bursts. These modules can either be used to perform memory reads and writes on a MemoryInterface on behalf of the DUT, or they can be extended to implement customized functionality.

The AxiSlave is a wrapper around AxiSlaveWrite and AxiSlaveRead. Similarly, AxiLiteSlave is a wrapper around AxiLiteSlaveWrite and AxiLiteSlaveRead. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same.

To use these modules, import the one you need and connect it to the DUT:

from cocotbext.axi import AxiBus, AxiSlave, MemoryRegion

axi_slave = AxiSlave(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst)
region = MemoryRegion(2**axi_slave.read_if.address_width)
axi_slave.target = region

The first argument to the constructor accepts an AxiBus or AxiLiteBus object. These objects are containers for the interface signals and include class methods to automate connections.

It is also possible to extend these modules; operation can be customized by overriding the internal _read() and _write() methods. See AxiRam and AxiLiteRam for an example.

AxiSlave and AxiLiteSlave constructor parameters

Attributes:

AXI and AXI lite RAM

The AxiRam and AxiLiteRam classes implement AXI RAMs and are capable of completing read and write operations from upstream AXI masters. The AxiRam module is capable of handling narrow bursts. These modules are extensions of the corresponding AxiSlave and AxiLiteSlave modules. Internally, SparseMemory is used to support emulating very large memories.

The AxiRam is a wrapper around AxiRamWrite and AxiRamRead. Similarly, AxiLiteRam is a wrapper around AxiLiteRamWrite and AxiLiteRamRead. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same.

To use these modules, import the one you need and connect it to the DUT:

from cocotbext.axi import AxiBus, AxiRam

axi_ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst, size=2**32)

The first argument to the constructor accepts an AxiBus or AxiLiteBus object. These objects are containers for the interface signals and include class methods to automate connections.

Once the module is instantiated, the memory contents can be accessed in a couple of different ways. First, the mmap object can be accessed directly via the mem attribute. Second, read(), write(), and various word-access wrappers are available. Hex dump helper methods are also provided for debugging. For example:

axi_ram.write(0x0000, b'test')
data = axi_ram.read(0x0000, 4)
axi_ram.hexdump(0x0000, 4, prefix="RAM")

Multi-port memories can be constructed by passing the mem object of the first instance to the other instances. For example, here is how to create a four-port RAM:

axi_ram_p1 = AxiRam(AxiBus.from_prefix(dut, "m00_axi"), dut.clk, dut.rst, size=2**32)
axi_ram_p2 = AxiRam(AxiBus.from_prefix(dut, "m01_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)
axi_ram_p3 = AxiRam(AxiBus.from_prefix(dut, "m02_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)
axi_ram_p4 = AxiRam(AxiBus.from_prefix(dut, "m03_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)

AxiRam and AxiLiteRam constructor parameters

Attributes:

Methods

AXI stream

The AxiStreamSource, AxiStreamSink, and AxiStreamMonitor classes can be used to drive, receive, and monitor traffic on AXI stream interfaces. The AxiStreamSource drives all signals except for tready and can be used to drive AXI stream traffic into a design. The AxiStreamSink drives the tready line only and as such can receive AXI stream traffic and exert backpressure. The AxiStreamMonitor drives no signals and as such can be connected to AXI stream interfaces anywhere within a design to passively monitor traffic.

To use these modules, import the one you need and connect it to the DUT:

from cocotbext.axi import (AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamMonitor)

axis_source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axis"), dut.clk, dut.rst)
axis_sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axis"), dut.clk, dut.rst)
axis_mon= AxiStreamMonitor(AxiStreamBus.from_prefix(dut.inst, "int_axis"), dut.clk, dut.rst)

The first argument to the constructor accepts an AxiStreamBus object. This object is a container for the interface signals and includes class methods to automate connections.

To send data into a design with an AxiStreamSource, call send()/send_nowait() or write()/write_nowait(). Accepted data types are iterables or AxiStreamFrame objects. Optionally, call wait() to wait for the transmit operation to complete. Example:

await axis_source.send(b'test data')
# wait for operation to complete (optional)
await axis_source.wait()

It is also possible to wait for the transmission of a specific frame to complete by passing an event in the tx_complete field of the AxiStreamFrame object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:

frame = AxiStreamFrame(b'test data', tx_complete=Event())
await axis_source.send(frame)
await frame.tx_complete.wait()
print(frame.tx_complete.data.sim_time_start)

To receive data with an AxiStreamSink or AxiStreamMonitor, call recv()/recv_nowait() or read()/read_nowait(). Optionally call wait() to wait for new receive data. recv() is intended for use with a frame-oriented interface, and by default compacts AxiStreamFrames before returning them. read() is intended for non-frame-oriented streams. Calling read() internally calls recv() for all frames currently in the queue, then compacts and coalesces tdata from all frames into a separate read queue, from which read data is returned. All sideband data is discarded.

data = await axis_sink.recv()

Signals

Constructor parameters:

Note: _bytesize, _bytelanes, len(tdata), and len(tkeep) are all related, in that _bytelanes is set from tkeep if it is connected, and byte_size*byte_lanes == len(tdata). So, if tkeep is connected, both _bytesize and _bytelanes will be computed internally and cannot be overridden. If tkeep is not connected, then either _bytesize or _bytelanes can be specified, and the other will be computed such that byte_size*byte_lanes == len(tdata).

Attributes:

Methods

AxiStreamBus object

The AxiStreamBus object is a container for the interface signals. Currently, it is an extension of cocotb.bus.Bus. Class methods from_entity and from_prefix are provided to facilitate signal name matching.

AxiStreamFrame object

The AxiStreamFrame object is a container for a frame to be transferred via AXI stream. The tdata field contains the packet data in the form of a list of bytes, which is either a bytearray if the byte size is 8 bits or a list of ints otherwise. tkeep, tid, tdest, and tuser can either be None, an int, or a list of ints.

Attributes:

Methods:

Address space abstraction

The address space abstraction provides a framework for cross-connecting multiple memory-mapped interfaces for testing components that interface with complex systems, including components with DMA engines.

MemoryInterface is the base class for all components in the address space abstraction. MemoryInterface provides the core read() and write() methods, which implement bounds checking, as well as word-access wrappers. Methods for creating Window and WindowPool objects are also provided. The function get_absolute_address() translates addresses to the system address space. MemoryInterface can be extended to implement custom functionality by overriding _read() and _write().

Window objects represent views onto a parent address space with some length and offset. read() and write() operations on a Window are translated to the equivalent operations on the parent address space. Multiple Window instances can overlap and access the same portion of address space.

WindowPool provides a method for dynamically allocating windows from a section of address space. It uses a standard memory management algorithm to provide naturally-aligned Window objects of the requested size.

Region is the base class for all components which implement a portion of address space. Region objects can be registered with AddressSpace objects to handle read() and write() operations in a specified region. Region can be extended by components that implement a portion of address space.

MemoryRegion is an extension of Region that uses an mmap instance to handle memory operations. MemoryRegion also provides hex dump methods as well as indexing and slicing.

SparseMemoryRegion is similar to MemoryRegion but is backed by SparseMemory instead of mmap and as such can emulate extremely large regions of address space.

PeripheralRegion is an extension of Region that can wrap another object that implements read() and write(), as an alternative to extending Region.

AddressSpace is the core object for handling address spaces. Region objects can be registered with AddressSpace with specified base address, size, and offset. The AddressSpace object will then direct read() and write() operations to the appropriate Regions, splitting requests appropriately when necessary and translating addresses. Regions registered with offset other than None are translated such that accesses to base address + N map to N + offset. Regions registered with an offset of None are not translated. Region objects registered with the same AddressSpace cannot overlap, however the same Region can be registered multiple times. AddressSpace also provides a method for creating Pool objects.

Pool is an extension of AddressSpace that supports dynamic allocation of MemoryRegions. It uses a standard memory management algorithm to provide naturally-aligned MemoryRegion objects of the requested size.

Example

This is a simple example that shows how the address space abstraction components can be used to connect a DUT to a simulated host system, including simulated RAM, an AXI interface from the DUT for DMA, and an AXI lite interface to the DUT for control.

from cocotbext.axi import AddressSpace, SparseMemoryRegion
from cocotbext.axi import AxiBus, AxiLiteMaster, AxiSlave

# system address space
address_space = AddressSpace(2**32)

# RAM
ram = SparseMemoryRegion(2**24)
address_space.register_region(ram, 0x0000_0000)
ram_pool = address_space.create_window_pool(0x0000_0000, 2**20)

# DUT control register interface
axil_master = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "s_axil_ctrl"), dut.clk, dut.rst)
address_space.register_region(axil_master, 0x8000_0000)
ctrl_regs = address_space.create_window(0x8000_0000, axil_master.size)

# DMA from DUT
axi_slave = AxiSlave(AxiBus.from_prefix(dut, "m_axi_dma"), dut.clk, dut.rst, target=address_space)

# exercise DUT DMA functionality
src_block = ram_pool.alloc_window(1024)
dst_block = ram_pool.alloc_window(1024)

test_data = b'test data'
await src_block.write(0, test_data)

await ctrl_regs.write_dword(DMA_SRC_ADDR, src_block.get_absolute_address(0))
await ctrl_regs.write_dword(DMA_DST_ADDR, dst_block.get_absolute_address(0))
await ctrl_regs.write_dword(DMA_LEN, len(test_data))
await ctrl_regs.write_dword(DMA_CONTROL, 1)

while await ctrl_regs.read_dword(DMA_STATUS) == 0:
    pass

assert await dst_block.read(0, len(test_data)) == test_data

AXI signals

AXI lite signals

AXI stream signals