The Four Modes: CPOL and CPHA

Draw any SPI mode with its launch and sample edges, fill the 4×4 mode table, and say why a wrong mode shifts by one bit instead of scrambling the byte.

Lesson
P7.2
Time
35 min
Runs on
Your browser
Check
4 questions

A display controller wrote pixels perfectly and read its status register as 52h instead of A5h, always exactly one bit off. The team searched for a bit-order bug for two days. The peripheral was mode 1 and the controller mode 0: each launched on the edge the other sampled.

Controller Mode 0, Peripheral Mode 1: Launching on the Sample EdgeThe peripheral changes MISO on the rising edge, the very edge on which the controller samples it. It wins only by its 1 ns launch delay.
01234567891011121314151617181920212223242526272829303132333435CS_nCLKMOSIMISO
  1. Cycle 2

    The controller samples MISO on this rising edge. The peripheral launches on it too, 1 ns later.

  2. Cycle 18

    Second byte: every bit the controller takes is the bit before. Bit 7 of the echo is late, and so is every bit after it.

Measured on the bench, two loopback bytes of A5h. The controller reads the second as 52h, A5h shifted one place right; the peripheral still gets A5h, with only 1 ns of hold.

Two Bits Define a Mode

CPOL is the level the clock rests at: 0 low, 1 high. CPHA picks the sampling edge: 0 samples the first edge away from rest, 1 the second. Each side launches on the opposite edge, so data has half a clock to settle.

Four Modes, Two Sampling EdgesCPOL picks where the clock rests; CPHA picks the first or second edge. The result is one of two sampling edges.
Four modes, two sampling edgesCPOL picks where the clock rests; CPHA picks the first or second edge. The result is one of two sampling edges. component CPOL 0 clock rests low component CPOL 1 clock rests high component Mode 0 CPHA 0: first edge component Mode 1 CPHA 1: second edge component Mode 2 CPHA 0: first edge component Mode 3 CPHA 1: second edge component Samples rising launches falling component Samples falling launches rising
  1. Rising-edge family

    Modes 0 and 3 sample on rising edges. They differ only in where the clock rests, which matters at the edges of CS.

  2. Falling-edge family

    Modes 1 and 2 sample on falling edges. A device from one family on a controller from the other launches on the edge the controller samples.

Read the diagram as text
  • CPOL 0: clock rests low
  • CPOL 1: clock rests high
  • Mode 0: CPHA 0: first edge
  • Mode 1: CPHA 1: second edge
  • Mode 2: CPHA 0: first edge
  • Mode 3: CPHA 1: second edge
  • Samples rising: launches falling
  • Samples falling: launches rising
  • CPOL 0 → Mode 0
  • CPOL 0 → Mode 1
  • CPOL 1 → Mode 2
  • CPOL 1 → Mode 3
  • Mode 0 → Samples rising
  • Mode 3 → Samples rising
  • Mode 1 → Samples falling
  • Mode 2 → Samples falling

Arrows lead from the rest level through the mode to its sampling edge.

Mode 0: CPOL 0, CPHA 0
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

    CPHA 0: the first bit is out as soon as CS falls, before any clock edge.

  2. Cycle 3

    The first edge, rising, is the sample edge. Each side changes its output on the falling edge between samples.

Generated from the transaction and decoded back. Dots mark the sample edges.

Mode 1: CPOL 0, CPHA 1
mode 1 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 1: 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

    CPHA 1: nothing to sample yet. The first edge, rising, launches bit 7.

  2. Cycle 4

    The second edge, falling, samples it. Launch and sample stay half a clock apart.

Generated from the transaction and decoded back. Dots mark the sample edges.

ModeCPOL, CPHAClock idlesSample edgeLaunch edgeFirst bit appears
00, 0lowrisingfallingwhen CS falls
10, 1lowfallingrisingon the first edge
21, 0highfallingrisingwhen CS falls
31, 1highrisingfallingon the first edge

CPHA 0 samples the first (leading) edge away from idle, CPHA 1 the second (trailing) edge. Modes 0 and 3 share their edges, as do modes 1 and 2.

The flash samples its input on rising edges and changes its output on falling edges. That is modes 0 and 3, which it supports both, and which differ only in where the clock rests.

The 4×4 Matrix

Set the controller to one mode and the peripheral to another, send A5h both ways, and measure what each side got and how much hold each line had. Sixteen runs fill the table.

Controller \ peripheralmode 0mode 1mode 2mode 3
mode 0a5 / a5, hold 6 ns52 / a5, hold 1 ns52 / a5, hold 1 nsa5 / a5, hold 6 ns
mode 1d2 / d2, hold 1 nsa5 / a5, hold 6 nsa5 / a5, hold 6 nsd2 / d2, hold 1 ns
mode 252 / a5, hold 1 nsa5 / a5, hold 6 nsa5 / a5, hold 6 ns52 / a5, hold 1 ns
mode 3a5 / a5, hold 6 nsd2 / d2, hold 1 nsd2 / d2, hold 1 nsa5 / a5, hold 6 ns

Measured: both sides send A5h and the peripheral echoes what it received. Each cell is the second transfer, "controller got / peripheral got", with the shorter hold time of the two lines. The flash needs 2 ns of hold (tCHDX, W25Q128JV §9.6).

Every pair that samples on the same edge works, with 6 ns of hold: modes 0 and 3 together, modes 1 and 2 together. Every other pair launches on the edge the other side samples. Only the 1 ns launch delay decides the race, and the bytes come back shifted by one bit.

Why a Wrong Mode Is a One-Bit Shift

Both sides still move one bit per clock. The only disagreement is which edge carries each bit, so the sampler takes every bit one launch early or late. A5h becomes 52h (one place right) or D2h, never noise.

Controller mode 0, peripheral inmode 0mode 1mode 2mode 3
First byte after CS falls, A5h sent backa5hd2hd2ha5h

Measured: a one-byte frame, the peripheral answering A5h from its transmit register (data/P7-2-measured.json).

The rest level still matters at the edges of CS. This model preloads bit 7 when CS falls without advancing its count, so the same-edge mode-0/mode-3 pair returns the first byte intact: A5h reads as a5h. Check the selected device’s CS and clock requirements before relying on a shared sampling edge.

Where You Will Meet It

Every SPI controller has a mode setting per device, and every bring-up has met a wrong one. A bench for it runs the device in all four controller modes and checks the bytes and the hold margin, not the bytes alone.

Asked in interviews: SPI modes
  1. What do CPOL and CPHA mean?

    CPOL is the idle level of the clock. CPHA selects whether data is sampled on the first (leading) or second (trailing) edge after the clock leaves idle; the other edge launches the next bit.

  2. Which modes are compatible?

    The model transfers intact bytes between 0 and 3 (rising), and between 1 and 2 (falling), including the first byte. The selected device must also accept the clock level at CS. Mixed pairs race on the shared edge.

  3. What does a mode mismatch look like?

    Bytes shifted by exactly one bit, often only in one direction, and hold times of a launch delay. It is never random garbage.

  4. Which mode does a typical serial flash use?

    Mode 0 or mode 3: it samples on rising edges and changes its output on falling edges.

Try It

The probe beside the bench records the last sample edge of each receiver and the shortest time from it to the next change of that receiver's input: the hold time each line actually had.

Set p0.tx_word = 8'ha5; and print received after the first transfer instead: the first byte after CS falls. The controller-0, peripheral-3 cell reads a5h: the first-bit preload also preserves this first transfer.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: ctrl 0 periph 0: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 0 periph 1: ctrl got 52, periph got a5, hold MOSI 1.0 MISO 1.0 ns ctrl 0 periph 2: ctrl got 52, periph got a5, hold MOSI 1.0 MISO 1.0 ns ctrl 0 periph 3: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 1 periph 0: ctrl got d2, periph got d2, hold MOSI 1.0 MISO 1.0 ns ctrl 1 periph 1: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 1 periph 2: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 1 periph 3: ctrl got d2, periph got d2, hold MOSI 1.0 MISO 1.0 ns ctrl 2 periph 0: ctrl got 52, periph got a5, hold MOSI 1.0 MISO 1.0 ns ctrl 2 periph 1: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 2 periph 2: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 2 periph 3: ctrl got 52, periph got a5, hold MOSI 1.0 MISO 1.0 ns ctrl 3 periph 0: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns ctrl 3 periph 1: ctrl got d2, periph got d2, hold MOSI 1.0 MISO 1.0 ns ctrl 3 periph 2: ctrl got d2, periph got d2, hold MOSI 1.0 MISO 1.0 ns ctrl 3 periph 3: ctrl got a5, periph got a5, hold MOSI 6.0 MISO 6.0 ns
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 with loopback: the controller is in mode 2 (CPOL 1, CPHA 0) and the peripheral in mode 1 (CPOL 0, CPHA 1). Two transfers of A5h. The bench prints the second byte the controller got and the MISO hold time in ns. 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]);
  realtime t_cs, t_ps, hold_mosi, hold_miso;     // last sample edge of each receiver; shortest hold seen
  always @(negedge cs_n) begin t_cs = -99; t_ps = -99; hold_mosi = 99; hold_miso = 99; end
  always @(sclk) if (!cs_n) begin
    if (sclk == (cpha ? cpol : !cpol)) t_cs = $realtime;            // the controller samples MISO
    if (sclk == (p0.cpha ? p0.cpol : !p0.cpol)) t_ps = $realtime;   // the peripheral samples MOSI
  end
  always @(mosi) if (!cs_n && $realtime - t_ps < hold_mosi) hold_mosi = $realtime - t_ps;
  always @(miso) if (!cs_n && $realtime - t_cs < hold_miso) hold_miso = $realtime - t_cs;
  initial begin
    set_mode(0, 0);
    p0.loopback = 1;                 // the peripheral echoes the byte it received
    #10;
    set_mode(1, 0);                      // controller mode 2
    p0.cpol = 0; p0.cpha = 1;            // peripheral mode 1
    cs_lo(); xfer(8'ha5, received); xfer(8'ha5, received); cs_hi();
    $display("%h %0.1f", received, hold_miso);
    $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 bugThe peripheral starts in mode 0. Each option sets up a loopback of A5h; a setup passes when the byte comes back right with at least 2 ns of hold on both lines. Which setup fails?
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
ConceptWhy is a mode mismatch a one-bit shift and not random data?
ConceptIn the controller-0, peripheral-1 cell the peripheral still received A5h. Why is that direction marked wrong too?

Remember

  • CPOL is the clock's rest level; CPHA picks the sampling edge, first or second; the other edge launches.
  • Modes 0 and 3 sample on rising edges, modes 1 and 2 on falling edges; pairs that share the edge transfer intact bytes in this model.
  • Any other pair launches on the edge the other side samples: a race with a launch delay of hold, and a one-bit shift.
  • The rest level matters at the CS edges, where the first bit appears.
  • The flash samples on rising edges and changes its output on falling edges, so it works in modes 0 and 3.

Practice