fpga-protocolo-spi-maestro-esclavo

SPI Protocol on FPGA: Full Duplex Master in Verilog

  • 6 min

The SPI is a synchronous, full duplex serial bus where a controller generates the clock and exchanges bits with one or more peripherals.

If we want to send pixels to a TFT LCD screen, read audio from a microphone, or write to an SD card, I2C falls short. We need raw speed.

SPI (Serial Peripheral Interface) is simple and can reach tens of megahertz when the device, board, and signal integrity allow it.

Today we are going to design an SPI controller in Verilog that will allow us to exchange data at full speed.

The Physics of SPI: Four Wires and Push-Pull

Unlike I2C, SPI does not skimp on wires. It uses 4 dedicated lines and Push-Pull logic (here we drive 3.3V and 0V with force, no weak pull-up resistors).

  1. SCK (Serial Clock): The clock that sets the pace. Generated by the Master.
  2. MOSI (Master Out Slave In): The Master speaks here and the Slave listens.
  3. MISO (Master In Slave Out): The Slave replies here (while the Master speaks).
  4. CS / SS (Chip Select): An individual line for each slave. Typically Active Low (0 to activate).

Push-Pull vs Open Drain:

Because SPI uses standard digital outputs (Push-Pull), the rising edges are very steep and clean. This is what allows us to achieve speeds of tens of MHz without signal degradation.

Exchange via Shift Registers

The most beautiful thing about SPI is understanding how it works internally. Imagine two circular shift registers connected in a ring: one in the Master and one in the Slave.

When the Master generates 8 clock pulses:

  1. The Master’s most significant bit goes out on MOSI and enters the Slave’s least significant bit.
  2. Simultaneously, the Slave’s most significant bit goes out on MISO and enters the Master.

It is an exchange. To read a byte from the slave, the master is obligated to send it a byte (even if it’s garbage, 0xFF or 0x00). You cannot receive without giving.

SPI Modes: CPOL and CPHA

This is where beginners often get frustrated. Not all SPI chips speak the same “dialect”. There are 4 modes defined by two parameters:

  1. CPOL (Clock Polarity):

    • 0: Clock idles Low (0).
    • 1: Clock idles High (1).
  2. CPHA (Clock Phase):

    • 0: Data is sampled on the first edge (rising if CPOL=0).
    • 1: Data is sampled on the second edge (falling if CPOL=0).

The most common mode (and the one used by Arduino by default) is Mode 0 (CPOL=0, CPHA=0). This is the one we will implement today.

  • Idle at 0.
  • Sample data on the Rising edge.
  • Shift data on the Falling edge.

Verilog Implementation

We are going to design a generic module. Unlike I2C, we don’t need complex states for ACK or addresses here. We just need to generate 8 (or N) clock pulses and shift the bits.

Design Strategy

  1. Generate an sck derived from the main clock.
  2. On the falling edge of SCK, update the MOSI pin with the next bit.
  3. On the rising edge of SCK, sample the MISO pin and store it.
module spi_master #(
    parameter CLK_HZ = 12000000,
    parameter SPI_HZ = 1000000   // 1 MHz
)(
    input wire clk,
    input wire rst,
    input wire start,
    input wire [7:0] data_in,    // Data to send
    output reg [7:0] data_out,   // Received data (MISO)
    output reg busy,             // 1 while transmitting
    output reg new_data,         // Pulse when done
    
    // Physical Pins
    output reg sck,
    output reg mosi,
    input wire miso
);

    // 1. SPI Clock Generator (SCK)
    // SCK must be 2x the SPI_HZ frequency to have both rising and falling edges
    localparam DIVIDER = CLK_HZ / (2 * SPI_HZ);
    localparam COUNT_WIDTH = (DIVIDER <= 1) ? 1 : $clog2(DIVIDER);
    reg [COUNT_WIDTH-1:0] clk_count;

    // States
    localparam IDLE = 0;
    localparam WORK = 1;
    reg state;
    
    reg [2:0] bit_cnt;      // Bit counter 0-7
    reg [7:0] shift_reg_tx; // Transmit shift register
    reg [7:0] shift_reg_rx; // Receive shift register

    // Clock divider logic
    wire sck_rise = (clk_count == DIVIDER-1) && (sck == 0);
    wire sck_fall = (clk_count == DIVIDER-1) && (sck == 1);

    always @(posedge clk) begin
        if (rst) begin
            state <= IDLE;
            sck <= 0; // CPOL = 0
            mosi <= 0;
            busy <= 0;
            new_data <= 0;
            clk_count <= 0;
        end else begin
            new_data <= 0; // Pulse defaults to off

            case (state)
                IDLE: begin
                    sck <= 0;
                    busy <= 0;
                    clk_count <= 0;
                    
                    if (start) begin
                        state <= WORK;
                        busy <= 1;
                        shift_reg_tx <= data_in; // Load data
                        shift_reg_rx <= 0;
                        bit_cnt <= 7;            // MSB First
                        mosi <= data_in[7];      // Output the first bit immediately
                        // Prepare for the first rising edge
                    end
                end

                WORK: begin
                    // Clock divider management
                    if (clk_count == DIVIDER-1) begin
                        clk_count <= 0;
                        sck <= ~sck; // Toggle the clock
                    end else begin
                        clk_count <= clk_count + 1;
                    end

                    // Events on SCK edges
                    
                    // A. RISING Edge: SAMPLE MISO
                    if (sck_rise) begin
                        shift_reg_rx <= {shift_reg_rx[6:0], miso};
                    end

                    // B. FALLING Edge: UPDATE MOSI (Shift)
                    if (sck_fall) begin
                        if (bit_cnt == 0) begin
                            // All 8 bits are complete!
                            state <= IDLE;
                            busy <= 0;
                            new_data <= 1;
                            data_out <= shift_reg_rx; // The last bit was captured on the previous edge
                        end else begin
                            // Prepare next bit
                            bit_cnt <= bit_cnt - 1;
                            shift_reg_tx <= {shift_reg_tx[6:0], 1'b0}; // Shift left
                            mosi <= shift_reg_tx[6]; // Output the next bit (now at index 6 after shift)
                        end
                    end
                end
            endcase
        end
    end

endmodule
Copied!

You may have noticed that the code does not control the CS pin. It is good practice to leave CS control to the top-level module.

Why? Because often you want to lower CS, send 3 bytes in a row, and then raise it. If the SPI module raised CS automatically after each byte, it would break the transaction.

Usage Example: Reading a Sensor

Imagine you want to read an SPI accelerometer. The top-level module would do this:

// Sequence in Top-Level
case (step)
    0: begin
        cs_pin <= 0; // 1. Activate slave
        data_to_send <= 8'h0B; // "READ" command
        start_spi <= 1;
        step <= 1;
    end
    1: begin
        start_spi <= 0;
        if (spi_done) step <= 2; // Wait for finish
    end
    2: begin
        // Now send 0x00 just to drive the clock and read the response
        data_to_send <= 8'h00; 
        start_spi <= 1;
        step <= 3;
    end
    3: begin
        start_spi <= 0;
        if (spi_done) begin
            read_data <= spi_data_out; // Here is the value!
            cs_pin <= 1; // 4. Deactivate slave
            step <= 0;
        end
    end
endcase
Copied!