SPI CMD/Read/Write Protocol

Bi-Directional SPI Communication

Many SPI devices support two-way communication using both SDO (serial data out) and SDI (serial data in) wires. The signalling is initiated using the CS_n wire, and timing is controlled using the SCLK wire. Since there is only one CS_n wire, a bi-directional SPI peripheral has no idea whether the operation is READ or WRITE.

To solve this problem, most peripherals operate in WRITE mode by default, watching for a command on the SDI wire. The device defines a set of command codes that can be issued via the SDI interface. All bus transactions begin with a command code, followed by either a READ from SDO or a WRITE on SDI.

On the controller side, in order to READ data from the peripheral the controller must first WRITE the appropriate command code onto SDI, then the peripheral immediately responds with the requested data on SDO.

Specification for spi_register Peripheral

To demonstrate a cmd/read/write SPI interface, we will use a simple spi_register peripheral design with these specifications:

We will use distinct terms for SPI bus operations as opposed to internal peripheral command codes. Here is a key to the terminology:

We will use the terms READ/WRITE to describe timing specifications for the SPI signals, and the terms GET/PUT refer to the peripheral’s internal logic.

Signal timing: posedge (controller) and negedge (peripheral)

In order for the controller and peripheral to synchronize reliably, the peripheral is usually synchronized to the negative edge of SCLK. If the controller is synchronized to the positive edge, then some of the state machine logic is simplified. This two-phase strategy is depicted in the timing diagram for a READ operation shown below.

Timing diagram for an SPI READ operation. The controller WRITEs a command on SDI, and the peripheral responds with requested data on SDO.

In the above diagram, all of the controller actions occur on the positive SCLK edges (gray lines). So CS_n and SDI change with the positive edges. The peripheral “sees” the SDI values on the negative SCLK edges (blue lines). For each bit of the command code (shaded in red), SDI is stable at the time the peripheral “sees” it.

When the peripheral answers on the SDO wire, the signal changes with the negative edges of SCLK. The controller “sees” the SDO values on the positive SCLK edges, so the signal is stable at the time it is sensed by the controller.

The timing for a WRITE operation is similar, shown in the diagram below. For the WRITE operation, all SDI changes occur on the positive edges of SCLK, and the values are sensed by the peripheral on the negative edges.

Timing diagram for a bi-directional SPI operation. The controller WRITEs a command on SDI, and then immediately writes the data on SDI.

System Design

In this assignment, you will create an SPI interface for a single-word 16-bit register. The spi_register “peripheral” will reside inside the FPGA, simulating an external device. The user (you) supplies input data using the sw inputs on the Basys3 board, and output data is displayed on the board’s led lights. To write data into the spi_register, the user presses a button designated as the wr_btn, causing the controller to write bits from sw into the spi_register. When the user presses rd_btn, the controller reads bits from the spi_register and displays them on led. A block diagram for this system is shown below.

Block diagram for the simpleSPI cmd/read/write demonstration with spi_register.

State Transition Diagram

The state transition diagram shown below is based on the simpleSPI READ interface that we developed for the PmodALS project. This version of the diagram is referred to as “high-level” because it does not show every precise operation, and some operations are described in normal English to clarify the behavior.

High-level state transition diagram for simpleSPI cmd/read/write interface

This FSM design is very similar to the read-only interface we studied previously, with a few key differences:

Modified Debouncer

We will trigger the GET and PUT commands using buttons on the Basys3 board. Each buttons requires a debouncer module. We will modify the debouncer so that it implements a rd/wr/done handshaking protocol with simpleSPI. The behavior should be like this:

The altered state machine looks like this:

     case (state)
       0: // WAIT FOR PRESS
         begin
        if (btn) begin
           clear <= 0;
           state <= 1;
        end
        else begin
           clear <= 1;
           
        end
         end // case: 0
       1: // PRESS
         begin
            // BUTTON SUSTAINED:
        if (t && btn) begin
           state <= 2;
           btn_db <= 1;
        end
        // BOUNCE:
        else if (!btn && !t) begin
           state <= 0;
           clear <= 1;         
        end
         end // case: 1
       2: // WAIT FOR DONE
         begin
        if (done) begin
           btn_db <= 0;
           state <= 0;
           clear <= 1;
        end
         end
     endcase // case (state)

Copy your debouncer.v Verilog source from the previous assignment into src/ and make the changes needed to implement the state machine described here.

Assigned Tasks

Copy your simpleSPI design from the previous assignment, and modify it to enable WRITE operations as follows:

  input             wr,
  input         rd,
  input      [15:0] d_in,
  output reg [15:0] d_out,
  output reg        done
  output reg CS_n,
  output reg SCLK,
  output reg SDI,  // from controller to peripheral
  input      SDO   // from peripheral to controller
    localparam GET_CODE = 8'h50;
    localparam PUT_CODE = 8'h46;

Make a top Module

Create a top module with the features listed below.

Testbench Simulation

A testbench is provided in src/testbench.v. This testbench simulates random button presses on rd_btn and wr_btn, with random values generated on sw. causing it to perform GET and PUT operations at random times. A VCD file is produced, and a text output is generated as in previous assignments.

If your design is correct from the start (an unlikely event), then your output should resemble the text below. SDI values are printed on the negedge of SCLK, and SDO values are printed on the posedge. When a PUT/GET operation begins, the testbench prints an information message “Starting GET” or “Starting PUT”. The next 8 lines should show the command bits on SDI (bolded below). For a GET operation, the next 16 lines should show the register bits (initially ABCDh). For a PUT operation, they should show the bits from d_in.

Starting GET
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 1   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 1   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: 0000 (0000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: 8000 (1000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 0 led: 8000 (1000000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: a000 (1010000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 0 led: a000 (1010000000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: a800 (1010100000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 0 led: a800 (1010100000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: aa00 (1010101000000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: ab00 (1010101100000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: ab80 (1010101110000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: abc0 (1010101111000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 0 led: abc0 (1010101111000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 0 led: abc0 (1010101111000000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: abc8 (1010101111001000)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: abcc (1010101111001100)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 0 led: abcc (1010101111001100)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: abcd (1010101111001101)  
negedge sw: 90a8 SDI: 0   posedge  SDO: 1 led: abcd (1010101111001101)  
Completed GET with data abcd

For a PUT operation, the SDO signal stays constant and all the data appears on SDI:

Starting PUT with data 7b69
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 0  SDO: 1 led: abcd (1010101111001101) 
negedge sw: 7b69 SDI: 1  SDO: 1 led: abcd (1010101111001101) 
Completed PUT with data 7b69

Following a PUT operation, the subsequent GET operation should return the same data:

Starting PUT with data 7b69

...

Completed PUT with data 7b69
Starting GET

...

Completed GET with data 7b69

Debugging and Troubleshooting

If your initial simulation result doesn’t have the correct behavior, you should diagnose the problem using a graphical waveform viewer, either use gtkwave to view the VCD file, or run the simulation using make gui. Closely inspect the timing for GET and PUT operations, paying careful attention to the positive and negative SCLK edges, and the state of your simpleSPI interface.

As your designs become increasingly complex, you will need to develop skills and habits to systematically diagnose design errors. For any design, you should contemplate possible test failures and imagine their possible causes. Below are some common examples.

This listing is the beginning of a Fishbone Diagram or Ishikawa Diagram, which is a systematic method to diagnose the root causes of a problem. An example diagram is shown below. When analyzing the problem, we trace back along the branches, first checking the most “obvious” or top-level items on the right, then exploring the deeper items toward the left.

Example of a fishbone diagram for the simpleSPI interface.

Let’s study an example failure. After completing the simpleSPI, top and debouncer modules, the testbench simulation runs with no output. The simulation ran with no error messages, so the problem is not due to a syntax error. At this point, we should inspect the signal waveforms using either make gui or gtkwave.

First, we check the top-level signals, clk and rst_n.

Clock and reset signals.

The top-level signals look good, so next we check SCLK.

SCLK signal

SCLK looks good, so next we’ll investigate the handshake signals:

Handshake signals

Here we can see that the buttons rd_btn and wr_btn are being “pressed”, and the system is stuck in a rd request, but the operation never starts.

Next we’ll examine the state register in simpleSPI, along with its dependencies (the dependencies are the signals used for any edge conditions in the state graph).

Interface state and its related signals.

Here we notice that simpleSPI arrives in state 2 (WAIT), but does not go any further. Judging from the state transition diagram, in order to reach state 3 the condition is t && (rd || wr). From the signal plot, we can see that rd goes high but t never does. This points to the tcounter as the problem.

Now examining all the signals related to the tcounter:

Signals related to tcounter

In this signal plot we notice something truly odd: the tcount is supposed to count up to ten, but instead it just toggles between 0 and 1. Since this is unexpected behavior, we examine the source code related to tcount:

   //-----------------------------------------
   // Alarm Timer Process
   //-----------------------------------------
   always @(posedge SCLK) begin
      if (t_rst) begin
     t       <= 0;
     t_count <= 0;
      end
      else begin
     if (t_count > 10) begin
        t <= 1;
     end
     else begin
        t_count <= t_count + 1;     
     end
      end
   end
   //-----------------------------------------

After studying each line and conditional statement, the code looks correct. The problem must be in some other section of the source code. There are two possibilities:

To track it down, we can do a text search for every occurrence of t_count. These are all the other lines that involve t_count (I cut out all the other signals so we can focus just on t_count):

   reg        t_count;    // timer count

   initial begin
      t_count    = 0;
   end

There’s the mistake: t_count is declared as a reg, which has only one bit by default. In order for t_count to count up to 10, it needs at least 4 bits. We fix the mistake:

   reg  [3:0]  t_count;    // timer count (4 bits)
   

Now repeating the simulation, we see the expected testbench output and the signal patterns look more correct:

Correct signal waveforms.

Implement and Test on the Basys3

Once the testbench simulation is verified to be correct, run make implement to create the bitstream, and program it onto the Basys3. The button mappings are:

After programming, perform a GET operation and verify that you see the value ABCDh on the LEDs. Then record a short video showing LEDs initially at led = 0000h, with the switches set to sw = 1071h. Press PUT then GET to show that 1071h appears on the LEDs. Turn in the video on Canvas.

Turn in your work using git:

git add src/*.v *.v *.rpt *.txt *.tcl *.bit *.xdc *.vcd
git commit . -m "Complete"
git push origin master

Indicate on Canvas that your assignment is done.