This (relatively large) code snippet is about implementing a register bank with an AXI-Lite interface.
Some time ago I published a previous version of this module, based on an AXI-Lite code from Xilinx. From the feedback I received on Reddit, I understood that the Xilinx code for AXI-Lite slave, regretfully, is broken. So I wrote down my own version of the code and tested it thoroughly.
The block implements several 32-bit registers. Of these registers, the code really implements:
- Two read-only registers: Version and date
- One read/write scratchpad register – Used to test that the module is alive, by writing a value and reading it back.
- Two additional registers, also of read/write type.
The block also takes care of the case where the host access undefined registers. More on this later in this article.
Lines 10 to 28 list the standard AXI signals, with three exceptions:
- read and write protect signals are not implemented
- write strobes signal is not implemented. Since I am planning to use this register bank on HW, I do not need to partially write registers. You need to add this port if the register will be used to communicate with a host processor that will issue partial writes (writing to one or more of the registers’ bytes).
The rest of the processes have a lot of comments in the code itself.
The code has two state machines, one for writing and one for reading. Both state machines implement timeouts for the signals that must be provided by the host once the register access has started. For the read access, the state machine checks that rready arrived or times out. For the write access, the state machine checks wvalid, and later on the transaction, it checks for bready.
Look at the assignment for the reading part and for the writing part of the registers, and try to identify how some of them are read-only and how there is a read/write register, also look at the same logic to see how the rresp (for reading) and bresp (for write) are assigned “00” values for error-free accesses, and “10” for exceptions:
- Register does not exist, or
- Trying to write to a read-only register
I have made extensive tests to the block, with reads and writes of diverse lengths, and checked for timeouts. You can see by yourself by running the testbench I have posted on GitHub.
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity axil_regs is generic ( C_DATA_W : integer := 32; C_ADDR_W : integer := 32 ); port ( s_axi_aclk : in std_logic; s_axi_aresetn : in std_logic; s_axi_awaddr : in std_logic_vector(C_ADDR_W - 1 downto 0); s_axi_awvalid : in std_logic; s_axi_awready : out std_logic; s_axi_wdata : in std_logic_vector(C_DATA_W - 1 downto 0); s_axi_wvalid : in std_logic; s_axi_wready : out std_logic; s_axi_bresp : out std_logic_vector(1 downto 0); s_axi_bvalid : out std_logic; s_axi_bready : in std_logic; s_axi_araddr : in std_logic_vector(C_ADDR_W - 1 downto 0); s_axi_arvalid : in std_logic; s_axi_arready : out std_logic; s_axi_rdata : out std_logic_vector(C_DATA_W - 1 downto 0); s_axi_rresp : out std_logic_vector(1 downto 0); s_axi_rvalid : out std_logic; s_axi_rready : in std_logic ); end axil_regs; architecture arch_imp of axil_regs is -- AXI4LITE signals signal axi_awaddr : std_logic_vector(C_ADDR_W - 1 downto 0); signal axi_awready : std_logic; signal axi_bresp : std_logic_vector(1 downto 0); signal axi_bvalid : std_logic; signal axi_araddr : std_logic_vector(C_ADDR_W - 1 downto 0); signal axi_arready : std_logic; signal axi_rdata : std_logic_vector(C_DATA_W - 1 downto 0); signal axi_rvalid : std_logic; --------------------------------------------- ---- Signals for user logic register space --------------------------------------------- ---- Number of Slave Registers 16 constant VER_ADDR : std_logic_vector(C_ADDR_W - 1 downto 0) := x"00"; constant DATE_ADDR : std_logic_vector(C_ADDR_W - 1 downto 0) := x"04"; constant SCRPAD_ADDR : std_logic_vector(C_ADDR_W - 1 downto 0) := x"08"; constant PWM_FREQ_DIV_ADDR : std_logic_vector(C_ADDR_W - 1 downto 0) := x"0C"; constant PWM_DUTY_ADDR : std_logic_vector(C_ADDR_W - 1 downto 0) := x"10"; signal reg_version : std_logic_vector(C_DATA_W - 1 downto 0) := x"0000_0001"; signal reg_date : std_logic_vector(C_DATA_W - 1 downto 0) := x"2110_0922"; signal reg_scratchpad : std_logic_vector(C_DATA_W - 1 downto 0); signal reg_pwm_freq_div : std_logic_vector(7 downto 0); signal reg_pwm_duty : std_logic_vector(7 downto 0); signal reg_data_out : std_logic_vector(C_DATA_W - 1 downto 0); constant REGS_TIMEOUT : integer range 0 to 15 := 15; signal timeout_rd : integer range 0 to REGS_TIMEOUT; signal timeout_wr : integer range 0 to REGS_TIMEOUT; signal waddr_strb : std_logic; signal bresp_strb : std_logic; signal wr_en : std_logic; signal raddr_strb : std_logic; signal rdata_strb : std_logic; type rd_sm is (idle, start_rd, rd_data); signal rd_st : rd_sm; type wr_sm is (idle, wr_data, wr_resp); signal wr_st : wr_sm; begin -- I/O Connections assignments s_axi_awready <= axi_awready; s_axi_bresp <= axi_bresp; s_axi_bvalid <= axi_bvalid; s_axi_arready <= axi_arready; s_axi_rdata <= axi_rdata; s_axi_rvalid <= axi_rvalid; ------------------------------------------------------------------- -- Registers write section waddr_strb <= s_axi_awvalid and axi_awready; bresp_strb <= s_axi_bready and axi_bvalid; s_axi_wready <= '1'; -- write registers state machine for control process (s_axi_aclk) begin if rising_edge(s_axi_aclk) then if s_axi_aresetn = '0' then axi_awready <= '1'; axi_bvalid <= '0'; wr_en <= '0'; wr_st <= idle; else case wr_st is when idle => if (waddr_strb = '1') then axi_awaddr <= s_axi_araddr; -- store write address axi_awready <= '0'; -- address received, stop receiving additional addresses axi_bvalid <= '0'; -- Check if waddr and wdata were sent on same clock if (s_axi_wvalid = '1') then axi_bvalid <= '1'; timeout_wr <= REGS_TIMEOUT - 1; -- load timeout for bresp phase wr_en <= '1'; wr_st <= wr_resp; else timeout_wr <= REGS_TIMEOUT - 1; -- load timeout for write data phase wr_en <= '1'; wr_st <= wr_data; end if; end if; when wr_data => if (s_axi_wvalid = '1') then axi_bvalid <= '1'; timeout_wr <= REGS_TIMEOUT - 1; -- load timeout for bresp phase wr_en <= '1'; wr_st <= wr_resp; elsif (timeout_wr = 0) then axi_awready <= '1'; -- write timeout, address bus is ready for new addr wr_st <= idle; else timeout_wr <= timeout_wr - 1; end if; when wr_resp => wr_en <= '0'; axi_bvalid <= '1'; if (bresp_strb = '1' or timeout_wr = 0) then axi_bvalid <= '0'; axi_awready <= '1'; -- data received (or timeout), address bus is ready for new addr wr_st <= idle; elsif (timeout_wr > 0) then timeout_wr <= timeout_wr - 1; end if; end case; end if; end if; end process; -- Implement memory mapped register select for write accesses process (s_axi_aclk) variable loc_addr : std_logic_vector(C_ADDR_W - 1 downto 0); begin if rising_edge(s_axi_aclk) then if s_axi_aresetn = '0' then reg_scratchpad <= (others => '0'); reg_pwm_freq_div <= (others => '0'); reg_pwm_duty <= (others => '0'); else loc_addr := s_axi_awaddr; if (wr_en = '1') then case loc_addr is when SCRPAD_ADDR => axi_bresp <= "00"; reg_scratchpad <= s_axi_wdata; when PWM_FREQ_DIV_ADDR => axi_bresp <= "00"; reg_pwm_freq_div <= s_axi_wdata(7 downto 0); when PWM_DUTY_ADDR => axi_bresp <= "00"; reg_pwm_duty <= s_axi_wdata(7 downto 0); when others => axi_bresp <= "10"; -- slave decoder error, register is read/only or does not exist end case; end if; end if; end if; end process; ------------------------------------------------------------------- -- Registers read section raddr_strb <= s_axi_arvalid and axi_arready; rdata_strb <= s_axi_rready and axi_rvalid; -- Read registers state machine for control process (s_axi_aclk) begin if rising_edge(s_axi_aclk) then if s_axi_aresetn = '0' then axi_arready <= '1'; axi_rvalid <= '0'; rd_st <= idle; else case rd_st is when idle => if (raddr_strb = '1') then axi_araddr <= s_axi_araddr; -- store read address axi_arready <= '0'; -- address received, stop receiving additional addresses axi_rvalid <= '0'; rd_st <= start_rd; end if; when start_rd => axi_rvalid <= '1'; timeout_rd <= REGS_TIMEOUT - 1; rd_st <= rd_data; when rd_data => if (rdata_strb = '1' or timeout_rd = 0) then axi_rvalid <= '0'; axi_arready <= '1'; -- data received (or timeout), address bus is ready for new addr rd_st <= idle; else timeout_rd <= timeout_rd - 1; end if; end case; end if; end if; end process; -- Read registers data process (s_axi_aclk) is variable loc_addr : std_logic_vector(C_ADDR_W - 1 downto 0); begin if (rising_edge (s_axi_aclk)) then -- Address decoding for registers read loc_addr := axi_araddr; -- Default values for rdata and rresp axi_rdata <= (others => '0'); s_axi_rresp <= "00"; case loc_addr is when VER_ADDR => axi_rdata <= reg_version; when DATE_ADDR => axi_rdata <= reg_date; when SCRPAD_ADDR => axi_rdata <= reg_scratchpad; when PWM_FREQ_DIV_ADDR => axi_rdata(7 downto 0) <= reg_pwm_freq_div; when PWM_DUTY_ADDR => axi_rdata(7 downto 0) <= reg_pwm_duty; when others => s_axi_rresp <= "10"; -- slave decode error, read register does not exist end case; end if; end process; end arch_imp;
The VHDL source code and test bench for this block are available on GitHub