MicroZed Chronicles: Verifying AXI Peripherals


The designs we implement in Vivado often use AXI interfaces. These might be AXI Lite for configuration and control, AXI Memory Mapped for high-speed memory mapped transfer, or AXI Stream for high-bandwidth streams.

These interfaces can be complex to verify, ensuring we get the protocol implemented correctly for bus transactions can present a challenge on its own. To help verify these interfaces in simulation, Vivado provides us with the AXI verification IP. This IP can be deployed in the following three different configurations:


  1. Generating AXI transactions as the bus master

  2. Responding to AXI transactions as a bus slave

  3. Pass-through in this mode is capable of AXI protocol checking

How the AXI VIP works is pretty complex (full details are described in PG267), however, the AXI VIP can be thought of as two elements:

  1. Static element – This is the element implemented within the Vivado design and contains the actual AXI interface(s).

  2. Dynamic element – Used in our test bench, this element contains the agent and the user environment. The agent, which is provided by the AXI VIP package, provides the virtual interface to the static element along with read and write drivers. The user environment is where we define the user accesses we wish to perform over the bus.

Before I progress too far in this blog, I should point out that we will be using Verilog and SystemVerilog to implement these functionalities. We have significantly reduced capabilities if we use a VHDL design.

To get started with this example, we are going to create a new project which contains the following on a block diagram:


  1. Simulation Clock Generation – Will provide the clock and reset

  2. AXI VIP IP – Configured as a master for AXI Lite operation

  3. AXI Block Memory Controller

  4. Block RAM

This will allow us to drive the test bench to read and write data from the Block RAM over the AXI bus. This will allow the test bench example to show the basics of the how we create and work with the agent to perform read and writes.

It is within the test bench that we create the agent and configure it to drive the AXI network as we desire. To get started, we need to create two files. The first is a SystemVerilog header file which contains all of the AXI transactions we wish to implement, and the second is the actual test bench. The test bench is also a SystemVerilog file which instantiates the unit under test and calls up the SystemVerilog header file to implement the transactions.

To get started with the header file, we need to include two packages. The first is the standard AXI VIP package, and the second is a specific package named after the instantiation of the component in the design. This name also includes the hierarchy of the block diagram as well as our included declarations.

import axi_vip_pkg::*;

import design_1_axi_vip_0_0_pkg::*;

The next step is to define the agent type. This can be one of five agents, master, slave or pass-through. There are two options for the slave and pass-through with or without memory model.

For our master example, we are going to use the master agent. We declare the agent using the name of the VIP instantiation and add on the type of agent we want. For example <AXI_VIP_name>_mst_t

design_1_axi_vip_0_0_mst_t agent;

The next step is to declare all of the signals needed in the test bench. This includes the read and write transaction types and information regarding the AXI transactions (e.g. read and write addresses).

The meat of our design then is to create the new agent and start the newly created agent.

agent = new("master vip agent",DUT.design_1_i.axi_vip_0.inst.IF);

agent.start_master();

We then perform a write to the Block RAM using the AXI VIP. To do this, we make use of the agent and create a write transaction which we can configure with command and data before performing the transaction.

mtestWID = $urandom_range(0,(1<<(0)-1));

mtestWADDR = 64'h4;

mtestWBurstLength = 0;

mtestWDataSize = xil_axi_size_t'(xil_clog2((32)/8));

mtestWBurstType = XIL_AXI_BURST_TYPE_INCR;

mtestWData = 32'h55aa55aa;

wr_trans = agent.wr_driver.create_transaction("write transaction");

wr_trans.set_write_cmd(mtestWADDR,mtestWBurstType,mtestWID,

mtestWBurstLength,mtestWDataSize);

wr_trans.set_data_block(mtestWData);

agent.wr_driver.send(wr_trans);

Performing a read is similar. Again, the agent is used to create a new read transaction.

mtestRID = $urandom_range(0,(1<<(0)-1));

mtestRADDR = 64'h0;

mtestRBurstLength = 0;

mtestRDataSize = xil_axi_size_t'(xil_clog2((32)/8));

mtestRBurstType = XIL_AXI_BURST_TYPE_INCR;

rd_trans = agent.rd_driver.create_transaction("read transaction");

rd_trans.set_read_cmd(mtestRADDR,mtestRBurstType,mtestRID,

mtestRBurstLength,mtestRDataSize);

agent.rd_driver.send(rd_trans);

Of course, in a more complex test bench, we would make the read and write elements as tasks such that they could be called and used as needed in the test bench.

The final stage is to create the test bench file which maps in the Device Under Test and calls up the module declared in the header file to execute the AXI transactions.

Once both these files have been created, we can run the simulation, adding in the DUT and AXI VIP to the wave window. You can see the AXI write and read operations below.



Now that we understand how we drive the AXI VIP in our Vivado projects, we can create more complex examples.

If you want to check out the more complex solutions using the AXI VIP, I recommend looking at the example design. This can be accessed by right clicking on the AXI VIP element in the design tab and selecting Open IP Example Design.



Hopefully, we now understand a little more about how we can verify our IP blocks which use AXI. I have uploaded this project to GitHub if you want to explore it more.