Many of complex IPs and components nowadays use AXI4-Lite interface for configuration. Its a simple and sufficient protocol, but one may wonder how to easily configure such components with these types of interfaces from a Test – Bench.

One of the options is to manually write a well-timed transactions from what is usually a “Stimuli” process in a TB. If there are however plenty of required register writes, this becomes somewhat a messy solution and one may start to think of a better alternative. One of the options is to include a simple AXI4 Lite master in a TB a define only the Addresses and Data to be written there. I personally found this solution quite simple and efficient. The same could be eventually made for read channel, but there are usually no requirements to use read channels at all in a TB. Code below however requires the usage of VHDL2008 standard.

--------------------------------------------------------
------- Beechwood.eu || Author: Vojtech Ters 2021
------- [email protected] / [email protected]
--------------------------------------------------------

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;

entity AXI4Lite_TB is
--  Port ( );
end AXI4Lite_TB;

architecture Behavioral of AXI4Lite_TB is
subtype sl is std_logic;
subtype slv is std_logic_vector;
constant PERIOD    : time := 10 ns;
constant AXI_WIDTH : natural := 32;
signal CLK  : sl;
signal RST  : sl;

------------------------------------
------- AXI4L Write Signals --------
signal AWAddr   : slv(AXI_WIDTH - 1 downto 0);
signal AWReady  : sl;
signal AWValid  : sl;

signal WData    : slv(AXI_WIDTH - 1 downto 0);
signal WValid   : sl;
signal WReady   : sl;

signal BResp    : slv(1 downto 0);
signal BValid   : sl;
signal BReady   : sl;

type State_t is (AXI4L_AWADDR,AXI4L_AWDATA,AXI4L_BRESP);
signal AXI4L_W_State : State_t;

--------------------------------------------------
-------- Custom Register Write Definitions -------
constant MAX_REGS : natural := 7;
constant IND_ADDR : natural := 0;
constant IND_DATA : natural := 1;
constant PTR_W    : natural := natural(ceil(log2(real(MAX_REGS))));
signal Trans_Cnt : slv(PTR_W - 1 downto 0);
signal Trans_Max : slv(PTR_W - 1 downto 0) := slv(to_unsigned(MAX_REGS,PTR_W));

type RegWrites_t is array(0 to MAX_REGS - 1,0 to 1) of slv(AXI_WIDTH - 1 downto 0);
signal RegWrites : RegWrites_t := (
(0) => (X"C0FE_BABE",X"0000_0001"),
(1) => (X"BEEF_BABE",X"0000_0010"),
(2) => (X"DEAD_C0DE",X"0000_0100"),
(3) => (X"BAAD_F00D",X"0000_1000"),
(4) => (X"DEAD_BABE",X"0001_0000"),
(5) => (X"DEAD_BEAF",X"0010_0000"),
(6) => (X"FEED_C0DE",X"0100_0000")
);


-------------------------------------------------------------------------------------------
begin ------- -------- -------- ------- Architecture begins ------- ------- ------- -------
-------------------------------------------------------------------------------------------

TB_AXI4Lite_W_Master:process(CLK)
begin
  if rising_edge(CLK) then
    if RST = '1' then
      WData   <= (others => '0');
      AWAddr  <= (others => '0');
      AWValid <= '0';
      WValid  <= '0';
      BReady  <= '0';

      AXI4L_W_State <= AXI4L_AWADDR;
      Trans_Cnt     <= (others => '0');
    else
      case AXI4L_W_State is
        --------------------------------------------------------------
        when AXI4L_AWADDR => -----------------------------------------
          if unsigned(Trans_Cnt) < unsigned(Trans_Max) then
            AWValid <= '1';
            AWAddr  <= RegWrites(to_integer(unsigned(Trans_Cnt)),IND_ADDR);
            if AWValid = '1' and AWReady = '1' then
              AXI4L_W_State <= AXI4L_AWDATA;
              AWValid       <= '0';
              AWAddr        <= (others => '0');
            end if;
          end if;
        --------------------------------------------------------------
        when AXI4L_AWDATA =>  ----------------------------------------
          WValid  <= '1';
          WData   <= RegWrites(to_integer(unsigned(Trans_Cnt)),IND_DATA);
          if WValid = '1' and WReady = '1' then
            Trans_Cnt     <= slv(unsigned(Trans_Cnt) + 1);
            AXI4L_W_State <= AXI4L_BRESP;
            WValid        <= '0';
            WData         <= (others => '0');
          end if;

        --------------------------------------------------------------
        when AXI4L_BRESP  =>  ----------------------------------------
          BReady <= '1';
          if BReady = '1' and BValid = '1' then
            AXI4L_W_State <= AXI4L_AWADDR;
            BReady        <= '0';
          end if;
      end case;
    end if;
  end if;
end process;

TB_AXI4Lite_W_Slave:process(CLK)
  variable WriteAddress : slv(AXI_WIDTH - 1 downto 0);
begin
  if rising_edge(CLK) then
    if RST = '1' then
      AWReady <= '0';
      WReady  <= '0';
      BValid  <= '0';
      BResp   <= (others => '0');

    else
      AWReady <= '1';
      WReady  <= '1';
      BValid  <= '1';

      --------------------------------------
      -- Just Latch Address for Reporting --
      if AWReady = '1' and AWValid = '1' then
        WriteAddress := AWAddr;
      end if;

      ----------------------------
      -- AWADDR and AWDATA Done --
      if WReady = '1' and WValid = '1' then
        report "Write to Address: " & to_hstring(WriteAddress) & " With Data: " & to_hstring(WData) severity note;
      end if;

    end if;
  end if;
end process;

Stimuli:process
begin
  Rst <= '1';
  wait for 100 ns;
  Rst <= '0';
  
  wait;
end process;

ClkGen:process
begin
  CLK <= '0';
  wait for PERIOD/2;
  CLK <= '1';
  wait for PERIOD/2;
end process;





end Behavioral;

 

 Of course: If you simulate the TB, you should see the following output from a console:

Note: Write to Address: C0FEBABE With Data: 00000001
Time: 135 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave
Note: Write to Address: BEEFBABE With Data: 00000010
Time: 195 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave
Note: Write to Address: DEADC0DE With Data: 00000100
Time: 255 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave
Note: Write to Address: BAADF00D With Data: 00001000
Time: 315 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave
Note: Write to Address: DEADBABE With Data: 00010000
Time: 375 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave
Note: Write to Address: DEADBEAF With Data: 00100000
Time: 435 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave
Note: Write to Address: FEEDC0DE With Data: 01000000
Time: 495 ns Iteration: 1 Process: /AXI4Lite_TB/TB_AXI4Lite_W_Slave

If this solution is not enough,then an alternative would be to use VHDL 2008 & std.textio and define addresses and data to be written in a .txt file. This would be a more re-usable solution and could be even used for a static design initialization. Note that the AXI4 Lite Slave shown in the TB supports a minimum of the AXI4 Signals and is always ready to accept data. In a real TB,this process should be switched for a custom DUT. You can read more about AXI4 / AXI4Lite specification at ARM Developer site. From my own experiences, I  have found the AxPROT signals useless and rarely ever use them.

I have also decided to share with you and automatic configuration from a .txt example which you may download on the link below. Each line in the .txt file corresponds to an AXI4-Lite write ([ADDR, DATA]). If you require your TB to be reconfigured several times with different parameters, I highly suggest to create a custom python / Matlab script and prepare the .txt file with another script. In a similar fashion, one may incorporate write strobes or include a delay after each write. Since most of the time, only write transactions are required,I have omitted the read side.

Sample output from the TB and the TXT Master:
  • # Loading TB ADDR: 00770088    DATA: CC00BB00
  • # Loading TB ADDR: DEADBEEF DATA: C0DEBABE
  • # Loading TB ADDR: BABADEDA DATA: CAFEBABE
  • # Loading TB ADDR: C0DEBEEF DATA: DEADC0DE
  • # Loading TB ADDR: 10100101    DATA: 02022020