SPI Controller
A Decently Complicated System
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.
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.
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 theCOPI
andCIPO
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 ofCOPI
andCIPO
can be either high or low at rest.
Now when a transaction needs to occur, the following things need to happen:
CS
should be set to 0 by the Controller. This is an "active low" signal and tells the Peripheral that a transaction is starting.- At about the same time, the Controller should set the first bit (if there is one) it wants to transmit on the
COPI
line. - In response to seeing
CS
drop, the Peripheral will set the first bit (if there is one) that it wants to transmit on theCIPO
line. - At some point in time later, dictated by the
DATA_CLK_PERIOD
, theDCLK
should go from 0 to 1 (Rising Edge) - 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
- the Controller should grab/store the value it reads on the
- At some point later, dicated by the
DATA_CLK_PERIOD
, theDCLK
line should drop from 1 to 0 (Falling Edge) - 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
- the Controller should set the next bit it wants to send onto the
- The process should repeat steps 5 to 7 until the total number of rising edge events of
DCLK
equalsDATA_WIDTH
in size. - After the last rising edge of
DATA_CLK
, the Controller should pullCS
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"):
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 (inclk_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 systemrst_in
: (input) The reset signal for the system (assume from aclk_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 indata_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 thedata_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 approximatelyDATA_CLK_PERIOD
clock cycles. Read thechip_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 thedata_out
line. In particular, whenchip_clk_out
transitions from low to high, that is when the receiving party should sample the value this module has placed ondata_out
. In order to ensure that data is maximally stable around this sample point, new bits of data should be placed ondata_out
whenchip_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 givenDATA_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.
- If
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 thespi_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:
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.
Submit the same file again in this box; this checker re-runs tests on your SPI controller with a different set of parameters.
No file selected
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.
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.
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.
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.