How to Design SPI Controller in VHDL

Serial Peripheral Interface Introduction

Before reading the post, if you need the VHDL code of the SPI controller, just put your email in the box you find in the post. There is no need to post a comment asking me for the code 🙂 If you don’t receive the email, please check your SPAM folder, enjoy!

The Serial Peripheral Interface (SPI) bus is a synchronous serial communication controller specification used for short-distance communication, primarily in embedded systems. The interface was developed by Motorola and has become a de-facto standard. Typical applications include sensors, Secure Digital cards, and liquid crystal displays.

SPI devices communicate in full-duplex mode using a master-slave architecture with a single master. The SPI master device originates from the frame for reading and writing. Multiple SPI slave devices are supported through selection with individual slave select (SS) lines as in Figure 2.

Figure 1 - SPI Master-single slave
Figure 1 – SPI Master-single slave

The SPI is a four-wire serial bus as you can see in Figure 1 and in Figure 2

Figure 2 - SPI Master-three slaves
Figure 2 – SPI Master-three slaves

For further information see the Wikipedia Page dedicated to the SPI.

SPI controller architecture overview

The SPI bus specifies four logic signals:

  • SCLK : Serial Clock (output from master).
  • MOSI : Master Output, Slave Input (output from master).
  • MISO : Master Input, Slave Output (output from slave).
  • SS : Slave Select (active low, output from master).

You can find alternate naming, but the functionalities are the same.

SPI timing example is shown in Figure 4. The MOSI can be clocked either on the rising or falling edge of SCKL. If MISO change on the rising edge of SCLK, MISO will change on falling and vice versa.

Figure 4 - SPI timing
Figure 4 – SPI timing

Data transmission begins on the falling edge of SS, then a number N of clock cycles will be provided. The MOSI is driven with the output data payload. The data payload can contain either data and command. If MOSI contains a command, i.e. read command, after a programmed number of SCLK cycles, MISO will be driven with the serial data value read from the slave.

SPI controller VHDL implementation

Before writing the SPI controller VHDL code, let’s review the SPI controller architecture of Figure 5

Figure 5 - SPI Controller interface
Figure 5 – SPI Controller interface
i_clk              : input clock
i_rstb             : input power on reset active low
i_tx_start         : input start sending i_data_parallel on serial line
o_tx_end           : serial data sending terminated
i_data_parallel    : parallel data to sent
o_data_parallel    : parallel received data
o_sclk             : serial clock output
o_ss               : slave select
o_mosi             : serial data output
i_miso             : serial data input

Figure 6 - SPI Controller FSM
Figure 6 – SPI Controller FSM

The SPI controller VHDL code will implement the FSM described in Figure 6. The input parallel data will be send using tx_start input signal. The FSM goes to “ST_TX_RX” state for a programmed number of clock cycles. During the data transmission, MISO input is sampled on the internal shift register. At the end of data transmission, the parallelized input is available on “o_parallel_data” output port and a pulse is generated on “o_tx_end” port.   A possible VHDL implementation of SPI controller is available below:


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

entity spi_controller is
generic(
  N                     : integer := 8;      -- number of bit to serialize
  CLK_DIV               : integer := 100 );  -- input clock divider to generate output serial clock; o_sclk frequency = i_clk/(2*CLK_DIV)
 port (
  i_clk                       : in  std_logic;
  i_rstb                      : in  std_logic;
  i_tx_start                  : in  std_logic;  -- start TX on serial line
  o_tx_end                    : out std_logic;  -- TX data completed; o_data_parallel available
  i_data_parallel             : in  std_logic_vector(N-1 downto 0);  -- data to sent
  o_data_parallel             : out std_logic_vector(N-1 downto 0);  -- received data
  o_sclk                      : out std_logic;
  o_ss                        : out std_logic;
  o_mosi                      : out std_logic;
  i_miso                      : in  std_logic);
end spi_controller;

architecture rtl of spi_controller is

type t_spi_controller_fsm is (
                          ST_RESET   ,
                          ST_TX_RX   ,
                          ST_END     );

signal r_counter_clock        : integer range 0 to CLK_DIV*2;
signal r_sclk_rise            : std_logic;
signal r_sclk_fall            : std_logic;
signal r_counter_clock_ena    : std_logic;

signal r_counter_data         : integer range 0 to N;
signal w_tc_counter_data      : std_logic;

signal r_st_present           : t_spi_controller_fsm;
signal w_st_next              : t_spi_controller_fsm;
signal r_tx_start             : std_logic;  -- start TX on serial line
signal r_tx_data              : std_logic_vector(N-1 downto 0);  -- data to sent
signal r_rx_data              : std_logic_vector(N-1 downto 0);  -- received data

begin

w_tc_counter_data  <= '0' when(r_counter_data>0) else '1';
--------------------------------------------------------------------
-- FSM
p_state : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_st_present            <= ST_RESET;
  elsif(rising_edge(i_clk)) then
    r_st_present            <= w_st_next;
  end if;
end process p_state;

p_comb : process(
                 r_st_present                       ,
                 w_tc_counter_data                  ,
                 r_tx_start                         ,
                 r_sclk_rise                        ,
                 r_sclk_fall                         )
begin
  case r_st_present is
    when  ST_TX_RX      => 
      if       (w_tc_counter_data='1') and (r_sclk_rise='1') then  w_st_next  <= ST_END       ;
      else                                                         w_st_next  <= ST_TX_RX     ;
      end if;

    when  ST_END      => 
      if(r_sclk_fall='1') then
        w_st_next  <= ST_RESET    ;	
      else
        w_st_next  <= ST_END    ;	
      end if;

    when  others            =>  -- ST_RESET
      if(r_tx_start='1') then   w_st_next  <= ST_TX_RX ;
      else                      w_st_next  <= ST_RESET ;
      end if;
  end case;
end process p_comb;

p_state_out : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_tx_start           <= '0';
    r_tx_data            <= (others=>'0');
    r_rx_data            <= (others=>'0');
    o_tx_end             <= '0';
    o_data_parallel      <= (others=>'0');
    
    r_counter_data       <= N-1;
    r_counter_clock_ena  <= '0';

    o_sclk               <= '1';
    o_ss                 <= '1';
    o_mosi               <= '1';
  elsif(rising_edge(i_clk)) then
    r_tx_start           <= i_tx_start;

    case r_st_present is
      when ST_TX_RX         =>
        o_tx_end             <= '0';
        r_counter_clock_ena  <= '1';
        if(r_sclk_rise='1') then
          o_sclk               <= '1';
          r_rx_data            <= r_rx_data(N-2 downto 0)&i_miso;
          if(r_counter_data>0) then
            r_counter_data       <= r_counter_data - 1;
          end if;
        elsif(r_sclk_fall='1') then
          o_sclk               <= '0';
          o_mosi               <= r_tx_data(N-1);
          r_tx_data            <= r_tx_data(N-2 downto 0)&'1';
        end if;
        o_ss                 <= '0';

      when ST_END          =>
        o_tx_end             <= r_sclk_fall;
        o_data_parallel      <= r_rx_data;
        r_counter_data       <= N-1;
        r_counter_clock_ena  <= '1';
        o_ss                 <= '0';
      
      when others               =>  -- ST_RESET
        r_tx_data            <= i_data_parallel;
        o_tx_end             <= '0';
        r_counter_data       <= N-1;
        r_counter_clock_ena  <= '0';

        o_sclk               <= '1';
        o_ss                 <= '1';
        o_mosi               <= '1';
    end case;
  end if;
end process p_state_out;


p_counter_clock : process(i_clk,i_rstb)
begin
  if(i_rstb='0') then
    r_counter_clock            <= 0;
    r_sclk_rise                <= '0';
    r_sclk_fall                <= '0';
  elsif(rising_edge(i_clk)) then
    if(r_counter_clock_ena='1') then  -- sclk = '1' by default 
      if(r_counter_clock=CLK_DIV-1) then  -- firse edge = fall
        r_counter_clock            <= r_counter_clock + 1;
        r_sclk_rise                <= '0';
        r_sclk_fall                <= '1';
      elsif(r_counter_clock=(CLK_DIV*2)-1) then
        r_counter_clock            <= 0;
        r_sclk_rise                <= '1';
        r_sclk_fall                <= '0';
      else
        r_counter_clock            <= r_counter_clock + 1;
        r_sclk_rise                <= '0';
        r_sclk_fall                <= '0';
      end if;
    else
      r_counter_clock            <= 0;
      r_sclk_rise                <= '0';
      r_sclk_fall                <= '0';
    end if;
  end if;
end process p_counter_clock;

end rtl;

In the following figures, there are three examples of SPI protocol simulation. In the Modelsim windows is clear the SPI protocol behavior: input parallel data is serialized on MOSI output port. The MISO input data is parallelized in the o_parallel_data port of the SPI controller.

Figure 7 shows an overall simulation view of three SPI cycles

Figure 7 - SPI Controller Modelsi simulation - All view
Figure 7 – SPI Controller Modelsi simulation – All view

Figure 8 shows a zoom on the second SPI cycle

Figure - 8 SPI Controller Modelsim simulation - zoom one cycle
Figure – 8 SPI Controller Modelsim simulation – zoom one cycle

Figure 9 shows a zoom on the SPI cycle start. The system clock is set to 10 ns in the simulation. The SCLK is generated dividing by 200 the system clock: 100 for high phase, 100 for low phase as specified in the generic “CLK_DIV”

Figure 7 - SPI Controller Modelsi simulation - cycle start
Figure 9 – SPI Controller Modelsim simulation – cycle start

The SPI controller VHDL code above is technology independent and can be implemented either on FPGA or ASIC.

Figure 10 shows Altera Quartus II RTL viewer of the SPI VHDL code implementation above.

Figure 10 - SPI Controller Quartus II RTL viewer
Figure 10 – SPI Controller Quartus II RTL viewer

The SPI controller VHDL code has been tested on Altera Cyclone III FPGA with 8 bit-serial and parallel data.

The implementation takes 58 Logic Element (LE) and performs @ 400 MHz as reported in the Quartus II area report and timing report below.

Figure - 11 SPI Controller Altera Quartus II -Area report for Cyclone III
Figure – 11 SPI Controller Altera Quartus II -Area report for Cyclone III
Figure - 12 SPI Controller Altera Quartus II -Timing report for Cyclone III
Figure – 12 SPI Controller Altera Quartus II -Timing report for Cyclone III

References:

https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus


If you appreciated this post, please help me to share it with your friend.

[social_sharing style=”style-7″ fb_like_url=”https://surf-vhdl.com/how-to-design-spi-controller-in-vhdl” fb_color=”light” fb_lang=”en_US” fb_text=”like” fb_button_text=”Share” tw_lang=”en” tw_url=”https://surf-vhdl.com/how-to-design-spi-controller-in-vhdl” tw_button_text=”Share” g_url=”https://surf-vhdl.com/how-to-design-spi-controller-in-vhdl” g_lang=”en-GB” g_button_text=”Share” linkedin_url=”https://surf-vhdl.com/how-to-design-spi-controller-in-vhdl” linkedin_lang=”en_US” alignment=”center”]

If you need to contact me, please write to: surf.vhdl@gmail.com

I appreciate any of your comment, please post below:

221 thoughts to “How to Design SPI Controller in VHDL”

  1. Thanks for the great code.

    I wonder which SPI mode is used (CPOL and CPHA)? and can all the modes be added easily?

    Thanks,
    Meto

        1. Hello Dear,
          do you have any idea how FPGA read data from DSP and microcontroller and vice versa, means write to DSP and microcontroller?
          Appreciate your answer

          1. generally, the micro is the master of the communication and the FPGA is a peripheral for the micro.

          1. please, check your spam or promotion folder, sometimes gmail put the mail there
            Ciao

    1. You should check the datasheet of the tuner, but I think yes.
      In any case, you can modify the code to easily adapt it to your device.
      You can download the test bench directly from this page,
      just insert your email address in the box inside the post

  2. This is a really great website. Thank you for all the material you have made available here. I’m hoping to be able to move working away from Microcontrollers to fpga’s in the near future!!

  3. I’m interested in the SPI testbench – I put my name and email in the box above as suggested, but it hasn’t provided access to it. I don;t see a link on this page, and searching for “testbench” yields no results. Can you send a link?

    1. if you put your email in the box above you will receive the test bench.
      Sometimes the email goes in the SPAM folder, please check there
      ciao

  4. thanks for sharing. Sir can you plz send me the SPI master-slave code with testbench. I need it for learning. Thanks in advance.

    1. Hi, put your email in the box you find in the post.
      If you do not receive anything, please check your spam folder
      Ciao!

        1. put your email in the box you find on this page and you will find it in your inbox.
          If you did it, please check your spam folder.
          Ciao
          Happy new year!

    1. I am RF student but have found writing VHDL code to represent the hardware circuits I have work
      with, only the simple circuits to date, and running the simulation is great.
      Your SPI code that will take me time to follow.
      My next step is to run the simulation
      Could you provide the test bench code

      1. just put your email in the box above and you will get the code. If you already do this, check your spam folder
        ciao

  5. What if i have to support “N” slaves? for example 3 slave i need to support using a single master
    But i have designed a SPI master for single slave. I dont want to just instantiate and connect to 3 slaves…
    What is the solution?
    I dont see any forums explaining about supporting multiple slave (in the code) … pls reply it will be helpful

  6. if my data is “010101010101010101010101010101010” then
    the how to get the out for the given data as
    if either 0 or 1 is there…
    i want to full cycle for either 0 or 1…
    how to get that?
    plsss…..spport me …

  7. hello! Send me testbench plzzzzzz, i need that
    i confirm email but not see testbench anywhere?

Leave a Reply

Your email address will not be published. Required fields are marked *