Yes and yes. The Xilinx SPI IP is meant to be connected to a processor of some sort. And why would you have an interrupt controller if you don't want a processor? If you want your SPI slave to control other blocks in your design, AXI is probably way overkill -- something much simpler would do the job.
I've done something like this in the past -- I needed to have two FPGAs communicating with each other over a limited set of pins, so I used a SPI-like protocol. On the slave side, I needed to access a status register and a pair of FIFOs. I did not use any Xilinx IP for any of this; writing a SPI slave is really quite straightforward.
Since my original project was in Verilog, and it included a lot of application-specific details, I was originally going to write this up as just a timing diagram and a block diagram. But as I started getting into the details, I decided to go ahead and write a VHDL module that illustrates the basic concept.
Let's assume a fairly simple set of SPI registers, each 8 bits wide.
There are up to 128 of them, so we need 7 bits of address, plus a WE (write enable) bit.
Our SPI protocol will be very straightforward.
The first 8 bits that the SPI master sends out on spi_mosi contain the WE bit and the 7-bit address.
The next 8 bits are data to be written if the WE bit is set, or 8 bits of dummy data otherwise.
Regardless, the current value of the selected register is returned on spi_miso during the second set of 8 clocks.
___ ________
spi_cs- \_________________________________________________________________________________________________/
_____ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ ___________
spi_sclk \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/ \__/
________ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ ____ ______
bit_cnt ______0_X__1__X__2__X__3__X__4__X__5__X__6__X__7__X__8__X__9__X_10__X_11__X_12__X_13__X_14__X_15__X_16_X_0____
_____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____
spi_mosi -----X_WE__X_A6__X_A5__X_A4__X_A3__X_A2__X_A1__X_A0__X_D7__X_D6__X_D5__X_D4__X_D3__X_D2__X_D1__X_D0__X--------
___________________________________________ ___________________________________________________________
address register ___________________________________________X_WE_A6:A0__________________________________________________
________________________________________________________________________________ ___________
internal write enable \_____/
_____ _____ _____ _____ _____ _____ _____ _____
spi_mis0 -----------------------------------------------------X_D7__X_D6__X_D5__X_D4__X_D3__X_D2__X_D1__X_D0__X--------
The key concept is to set up a bit counter that is cleared asynchronously by the negation of spi_cs-
and clocked by the active edge of spi_sclk.
There is also a 7-bit shift register that is continuously capturing the value of spi_mosi on the active edge of spi_sclk.
Note that on the 8th rising edge of the clock (bit_cnt = 7), the shift register contains the WE bit and A6 through A1,
and A0 is the value currently on spi_mosi.
On this clock edge, these 8 bits are captured in an internal address register.
On the next falling edge of the clock, this address is used to select the register data that gets loaded into the spi_miso shift register.
In the meantime, the shift register is capturing the next 8 bits on the spi_mosi line, in case this is a write cycle.
When we get to the 16th clock (bit_cnt = 15), the write data is available from the shift register and the spi_mosi line as before,
and during this clock, write enable to the register (or memory) is asserted.
Anyway, here's the code. As an example, it just has three registers that can be written and read back. Of course, any of these registers can be sent out to other logic to control it, and read data can also come from external sources. I hope this is useful to you!
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave is
port (
spi_cs_n : in std_logic;
spi_sclk : in std_logic;
spi_mosi : in std_logic;
spi_miso : out std_logic
);
end spi_slave;
architecture rtl of spi_slave is
signal mosi_reg : std_logic_vector (6 downto 0);
signal mosi_data : std_logic_vector (7 downto 0);
signal miso_reg : std_logic_vector (7 downto 0);
signal bit_cnt : unsigned (3 downto 0);
signal we : std_logic;
signal addr_reg : std_logic_vector (6 downto 0);
-- Example registers
signal reg0 : std_logic_vector (7 downto 0) := X"00";
signal reg1 : std_logic_vector (7 downto 0) := X"11";
signal reg2 : std_logic_vector (7 downto 0) := X"22";
begin
-- Bit counter
process (spi_cs_n, spi_sclk) is
begin
if spi_cs_n = '1' then
bit_cnt <= (others => '0');
elsif rising_edge (spi_sclk) then
bit_cnt <= bit_cnt + 1;
end if;
end process;
-- MOSI shift register, address register
mosi_data <= mosi_reg (6 downto 0) & spi_mosi;
process (spi_sclk) is
begin
if rising_edge (spi_sclk) then
mosi_reg <= mosi_data (6 downto 0);
if bit_cnt = 7 then
we <= mosi_data (7);
addr_reg <= mosi_data (6 downto 0);
end if;
end if;
end process;
-- MISO shift register (register reads)
process (spi_sclk) is
begin
if falling_edge (spi_sclk) then
if bit_cnt = 8 then
-- Perform data read operation
case addr_reg is
when "0000000" => miso_reg <= reg0;
when "0000001" => miso_reg <= reg1;
when "0000010" => miso_reg <= reg2;
when others => null;
end case;
else
miso_reg <= miso_reg (6 downto 0) & '1';
end if;
end if;
end process;
spi_miso <= miso_reg(7);
-- Register writes
process (spi_sclk) is
begin
if rising_edge (spi_sclk) then
if we = '1' and bit_cnt = 15 then
-- Perform data write operation
case addr_reg is
when "0000000" => reg0 <= mosi_data;
when "0000001" => reg1 <= mosi_data;
when "0000010" => reg2 <= mosi_data;
when others => null;
end case;
end if;
end if;
end process;
end rtl;