SPI Controller

A Decently Complicated System

The questions below are due on Wednesday September 18, 2024; 11:59:00 PM.
 
You are not logged in.

Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.

On this page you're going to develop a controller for SPI Communication. SPI stands for "Serial Peripheral Interface" and is used in a variety of chip-to-chip communication situations. This module will exist to interface internal FPGA logic to external chips. Therefore it is best to visualize it as having two sets of interaces. One set (the left side shown below) will talk to other parts of your internal logic on the FPGA. The other side (right side below) will be routed directly to external pins on the FPGA to whatever device we're interested in. You'll sometimes here modules like this referred to as "bus managers" or "bus interfaces" since they allow two sets of logic speaking in different protocols to exchange information with one another.

The spi_con module. Arrows (and names) indicate the direction of data into and out of this device.

SPI Communication in General

One conclusion to be drawn from the first two week's assignments is that when transferring data around in digital electronics, one will often avoid doing many things at once by instead doing a subset of things very, very quickly one after the other. With the eight seven-segment LED display, for example, we don't want to have to set 56 values at once (it is expensive in terms of hardware IO...annoying overall). Instead we just sequentially convey a smaller amount of values quickly, one-after-the other and rely on the downstream consumer to integrate it all together1. This approach could be called "time-division-multiplexing," but you could also call this data serialization as well, since we're sending a collection of data in series rather than all at once (just like a serial novel or a serial killer).

Transferring data in small units, one after the other, rather than in parallel is extremely common, and that's the idea in SPI. If you need to transfer an n-bit message, instead of using n wires to do so, you instead send those n bits one at a time in n steps.2 How do you know when to read the value on a line? Well you also send along a data clock signal, and the two parties agree ahead of time to set their bits on one edge of this data clock and read their bits on the opposite end of the data clock.

Your mission is to write a version of an SPI Controller here. A device that can both send and receive (and manage the transactions) on an SPI bus.

What is the situation of a basic SPI network? There are two parties:

  • A Controller device that is in charge of managing the communication bus ("bus" refers to all the wires between the two modules)
  • A Peripheral device that expects another party to be in charge of the communication bus. It will respond to the control signals.

A basic SPI relationship. Arrows show the direction of data flow.

I'm Me! Are You Me Too? No You're You, I'm Me.

You may have noticed that both the Controller and the Peripheral have pins called data_out and data_in. However you don't connect one's data_in to the other's data_in just as you don't talk with your mouth to your friend who listens with their mouth. If we were to label the two data wires based off of one or the other's device, then you'd run into the issue of asking "which data_in are we talking about? In order to resolve the issue, we'll usually assign the two data lines a pair of global names reflective of the direction of data flow3.

  • COPI: standing for Controller Out Peripheral In for data moving from the Controller to the Peripheral Device.
  • CIPO: standing for Controller In Peripheral Out for data moving from the Peripheral to the Controller Device.

There are two additional wires then:

  • CS: which stands for "Chip Select". That is a signal that Controller uses to indicate the start of a data exchange.
  • DCLK: which stands for "Data Clock" and is a periodic signal that the Controller uses to synchronize the reading and writing of data on the COPI and CIPO lines.

A Basic Transaction

So what would a standard data transaction look like in SPI?

  • At rest, the only wire of the four that has a universally required value is the Chip Select (CS) line. It must be held high. Every other wire can vary based on the particular type of SPI. We will be doing SPI Mode 0 for the sake of simplification. That means that at rest, DCLK should be held low. The value of COPI and CIPO can be either high or low at rest.

Now when a transaction needs to occur, the following things need to happen:

  1. CS should be set to 0 by the Controller. This is an "active low" signal and tells the Peripheral that a transaction is starting.
  2. At about the same time, the Controller should set the first bit (if there is one) it wants to transmit on the COPI line.
  3. In response to seeing CS drop, the Peripheral will set the first bit (if there is one) that it wants to transmit on the CIPO line.
  4. At some point in time later, dictated by the DATA_CLK_PERIOD, the DCLK should go from 0 to 1 (Rising Edge)
  5. At the same time and upon seeing DCLK go from low-to-high:
    • the Controller should grab/store the value it reads on the CIPO line
    • the Peripheral should grab/store the value it reads on the COPI line
  6. At some point later, dicated by the DATA_CLK_PERIOD, the DCLK line should drop from 1 to 0 (Falling Edge)
  7. At the same time and upon seeing DCLK go from high-to-low:
    • the Controller should set the next bit it wants to send onto the COPI line
    • the Peripheral should set the next bit it wants to send onto the CIPO line
  8. The process should repeat steps 5 to 7 until the total number of rising edge events of DCLK equals DATA_WIDTH in size.
  9. After the last rising edge of DATA_CLK, the Controller should pull CS back to 1/high to complete the transaction. At that point, both parties can process their data.

An example signal trace with a DATA_CLK_PERIOD=4 relative to (sys_clk) and a DATA_WIDTH=8 is shown below. Note that the controller is sending the 8-bit message [7:0] copi to the Peripheral, and the Peripheral is sending the 8-bit message [7:0] cipo to the Controller. Notice that the bits are send most-significant-bit first ("msb-first"):

A basic SPI transaction with DATA_WIDTH=8 and DATA_CLK_PERIOD=4.

Your job is to make a full SPI controller that matches this pattern. It should be able to both write out a message and read in a message. The module is parameterizable. Specifically, it should have two parameters, which are directly tied to SPI features already discussed:

  • DATA_WIDTH: that specifies how many bits the message to be transmitted is. Default value should be 8.
  • DATA_CLK_PERIOD: that specifies how long (in clk_in clock cycles), each bit of data should be on the transmission line while being sent. Default value should be 100. On a 100 MHz system, this corresponds to 1 microsecond per bit or a data rate of 1 Mbps.

On the FPGA side, the module will have the following signals:

Two "utility" connections that make the system run:

  • clk_in: (input) The system clock for the system
  • rst_in: (input) The reset signal for the system (assume from a clk_in synchronized system)

Two "logic input/command" connections that describe what to transmit and when to start a transaction:

  • [DATA_WIDTH-1:0] data_in: (input) The data to be serialized out (the "copi" data). This data must be in place when the trigger is pulled high.
  • trigger_in: (input) A signal that will trigger the transmission of the serialized data. When high, if the module is not already transmitting data, the module should grab the data in data_in, and start to transmit it beginning with the Most Significant Bit and going downwards.

Two "logic output" connections that provide the result of the data read in during a transaction:

  • [DATA_WIDTH-1:0] data_out: Where the deserialized data appears.
  • data_valid_out: A signal that is high for one cycle when a new data frame has been fully received and it is present on the data_out output.

In addition there are four connections that will get routed directly to the outside world to interface with an SPI Peripheral device:

  • chip_data_out: (output) that contains the actual data being sent. During transmission, each bit (starting with the msb) will exist on this line for approximately DATA_CLK_PERIOD clock cycles. Read the chip_clk_out pin information carefully for caveats about this timing.
  • chip_clk_out: (output) a 50% duty cycle synchronization signal. We call it a "clock" but it shouldn't be used the same as your system clock. No Flip-Flops should be clocked off of this signal. Instead, what it does is provide the receiving party downstream a signal to know when to sample the values on the data_out line. In particular, when chip_clk_out transitions from low to high, that is when the receiving party should sample the value this module has placed on data_out. In order to ensure that data is maximally stable around this sample point, new bits of data should be placed on data_out when chip_clk_out falls from high to low. The duty cycle of this signal must be exactly 50%. Any deviation (even to 49%) will result in additional harmonic distortion noise from the clock line which can impede the reading of data. Consequently, the module must find the closest lower even-count period for a given DATA_CLK_PERIOD. This sounds confusing, but shouldn't be. Here's an example:
    • If DATA_CLK_PERIOD is specified to be 42, the module can implement the data period as requested (high for 21 clock cycles, and low for 21 clock cycles).
    • If DATA_CLK_PERIOD is specified to be 39, the module must actually implement a data period of 38, so that it can cleanly have its signal on for 19 clock cycles and off for 19 clock cycles.
  • chip_sel_out: A signal that normally sits high, but is dropped low before the start of transmission of data and is brought up after the completion of the transmission of data.
  • chip_data_in: (input) The line conveying the data from the Peripheral device. To be read by the spi_con module at the appropriate times to assemble the output.

All of these signals, in the context of the original example SPI transaction are shown below:

A basic SPI transaction with DATA_WIDTH=8 and DATA_CLK_PERIOD=4 now showing the FPGA-side signals

Build the module to do this.

Reset

On additional feature, on assertion of rst_in (as 1), the module should put 0's on all of its controllable outputs except for chip_sel_out. A starting skeleton is provide below:

Data In

Do not assume that data_in will hold its values for you while data is being transmitted. Upon the triggering, the values at data_in should be grabbed and stored internally to the module.

module spi_con
     #(parameter DATA_WIDTH = 8,
       parameter DATA_CLK_PERIOD = 100
      )
      (input wire   clk_in, //system clock (100 MHz)
       input wire   rst_in, //reset in signal
       input wire   [DATA_WIDTH-1:0] data_in, //data to send
       input wire   trigger_in, //start a transaction
       output logic [DATA_WIDTH-1:0] data_out, //data received!
       output logic data_valid_out, //high when output data is present.

       output logic chip_data_out, //(COPI)
       input wire   chip_data_in, //(CIPO)
       output logic chip_clk_out, //(DCLK)
       output logic chip_sel_out // (CS)
      );
  //your code here
endmodule

We've included a starting testbench for this module here: test_spi_con.py. This testbench has some minimal assertion checks in place as is, but you really need to update and modify this to test a variety of inputs and watch the outputs. Viewing your generate waveforms from this module will be key so make sure you're using your waveform viewer to determine performance.

SPI Controller waveform 1

Waveform of a working module with test code as included in testbench above.

module spi_con
Submit your tested SPI controller module here!
Code Skeleton
  No file selected

module spi_con
Submit the same file again in this box; this checker re-runs tests on your SPI controller with a different set of parameters.
Code Skeleton
  No file selected

testbench test_spi_con.py
Upload the testbench you used for creating your SPI controller! It's not being evaluated, as long as you upload something you'll get 100% for this, but we want to learn more about how you all are writing testbenches.
 No file selected


 
Footnotes

1The term "integrate" here is broad. Our eye literally integrates the light it experiences into one smooth image of the numbers, but there's other ways to integrate as well. (click to return to text)

2You can send two or even four bits at once as well in SPI...that's known as quad-SPI and you'll see that in certain devices like FLASH chips, but we'll ignore that here. (click to return to text)

3Historically the Controller and Peripheral Devices were known as the "Master" and "Slave" devices, respectively. Consequently the COPI and CIPO lines would be known as "MOSI" and "MISO." Because that old terminology has a lot of negative associations with it, there has been a movement in some areas towards the Controller/Peripheral nomenclature. We'll use the newer one in this class. I mention this because you will find tons of documentation using a mishmash of both old and/or new and/or alternates. (click to return to text)