SPI Slave Verilog Code (Mode 0): Shift Register + Testbench
The Serial Peripheral Interface (SPI) is the workhorse link between a microcontroller and the flash chips, ADCs, and sensors around it. On the slave side the whole protocol collapses into one idea: a shift register that moves one bit per clock edge, while a chip-select line marks where each byte begins and ends. This tutorial builds working, runnable SPI slave Verilog code for the most common configuration, mode 0, and drives it with a testbench that behaves like a real SPI master. Every listing below compiles with Icarus Verilog using iverilog -g2005 -o sim FILES && vvp sim, and the simulation log under each block is the actual output.
SPI mode 0 in one paragraph
SPI mode 0 means CPOL=0 (the clock idles low) and CPHA=0 (data is sampled on the leading, rising edge and shifted on the falling edge). Bytes travel MSB-first. Chip select (cs_n) is active-low: pulling it low opens a transaction, releasing it high closes one. Because CPHA is 0, the first data bit must already be valid on the line before the first rising edge, so both master and slave present their MSB the moment cs_n goes low. Keep those four facts in mind and the code reads itself.
The SPI slave Verilog code
The design below has two independent paths. A receive shift register samples mosi on every rising sclk edge; a transmit multiplexer drives miso and advances on every falling edge. The counters reset whenever cs_n is high, so framing is automatic.
// spi_slave.v - SPI slave, mode 0 (CPOL=0, CPHA=0), 8-bit, MSB first
// Receive shift register sampled on rising SCLK; transmit MUX changes on
// falling SCLK; cs_n frames one 8-bit transaction.
module spi_slave (
input wire sclk, // SPI clock from the master (mode 0)
input wire cs_n, // active-low chip select / frame
input wire mosi, // master-out slave-in
output wire miso, // master-in slave-out (tri-stated when idle)
input wire [7:0] tx_data, // byte this slave shifts back, MSB first
output reg [7:0] rx_data, // last fully received byte
output reg rx_valid // high after a complete byte
);
reg [2:0] rxcnt; // counts the 8 sampled bits
reg [2:0] txbit; // index of the bit currently on MISO
reg [7:0] rxsh; // receive shift register
// Transmit: present MSB first, advance on each falling SCLK edge.
assign miso = cs_n ? 1'bz : tx_data[3'd7 - txbit];
always @(negedge sclk or posedge cs_n) begin
if (cs_n)
txbit <= 3'd0; // idle: point back at the MSB
else
txbit <= txbit + 3'd1; // shift out on the falling edge
end
// Receive: sample MOSI on the rising edge, shift in MSB first.
always @(posedge sclk or posedge cs_n) begin
if (cs_n) begin
rxcnt <= 3'd0; // deasserting CS ends the frame
end else begin
rxsh <= {rxsh[6:0], mosi};
if (rxcnt == 3'd7) begin
rx_data <= {rxsh[6:0], mosi};
rx_valid <= 1'b1;
rxcnt <= 3'd0;
end else begin
if (rxcnt == 3'd0) rx_valid <= 1'b0;
rxcnt <= rxcnt + 3'd1;
end
end
end
endmodule
// tb_spi_slave.v - a mode-0 SPI master drives one byte per frame
module tb_spi_slave;
reg sclk, cs_n, mosi;
wire miso;
reg [7:0] tx_data;
wire [7:0] rx_data;
wire rx_valid;
reg [7:0] mosi_byte; // byte the master sends to the slave
reg [7:0] miso_byte; // byte the master reads back from the slave
integer k;
spi_slave dut (
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
.tx_data(tx_data), .rx_data(rx_data), .rx_valid(rx_valid)
);
// Clock one byte: CS low, 8 SCLK pulses, MSB first, CS high.
task spi_byte(input [7:0] data);
begin
mosi_byte = data;
cs_n = 1'b0; // start the frame
mosi = mosi_byte[7]; // MSB valid before the first edge
#10;
for (k = 0; k < 8; k = k + 1) begin
sclk = 1'b1; #10; // rising: both sample
miso_byte = {miso_byte[6:0], miso};
sclk = 1'b0; // falling: change data
if (k < 7) mosi = mosi_byte[6-k];
#10;
end
cs_n = 1'b1; // end the frame
#10;
end
endtask
initial begin
sclk = 1'b0; mosi = 1'b0; miso_byte = 8'h00;
cs_n = 1'b0; tx_data = 8'hA5; // settle, then raise CS to arm slave
#10 cs_n = 1'b1;
#10;
spi_byte(8'h3C); // master sends 0x3C, slave returns 0xA5
$display("Frame 1: slave rx=0x%02h valid=%b | master rx=0x%02h",
rx_data, rx_valid, miso_byte);
tx_data = 8'h5A; // queue the next response while idle
spi_byte(8'hF0); // master sends 0xF0, slave returns 0x5A
$display("Frame 2: slave rx=0x%02h valid=%b | master rx=0x%02h",
rx_data, rx_valid, miso_byte);
#20 $finish;
end
endmoduleFrame 1: slave rx=0x3c valid=1 | master rx=0xa5 Frame 2: slave rx=0xf0 valid=1 | master rx=0x5a /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpio0zgfu8/spi_slave.v:96: $finish called at 400 (1s)
How the shift register and CS framing work
The testbench plays the master. For each byte it pulls cs_n low, presents the MSB, then issues eight clock pulses. On every rising edge it reads miso; on every falling edge it changes mosi to the next bit. Because the slave uses the same edge convention, master and slave stay in lockstep and exchange a full byte in both directions at once.
The receive path
On each rising edge the slave does rxsh <= {rxsh[6:0], mosi}, pushing the new bit into the low end of the register. After the eighth bit, the fully assembled byte is copied to rx_data and rx_valid goes high. The count then wraps to zero so the next frame starts clean.
The transmit path
Rather than a second shift register, the output is a simple multiplexer: miso = tx_data[7 - txbit]. The index txbit starts at 0 (so the MSB appears first) and increments on each falling edge, exactly when mode 0 expects the slave to change its output. Reading tx_data directly means you can prepare the next response byte any time the bus is idle.
Chip-select framing
Both counters sit in always blocks sensitive to posedge cs_n. When the master releases chip select, the counters snap back to zero, so a short or glitched frame can never leave the slave half-way through a byte. The signals are summarised below.
| Signal | Direction | Role |
|---|---|---|
sclk | input | Serial clock; mode-0 sample on rising, shift on falling |
cs_n | input | Active-low frame; resets the bit counters when high |
mosi | input | Serial data from master, MSB first |
miso | output | Serial data to master; high-Z when not selected |
tx_data | input | Byte the slave shifts back this frame |
rx_data / rx_valid | output | Captured byte and its one-frame valid flag |
A minimal receive-only slave
If you only need to capture bytes, the design shrinks to one shift register and one counter. The testbench clocks 0xB7 in bit by bit, and $monitor prints the line as it fills so you can watch the shift register advance one bit per rising edge.
// spi_rx.v - minimal mode-0 SPI receive slave (shift register + bit counter)
module spi_rx (
input wire sclk, // mode 0: sample on the rising edge
input wire cs_n, // active-low frame
input wire mosi, // serial data in, MSB first
output reg [7:0] data, // last byte captured
output reg done // high once 8 bits are in
);
reg [2:0] cnt;
reg [7:0] sh;
always @(posedge sclk or posedge cs_n) begin
if (cs_n) begin
cnt <= 3'd0; // CS high: reset the bit counter
end else begin
sh <= {sh[6:0], mosi}; // shift MOSI in from the LSB side
if (cnt == 3'd7) begin
data <= {sh[6:0], mosi}; // 8th bit completes the byte
done <= 1'b1;
cnt <= 3'd0;
end else begin
if (cnt == 3'd0) done <= 1'b0;
cnt <= cnt + 3'd1;
end
end
end
endmodule
// tb_spi_rx.v - drive 0xB7 into the slave, one bit per SCLK pulse
module tb_spi_rx;
reg sclk, cs_n, mosi;
wire [7:0] data;
wire done;
reg [7:0] b;
integer k;
spi_rx dut (.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .data(data), .done(done));
initial begin
sclk = 1'b0; cs_n = 1'b1; mosi = 1'b0;
$monitor("t=%0t cs_n=%b sclk=%b mosi=%b done=%b data=0x%02h",
$time, cs_n, sclk, mosi, done, data);
#10;
b = 8'hB7; // 1011_0111
cs_n = 1'b0; // open the frame
mosi = b[7]; // MSB first
#10;
for (k = 0; k < 8; k = k + 1) begin
sclk = 1'b1; #10; // rising edge: slave samples
sclk = 1'b0; if (k < 7) mosi = b[6-k];
#10;
end
cs_n = 1'b1; // close the frame
#10;
$display("Captured byte = 0x%02h, done = %b", data, done);
#10 $finish;
end
endmodulet=0 cs_n=1 sclk=0 mosi=0 done=x data=0xxx t=10 cs_n=0 sclk=0 mosi=1 done=x data=0xxx t=20 cs_n=0 sclk=1 mosi=1 done=0 data=0xxx t=30 cs_n=0 sclk=0 mosi=0 done=0 data=0xxx t=40 cs_n=0 sclk=1 mosi=0 done=0 data=0xxx t=50 cs_n=0 sclk=0 mosi=1 done=0 data=0xxx t=60 cs_n=0 sclk=1 mosi=1 done=0 data=0xxx t=70 cs_n=0 sclk=0 mosi=1 done=0 data=0xxx t=80 cs_n=0 sclk=1 mosi=1 done=0 data=0xxx t=90 cs_n=0 sclk=0 mosi=0 done=0 data=0xxx t=100 cs_n=0 sclk=1 mosi=0 done=0 data=0xxx t=110 cs_n=0 sclk=0 mosi=1 done=0 data=0xxx t=120 cs_n=0 sclk=1 mosi=1 done=0 data=0xxx t=130 cs_n=0 sclk=0 mosi=1 done=0 data=0xxx t=140 cs_n=0 sclk=1 mosi=1 done=0 data=0xxx t=150 cs_n=0 sclk=0 mosi=1 done=0 data=0xxx t=160 cs_n=0 sclk=1 mosi=1 done=1 data=0xb7 t=170 cs_n=0 sclk=0 mosi=1 done=1 data=0xb7 t=180 cs_n=1 sclk=0 mosi=1 done=1 data=0xb7 Captured byte = 0xb7, done = 1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmprqmb2lqp/spi_rx.v:56: $finish called at 200 (1s)
Common mistakes
- Sampling on the wrong edge. Mode 0 samples on the rising edge and changes data on the falling edge. Copying a mode-3 example (which idles clock high) into a mode-0 system shifts every bit by half a clock and you read garbage.
- Forgetting MSB-first ordering. SPI is almost always MSB-first. If your shift direction or bit index is reversed, the byte comes out bit-reversed even though every edge looks correct on a waveform.
- Presenting MISO too late. With CPHA=0 the first bit is sampled on the very first rising edge, so the MSB must be valid at the moment
cs_nfalls, not after the first edge. Driving it fromtx_data[7]at reset avoids the off-by-one. - Not tri-stating MISO. Driving
misowith a hard 0 or 1 while deselected fights other slaves on a shared bus. Return1'bzwhencs_nis high so only the selected device drives the line. - Skipping the counter reset on CS. Without resetting the bit counter when
cs_nis high, one noisy or short frame desynchronises every byte that follows. Tie the reset tocs_nas shown. - Ignoring clock-domain crossing in real silicon. Clocking logic directly on a slow external
sclkis fine for learning and simulation, but on an FPGA you should synchronisecs_nandmosiinto the system clock, or oversamplesclk, to avoid metastability.
Quick check
In SPI mode 0, on which SCLK edge does the slave sample MOSI?
The rising (leading) edge. Because CPOL=0 and CPHA=0, data is sampled on the rising edge and the slave changes its output on the falling edge.
What marks the boundary of one byte transaction?
The chip-select line cs_n. Pulling it low opens the frame; releasing it high closes it and resets the bit counters, so framing cannot drift.
Why must the MISO MSB be valid before the first rising edge?
With CPHA=0 the first bit is captured on the very first rising edge, so it has to be on the line the instant cs_n goes low — that is why the transmit index starts at 0 (the MSB).
Why return 1’bz on MISO when the slave is not selected?
So several slaves can share one MISO wire. Only the device whose cs_n is low drives the line; the rest stay high-impedance and keep off the bus.
Share your question in comments or talk to our mentor team for batch guidance.
Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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