How to create MAXI interface with address offset = slave

Advanced extensible Interface (AXI) is an on-chip bus communication protocol developed by ARM.

This blog post is about creating a custom IP with one MAXI (Master-AXI) port. One MAXI port can communicate with one or multiple Slave-AXI ports of other IPs. Generally on FPGAs, it is used to connect to memory such as DDR3/DDR4 or HBM memory supported by Xilinx Ultrascale + families.

Here are the steps I will follow:

  • Create an IP in C++ language by using Xilinx Vitis HLS 2022.1 version.
  • Then, export the IP and make a project in Vivado 2022.1 for Zybo z720 FPGA development board from Digilent.
  • Write the SDK code
  • Dump bitstream on hardware and see results on ILA

The code below does the following steps:

  1. Reads 32 packets of data from memory, where each packet is 32- bit using the “memcpy” function and then store it in a local_buff array.
  2. Update the contents of local buffer by adding +100
  3. Write 32 packets of data to memory, where each packet is 32-bit using “memcpy” function

#include <stdio.h>
#include <string.h>

void test_maxi(volatile int *MAXI_BUS){
#pragma HLS INTERFACE s_axilite port=return bundle=BUS_A
#pragma HLS INTERFACE mode=m_axi depth=32 port=MAXI_BUS offset=slave

  int local_buff[32];

  // this function will create read transaction on MAXI BUS
  memcpy(local_buff,(const int*)MAXI_BUS,32*sizeof(int));

  for(unsigned int i=0; i < 32; i++){
    local_buff[i] = local_buff[i] + 100;

// this function will create write transaction on MAXI BUS
  memcpy((int *)MAXI_BUS,local_buff,32*sizeof(int));

Some notes about this code:

  • The function “test_maxi” is the top-function of this IP.
  • Here the “volatile” qualifier is necessary as per Xilinx documentation so it creates proper read/write functionality.
  • “Int” defines the size of data-bus, which is 32-bit. If you want a 64 bit data-bus, use uint64_t.
  • If you want to use a wider data-bus such as 128-bit or 256-bit or any other use ap_int.h header file, which we will cover in future blog posts.

Here two pragmas are used:

Pragmas in general are pre-processor definitions which are evaluated before the code starts to compile.

#pragma HLS INTERFACE s_axilite port=return bundle=BUS_A

#pragma HLS INTERFACE mode=m_axi depth=32 port=MAXI_BUS offset=slave

The first pragma (i.e #pragma HLS INTERFACE s_axilite port=return bundle=BUS_A) is used to create an axilite interface which is used to control and monitor the functionality of the IP. This allows us, if desired, to check if the IP is ready, idle or busy.

Not using this pragma will create an ap_ctrl port which you will have control by some external logic, such as connecting a constant 1 to ap_start, which we generally avoid to maintain consistency of development workflow.

The second pragma (i.e #pragma HLS INTERFACE mode=m_axi depth=32 port=MAXI_BUS offset=slave) is important to create MAXI port on this IP.

Not using this pragma will not create a proper functional MAXI port. Using the offset = slave parameter of this pragma, sets the address to read/write on axilite port.

It supports three options:

  1. Offset = slave
    Creates axilite ports of address read/write
  2. Offset = direct
    Creates 32-bit input port on IP
  3. Offset = off
    Uses internal address, by default starts on zero.

The parameter depth = 32 is optional, however, it is better to have this parameter if you know how much data you want to read.

The function memcpy(local_buff,(const int*)MAXI_BUS,32*sizeof(int)); will create MAXI read transaction, it will read data from memory and store it into local_buff variable. You can replace memcpy function and use the below mentioned code for loop.

The data will be read from the address provided on axilite interface.

for(unsigned int i = 0; i < 32 ; i++){
Local_buff [ i ] = MAXI_BUS [ i ] ;

The next for loop, i.e:

for(unsigned int i=0; i < 32; i++){
   local_buff[i] = local_buff[i] + 100;

Will update the value of local_buff with +100.

The function memcpy((int *)MAXI_BUS,local_buff,32*sizeof(int)); will create MAXI write transaction, it will write data to memory. The data will be written on the address provided on axilite interface. You can replace memcpy function and use the code below mentioned for loop:

for(unsigned int i = 0; i < 32 ; i++){
MAXI_BUS [ i ] = Local_buff [ i ] ;

Next, I will create a test-bench. A test bench is important as it’s an efficient means to QA and check the validity and output of code.

#include <stdio.h>

void test_maxi(volatile int *MAXI_BUS);

int main()
  int i;
  int data_in[32];
  int data_out[32];

  //Put data into data_in
  for(i=0; i < 32; i++){
  data_in[i] = i;

  //Call the hardware function

  //Run a software version of the hardware function to validate results
  for(i=0; i < 32; i++){
  data_out[i] = i + 100;

  //Compare results
  for(i=0; i < 32; i++){
    if(data_out[i] != data_in[i]){
      printf("i = %d data_in = %d B= %d\n",i,data_in[i],data_out[i]);
      printf("ERROR HW and SW results mismatch\n");
      return 1;
  printf("Success HW and SW results match\n");
  return 0;

Once we run through simulation and synthesis, next I will export the IP.

Once this dialogue box appears:

  • Choose 'Export Format' as Vivado IP
  • In 'Output Location' choose your preferred location on your drive
  • In 'Display Name' type in your preferred name


  • Launch Vivado and create a new project with Zybo z720 as target FPGA
  • Add the IP in the IP catalogue

Here’s the TCL script to create a project from scratch.
Below is the block design for reference.

Once the block design is made, then make an HDL wrapper and generate bitstream.After generating bitstream, export design1_wrapper.xsa file to your preferred location, which is used to create Vitis SDK code.

Below is the SDK code:

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "string.h"
#include "xparameters.h"
#include "xtest_maxi.h"
#include "xil_io.h"
#include "xil_cache.h"

#define DDR_BASE_ADDR 0x0000000000000000

XTest_maxi test_maxi_ip;

int main()

    print("test maxi application \n\r");

    uint32_t data_to_ddr = 0;
    uint32_t data_from_ddr = 0;

    // writing data to ddr
    for(unsigned int i = 0; i < 32768; i++){
    Xil_Out32(DDR_BASE_ADDR + (i * 4), data_to_ddr);
    XTest_maxi_Initialize(&test_maxi_ip, XPAR_TEST_MAXI_0_DEVICE_ID);
    XTest_maxi_Set_a(&test_maxi_ip, DDR_BASE_ADDR);
    for(unsigned int i = 0; i < 32768; i++){
    data_from_ddr = Xil_In32(DDR_BASE_ADDR + (i * 4));
    xil_printf("data:%x from address:%x \r\n",data_from_ddr,(DDR_BASE_ADDR + (i * 4)));

    print("Done \r\n");
    return 0;


  • #include "xparameters.h"
    This header file contains the address map of your block design
  • #include "xtest_maxi.h"
    This header file is generated by Vitis_HLS tool which contains all the API function call related to IP.

The SDK code does the following steps:

  1. Write some data to DDR memory using Xil_Out32 function
  2. Start the IP made by Vitis_HLS
  3. Read updated data from memory which our custom IP have modified

The function XTest_maxi_Initialize(&test_maxi_ip, XPAR_TEST_MAXI_0_DEVICE_ID); will initialize the IP. In simple words, this function looks up for this IP related to its device ID.

The function XTest_maxi_Set_a(&test_maxi_ip, DDR_BASE_ADDR); will set the address to read from and then the address to write, which is the same in this case.

For example, if you set the value at 0, then it will read from address 0 and write to address 0. You can change this address if required.

The function XTest_maxi_Start(&test_maxi_ip); will start the IP

Below is the ILA screenshot of data read

Below is the ILA screenshot of data write