Wednesday, March 30, 2011

Building an SPI interface... (part 1)

The SPI interface is a very useful way to transfer data from one device to another.  Previously we looked at the TWI/I2C bus (which is great for having multiple devices connected to a serial bus and share both the clock and data lines) those devices have unique addresses and data is transferred in bytes (8 bits at a time).  The SPI bus connects each device to three lines (a clock, MISO - master in slave out and MOSI - master out slave in).  Here the intended device has it's own unique enable line (which gets rid of the protocol overhead of addressing devices at the cost of more wires on the board).  There is no requirement for the speed, the number of bits transferred or whether the master or slave has to send data on each clock pulse - in practice it's a much more quicker and efficient way to move data.

With SPI the master is responsible for generating the clock and because the lines are usually true rather than open collector/open drain the slave can't do anything to slow the clock down.  The clock line has two important characteristics - Polarity and Phase - which together determine when the master and slave set and read the MOSI and MISO lines and can be grouped into 4 modes. 
  • mode 0 write on falling clock read on rising clock
  • mode 1 write on rising clock read on falling clock
  • mode 2 write on rising clock read on falling clock
  • mode 3 write on falling clock read on rising clock
This is from the point of view of the master (remember that the master is the device creating the clock).  The key things to remember is that the master needs to generate the clock when it wants to either read from the slave or write to the slave.  It is important to know the mode (polarity and phase) because the clock needs to be held in the right state when the master doesn't want to transmit or receive.  I am adding the ability to tristate the clock line so that a hybrid master/slave or a backup master can be physically wired.  Since the SPI bus can run at multiple speeds and these speeds may be different each of the devices sharing the bus I am adding an 8 bit interface for setting the clock divider.  The hdl isn't that hard in verilog.
assign spi_clock = (spi_clock_enable) ? spi_clock_write : 1'bz;

   // This part drives spi_clock
   // write is toggled on the delay counter, use enable elsewhere to
   // take the line hot.
   always @ (posedge clock) begin
      if (reset) begin
         // reset all the control registers/delays
         spi_clock_delay  <= clock_divider;        
         spi_clock_enable <= 1'b0;
         push_intent      <= 1'b0;
      end
      else if (!reset) begin   
         // here we will run the spi_clock with high/low intervals
         if (!slave) begin
            spi_clock_enable <= (rec_intent || send_intent || spi_clock_live);
         end
         else if (slave) begin
            spi_clock_enable <= 1'b0;
         end
                       
         if (!push_intent) begin
            if (rec_intent || send_intent) begin
               push_intent <= 1'b1;
            end
         end
         else if (push_intent) begin
            if (!rec_intent && !send_intent) begin
               if (polarity == 1'b0 && phase == 1'b0 && busdetect_spi) begin
                  // mode 0 write on falling clock read on rising clock
                  push_intent <= 1'b0;
               end  
               else if (polarity == 1'b0 && phase == 1'b1 && !busdetect_spi) begin
                  // mode 1 write on rising clock read on falling clock
                  push_intent <= 1'b0;
               end  
               else if (polarity == 1'b1 && phase == 1'b0 && !busdetect_spi) begin
                  // mode 2 write on rising clock read on falling clock
                  push_intent <= 1'b0;
               end  
               else if (polarity == 1'b1 && phase == 1'b1 && busdetect_spi) begin
                  // mode 3 write on falling clock read on rising clock
                  push_intent <= 1'b0;
               end  
            end
         end
                               
         if (spi_clock_delay == 0 && (push_intent) && spi_clock_enable) begin
            // spi_clock flip state
            spi_clock_delay <= clock_divider;        
            spi_clock_write <= ~spi_clock_write;
         end
         else if (spi_clock_delay > 0 && (push_intent) && spi_clock_enable) begin
            spi_clock_delay <= spi_clock_delay - 1;
         end               
         else if (!spi_clock_live && !push_intent) begin
            // set polarity prior to enable so we start in a happy state
            spi_clock_write <= polarity;
            spi_clock_delay <= clock_divider;
         end   
                       
      end // clock not in reset so drive spi_clock           
   end // posedge clock - spi_clock or hard reset

To give you a quick idea of how this module works, here's what the output looks like for most of the input combinations.  The entire verilog files can be found here.