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
| Mistake | What goes wrong and the fix |
|---|---|
| Sampling and shifting on the same edge | Data 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_DIV | The right shift truncates, so the half-periods become unequal. Use an even divisor of 2 or more. |
| Not presenting the MSB before the first edge | CPHA=0 samples on the first edge, so the first bit is lost. Load mosi when cs_n asserts. |
done held high | Downstream logic re-triggers. Keep done a single clock pulse. |
Reading rx_data before done | You capture a partial shift register. Always wait for the done pulse. |
Leaving cs_n high | The 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.
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