SPI in One Picture
Name the four SPI wires and who drives each, read a frame byte by byte, and say why chip select is the frame and not a strobe.
- Lesson
- P7.1
- Time
- 20 min
- Runs on
- Your browser
- Check
- 4 questions
A sensor driver wrote a configuration byte and read back the old value every time. The engineer had raised chip select between the command byte and the data byte, to be safe. The sensor took each byte as a new command, and the write never happened.
- The controller owns the clock
Only the controller drives CLK and MOSI. Every peripheral sees them, selected or not.
- One select per peripheral
CS0 low picks peripheral 0. A peripheral whose select is high ignores the clock.
- MISO fans in
Only the selected peripheral may drive MISO; the other must let go of it.
Read the diagram as text
- Controller: drives CLK, MOSI, CS0, CS1
- Peripheral 0: selected by CS0
- Peripheral 1: selected by CS1
- CLK, MOSI: shared, controller drives
- MISO: shared, selected peripheral drives
- Controller → CLK, MOSI: clock, data out
- CLK, MOSI → Peripheral 0
- CLK, MOSI → Peripheral 1
- Controller → Peripheral 0: CS0
- Peripheral 1 → MISO
- Peripheral 0 → MISO
- MISO → Controller: data in
Arrows point from the driver. CS1 is drawn by the same rule as CS0.
Four Wires
SPI has one clock, driven only by the controller, one line out (MOSI), one line in (MISO), and one active-low select per peripheral. The controller picks a peripheral by pulling its select low and then clocks the bits.
| Wire | Direction | Driven by | When nobody uses it |
|---|---|---|---|
| CLK | controller to all | the controller only | at its idle level (CPOL, lesson P7.2) |
| MOSI | controller to all | the controller | any value: nobody samples it |
| MISO | peripheral to controller | the selected peripheral | released (z): every peripheral lets go |
| CS_n, one per peripheral | controller to one | the controller | high: deselected |
The flash datasheet calls the controller the "master" (W25Q128JV §6.1); this site says controller and peripheral.
A Write in One Frame
The bench peripheral reads its first byte as a command: bit 7 set means write, bits 6:0 name the register. Writing A5h to DATA (register 2) takes two bytes under one select.
Read phases and sample points (half clock)
- mode 0: columns 1 through 18.
- MOSI sampled at 3, 5, 7, 9, 11, 13, 15, 17.
- MISO sampled at 3, 5, 7, 9, 11, 13, 15, 17.
- Cycle 1
CS falls: the frame starts. Both sides put their first bit out: MOSI bit 7 of 82h, MISO bit 7 of 3Ch.
- Cycle 3
First rising edge: the peripheral takes MOSI, the controller takes MISO. One bit each way.
- Cycle 16
Eighth rising edge: the peripheral has the command 82h (write, register 2) and the controller has 3Ch.
Generated from the transaction and decoded back. Dots mark the edges where both sides sample: the rising edges in mode 0.
Read phases and sample points (half clock)
- mode 0: columns 1 through 18.
- MOSI sampled at 3, 5, 7, 9, 11, 13, 15, 17.
- MISO sampled at 3, 5, 7, 9, 11, 13, 15, 17.
Generated and decoded back. The peripheral answers 00h during a write; the write itself happens when CS rises.
Bits go most significant first. That is the flash's rule and most peripherals' habit, though not a law of SPI. The write takes effect when CS rises, and only if the frame ended on a whole byte.
Every Byte Out Is a Byte In
Every clock moves one bit each way, whether or not either side cares. While the controller sends a command, the peripheral is already sending something: here its ID, 3ch. To read, the controller sends a filler byte and takes what comes back.
SPI m0 02/3c
SPI m0 00/a5
SPI m0 03/3c
SPI m0 00/02The log line SPI m0 02/3c reads: mode 0, the controller sent 02h, the peripheral sent 3Ch. Lesson P7.5 builds that monitor.
Chip Select Is the Frame
Raising CS between the bytes makes two frames of one byte each. The peripheral takes 82h as a command with no data and ignores it. It then takes 5Ah as a new command, a read of register 5Ah. Nothing on the wires is illegal; the transaction is simply different.
One frame
cs_lo();
xfer(8'h82, received);
xfer(8'h5a, received);
cs_hi();
DATA becomes 5Ah when CS rises.
CS raised between the bytes
cs_between_bytes = 1;
cs_lo();
xfer(8'h82, received);
xfer(8'h5a, received);
cs_hi();
DATA keeps A5h: the read after it returns a5.
The flash says the same in one line: an instruction starts when CS falls and ends when CS rises (W25Q128JV §8).
Where You Will Meet It
Every sensor, display, flash, codec and radio with an SPI port works this way. A bench for one writes and reads back registers, counts frames, and raises CS in the wrong places on purpose.
Asked in interviews: SPI basics
What are the four SPI signals?
SCLK from the controller, MOSI from the controller, MISO from the selected peripheral, and one active-low chip select per peripheral.
Why is SPI called full duplex?
Every clock shifts one bit out on MOSI and one bit in on MISO at the same time. A byte sent is always a byte received.
What does chip select do besides selecting?
It frames the transaction. The peripheral starts decoding when CS falls and acts when CS rises.
Is SPI MSB first?
Most devices, and every flash instruction, shift the most significant bit first, but it is a device convention, not part of a standard.
Try It
The monitor prints one line per byte. The register map is the bench's own: 0 is the ID (3Ch), 1 is CTRL, 2 is DATA, 3 counts frames. It is a teaching device, not the flash.
Write CTRL with 81h and a value, then read it with 01h. Read the frame count again after the twin and see how its two short frames change it.
SPI m0 82/3c SPI m0 a5/00 SPI m0 02/3c SPI m0 00/a5 SPI m0 03/3c SPI m0 00/02 SPI m0 82/3c SPI m0 5a/3c SPI m0 02/3c SPI m0 00/a5Read included file: spi_periph.sv
`timescale 1ns/1ps
// A teaching register device, not the Winbond register map. Writes commit at CS
// rising only when the entire command/data frame ended on a byte boundary.
module spi_periph #(parameter CPOL=0,CPHA=0)(input wire cs_n,sclk,mosi,inout wire miso,output wire oe);
int cpol=CPOL,cpha=CPHA,wrong_mode=0;
bit drive_when_deselected=0,late_miso=0,lsb_first=0,loopback=0,chain=0;
realtime t_clqv=1.0;
reg [7:0] tx_word=8'h3c,last_rx=0,ctrl=0,data_reg=0,shift=0,response=8'h3c,pending=0;
reg out=0;int count=0,words=0,frames=0;reg [7:0] command=0;
wire p=cpol[0]^((wrong_mode&2)!=0),h=cpha[0]^((wrong_mode&1)!=0);
assign oe=!cs_n||drive_when_deselected;assign miso=oe?out:1'bz;
function automatic bit txbit(input int i);txbit=chain?shift[7]:response[lsb_first?i:7-i];endfunction
function automatic [7:0] reg_value(input reg [6:0] addr);
case(addr)0:reg_value=8'h3c;1:reg_value=ctrl;2:reg_value=data_reg;3:reg_value=frames[7:0];default:reg_value=0;endcase
endfunction
always @(negedge cs_n)begin
count=0;shift=chain?tx_word:0;words=0;command=0;response=tx_word;
// Preload bit 7 without advancing count. A same-edge 0/3 (or 2/1)
// pair may sample before the peripheral sees its first launch edge.
// CPHA=1's leading launch repeats this bit; only sampling advances count.
out<=#(t_clqv)txbit(0);
end
always @(posedge cs_n)begin
if(words>0)begin
if(!chain&&!loopback&&count==0&&words>=2&&command[7])begin
case(command[6:0])1:ctrl=pending;2:data_reg=pending;default:;endcase
end
frames++;if(chain)tx_word=shift;
end
end
always @(sclk)if(!cs_n)begin
if(sclk==(h?p:!p))begin
shift={shift[6:0],mosi};count++;
if(count==8)begin
last_rx=shift;count=0;
if(!chain)begin
if(loopback)response=shift;
else if(words==0)begin command=shift;response=shift[7]?0:reg_value(shift[6:0]);end
else pending=shift;
end
words++;
end
if(late_miso)out<=#(t_clqv)txbit(count);
end else if(!late_miso)out<=#(t_clqv)txbit(count);
end
endmodule
Read included file: spi_ctrl_tasks.svh
int cpol=0,cpha=0;realtime half_period=5.0,t_launch=1.0;bit cs_too_early=0,sample_late=0,cs_between_bytes=0;int clocks=0;
task automatic set_mode(input int p,input int h);cpol=p;cpha=h;sclk=p;endtask
task automatic set_period(input real ns);if(ns<=0)$fatal(1,"SPI period must be positive");half_period=ns/2;endtask
task automatic cs_lo(input int id=0);sclk=cpol;#(half_period);cs_n=0;#(half_period);endtask
task automatic cs_hi(input int id=0);#(half_period);cs_n=1;#(half_period);endtask
task automatic xfer(input reg [7:0] value,output reg [7:0] received);
received=0;
for(int b=7;b>=0;b--)begin
if(t_launch>=half_period)$fatal(1,"launch delay must fit half-period");
if(!cpha)begin
#(t_launch);mosi=value[b];#(half_period-t_launch);
if(!sample_late)received[b]=miso;sclk=!cpol;clocks++;
#(half_period);if(sample_late)received[b]=miso;sclk=cpol;
end else begin
#(half_period);sclk=!cpol;clocks++;#(t_launch);mosi=value[b];#(half_period-t_launch);
if(!sample_late)received[b]=miso;sclk=cpol;
if(sample_late)begin #(half_period);received[b]=miso;end
end
if(cs_too_early&&b==1)cs_n=1;
end
if(cs_between_bytes)begin cs_hi();cs_lo();end
endtask
Read included file: spi_checker.sv
`timescale 1ns/1ps
// Mode-generic teaching checker. tCLQV margin uses the configured receiver setup;
// positive margin is a bound, not a guarantee that a particular byte is correct.
module spi_checker(input wire cs_n,sclk,mosi,miso,oe,input wire cpol,cpha);
int broken=0,samples=0;realtime changed=0,mosi_changed=0,last_sample=-100,last_launch=-100,last_edge=-100,selected=-100,deselected=-100;
bit enabled=0,sample_late=0;realtime tdvch=1.0,tchdx=2.0,tclqv=1.0,receiver_setup=0.0;
realtime tslch=3.0,tchsh=3.0,tshch=3.0,tchsl=3.0,tshsl=10.0;
task automatic bad(input string rule,input real margin);broken++;$display("SPI CHECK %s margin %0.3f ns",rule,margin);endtask
// Late sampling consumes the old bit just before the next launch. Changes
// after that launch are legal; its setup bound is checked by tclqv below.
always @(miso)begin changed=$realtime;
if(enabled&&!sample_late&&!cs_n&&samples>0&&sclk==(cpha?cpol:!cpol))bad("same_edge",$realtime-last_sample);
end
always @(mosi)begin
mosi_changed=$realtime;
if(enabled&&!cs_n&&$realtime-last_sample<tchdx)bad("tchdx",$realtime-last_sample-tchdx);
end
task automatic timing_margin(input real half_period,input real tclqv_max,input real setup_ns);
real margin; margin=(sample_late?2:1)*half_period-tclqv_max-setup_ns;
$display("SPI MARGIN half=%0.3f tCLQV=%0.3f setup=%0.3f margin=%0.3f ns",half_period,tclqv_max,setup_ns,margin);
if(margin<0)bad("tclqv",margin);
endtask
always @(negedge cs_n)begin
if(sclk!==cpol)bad("cs_clk_idle",0);
if(enabled&&$realtime-deselected<tshsl)bad("tshsl",$realtime-deselected-tshsl);
if(enabled&&$realtime-last_edge<tchsl)bad("tchsl",$realtime-last_edge-tchsl);
selected=$realtime;samples=0;enabled=1;
end
always @(sclk)begin
if(enabled)begin
if(!cs_n&&$realtime-selected<tslch)bad("tslch",$realtime-selected-tslch);
if(cs_n&&$realtime-deselected<tshch)bad("tshch",$realtime-deselected-tshch);
end
last_edge=$realtime;
if(!cs_n)begin
if(sclk==(cpha?cpol:!cpol))begin
samples++;last_sample=$realtime;
if($realtime-mosi_changed<tdvch)bad("tdvch",$realtime-mosi_changed-tdvch);
if(!sample_late&&last_launch>=selected&&$realtime-last_launch<tclqv+receiver_setup)bad("tclqv",$realtime-last_launch-tclqv-receiver_setup);
end else begin
if(sample_late&&last_launch>=selected&&$realtime-last_launch<tclqv+receiver_setup)bad("tclqv",$realtime-last_launch-tclqv-receiver_setup);
last_launch=$realtime;
end
end
end
always @(posedge cs_n)if(enabled)begin
if(sclk!==cpol)bad("cs_clk_idle",0);
if($realtime-last_edge<tchsh)bad("tchsh",$realtime-last_edge-tchsh);
if(samples%8!=0)bad("byte_boundary",0);
deselected=$realtime;#1;if(oe)bad("miso_released",0);
end
endmodule
Read included file: spi_mon.sv
`timescale 1ns/1ps
module spi_mon(input wire cs_n,sclk,mosi,miso,input wire cpol,cpha);
reg [7:0] a=0,b=0;int count=0,frames=0;bit trace=0,sample_wrong_edge=0;
always @(negedge cs_n)count=0;
always @(sclk)if(!cs_n&&sclk==((cpha^sample_wrong_edge)?cpol:!cpol))begin
a={a[6:0],mosi};b={b[6:0],miso};count++;
if(count==8)begin frames++;if(trace)$display("SPI m%0d %02h/%02h",2*cpol+cpha,a,b);count=0;end
end
endmodule
Check Yourself
Remember
- SPI has four kinds of wire: CLK and MOSI from the controller, MISO from the selected peripheral, and one active-low select per peripheral.
- Chip select frames the transaction: it falls before the first bit and rises after the last, and the peripheral acts when it rises.
- Every clock moves one bit each way; a byte out is always a byte in.
- Most devices shift the most significant bit first; the flash requires it.
Practice
- An Eight-Clock SPI Peripheral Implement · Easy