HOW TO CREATE an AXI4-FULL CUSTOM IP with AXI4-LITE and UART INTERFACES in VIVADO

In this post, I will show how to create a custom IP in Vivado, which has an AXI4-Lite interface, an AXI4-Full interface and a UART interface. Long time ago, when I first met with Zynq and a Microzed SOM, I started learning how to generate a custom AXI4-Lite IP from Mr. Adam Taylor’s Microzed Chronicles blog. Now it is my time to contribute to the digital design community by showing AXI4-Full IP generation and an example code utilizing a UART interface.

I will use Xilinx Vivado 2020.1 version. After opening Vivado, click Tools -> Create and Package New IP:

Select “Create AXI4 Peripheral”:

Fill the naming parts and select IP repository. It is a good practice to create an IP repo folder and save your all IPs to that folder:

In the interfaces window, I added an extra interface to the default. The S00_AXI interface is a slave AXI4-Lite interface with 4 registers:

S01_AXI interface is a slave AXI4-Full interface with 1024 bytes memory:

In the last page, if you select Edit IP, a new temporary Vivado projects will be opened and you can edit the VHDL files handling AXI interfaces, add your own HDL modules and modify the default files. You can also make these things after your creation of IP by selecting Add IP to the repository.

If you want to edit your IP later, you can open IP Catalog in Vivado, find your IP and select “Edit in IP Packager”:

Vivado generates 3 VHDL files:

– uart_axifull_v1_0.vhd

– uart_axifull_v1_0_S00_AXI.vhd

– uart_axifull_v1_0_S01_AXI.vhd

The first file is the top module, which only instantiates other two files inside it and make I/O connections. S00_AXI file is AXI-Lite interface and S01_AXI file AXI-Full interface. I also added uart_tx.vhd file and instantiate it inside the AXI-Full module:

I first show the modifications on the top module. I added 3 more generic parameters, one is for clock frequency and other two are about uart configurations. I also changed *USER_WIDTH generic parameters from 0 to 1. You don’t need to change user width parameters but if you keep them default as 0, the port definitions will be -1 downto 0, which generates warning in Modelsim when simulating the design, so I changed them to 1.

    generic (
        -- Users to add parameters here
        -- MBA START
        c_clkfreq : integer := 100_000_000;
        c_baudrate : integer := 115_200;
        c_stopbit : integer := 2;
        -- MBA END
        -- User parameters ends
        -- Do not modify the parameters beyond this line

        -- Parameters of Axi Slave Bus Interface S00_AXI
        C_S00_AXI_DATA_WIDTH    : integer   := 32;
        C_S00_AXI_ADDR_WIDTH    : integer   := 4;

        -- Parameters of Axi Slave Bus Interface S01_AXI
        C_S01_AXI_ID_WIDTH  : integer   := 1;
        C_S01_AXI_DATA_WIDTH    : integer   := 32;
        C_S01_AXI_ADDR_WIDTH    : integer   := 10;
        C_S01_AXI_AWUSER_WIDTH  : integer   := 1;
        C_S01_AXI_ARUSER_WIDTH  : integer   := 1;
        C_S01_AXI_WUSER_WIDTH   : integer   := 1;
        C_S01_AXI_RUSER_WIDTH   : integer   := 1;
        C_S01_AXI_BUSER_WIDTH   : integer   := 1
    );

I added only 1 port signal which is the uart transmitter tx:

        -- Users to add ports here
        tx_o            : out std_logic;
        -- User ports ends

Vivado generates AXI4-Lite and AXI4-Full ports automatically:

        -- Ports of Axi Slave Bus Interface S00_AXI
        s00_axi_aclk    : in std_logic;
        s00_axi_aresetn : in std_logic;
        s00_axi_awaddr  : in std_logic_vector(C_S00_AXI_ADDR_WIDTH-1 downto 0);
        s00_axi_awprot  : in std_logic_vector(2 downto 0);
        s00_axi_awvalid : in std_logic;
        s00_axi_awready : out std_logic;
        s00_axi_wdata   : in std_logic_vector(C_S00_AXI_DATA_WIDTH-1 downto 0);
        s00_axi_wstrb   : in std_logic_vector((C_S00_AXI_DATA_WIDTH/8)-1 downto 0);
        s00_axi_wvalid  : in std_logic;
        s00_axi_wready  : out std_logic;
        s00_axi_bresp   : out std_logic_vector(1 downto 0);
        s00_axi_bvalid  : out std_logic;
        s00_axi_bready  : in std_logic;
        s00_axi_araddr  : in std_logic_vector(C_S00_AXI_ADDR_WIDTH-1 downto 0);
        s00_axi_arprot  : in std_logic_vector(2 downto 0);
        s00_axi_arvalid : in std_logic;
        s00_axi_arready : out std_logic;
        s00_axi_rdata   : out std_logic_vector(C_S00_AXI_DATA_WIDTH-1 downto 0);
        s00_axi_rresp   : out std_logic_vector(1 downto 0);
        s00_axi_rvalid  : out std_logic;
        s00_axi_rready  : in std_logic;

        -- Ports of Axi Slave Bus Interface S01_AXI
        s01_axi_aclk    : in std_logic;
        s01_axi_aresetn : in std_logic;
        s01_axi_awid    : in std_logic_vector(C_S01_AXI_ID_WIDTH-1 downto 0);
        s01_axi_awaddr  : in std_logic_vector(C_S01_AXI_ADDR_WIDTH-1 downto 0);
        s01_axi_awlen   : in std_logic_vector(7 downto 0);
        s01_axi_awsize  : in std_logic_vector(2 downto 0);
        s01_axi_awburst : in std_logic_vector(1 downto 0);
        s01_axi_awlock  : in std_logic;
        s01_axi_awcache : in std_logic_vector(3 downto 0);
        s01_axi_awprot  : in std_logic_vector(2 downto 0);
        s01_axi_awqos   : in std_logic_vector(3 downto 0);
        s01_axi_awregion    : in std_logic_vector(3 downto 0);
        s01_axi_awuser  : in std_logic_vector(C_S01_AXI_AWUSER_WIDTH-1 downto 0);
        s01_axi_awvalid : in std_logic;
        s01_axi_awready : out std_logic;
        s01_axi_wdata   : in std_logic_vector(C_S01_AXI_DATA_WIDTH-1 downto 0);
        s01_axi_wstrb   : in std_logic_vector((C_S01_AXI_DATA_WIDTH/8)-1 downto 0);
        s01_axi_wlast   : in std_logic;
        s01_axi_wuser   : in std_logic_vector(C_S01_AXI_WUSER_WIDTH-1 downto 0);
        s01_axi_wvalid  : in std_logic;
        s01_axi_wready  : out std_logic;
        s01_axi_bid : out std_logic_vector(C_S01_AXI_ID_WIDTH-1 downto 0);
        s01_axi_bresp   : out std_logic_vector(1 downto 0);
        s01_axi_buser   : out std_logic_vector(C_S01_AXI_BUSER_WIDTH-1 downto 0);
        s01_axi_bvalid  : out std_logic;
        s01_axi_bready  : in std_logic;
        s01_axi_arid    : in std_logic_vector(C_S01_AXI_ID_WIDTH-1 downto 0);
        s01_axi_araddr  : in std_logic_vector(C_S01_AXI_ADDR_WIDTH-1 downto 0);
        s01_axi_arlen   : in std_logic_vector(7 downto 0);
        s01_axi_arsize  : in std_logic_vector(2 downto 0);
        s01_axi_arburst : in std_logic_vector(1 downto 0);
        s01_axi_arlock  : in std_logic;
        s01_axi_arcache : in std_logic_vector(3 downto 0);
        s01_axi_arprot  : in std_logic_vector(2 downto 0);
        s01_axi_arqos   : in std_logic_vector(3 downto 0);
        s01_axi_arregion    : in std_logic_vector(3 downto 0);
        s01_axi_aruser  : in std_logic_vector(C_S01_AXI_ARUSER_WIDTH-1 downto 0);
        s01_axi_arvalid : in std_logic;
        s01_axi_arready : out std_logic;
        s01_axi_rid : out std_logic_vector(C_S01_AXI_ID_WIDTH-1 downto 0);
        s01_axi_rdata   : out std_logic_vector(C_S01_AXI_DATA_WIDTH-1 downto 0);
        s01_axi_rresp   : out std_logic_vector(1 downto 0);
        s01_axi_rlast   : out std_logic;
        s01_axi_ruser   : out std_logic_vector(C_S01_AXI_RUSER_WIDTH-1 downto 0);
        s01_axi_rvalid  : out std_logic;
        s01_axi_rready  : in std_logic

I created 2 signals in top module, data_length is the number of bytes to be transmitted through UART interface and sent_trig will inform AXI-Full module to start sending bytes:

    -- MBA START
    signal data_length  : std_logic_vector (9 downto 0) := (others => '0');
    signal sent_trig    : std_logic := '0';
    -- MBA END

These two signals will be output ports of AXI4-Lite module and input ports of AXI4-Full module:

    component uart_axifull_v1_0_S00_AXI is
        generic (
        C_S_AXI_DATA_WIDTH  : integer   := 32;
        C_S_AXI_ADDR_WIDTH  : integer   := 4
        );
        port (
        -- MBA START
        data_length_o   : out std_logic_vector (9 downto 0);
        sent_trig_o     : out std_logic;
        -- MBA END
        S_AXI_ACLK  : in std_logic;

    component uart_axifull_v1_0_S01_AXI is
        generic (
        -- MBA START
        c_clkfreq : integer := 100_000_000;
        c_baudrate : integer := 115_200;
        c_stopbit : integer := 2;
        -- MBA END          
        C_S_AXI_ID_WIDTH    : integer   := 1;
        C_S_AXI_DATA_WIDTH  : integer   := 32;
        C_S_AXI_ADDR_WIDTH  : integer   := 10;
        C_S_AXI_AWUSER_WIDTH    : integer   := 0;
        C_S_AXI_ARUSER_WIDTH    : integer   := 0;
        C_S_AXI_WUSER_WIDTH : integer   := 0;
        C_S_AXI_RUSER_WIDTH : integer   := 0;
        C_S_AXI_BUSER_WIDTH : integer   := 0
        );
        port (
        -- MBA START
        data_length_i   : in std_logic_vector (9 downto 0);
        sent_trig_i     : in std_logic;
        tx_o            : out std_logic;
        -- MBA END          
        S_AXI_ACLK  : in std_logic;

Instantiation of these modules is not so difficult:

uart_axifull_v1_0_S00_AXI_inst : uart_axifull_v1_0_S00_AXI
    generic map (
        C_S_AXI_DATA_WIDTH  => C_S00_AXI_DATA_WIDTH,
        C_S_AXI_ADDR_WIDTH  => C_S00_AXI_ADDR_WIDTH
    )
    port map (
        -- MBA START
        data_length_o   => data_length  ,
        sent_trig_o     => sent_trig        ,
        -- MBA END      
        S_AXI_ACLK  => s00_axi_aclk,

uart_axifull_v1_0_S01_AXI_inst : uart_axifull_v1_0_S01_AXI
    generic map (
        -- MBA START
        c_clkfreq  => c_clkfreq ,
        c_baudrate => c_baudrate,
        c_stopbit  => c_stopbit ,
        -- MBA END      
        C_S_AXI_ID_WIDTH    => C_S01_AXI_ID_WIDTH,
        C_S_AXI_DATA_WIDTH  => C_S01_AXI_DATA_WIDTH,
        C_S_AXI_ADDR_WIDTH  => C_S01_AXI_ADDR_WIDTH,
        C_S_AXI_AWUSER_WIDTH    => C_S01_AXI_AWUSER_WIDTH,
        C_S_AXI_ARUSER_WIDTH    => C_S01_AXI_ARUSER_WIDTH,
        C_S_AXI_WUSER_WIDTH => C_S01_AXI_WUSER_WIDTH,
        C_S_AXI_RUSER_WIDTH => C_S01_AXI_RUSER_WIDTH,
        C_S_AXI_BUSER_WIDTH => C_S01_AXI_BUSER_WIDTH
    )
    port map (
        -- MBA START
        data_length_i   => data_length, 
        sent_trig_i     => sent_trig,       
        tx_o            => tx_o,            
        -- MBA END      
        S_AXI_ACLK  => s01_axi_aclk,

So we finished modifying top module (uart_axifull_v1_0.vhd). Now it is time to modify AXI4-Lite module file. In Vivado’s default template, slave registers are defined as signals:

    ------------------------------------------------
    ---- Signals for user logic register space example
    --------------------------------------------------
    ---- Number of Slave Registers 4
    signal slv_reg0 :std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);
    signal slv_reg1 :std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);
    signal slv_reg2 :std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);
    signal slv_reg3 :std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);

I will use slv_reg0 for data_length and slv_reg1 to check is PS wants to sent data. In my scenario, PS needs to write 0x000000BA to slv_reg1 to initiate the uart transmission of bytes inside the AXI4-Full interface. So I created a signal to check is slv_reg1 changed from 0x00000000 to 0x000000BA:

    -- MBA START
    signal slv_reg1_prev : std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0) := (others => '0');
    -- MBA END

I did not change anything about AXI4 protocol processes of Vivado default file. I only added a process block for registering data_length value and checking slv_reg1:

    -- Add user logic here
    -- MBA START
    process (S_AXI_ACLK) begin
    if (rising_edge(S_AXI_ACLK)) then

        slv_reg1_prev   <= slv_reg1;
        sent_trig_o     <= '0';

        if (slv_reg1_prev = x"00000000" and slv_reg1 = x"000000BA") then
            data_length_o   <= slv_reg0(9 downto 0);
            sent_trig_o     <= '1';
        end if;

    end if;
    end process;
    -- MBA END
    -- User logic ends

So that’s the end of modifications of AXI4-Lite interface module. Easy huh! Now let’s finish the design by modifying AXI4-Full interface module. I will give more details about AXI4-Full module as my intention in this post is to give information about AXI4-Full implementation in VHDL.

Reading, understanding and modifying Vivado generated AXI4-Lite module is not a very difficult job as the AXI4 protocol is handled by default processes and user can add functionality through registers. However, AXI4-Full module needs some attention as the data transfer is not through registers but block RAM. Therefore, the user needs to read & write to block RAM.

Vivado generated default AXI4-Full module handles block RAM interface through mem_address and mem_select signals. It is a single port BRAM. I will change this BRAM interface to simple dual port BRAM. So I added my own signals to access BRAM and read data:

    ------------------------------------------------
    ---- Signals for user logic memory space example
    --------------------------------------------------
    signal mem_address : std_logic_vector(OPT_MEM_ADDR_BITS downto 0);
    -- MBA START
    signal mba_mem_address : std_logic_vector(OPT_MEM_ADDR_BITS downto 0);
    -- MBA END
    signal mem_select : std_logic_vector(USER_NUM_MEM-1 downto 0);
    type word_array is array (0 to USER_NUM_MEM-1) of std_logic_vector(C_S_AXI_DATA_WIDTH-1 downto 0);
    signal mem_data_out : word_array;
    -- MBA START
    signal mba_mem_data_out : word_array;
    -- MBA END

I added signals and component for uart transmission:

    -- MBA START
    signal din          : std_logic_vector (7 downto 0) := (others => '0');
    signal tx_start     : std_logic := '0';
    signal tx_done_tick : std_logic := '0';
    signal sending      : std_logic := '0';
    signal cntr         : std_logic_vector (9 downto 0) := (others => '0');

    component uart_tx is
        generic (
        c_clkfreq       : integer := 100_000_000;
        c_baudrate      : integer := 115_200;
        c_stopbit       : integer := 2
        );
        port (
        clk             : in std_logic;
        din_i           : in std_logic_vector (7 downto 0);
        tx_start_i      : in std_logic;
        tx_o            : out std_logic;
        tx_done_tick_o  : out std_logic
        );
    end component;
    
    -- MBA END

There are 7 process blocks to handle AXI4-Full protocol in Vivado generated default file. You don’t need to modify these blocks. After these 7 process blocks, the module use if-generate and for-generate blocks to create a block RAM and access this RAM. We need to modify this BRAM generation part to add a second port.

The if-generate block assign mem_address signal to address value according to AXI4 transaction situation, I mean if there is a write or read transaction, then the mem_address takes value according to these operations:

    -- ------------------------------------------
    -- -- Example code to access user logic memory region
    -- ------------------------------------------

    gen_mem_sel: if (USER_NUM_MEM >= 1) generate
    begin
      mem_select  <= "1";
      mem_address <= axi_araddr(ADDR_LSB+OPT_MEM_ADDR_BITS downto ADDR_LSB) when axi_arv_arr_flag = '1' else
                     axi_awaddr(ADDR_LSB+OPT_MEM_ADDR_BITS downto ADDR_LSB) when axi_awv_awr_flag = '1' else
                     (others => '0');
    end generate gen_mem_sel;

Because we will use our own address, we don’t change this part. But if you won’t add a second port to the BRAM and want to use same port, then you need to modify mem_address assignment part and add your own control signals to change the address of the BRAM.

Then BRAM generation for-generate block comes. I modified this part and added my own mba_mem_rden signal and always enabled the read for Port B:

    -- implement Block RAM(s)
    BRAM_GEN : for i in 0 to USER_NUM_MEM-1 generate
      -- MBA START 
      signal mba_mem_rden : std_logic;
      -- MBA END
      signal mem_rden : std_logic;
      signal mem_wren : std_logic;
    begin
      mem_wren <= axi_wready and S_AXI_WVALID ;
      mem_rden <= axi_arv_arr_flag ;
      -- MBA START
      mba_mem_rden  <= '1'; -- always enabled read
      -- MBA END

The Vivado generated file handles BRAM generation by generating 4 BRAM for 4 bytes for 32-bit AXI width. So inside BRAM_GEN generate block there is a BYTE_BRAM_GEN for-generate block from index 0 to 3:

     BYTE_BRAM_GEN : for mem_byte_index in 0 to (C_S_AXI_DATA_WIDTH/8-1) generate
       signal byte_ram : BYTE_RAM_TYPE;
       signal data_in  : std_logic_vector(8-1 downto 0);
       signal data_out : std_logic_vector(8-1 downto 0);
       -- MBA START
       signal mba_data_out : std_logic_vector(8-1 downto 0);
       -- MBA END

I added only mba_data_out and not mba_data_in because for this project I only need read functionality in Port B.

Then I added mba_data_out assignment part:

     begin
       --assigning 8 bit data
       data_in  <= S_AXI_WDATA((mem_byte_index*8+7) downto mem_byte_index*8);
       data_out <= byte_ram(to_integer(unsigned(mem_address)));
       -- MBA START
       mba_data_out <= byte_ram(to_integer(unsigned(mba_mem_address)));
       -- MBA END

I did not change BRAM write process:

       BYTE_RAM_PROC : process( S_AXI_ACLK ) is
       begin
         if ( rising_edge (S_AXI_ACLK) ) then
           if ( mem_wren = '1' and S_AXI_WSTRB(mem_byte_index) = '1' ) then
             byte_ram(to_integer(unsigned(mem_address))) <= data_in;
           end if;
         end if;
      
       end process BYTE_RAM_PROC;

But I changed and added second port read functionality to BRAM:

       process( S_AXI_ACLK ) is
         begin
           if ( rising_edge (S_AXI_ACLK) ) then
             if ( mem_rden = '1') then 
               mem_data_out(i)((mem_byte_index*8+7) downto mem_byte_index*8) <= data_out;
             end if;
             -- MBA START
             if ( mba_mem_rden = '1') then 
               mba_mem_data_out(i)((mem_byte_index*8+7) downto mem_byte_index*8) <= mba_data_out;
             end if;             
             -- MBA END
           end if;
       end process;

I also did not touch axi read data process:

    --Output register or memory read data

    process(mem_data_out, axi_rvalid ) is
    begin
      if (axi_rvalid = '1') then
        -- When there is a valid read address (S_AXI_ARVALID) with 
        -- acceptance of read address by the slave (axi_arready), 
        -- output the read dada 
        axi_rdata <= mem_data_out(0);  -- memory range 0 read data
      else
        axi_rdata <= (others => '0');
      end if;  
    end process;

Then user logic part comes. I first instantiated the uart transmitter module:

    -- Add user logic here
    -- MBA START
    I_UART_TX : uart_tx
        generic map (
          c_clkfreq  => c_clkfreq ,
          c_baudrate => c_baudrate,
          c_stopbit  => c_stopbit 
        )
        port map(
          clk            => S_AXI_ACLK          ,
          din_i          => din         ,
          tx_start_i     => tx_start    ,
          tx_o           => tx_o            ,
          tx_done_tick_o => tx_done_tick
        );

And then write a process block which reads sent_trig_i input signal coming from AXI4-Lite module and then start transmitting bytes up to data_length_i value.

    process (S_AXI_ACLK) begin  
    if rising_edge(S_AXI_ACLK) then
        
        if (sent_trig_i = '1') then
            sending         <= '1';
            mba_mem_address <= (others => '0');
            tx_start        <= '1';
            din             <= mba_mem_data_out(0)(to_integer(unsigned(cntr(1 downto 0)))*8+7 downto to_integer(unsigned(cntr(1 downto 0)))*8);
            cntr            <= (0 => '1', others => '0');
        end if;

        if (sending='1') then
            
            tx_start    <= '0';

            if (tx_done_tick = '1') then
                if (cntr = data_length_i) then
                    sending         <= '0';
                    mba_mem_address <= (others => '0');
                    cntr            <= (others => '0');
                else
                    din         <= mba_mem_data_out(0)(to_integer(unsigned(cntr(1 downto 0)))*8+7 downto to_integer(unsigned(cntr(1 downto 0)))*8);
                    tx_start    <= '1';
                    cntr        <= std_logic_vector(unsigned(cntr) + 1);
                    if (cntr(1 downto 0) = "11") then
                        mba_mem_address <= std_logic_vector(unsigned(mba_mem_address) + 1);
                    end if;
                end if;
            end if;
                        
            
        end if;
        

    end if;
    end process;

The synthesis report showed that the BRAM is dual port: Port A is for AXI4-Full interface handling, has read and write functionalities. Port B is for user usage and only has read functionality, as I only wanted to read PS written data to be sent through uart_tx module so no need to write functionality in this case:

+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
| PORT A (Depth x Width) | W | R | PORT B (Depth x Width) | W | R | Ports driving FF | RAMB18 | RAMB36 |
+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
| 256 x 8(READ_FIRST)    | W | R | 256 x 8(WRITE_FIRST)   |   | R | Port A and B     | 1      | 0      |
| 256 x 8(READ_FIRST)    | W | R | 256 x 8(WRITE_FIRST)   |   | R | Port A and B     | 1      | 0      |
| 256 x 8(READ_FIRST)    | W | R | 256 x 8(WRITE_FIRST)   |   | R | Port A and B     | 1      | 0      |
| 256 x 8(READ_FIRST)    | W | R | 256 x 8(WRITE_FIRST)   |   | R | Port A and B     | 1      | 0      |
+------------------------+---+---+------------------------+---+---+------------------+--------+--------+

In this post, I showed how to create a custom IP having an AXI4-Lite interface and an AXI4-Full interface and how to modify these modules to add user functionality. I will show how to verify this design by using axi_bfm_pkg, axilite_bfm_pkg and uart_bfm_pkg of UVVM library in the next post.

You can find the codes in my github page:

https://github.com/mbaykenar/apis_anatolia/tree/main/website/axifull_ip

Regards,

Mehmet Burak AYKENAR

You can connect me via LinledIn: Just sent me an invitation

https://tr.linkedin.com/in/mehmet-burak-aykenar-73326419a

One thought to “HOW TO CREATE an AXI4-FULL CUSTOM IP with AXI4-LITE and UART INTERFACES in VIVADO”

  1. Abi merhaba sizlere bir konu danışmak istedim. PL tarafı için SPI modulü olusturdum ve 4 ayrı adc entegresinden sırasıyla okuma yapıyorum ve 1bytelik verileri 2byte olarak anlamdırıp Axi-full üstünde bulunan memory kısmına yazmak istiyorum. Ama benim için olması gereken memory genişligi 382 * (4*byte ) burda kulanılan BRAM üstünde 256 * 4 byte bir alan var nasıl arttırma işlemi yapabilirim?

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir