SPI Master Verilog Code: Mode-0 with Loopback Testbench

The Serial Peripheral Interface (SPI) is a four-wire, full-duplex synchronous bus used to talk to flash memories, ADCs, sensors, and displays. A master drives the clock (SCLK) and chip-select (CS), shifts a byte out on MOSI, and shifts a byte in on MISO at the same time. This tutorial builds complete, runnable SPI master Verilog code: a Mode-0 master with a parameterized clock divider and an 8-bit shift register that transmits and receives at once, plus a self-checking testbench that wires MOSI back to MISO so every byte sent must return unchanged. Every block below is real Verilog-2005. Compile it with iverilog -g2005 -o sim file.v and run vvp sim; the simulation log shown is the actual output.

The complete SPI master and loopback testbench

The handshake is deliberately simple. Pulse start high for one cycle with your byte on tx_data; the master raises busy, runs the transfer, then emits a one-cycle done with the received byte on rx_data. While busy is high, new start pulses are ignored, so a FIFO or state machine upstream can simply watch done before loading the next byte. Reset is asynchronous and active-low. The single shift register does double duty: as its MSB leaves on MOSI, the incoming MISO bit enters at the LSB, so after eight SCLK pulses it holds the received byte.

// spi_master_mode0: SPI master, Mode 0 (CPOL=0, CPHA=0)
// CLK_DIV = number of system-clock cycles in one SCLK period (even, >= 2).
module spi_master_mode0 #(
    parameter CLK_DIV = 4
)(
    input  wire       clk,       // system clock
    input  wire       rst_n,     // active-low reset
    input  wire       start,     // pulse to launch one 8-bit transfer
    input  wire [7:0] tx_data,   // byte to transmit, MSB first
    output reg  [7:0] rx_data,   // byte received from the slave
    output reg        busy,      // high while a transfer runs
    output reg        done,      // single-cycle pulse at completion
    output reg        sclk,      // serial clock to the slave
    output reg        mosi,      // master out, slave in
    input  wire       miso,      // master in, slave out
    output reg        cs_n       // active-low chip select
);
    localparam HALF = CLK_DIV >> 1;   // system cycles per SCLK half-period

    reg [15:0] div_cnt;
    reg [7:0]  shift_reg;
    reg [3:0]  bit_cnt;
    reg        phase;             // 0 = next edge is rising, 1 = next is falling
    reg        running;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            div_cnt   <= 16'd0;
            shift_reg <= 8'd0;
            bit_cnt   <= 4'd0;
            phase     <= 1'b0;
            running   <= 1'b0;
            sclk      <= 1'b0;
            mosi      <= 1'b0;
            cs_n      <= 1'b1;
            busy      <= 1'b0;
            done      <= 1'b0;
            rx_data   <= 8'd0;
        end else begin
            done <= 1'b0;
            if (!running) begin
                if (start) begin
                    shift_reg <= tx_data;
                    mosi      <= tx_data[7];   // present MSB before first edge
                    cs_n      <= 1'b0;
                    busy      <= 1'b1;
                    running   <= 1'b1;
                    sclk      <= 1'b0;
                    phase     <= 1'b0;
                    bit_cnt   <= 4'd0;
                    div_cnt   <= 16'd0;
                end
            end else if (div_cnt == HALF - 1) begin
                div_cnt <= 16'd0;
                if (phase == 1'b0) begin
                    sclk      <= 1'b1;                      // rising edge
                    shift_reg <= {shift_reg[6:0], miso};   // sample MISO
                    bit_cnt   <= bit_cnt + 4'd1;
                    phase     <= 1'b1;
                end else begin
                    sclk  <= 1'b0;                          // falling edge
                    phase <= 1'b0;
                    if (bit_cnt == 4'd8) begin
                        running <= 1'b0;
                        busy    <= 1'b0;
                        cs_n    <= 1'b1;
                        done    <= 1'b1;
                        rx_data <= shift_reg;
                    end else begin
                        mosi <= shift_reg[7];               // shift next bit out
                    end
                end
            end else begin
                div_cnt <= div_cnt + 16'd1;
            end
        end
    end
endmodule

// Testbench: MOSI is wired back to MISO, so every byte sent returns unchanged.
module tb_spi_loopback;
    reg        clk = 1'b0;
    reg        rst_n;
    reg        start;
    reg  [7:0] tx_data;
    wire [7:0] rx_data;
    wire       busy, done, sclk, mosi, cs_n;

    always #1 clk = ~clk;           // 2 time-unit system clock

    spi_master_mode0 #(.CLK_DIV(4)) dut (
        .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data),
        .rx_data(rx_data), .busy(busy), .done(done),
        .sclk(sclk), .mosi(mosi), .miso(mosi), .cs_n(cs_n)  // loopback: miso = mosi
    );

    initial begin
        rst_n = 1'b0; start = 1'b0; tx_data = 8'h00;
        #4 rst_n = 1'b1;
        @(posedge clk);
        tx_data = 8'hA5; start = 1'b1;   // send 0xA5
        @(posedge clk) start = 1'b0;
        wait (done);
        #1;
        $display("sent=0x%02h  received=0x%02h  %s",
                 8'hA5, rx_data, (rx_data == 8'hA5) ? "PASS" : "FAIL");
        #4 $finish;
    end
endmodule
sent=0xa5  received=0xa5  PASS
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmprk0qhorl/spi_loopback.v:107: $finish called at 74 (1s)

How the SPI master Verilog code works

Mode 0 timing (CPOL=0, CPHA=0)

Mode 0 means the clock idles low (CPOL=0) and data is sampled on the leading rising edge while it is changed on the trailing falling edge (CPHA=0). The design encodes this with a phase flag: on a rising edge it drives sclk high and samples miso into the shift register; on the following falling edge it drives sclk low and places the next bit on mosi. Because CPHA=0 samples on the very first edge, the master loads tx_data[7] onto MOSI the moment cs_n goes low, before any clock edge arrives.

The parameterized clock divider

SCLK is generated by counting system-clock cycles. CLK_DIV is the number of clk cycles in one full SCLK period, so HALF system cycles pass between each edge, where HALF is CLK_DIV shifted right by one. The counter div_cnt runs from zero to HALF - 1 and produces one edge per overflow. With a 50 MHz clk and CLK_DIV of 8, SCLK is 6.25 MHz; with CLK_DIV of 100 it drops to 500 kHz. Keep CLK_DIV even and at least 2 so both half-periods are equal in length.

8-bit shift register for TX and RX

The shift_reg is loaded with tx_data at the start of a transfer. On every sampling edge it shifts left and takes miso into bit zero, concatenating the lower seven bits with the new input bit. A 4-bit bit_cnt counts the eight sampled bits; when it reaches 8 the master raises a one-cycle done pulse, copies the shift register into rx_data, and releases cs_n high. Because transmit and receive share one register, SPI is inherently full-duplex: you always receive a byte for every byte you send.

Sending several bytes back to back

Real drivers send more than one byte. This testbench wraps a transfer in a reusable task and sends 0x3C then 0xF0, checking each returned byte against what was sent. Notice that the second transfer only begins after done from the first, which is the handshake you would use in hardware.

// SPI master, Mode 0 (CPOL=0, CPHA=0), with a multi-byte loopback testbench.
module spi_master_mode0 #(
    parameter CLK_DIV = 4
)(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       start,
    input  wire [7:0] tx_data,
    output reg  [7:0] rx_data,
    output reg        busy,
    output reg        done,
    output reg        sclk,
    output reg        mosi,
    input  wire       miso,
    output reg        cs_n
);
    localparam HALF = CLK_DIV >> 1;

    reg [15:0] div_cnt;
    reg [7:0]  shift_reg;
    reg [3:0]  bit_cnt;
    reg        phase;
    reg        running;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            div_cnt <= 16'd0; shift_reg <= 8'd0; bit_cnt <= 4'd0;
            phase <= 1'b0; running <= 1'b0; sclk <= 1'b0; mosi <= 1'b0;
            cs_n <= 1'b1; busy <= 1'b0; done <= 1'b0; rx_data <= 8'd0;
        end else begin
            done <= 1'b0;
            if (!running) begin
                if (start) begin
                    shift_reg <= tx_data; mosi <= tx_data[7];
                    cs_n <= 1'b0; busy <= 1'b1; running <= 1'b1;
                    sclk <= 1'b0; phase <= 1'b0; bit_cnt <= 4'd0; div_cnt <= 16'd0;
                end
            end else if (div_cnt == HALF - 1) begin
                div_cnt <= 16'd0;
                if (phase == 1'b0) begin
                    sclk <= 1'b1;
                    shift_reg <= {shift_reg[6:0], miso};
                    bit_cnt <= bit_cnt + 4'd1;
                    phase <= 1'b1;
                end else begin
                    sclk <= 1'b0; phase <= 1'b0;
                    if (bit_cnt == 4'd8) begin
                        running <= 1'b0; busy <= 1'b0; cs_n <= 1'b1;
                        done <= 1'b1; rx_data <= shift_reg;
                    end else begin
                        mosi <= shift_reg[7];
                    end
                end
            end else begin
                div_cnt <= div_cnt + 16'd1;
            end
        end
    end
endmodule

// Testbench: a small task sends one byte and waits; two bytes go back-to-back.
module tb_spi_multibyte;
    reg        clk = 1'b0;
    reg        rst_n;
    reg        start;
    reg  [7:0] tx_data;
    wire [7:0] rx_data;
    wire       busy, done, sclk, mosi, cs_n;
    integer    pass = 0;

    always #1 clk = ~clk;

    spi_master_mode0 #(.CLK_DIV(4)) dut (
        .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data),
        .rx_data(rx_data), .busy(busy), .done(done),
        .sclk(sclk), .mosi(mosi), .miso(mosi), .cs_n(cs_n)
    );

    task send_byte(input [7:0] b);
        begin
            @(posedge clk);
            tx_data = b; start = 1'b1;
            @(posedge clk) start = 1'b0;
            wait (done); #1;
            if (rx_data == b) pass = pass + 1;
            $display("tx=0x%02h -> rx=0x%02h  %s", b, rx_data,
                     (rx_data == b) ? "ok" : "BAD");
        end
    endtask

    initial begin
        rst_n = 1'b0; start = 1'b0; tx_data = 8'h00;
        #4 rst_n = 1'b1;
        send_byte(8'h3C);
        send_byte(8'hF0);
        $display("result: %0d/2 bytes matched", pass);
        #4 $finish;
    end
endmodule
tx=0x3c -> rx=0x3c   ok
tx=0xf0 -> rx=0xf0   ok
result: 2/2 bytes matched
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpupay791f/spi_multibyte.v:97: $finish called at 140 (1s)

Changing the SCLK rate with CLK_DIV

Because the divisor is a parameter, you set the SCLK speed per instance without touching the logic. This testbench runs two masters from the same clk, one at CLK_DIV of 4 and one at CLK_DIV of 12, and prints when each finishes the same byte. The slower divider takes proportionally longer, exactly as the printed ratio shows, while both recover the identical received byte.

// SPI master, Mode 0, used to show how CLK_DIV sets the SCLK rate.
module spi_master_mode0 #(
    parameter CLK_DIV = 4
)(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       start,
    input  wire [7:0] tx_data,
    output reg  [7:0] rx_data,
    output reg        busy,
    output reg        done,
    output reg        sclk,
    output reg        mosi,
    input  wire       miso,
    output reg        cs_n
);
    localparam HALF = CLK_DIV >> 1;

    reg [15:0] div_cnt;
    reg [7:0]  shift_reg;
    reg [3:0]  bit_cnt;
    reg        phase;
    reg        running;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            div_cnt <= 16'd0; shift_reg <= 8'd0; bit_cnt <= 4'd0;
            phase <= 1'b0; running <= 1'b0; sclk <= 1'b0; mosi <= 1'b0;
            cs_n <= 1'b1; busy <= 1'b0; done <= 1'b0; rx_data <= 8'd0;
        end else begin
            done <= 1'b0;
            if (!running) begin
                if (start) begin
                    shift_reg <= tx_data; mosi <= tx_data[7];
                    cs_n <= 1'b0; busy <= 1'b1; running <= 1'b1;
                    sclk <= 1'b0; phase <= 1'b0; bit_cnt <= 4'd0; div_cnt <= 16'd0;
                end
            end else if (div_cnt == HALF - 1) begin
                div_cnt <= 16'd0;
                if (phase == 1'b0) begin
                    sclk <= 1'b1;
                    shift_reg <= {shift_reg[6:0], miso};
                    bit_cnt <= bit_cnt + 4'd1;
                    phase <= 1'b1;
                end else begin
                    sclk <= 1'b0; phase <= 1'b0;
                    if (bit_cnt == 4'd8) begin
                        running <= 1'b0; busy <= 1'b0; cs_n <= 1'b1;
                        done <= 1'b1; rx_data <= shift_reg;
                    end else begin
                        mosi <= shift_reg[7];
                    end
                end
            end else begin
                div_cnt <= div_cnt + 16'd1;
            end
        end
    end
endmodule

// Testbench: two masters, same clock, different CLK_DIV. The slower divider
// takes proportionally longer to clock the same byte out.
module tb_spi_divider;
    reg        clk = 1'b0;
    reg        rst_n;
    reg        start;
    reg  [7:0] tx_data;
    wire [7:0] rx_a, rx_b;
    wire       busy_a, busy_b, done_a, done_b;
    wire       sclk_a, mosi_a, cs_a, sclk_b, mosi_b, cs_b;
    integer    t_a, t_b;

    always #1 clk = ~clk;

    spi_master_mode0 #(.CLK_DIV(4)) fast (
        .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data),
        .rx_data(rx_a), .busy(busy_a), .done(done_a),
        .sclk(sclk_a), .mosi(mosi_a), .miso(mosi_a), .cs_n(cs_a));

    spi_master_mode0 #(.CLK_DIV(12)) slow (
        .clk(clk), .rst_n(rst_n), .start(start), .tx_data(tx_data),
        .rx_data(rx_b), .busy(busy_b), .done(done_b),
        .sclk(sclk_b), .mosi(mosi_b), .miso(mosi_b), .cs_n(cs_b));

    initial begin : stim
        rst_n = 1'b0; start = 1'b0; tx_data = 8'h5A;
        #4 rst_n = 1'b1;
        @(posedge clk);
        start = 1'b1;
        @(posedge clk) start = 1'b0;
    end

    initial begin : watch
        @(posedge done_a); t_a = $time;
        $display("CLK_DIV=4   done at t=%0d, rx=0x%02h",  t_a, rx_a);
    end

    initial begin : finish_up
        @(posedge done_b); t_b = $time;
        $display("CLK_DIV=12  done at t=%0d, rx=0x%02h",  t_b, rx_b);
        $display("slow/fast transfer-time ratio = %0d:%0d", t_b, t_a);
        #4 $finish;
    end
endmodule
CLK_DIV=4   done at t=69, rx=0x5a
CLK_DIV=12  done at t=197, rx=0x5a
slow/fast transfer-time ratio = 197:69
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp3u6q7spe/spi_divider.v:102: $finish called at 201 (1s)

Common mistakes

MistakeWhat goes wrong and the fix
Sampling and shifting on the same edgeData is read while it is still changing. In Mode 0, sample MISO on the rising edge and update MOSI on the falling edge.
Odd CLK_DIVThe right shift truncates, so the half-periods become unequal. Use an even divisor of 2 or more.
Not presenting the MSB before the first edgeCPHA=0 samples on the first edge, so the first bit is lost. Load mosi when cs_n asserts.
done held highDownstream logic re-triggers. Keep done a single clock pulse.
Reading rx_data before doneYou capture a partial shift register. Always wait for the done pulse.
Leaving cs_n highThe slave never selects and ignores the frame. Drive cs_n low for the whole byte.

Quick check

In SPI Mode 0, on which SCLK edge does the master sample MISO?

The rising (leading) edge. MOSI is updated on the falling edge, so the data is stable before the next sample.

With a 50 MHz system clock and CLK_DIV of 8, what is the SCLK frequency?

50 MHz divided by 8 is 6.25 MHz. CLK_DIV sets how many system-clock cycles make up one SCLK period.

Why is MOSI wired to MISO in the testbench?

It creates a loopback: whatever the master shifts out is fed straight back in, so rx_data must equal tx_data. That gives a self-check without modelling a real slave.

Why does the master drive the top bit of tx_data onto MOSI before the first clock edge?

Because CPHA=0 samples on the first (leading) edge, the first bit must already be valid before that edge; there is no earlier edge to set it up.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD