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.

One Controller, Two Peripherals, Four Kinds of WireThe clock and MOSI fan out from the controller; MISO fans in; each peripheral has its own select.
One controller, two peripherals, four kinds of wireThe clock and MOSI fan out from the controller; MISO fans in; each peripheral has its own select. component Controller drives CLK, MOSI,CS0, CS1 component Peripheral 0 selected by CS0 component Peripheral 1 selected by CS1 component CLK, MOSI shared, controllerdrives component MISO shared, selectedperipheral drives clock, data outCS0data in
  1. The controller owns the clock

    Only the controller drives CLK and MOSI. Every peripheral sees them, selected or not.

  2. One select per peripheral

    CS0 low picks peripheral 0. A peripheral whose select is high ignores the clock.

  3. 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.

WireDirectionDriven byWhen nobody uses it
CLKcontroller to allthe controller onlyat its idle level (CPOL, lesson P7.2)
MOSIcontroller to allthe controllerany value: nobody samples it
MISOperipheral to controllerthe selected peripheralreleased (z): every peripheral lets go
CS_n, one per peripheralcontroller to onethe controllerhigh: 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.

Byte 1: The Command 82h, and the ID Coming Back
mode 0 012345678910111213141516171819CS_nCLKMOSIMISOMOSI sampled at 3MOSI sampled at 5MOSI sampled at 7MOSI sampled at 9MOSI sampled at 11MOSI sampled at 13MOSI sampled at 15MOSI sampled at 17MISO sampled at 3MISO sampled at 5MISO sampled at 7MISO sampled at 9MISO sampled at 11MISO sampled at 13MISO sampled at 15MISO sampled at 17
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.
  1. Cycle 1

    CS falls: the frame starts. Both sides put their first bit out: MOSI bit 7 of 82h, MISO bit 7 of 3Ch.

  2. Cycle 3

    First rising edge: the peripheral takes MOSI, the controller takes MISO. One bit each way.

  3. 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.

Byte 2, same frame: the data A5h
mode 0 012345678910111213141516171819CS_nCLKMOSIMISOMOSI sampled at 3MOSI sampled at 5MOSI sampled at 7MOSI sampled at 9MOSI sampled at 11MOSI sampled at 13MOSI sampled at 15MOSI sampled at 17MISO sampled at 3MISO sampled at 5MISO sampled at 7MISO sampled at 9MISO sampled at 11MISO sampled at 13MISO sampled at 15MISO sampled at 17
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.

The monitor's log of a read of DATA and a read of the frame count: mosi/miso per byte. COUNT shows 2 frames so far.
SPI m0 02/3c
SPI m0 00/a5
SPI m0 03/3c
SPI m0 00/02

The 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
  1. 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.

  2. 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.

  3. What does chip select do besides selecting?

    It frames the transaction. The peripheral starts decoding when CS falls and acts when CS rises.

  4. 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.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 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/a5
Read 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

Predict the outputBench defaults: mode 0, 100 MHz, a fresh peripheral, monitor off. The controller sends 02h then 00h under one select and prints the two bytes it received. What does it print?
`timescale 1ns/1ps
module tb;
  reg cs_n = 1, sclk = 0, mosi = 0; tri miso; reg [7:0] received;
  `include "spi_ctrl_tasks.svh"              // set_mode, set_period, cs_lo, cs_hi, xfer
  wire [0:0] oe;                              // driver view: who drives MISO
  spi_periph #(.CPOL(0), .CPHA(0)) p0 (cs_n, sclk, mosi, miso, oe[0]);
  spi_checker ck (cs_n, sclk, mosi, miso, |oe, cpol[0], cpha[0]);   // prints SPI CHECK <rule>
  reg [7:0] first;
  initial begin
    set_mode(0, 0);
    #10;
    cs_lo(); xfer(8'h02, received); first = received; xfer(8'h00, received); cs_hi();
    $display("%h %h", first, received);
    $finish;
  end
  initial begin #100000; $fatal(1, "watchdog"); end
endmodule
Read 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
Spot the bugEach option tries to write A5h to DATA (register 2). The bench then reads DATA back. Which option leaves DATA unchanged?
Read 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
ConceptWhat does the controller receive while it sends a command byte?
Read the waveformIn the generated trace of the command byte, at which edges do both sides sample?
Byte 1: The Command 82h, and the ID Coming Back
mode 0 012345678910111213141516171819CS_nCLKMOSIMISOMOSI sampled at 3MOSI sampled at 5MOSI sampled at 7MOSI sampled at 9MOSI sampled at 11MOSI sampled at 13MOSI sampled at 15MOSI sampled at 17MISO sampled at 3MISO sampled at 5MISO sampled at 7MISO sampled at 9MISO sampled at 11MISO sampled at 13MISO sampled at 15MISO sampled at 17
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.
  1. Cycle 1

    CS falls: the frame starts. Both sides put their first bit out: MOSI bit 7 of 82h, MISO bit 7 of 3Ch.

  2. Cycle 3

    First rising edge: the peripheral takes MOSI, the controller takes MISO. One bit each way.

  3. 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.

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