Module 10: Capstone Projects · Project 3 — SPI Master Controller

Project 3, Part 2: The Shift Register and Chip Select

The previous article specified the modes. This one builds the datapath.

The structural difference between SPI and everything built for UART: SPI is full-duplex. Data moves in both directions, master to slave and slave to master, on every clock edge, at the same time. There is no separate transmit phase and receive phase. The shift register here is built around that simultaneity.

One register, two data lines

SPI uses two data lines, MOSI (master out, slave in) and MISO (master in, slave out), alongside the shared clock and an active-low chip select CS_n. The shift register’s job is to shift a bit out on MOSI at the same moment it shifts a new bit in from MISO, every clock edge, for as many bits as the transaction needs (8 here).

Shift register: MSB out on MOSI, MISO in at the LSB, shifting left
Figure 1: One shift register serving both directions

module spi_shift_reg #(parameter WIDTH = 8) (
    input  logic             clk, rst_n,
    input  logic             shift_en,        // one pulse per SPI bit period
    input  logic             load,            // load a new byte to transmit
    input  logic [WIDTH-1:0] tx_data,
    input  logic             miso,
    output logic             mosi,
    output logic [WIDTH-1:0] rx_data
);
    logic [WIDTH-1:0] shift_reg;

    assign mosi = shift_reg[WIDTH-1];   // MSB first, SPI's overwhelming convention

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            shift_reg <= {WIDTH{1'b0}};
        end else if (load) begin
            shift_reg <= tx_data;
        end else if (shift_en) begin
            shift_reg <= {shift_reg[WIDTH-2:0], miso};   // shift LEFT, capturing miso at the bottom
        end
    end

    assign rx_data = shift_reg;   // after WIDTH shifts, the full received byte sits here
endmodule

This is the universal shift register from the end of Section 3.4, a register with a load mode and a shift mode, with one twist that makes it do SPI’s full-duplex job: the bit going out (mosi, read from the top) and the bit coming in (miso, written at the bottom) move in the same clock edge, in the same shift_en branch. After 8 shifts the register holds the received byte and every bit of the loaded byte has gone out. One register, one clock domain, doing what UART needed a separate PISO and SIPO for across two independently clocked FSMs.

Chip select: framing a transaction

CS_n is SPI’s equivalent of UART’s start and stop bits. It tells the slave a transaction is beginning and ending, except that instead of being embedded in the data stream it is a separate line.

// CS_n timing relative to the shift register and clock generation:
// CS_n must go LOW before the first SCLK edge of a transaction, stay LOW
// for the whole transaction, and go HIGH only after the last bit has shifted.
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cs_n <= 1'b1;   // idle: chip NOT selected
    end else begin
        unique case (spi_state)
            SPI_IDLE:  cs_n <= 1'b1;
            SPI_START: cs_n <= 1'b0;   // assert CS_n a setup period BEFORE the first clock edge
            SPI_XFER:  cs_n <= 1'b0;   // held low for the entire 8-bit transfer
            SPI_DONE:  cs_n <= 1'b1;   // release only after the transfer completes
        endcase
    end
end

The timing requirement worth calling out: CS_n must go low with enough margin before the first clock edge for the slave to notice it has been selected and prepare to respond. That is a setup time, the same idea as a flip-flop’s setup time from Module 3, applied at the chip-to-chip level. Most slave datasheets specify this margin explicitly, and violating it is a common SPI integration bug, distinct from but as real as the mode mismatch from the previous article.

Practice Exercise

A slave datasheet requires CS_n to be asserted at least 50 ns before the first SCLK edge, and the master’s system clock is 20 MHz (50 ns period). How many master clock cycles of margin are needed between asserting CS_n and the first SCLK edge?

Solution

At 20 MHz one cycle is 50 ns. To guarantee at least 50 ns between CS_n asserting and the first edge, the FSM has to hold in SPI_START (asserting CS_n, not yet toggling SCLK) for at least 1 full cycle before moving to SPI_XFER. In practice a design would add margin, 2 cycles rather than exactly 1, to absorb any extra setup requirement or clock-generation latency inside the master rather than designing to the bare minimum.

SPI’s full-duplex nature lets one shift register do the job UART needed two structures for, shifting a bit out at the top while shifting one in at the bottom in the same edge. Chip select frames a transaction as UART’s start and stop bits frame a byte, but as a separate line with its own setup requirement relative to the first clock edge. In the next article, the last of this project, we build the controller FSM that sequences shift register, chip select and clock generation according to whichever mode is configured, and the testbench that proves all four.

← Project 3, Part 1: SPI and Its Four Modes Project 3, Part 3: The Mode-Configurable Master, Verified →