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
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?