Bird
Raised Fist0
Verilogprogramming~10 mins

SPI controller implementation in Verilog

Choose your learning style10 modes available
Introduction

An SPI controller helps a chip talk to other devices using the SPI communication method. It controls sending and receiving data bit by bit.

When you want to connect a microcontroller to sensors or memory chips using SPI.
When you need to send data fast between two chips on a circuit board.
When you want to control multiple devices with one main controller using SPI signals.
When you want to read data from an SPI-based temperature sensor.
When you want to write data to an SPI flash memory chip.
Syntax
Verilog
module spi_controller(
    input wire clk,
    input wire rst_n,
    input wire start,
    input wire [7:0] data_in,
    output reg mosi,
    output reg sclk,
    output reg cs_n,
    output reg [7:0] data_out,
    output reg done
);

// Internal registers and states
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
reg [1:0] state;

localparam IDLE = 2'b00, TRANSFER = 2'b01, DONE = 2'b10;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
        cs_n <= 1'b1;
        sclk <= 1'b0;
        mosi <= 1'b0;
        bit_cnt <= 3'd7;
        done <= 1'b0;
        data_out <= 8'd0;
        shift_reg <= 8'd0;
    end else begin
        case(state)
            IDLE: begin
                done <= 1'b0;
                sclk <= 1'b0;
                cs_n <= 1'b1;
                if (start) begin
                    cs_n <= 1'b0; // Activate chip select
                    shift_reg <= data_in;
                    bit_cnt <= 3'd7;
                    state <= TRANSFER;
                end
            end
            TRANSFER: begin
                sclk <= ~sclk; // Toggle clock
                if (sclk == 1'b0) begin
                    // On falling edge, output next bit
                    mosi <= shift_reg[bit_cnt];
                end else begin
                    // On rising edge, sample MISO (not implemented here, assume 0)
                    // shift_reg[bit_cnt] <= miso; // If MISO input existed
                    if (bit_cnt == 0) begin
                        state <= DONE;
                    end else begin
                        bit_cnt <= bit_cnt - 1;
                    end
                end
            end
            DONE: begin
                cs_n <= 1'b1; // Deactivate chip select
                done <= 1'b1;
                state <= IDLE;
            end
        endcase
    end
end

endmodule

This module uses a simple state machine with IDLE, TRANSFER, and DONE states.

The SPI clock (sclk) toggles every clock cycle to send bits out on MOSI.

Examples
Instantiates the SPI controller module with an 8-bit data input of 0xA5.
Verilog
spi_controller spi(
    .clk(clk),
    .rst_n(rst_n),
    .start(start),
    .data_in(8'hA5),
    .mosi(mosi),
    .sclk(sclk),
    .cs_n(cs_n),
    .data_out(data_out),
    .done(done)
);
Checks when the SPI transfer is done to take next action.
Verilog
always @(posedge clk) begin
    if (done) begin
        // SPI transfer finished
    end
end
Sample Program

This testbench resets the SPI controller, starts a transfer of 0x3C, and waits until done is high. It then prints the last MOSI bit sent.

Verilog
module test_spi;

reg clk = 0;
reg rst_n = 0;
reg start = 0;
reg [7:0] data_in = 8'h3C;
wire mosi;
wire sclk;
wire cs_n;
wire done;
wire [7:0] data_out;

spi_controller spi(
    .clk(clk),
    .rst_n(rst_n),
    .start(start),
    .data_in(data_in),
    .mosi(mosi),
    .sclk(sclk),
    .cs_n(cs_n),
    .data_out(data_out),
    .done(done)
);

// Clock generation
always #5 clk = ~clk;

initial begin
    rst_n = 0;
    #10;
    rst_n = 1;
    #10;
    start = 1;
    #10;
    start = 0;
    // Wait for done
    wait(done == 1);
    $display("SPI transfer done. MOSI last bit: %b", mosi);
    $finish;
end

endmodule
OutputSuccess
Important Notes

The SPI controller here only sends data (MOSI) and does not read MISO data.

Chip select (cs_n) is active low and controls when the SPI device listens.

The clock toggles every main clock cycle, so the SPI clock is half the frequency of clk.

Summary

An SPI controller manages sending data bit by bit using MOSI, SCLK, and CS signals.

It uses a simple state machine to control the transfer process.

This example shows how to implement and test a basic SPI controller in Verilog.

Practice

(1/5)
1. What is the main role of the cs (chip select) signal in an SPI controller?
easy
A. To enable communication with the specific SPI device
B. To provide the clock signal for data transfer
C. To send data from master to slave
D. To receive data from the slave device

Solution

  1. Step 1: Understand the function of cs in SPI

    The cs signal is used to select which SPI device the master wants to communicate with by enabling it.
  2. Step 2: Differentiate cs from other signals

    The clock signal is sclk, data sent from master is mosi, and data received is miso. So cs is not for clock or data transfer but device selection.
  3. Final Answer:

    To enable communication with the specific SPI device -> Option A
  4. Quick Check:

    cs = device enable [OK]
Hint: Remember: cs selects device, sclk clocks data [OK]
Common Mistakes:
  • Confusing cs with clock signal
  • Thinking cs sends or receives data
  • Mixing up mosi and miso signals
2. Which of the following is the correct Verilog syntax to declare a 8-bit register named shift_reg?
easy
A. reg shift_reg:8;
B. reg shift_reg[7:0];
C. wire [7:0] shift_reg;
D. reg [7:0] shift_reg;

Solution

  1. Step 1: Recall Verilog register declaration syntax

    Registers are declared with reg [width-1:0] name; format. For 8 bits, it is [7:0].
  2. Step 2: Check each option

    reg [7:0] shift_reg; uses correct syntax. reg shift_reg[7:0]; reverses the range and name. wire [7:0] shift_reg; uses wire, not reg. reg shift_reg:8; uses invalid colon syntax.
  3. Final Answer:

    reg [7:0] shift_reg; -> Option D
  4. Quick Check:

    reg [7:0] name; = correct reg declaration [OK]
Hint: Use reg [7:0] name; for 8-bit registers [OK]
Common Mistakes:
  • Using wire instead of reg for storage
  • Incorrect bit range syntax
  • Misplacing brackets around name
3. Given this snippet of an SPI controller shift register update inside a clocked always block:
always @(posedge sclk) begin
  if (cs == 0) begin
    shift_reg <= {shift_reg[6:0], miso};
  end
end

What does this code do on each rising edge of sclk when cs is low?
medium
A. Loads miso into the MSB of shift_reg without shifting
B. Shifts shift_reg right by 1 and inserts miso at MSB
C. Shifts shift_reg left by 1 and inserts miso at LSB
D. Clears shift_reg to zero

Solution

  1. Step 1: Analyze the shift operation

    The code concatenates shift_reg[6:0] (bits 6 down to 0) with miso at the right side, effectively shifting left by 1 bit and inserting miso at the least significant bit (LSB).
  2. Step 2: Confirm behavior when cs is low

    The shift happens only when cs == 0, which means the device is selected and data is being received bit by bit.
  3. Final Answer:

    Shifts shift_reg left by 1 and inserts miso at LSB -> Option C
  4. Quick Check:

    shift_reg <= {shift_reg[6:0], miso} = left shift + miso LSB [OK]
Hint: Concatenation {a,b} puts b at LSB, shifting left [OK]
Common Mistakes:
  • Thinking it shifts right instead of left
  • Confusing MSB and LSB positions
  • Ignoring the condition on cs signal
4. Consider this Verilog snippet for SPI clock generation:
always @(posedge clk) begin
  if (cs == 0) begin
    sclk <= ~sclk;
  end else begin
    sclk <= 0;
  end
end

What is the main problem with this code?
medium
A. It toggles sclk on every clk edge, causing sclk frequency to be half of clk only when cs is low
B. It does not reset sclk properly when cs is high
C. It causes sclk to toggle asynchronously to clk
D. It will cause a synthesis error due to missing sensitivity list

Solution

  1. Step 1: Understand clock toggling logic

    The code toggles sclk on every rising edge of clk if cs is low, effectively dividing the clock frequency by 2 during active communication.
  2. Step 2: Identify the problem

    This behavior is expected for SPI clock generation. The code also resets sclk to 0 when cs is high, which is correct. There is no asynchronous toggling or sensitivity list issue.
  3. Final Answer:

    It toggles sclk on every clk edge, causing sclk frequency to be half of clk only when cs is low -> Option A
  4. Quick Check:

    sclk toggles at half clk frequency when cs=0 [OK]
Hint: Toggling sclk halves clk frequency during cs low [OK]
Common Mistakes:
  • Thinking sclk toggles asynchronously
  • Expecting sclk to run continuously
  • Confusing sensitivity list with always block usage
5. You want to implement an SPI controller that sends 8 bits from a register tx_data over mosi and receives 8 bits into rx_data from miso. Which approach correctly handles the bit counting and shifting in Verilog?
hard
A. Use an 8-bit counter that decrements on each sclk falling edge when cs is high, shift tx_data right to output MSB on mosi, and shift in miso into rx_data.
B. Use a 3-bit counter that increments on each sclk rising edge when cs is low, shift tx_data left to output MSB on mosi, and shift in miso into rx_data.
C. Use a 4-bit counter that increments on each clk rising edge regardless of cs, shift tx_data right to output LSB on mosi, and ignore miso.
D. Use a 3-bit counter that resets only when cs is high, shift tx_data left to output LSB on mosi, and shift in miso into rx_data.

Solution

  1. Step 1: Determine counter size and control

    For 8 bits, a 3-bit counter (0 to 7) is enough. It should increment on sclk rising edge only when cs is low (active communication).
  2. Step 2: Understand shifting direction and data lines

    SPI usually sends MSB first, so shifting tx_data left outputs MSB on mosi. Incoming bits from miso are shifted into rx_data.
  3. Step 3: Evaluate options

    Use a 3-bit counter that increments on each sclk rising edge when cs is low, shift tx_data left to output MSB on mosi, and shift in miso into rx_data. matches correct counter size, control, and shifting direction. Use a 4-bit counter that increments on each clk rising edge regardless of cs, shift tx_data right to output LSB on mosi, and ignore miso. uses wrong counter size and ignores miso. Use an 8-bit counter that decrements on each sclk falling edge when cs is high, shift tx_data right to output MSB on mosi, and shift in miso into rx_data. uses wrong edge and counter behavior. Use a 3-bit counter that resets only when cs is high, shift tx_data left to output LSB on mosi, and shift in miso into rx_data. incorrectly outputs LSB first and counter reset logic.
  4. Final Answer:

    Use a 3-bit counter that increments on each sclk rising edge when cs is low, shift tx_data left to output MSB on mosi, and shift in miso into rx_data. -> Option B
  5. Quick Check:

    3-bit counter + sclk rising + MSB first shift = correct SPI send/receive [OK]
Hint: Count 8 bits with 3-bit counter, shift MSB first [OK]
Common Mistakes:
  • Using wrong counter size or clock edge
  • Shifting in wrong direction for MSB first
  • Ignoring chip select signal during counting
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard