1

I'm trying to use the Block based design in Vivado for the first time. I am using a Spartan 7 and don't want a Microblaze in the system.

My simple system was to have a SPI slave (for incoming data), an AXI interconnect, AXI interrupt controller, and a block of AXI attached memory.

I've fallen at the first hurdle in that the Xilinx SPI IP presents an AXI slave port only, and I would have thought that a SPI slave would be an AXI master so that I could write to the internal memory (which would be a slave attached to the AXI interconnect).

Do I need a different SPI IP block or am I doing this all wrong?

toolic
  • 5,637
  • 5
  • 20
  • 33
BlueTwin
  • 23
  • 5
  • 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. Maybe take a step back and tell us a bit about what you're trying to accomplish. – Dave Tweed Aug 13 '23 at 11:43
  • Use a open core SPI master, what a difference master and slave ? Two pin swap ? MOSI <---> MISO – dsgdfg Aug 13 '23 at 12:08
  • Thank you Dave I'm guilty of the "XY problem" here. The end goal is for a register mapped design with the entire register space accessible via SPI by an external SPI master (MCU). Inside the addressable space is a set of registers. One register (the most important) stores a set of parameters and instructions for generating single shot serial burst o/ps. Other addressable registers store build information, access some GPIO etc. There would be a state machine driven by the register content to control burst generation. AXI seemed like a flexible option to implement this. – BlueTwin Aug 13 '23 at 14:16
  • OK, sounds like a reasonable thing to do. But as I said, AXI memory protocol would be way overkill for this. 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. I'll dig through my notes and write up an answer in a bit. – Dave Tweed Aug 13 '23 at 15:40
  • Many thanks that does sound similar. I'd appreciate any pointers you have and I'll carry on writing it all in VHDL (it was just the implementation of the addressable register code that was a bit daunting)... I'll probably learn more that way too. – BlueTwin Aug 13 '23 at 15:57

1 Answers1

1

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;
Dave Tweed
  • 168,369
  • 17
  • 228
  • 393
  • 1
    Many thanks indeed, Dave. This clean and compact solution is extraordinarily helpful to my understanding. – BlueTwin Aug 16 '23 at 05:50
  • I'm glad it helps you. I was very disappointed when @VoltageSpike closed the question, and I'm still hoping that it can be reopened so that others will find it, too. – Dave Tweed Aug 16 '23 at 11:40